nltk-2.0~b9/0000755000175000017500000000000011423120574012555 5ustar bhavanibhavaninltk-2.0~b9/nltk-2.0b9.pkg/0000755000175000017500000000000011423120503015025 5ustar bhavanibhavaninltk-2.0~b9/nltk/0000755000175000017500000000000011423120573013524 5ustar bhavanibhavaninltk-2.0~b9/javasrc/0000755000175000017500000000000011423120573014205 5ustar bhavanibhavaninltk-2.0~b9/nltk-2.0b9.pkg/Contents/0000755000175000017500000000000011423120504016623 5ustar bhavanibhavaninltk-2.0~b9/nltk/toolbox/0000755000175000017500000000000011423120574015213 5ustar bhavanibhavaninltk-2.0~b9/nltk/tokenize/0000755000175000017500000000000011423120574015355 5ustar bhavanibhavaninltk-2.0~b9/nltk/test/0000755000175000017500000000000011423120574014504 5ustar bhavanibhavaninltk-2.0~b9/nltk/tag/0000755000175000017500000000000011423120573014277 5ustar bhavanibhavaninltk-2.0~b9/nltk/stem/0000755000175000017500000000000011423120573014474 5ustar bhavanibhavaninltk-2.0~b9/nltk/sem/0000755000175000017500000000000011423120573014310 5ustar bhavanibhavaninltk-2.0~b9/nltk/parse/0000755000175000017500000000000011423120573014636 5ustar bhavanibhavaninltk-2.0~b9/nltk/model/0000755000175000017500000000000011423120573014624 5ustar bhavanibhavaninltk-2.0~b9/nltk/misc/0000755000175000017500000000000011423120573014457 5ustar bhavanibhavaninltk-2.0~b9/nltk/metrics/0000755000175000017500000000000011423120573015172 5ustar bhavanibhavaninltk-2.0~b9/nltk/inference/0000755000175000017500000000000011423120573015462 5ustar bhavanibhavaninltk-2.0~b9/nltk/examples/0000755000175000017500000000000011423120573015342 5ustar bhavanibhavaninltk-2.0~b9/nltk/etree/0000755000175000017500000000000011423120573014630 5ustar bhavanibhavaninltk-2.0~b9/nltk/draw/0000755000175000017500000000000011423120573014461 5ustar bhavanibhavaninltk-2.0~b9/nltk/corpus/0000755000175000017500000000000011423120573015037 5ustar bhavanibhavaninltk-2.0~b9/nltk/cluster/0000755000175000017500000000000011423120573015205 5ustar bhavanibhavaninltk-2.0~b9/nltk/classify/0000755000175000017500000000000011423120573015341 5ustar bhavanibhavaninltk-2.0~b9/nltk/chunk/0000755000175000017500000000000011423120573014634 5ustar bhavanibhavaninltk-2.0~b9/nltk/chat/0000755000175000017500000000000011423120573014443 5ustar bhavanibhavaninltk-2.0~b9/nltk/ccg/0000755000175000017500000000000011423120573014260 5ustar bhavanibhavaninltk-2.0~b9/nltk/app/0000755000175000017500000000000011423120573014304 5ustar bhavanibhavaninltk-2.0~b9/javasrc/org/0000755000175000017500000000000011423120573014774 5ustar bhavanibhavaninltk-2.0~b9/nltk-2.0b9.pkg/Contents/Resources/0000755000175000017500000000000011423120503020574 5ustar bhavanibhavaninltk-2.0~b9/nltk/corpus/reader/0000755000175000017500000000000011423120573016301 5ustar bhavanibhavaninltk-2.0~b9/javasrc/org/nltk/0000755000175000017500000000000011423120573015744 5ustar bhavanibhavaninltk-2.0~b9/nltk-2.0b9.pkg/Contents/Resources/English.lproj/0000755000175000017500000000000011423120503023312 5ustar bhavanibhavaninltk-2.0~b9/nltk-2.0b9.pkg/Contents/Resources/en.lproj/0000755000175000017500000000000011423120503022323 5ustar bhavanibhavaninltk-2.0~b9/javasrc/org/nltk/mallet/0000755000175000017500000000000011423120573017222 5ustar bhavanibhavaninltk-2.0~b9/setup.py0000755000175000017500000000346011423120572014273 0ustar bhavanibhavani#!/usr/bin/env python # # Distutils setup script for the Natural Language Toolkit # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # Ewan Klein # URL: # For license information, see LICENSE.TXT from distutils.core import setup import nltk setup( ############################################# ## Distribution Metadata name = "nltk", description = "Natural Language Toolkit", version = nltk.__version__, url = nltk.__url__, long_description = nltk.__longdescr__, license = nltk.__license__, keywords = nltk.__keywords__, maintainer = nltk.__maintainer__, maintainer_email = nltk.__maintainer_email__, author = nltk.__author__, author_email = nltk.__author__, classifiers = nltk.__classifiers__, # platforms = , ############################################# ## Package Data package_data = {'nltk': ['nltk.jar', 'test/*.doctest']}, ############################################# ## Package List packages = ['nltk', 'nltk.app', 'nltk.chat', 'nltk.chunk', 'nltk.ccg', 'nltk.classify', 'nltk.corpus', 'nltk.corpus.reader', 'nltk.cluster', 'nltk.draw', 'nltk.examples', 'nltk.inference', 'nltk.metrics', 'nltk.misc', 'nltk.model', 'nltk.parse', 'nltk.sem', 'nltk.stem', 'nltk.tag', 'nltk.tokenize', 'nltk.toolbox', 'nltk.etree', ], ) nltk-2.0~b9/README.txt0000644000175000017500000000466711327452002014265 0ustar bhavanibhavaniNatural Language Toolkit (NLTK) www.nltk.org Authors: Steven Bird Edward Loper Ewan Klein Copyright (C) 2001-2010 NLTK Project For license information, see LICENSE.txt NLTK -- the Natural Language Toolkit -- is a suite of open source Python modules, data sets and tutorials supporting research and development in Natural Language Processing. Documentation: A substantial amount of documentation about how to use NLTK, including a textbook and API documention, is available from the NLTK website: http://www.nltk.org/ - The book covers a wide range of introductory topics in NLP, and shows how to do all the processing tasks using the toolkit. - The toolkit's reference documentation describes every module, interface, class, method, function, and variable in the toolkit. This documentation should be useful to both users and developers. Mailing Lists: There are several mailing lists associated with NLTK: - nltk: Public information and announcements about NLTK (very low volume) http://groups.google.com/group/nltk - nltk-users: Discussions amongst NLTK users http://groups.google.com/group/nltk-users - nltk-dev: Discussions amongst NLTK developers http://groups.google.com/group/nltk-dev - nltk-translation: Discussions about translating the NLTK book http://groups.google.com/group/nltk-translation - nltk-commits: Subversion commit logs for NLTK http://groups.google.com/group/nltk-commits Contributing: If you would like to contribute to NLTK, please see http://www.nltk.org/contribute Donating: Have you found the toolkit helpful? Please support NLTK development by donating to the project via PayPal, using the link on the NLTK homepage. Redistributing: NLTK source code is distributed under the Apache 2.0 License. NLTK documentation is distributed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 United States license. NLTK corpora are provided under the terms given in the README file for each corpus; all are redistributable, and available for non-commercial use. NLTK may be freely redistributed, subject to the provisions of these licenses. Citing: If you publish work that uses NLTK, please cite the NLTK book, as follows: Bird, Steven, Edward Loper and Ewan Klein (2009). Natural Language Processing with Python. O'Reilly Media Inc. nltk-2.0~b9/PKG-INFO0000644000175000017500000000301011423120574013644 0ustar bhavanibhavaniMetadata-Version: 1.0 Name: nltk Version: 2.0b9 Summary: Natural Language Toolkit Home-page: http://www.nltk.org/ Author: Steven Bird, Edward Loper, Ewan Klein Author-email: sb@csse.unimelb.edu.au License: Apache License, Version 2.0 Description: The Natural Language Toolkit (NLTK) is a Python package for processing natural language text. NLTK requires Python 2.4 or higher. Keywords: NLP,CL,natural language processing,computational linguistics,parsing,tagging,tokenizing,syntax,linguistics,language,natural language Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Education Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2.4 Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces Classifier: Topic :: Scientific/Engineering :: Information Analysis Classifier: Topic :: Text Processing Classifier: Topic :: Text Processing :: Filters Classifier: Topic :: Text Processing :: General Classifier: Topic :: Text Processing :: Indexing Classifier: Topic :: Text Processing :: Linguistic nltk-2.0~b9/LICENSE.txt0000644000175000017500000000106111327452002014373 0ustar bhavanibhavaniCopyright (C) 2001-2010 NLTK Project Licensed under the Apache License, Version 2.0 (the 'License'); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. nltk-2.0~b9/INSTALL.txt0000644000175000017500000000026211224562502014424 0ustar bhavanibhavaniTo install NLTK, run setup.py from an administrator account, e.g.: sudo python setup.py install For full installation instructions, please see http://www.nltk.org/download nltk-2.0~b9/nltk/yamltags.py0000644000175000017500000000205011140171432015710 0ustar bhavanibhavaniimport yaml """ Register YAML tags in the NLTK namespace with the YAML loader, by telling it what module and class to look for. NLTK uses simple '!' tags to mark the types of objects, but the fully-qualified "tag:nltk.sourceforge.net,2007:" prefix is also accepted in case anyone ends up using it. """ def custom_import(name): components = name.split('.') module_path = '.'.join(components[:-1]) mod = __import__(module_path) for comp in components[1:]: mod = getattr(mod, comp) return mod def metaloader(classpath): def loader(*args, **kwds): classref = custom_import(classpath) return classref.from_yaml(*args, **kwds) return loader def register_tag(tag, classpath): yaml.add_constructor(u'!'+tag, metaloader(classpath)) yaml.add_constructor(u'tag:nltk.sourceforge.net,2007:'+tag, metaloader(classpath)) register_tag(u'tag.Unigram', 'nltk.tag.unigram.Unigram') register_tag(u'tag.Brill', 'nltk.tag.brill.Brill') __all__ = ['custom_import', 'metaloader', 'register_tag'] nltk-2.0~b9/nltk/util.py0000644000175000017500000011225111423114517015056 0ustar bhavanibhavani# Natural Language Toolkit: Utility functions # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT import locale import re import types import textwrap import pydoc import bisect import os from itertools import islice, chain from pprint import pprint from nltk.compat import defaultdict from nltk.internals import Deprecated, slice_bounds ###################################################################### # Short usage message ###################################################################### def usage(obj, selfname='self'): import inspect str(obj) # In case it's lazy, this will load it. if not isinstance(obj, (types.TypeType, types.ClassType)): obj = obj.__class__ print '%s supports the following operations:' % obj.__name__ for (name, method) in sorted(pydoc.allmethods(obj).items()): if name.startswith('_'): continue if getattr(method, '__deprecated__', False): continue args, varargs, varkw, defaults = inspect.getargspec(method) if (args and args[0]=='self' and (defaults is None or len(args)>len(defaults))): args = args[1:] name = '%s.%s' % (selfname, name) argspec = inspect.formatargspec( args, varargs, varkw, defaults) print textwrap.fill('%s%s' % (name, argspec), initial_indent=' - ', subsequent_indent=' '*(len(name)+5)) ########################################################################## # IDLE ########################################################################## def in_idle(): """ @rtype: C{boolean} @return: true if this function is run within idle. Tkinter programs that are run in idle should never call C{Tk.mainloop}; so this function should be used to gate all calls to C{Tk.mainloop}. @warning: This function works by checking C{sys.stdin}. If the user has modified C{sys.stdin}, then it may return incorrect results. """ import sys, types return (type(sys.stdin) == types.InstanceType and \ sys.stdin.__class__.__name__ == 'PyShell') ########################################################################## # PRETTY PRINTING ########################################################################## def pr(data, start=0, end=None): """ Pretty print a sequence of data items @param data: the data stream to print @type data: C{sequence} or C{iterator} @param start: the start position @type start: C{int} @param end: the end position @type end: C{int} """ pprint(list(islice(data, start, end))) def print_string(s, width=70): """ Pretty print a string, breaking lines on whitespace @param s: the string to print, consisting of words and spaces @type s: C{string} @param width: the display width @type width: C{int} """ print '\n'.join(textwrap.wrap(s, width=width)) def tokenwrap(tokens, separator=" ", width=70): """ Pretty print a list of text tokens, breaking lines on whitespace @param tokens: the tokens to print @type tokens: C{list} @param separator: the string to use to separate tokens @type separator: C{str} @param width: the display width (default=70) @type width: C{int} """ return '\n'.join(textwrap.wrap(separator.join(tokens), width=width)) ########################################################################## # Indexing ########################################################################## class Index(defaultdict): def __init__(self, pairs): defaultdict.__init__(self, list) for key, value in pairs: self[key].append(value) ###################################################################### ## Regexp display (thanks to David Mertz) ###################################################################### def re_show(regexp, string, left="{", right="}"): """ Search C{string} for substrings matching C{regexp} and wrap the matches with braces. This is convenient for learning about regular expressions. @param regexp: The regular expression. @type regexp: C{string} @param string: The string being matched. @type string: C{string} @param left: The left delimiter (printed before the matched substring) @type left: C{string} @param right: The right delimiter (printed after the matched substring) @type right: C{string} @rtype: C{string} @return: A string with markers surrounding the matched substrings. """ print re.compile(regexp, re.M).sub(left + r"\g<0>" + right, string.rstrip()) ########################################################################## # READ FROM FILE OR STRING ########################################################################## # recipe from David Mertz def filestring(f): if hasattr(f, 'read'): return f.read() elif isinstance(f, basestring): return open(f).read() else: raise ValueError, "Must be called with a filename or file-like object" ########################################################################## # Breadth-First Search ########################################################################## def breadth_first(tree, children=iter, depth=-1, queue=None): """Traverse the nodes of a tree in breadth-first order. (No need to check for cycles.) The first argument should be the tree root; children should be a function taking as argument a tree node and returning an iterator of the node's children. """ if queue == None: queue = [] queue.append(tree) while queue: node = queue.pop(0) yield node if depth != 0: try: queue += children(node) depth -= 1 except: pass ########################################################################## # Guess Character Encoding ########################################################################## # adapted from io.py in the docutils extension module (http://docutils.sourceforge.net) # http://www.pyzine.com/Issue008/Section_Articles/article_Encodings.html def guess_encoding(data): """ Given a byte string, attempt to decode it. Tries the standard 'UTF8' and 'latin-1' encodings, Plus several gathered from locale information. The calling program *must* first call:: locale.setlocale(locale.LC_ALL, '') If successful it returns C{(decoded_unicode, successful_encoding)}. If unsuccessful it raises a C{UnicodeError}. """ successful_encoding = None # we make 'utf-8' the first encoding encodings = ['utf-8'] # # next we add anything we can learn from the locale try: encodings.append(locale.nl_langinfo(locale.CODESET)) except AttributeError: pass try: encodings.append(locale.getlocale()[1]) except (AttributeError, IndexError): pass try: encodings.append(locale.getdefaultlocale()[1]) except (AttributeError, IndexError): pass # # we try 'latin-1' last encodings.append('latin-1') for enc in encodings: # some of the locale calls # may have returned None if not enc: continue try: decoded = unicode(data, enc) successful_encoding = enc except (UnicodeError, LookupError): pass else: break if not successful_encoding: raise UnicodeError( 'Unable to decode input data. Tried the following encodings: %s.' % ', '.join([repr(enc) for enc in encodings if enc])) else: return (decoded, successful_encoding) ########################################################################## # Invert a dictionary ########################################################################## def invert_dict(d): from nltk.compat import defaultdict inverted_dict = defaultdict(list) for key in d: if hasattr(d[key], '__iter__'): for term in d[key]: inverted_dict[term].append(key) else: inverted_dict[d[key]] = key return inverted_dict ########################################################################## # Utilities for directed graphs: transitive closure, and inversion # The graph is represented as a dictionary of sets ########################################################################## def transitive_closure(graph, reflexive=False): """ Calculate the transitive closure of a directed graph, optionally the reflexive transitive closure. The algorithm is a slight modification of the "Marking Algorithm" of Ioannidis & Ramakrishnan (1998) "Efficient Transitive Closure Algorithms". @param graph: the initial graph, represented as a dictionary of sets @type graph: C{dict} of C{set}s @param reflexive: if set, also make the closure reflexive @type reflexive: C{bool} @return: the (reflexive) transitive closure of the graph @rtype: C{dict} of C{set}s """ if reflexive: base_set = lambda k: set([k]) else: base_set = lambda k: set() # The graph U_i in the article: agenda_graph = dict((k, v.copy()) for (k,v) in graph.iteritems()) # The graph M_i in the article: closure_graph = dict((k, base_set(k)) for k in graph) for i in graph: agenda = agenda_graph[i] closure = closure_graph[i] while agenda: j = agenda.pop() closure.add(j) closure |= closure_graph.setdefault(j, base_set(j)) agenda |= agenda_graph.get(j, base_set(j)) agenda -= closure return closure_graph def invert_graph(graph): """ Inverts a directed graph. @param graph: the graph, represented as a dictionary of sets @type graph: C{dict} of C{set}s @return: the inverted graph @rtype: C{dict} of C{set}s """ inverted = {} for key, values in graph.iteritems(): for value in values: inverted.setdefault(value, set()).add(key) return inverted ########################################################################## # HTML Cleaning ########################################################################## def clean_html(html): """ Remove HTML markup from the given string. @param html: the HTML string to be cleaned @type html: C{string} @rtype: C{string} """ # First we remove inline JavaScript/CSS: cleaned = re.sub(r"(?is)<(script|style).*?>.*?()", "", html.strip()) # Then we remove html comments. This has to be done before removing regular # tags since comments can contain '>' characters. cleaned = re.sub(r"(?s)[\n]?", "", cleaned) # Next we can remove the remaining tags: cleaned = re.sub(r"(?s)<.*?>", " ", cleaned) # Finally, we deal with whitespace cleaned = re.sub(r" ", " ", cleaned) cleaned = re.sub(r" ", " ", cleaned) cleaned = re.sub(r" ", " ", cleaned) return cleaned.strip() def clean_url(url): from urllib import urlopen html = urlopen(url).read() return clean_html(html) ########################################################################## # Ngram iteration ########################################################################## # add a flag to pad the sequence so we get peripheral ngrams? def ngrams(sequence, n, pad_left=False, pad_right=False, pad_symbol=None): """ A utility that produces a sequence of ngrams from a sequence of items. For example: >>> ngrams([1,2,3,4,5], 3) [(1, 2, 3), (2, 3, 4), (3, 4, 5)] Use ingram for an iterator version of this function. Set pad_left or pad_right to true in order to get additional ngrams: >>> ngrams([1,2,3,4,5], 2, pad_right=True) [(1, 2), (2, 3), (3, 4), (4, 5), (5, None)] @param sequence: the source data to be converted into ngrams @type sequence: C{sequence} or C{iterator} @param n: the degree of the ngrams @type n: C{int} @param pad_left: whether the ngrams should be left-padded @type pad_left: C{boolean} @param pad_right: whether the ngrams should be right-padded @type pad_right: C{boolean} @param pad_symbol: the symbol to use for padding (default is None) @type pad_symbol: C{any} @return: The ngrams @rtype: C{list} of C{tuple}s """ if pad_left: sequence = chain((pad_symbol,) * (n-1), sequence) if pad_right: sequence = chain(sequence, (pad_symbol,) * (n-1)) sequence = list(sequence) count = max(0, len(sequence) - n + 1) return [tuple(sequence[i:i+n]) for i in range(count)] def bigrams(sequence, **kwargs): """ A utility that produces a sequence of bigrams from a sequence of items. For example: >>> bigrams([1,2,3,4,5]) [(1, 2), (2, 3), (3, 4), (4, 5)] Use ibigrams for an iterator version of this function. @param sequence: the source data to be converted into bigrams @type sequence: C{sequence} or C{iterator} @return: The bigrams @rtype: C{list} of C{tuple}s """ return ngrams(sequence, 2, **kwargs) def trigrams(sequence, **kwargs): """ A utility that produces a sequence of trigrams from a sequence of items. For example: >>> trigrams([1,2,3,4,5]) [(1, 2, 3), (2, 3, 4), (3, 4, 5)] Use itrigrams for an iterator version of this function. @param sequence: the source data to be converted into trigrams @type sequence: C{sequence} or C{iterator} @return: The trigrams @rtype: C{list} of C{tuple}s """ return ngrams(sequence, 3, **kwargs) def ingrams(sequence, n, pad_left=False, pad_right=False, pad_symbol=None): """ A utility that produces an iterator over ngrams generated from a sequence of items. For example: >>> list(ingrams([1,2,3,4,5], 3)) [(1, 2, 3), (2, 3, 4), (3, 4, 5)] Use ngrams for a list version of this function. Set pad_left or pad_right to true in order to get additional ngrams: >>> list(ingrams([1,2,3,4,5], 2, pad_right=True)) [(1, 2), (2, 3), (3, 4), (4, 5), (5, None)] @param sequence: the source data to be converted into ngrams @type sequence: C{sequence} or C{iterator} @param n: the degree of the ngrams @type n: C{int} @param pad_left: whether the ngrams should be left-padded @type pad_left: C{boolean} @param pad_right: whether the ngrams should be right-padded @type pad_right: C{boolean} @param pad_symbol: the symbol to use for padding (default is None) @type pad_symbol: C{any} @return: The ngrams @rtype: C{iterator} of C{tuple}s """ sequence = iter(sequence) if pad_left: sequence = chain((pad_symbol,) * (n-1), sequence) if pad_right: sequence = chain(sequence, (pad_symbol,) * (n-1)) history = [] while n > 1: history.append(sequence.next()) n -= 1 for item in sequence: history.append(item) yield tuple(history) del history[0] def ibigrams(sequence, **kwargs): """ A utility that produces an iterator over bigrams generated from a sequence of items. For example: >>> list(ibigrams([1,2,3,4,5])) [(1, 2), (2, 3), (3, 4), (4, 5)] Use bigrams for a list version of this function. @param sequence: the source data to be converted into bigrams @type sequence: C{sequence} or C{iterator} @return: The bigrams @rtype: C{iterator} of C{tuple}s """ for item in ingrams(sequence, 2, **kwargs): yield item def itrigrams(sequence, **kwargs): """ A utility that produces an iterator over trigrams generated from a sequence of items. For example: >>> list(itrigrams([1,2,3,4,5]) [(1, 2, 3), (2, 3, 4), (3, 4, 5)] Use trigrams for a list version of this function. @param sequence: the source data to be converted into trigrams @type sequence: C{sequence} or C{iterator} @return: The trigrams @rtype: C{iterator} of C{tuple}s """ for item in ingrams(sequence, 3, **kwargs): yield item ########################################################################## # Ordered Dictionary ########################################################################## class OrderedDict(dict): def __init__(self, data=None, **kwargs): self._keys = self.keys(data, kwargs.get('keys')) self._default_factory = kwargs.get('default_factory') if data is None: dict.__init__(self) else: dict.__init__(self, data) def __delitem__(self, key): dict.__delitem__(self, key) self._keys.remove(key) def __getitem__(self, key): try: return dict.__getitem__(self, key) except KeyError: return self.__missing__(key) def __iter__(self): return (key for key in self.keys()) def __missing__(self, key): if not self._default_factory and key not in self._keys: raise KeyError() else: return self._default_factory() def __setitem__(self, key, item): dict.__setitem__(self, key, item) if key not in self._keys: self._keys.append(key) def clear(self): dict.clear(self) self._keys.clear() def copy(self): d = dict.copy(self) d._keys = self._keys return d def items(self): return zip(self.keys(), self.values()) def keys(self, data=None, keys=None): if data: if keys: assert isinstance(keys, list) assert len(data) == len(keys) return keys else: assert isinstance(data, dict) or \ isinstance(data, OrderedDict) or \ isinstance(data, list) if isinstance(data, dict) or isinstance(data, OrderedDict): return data.keys() elif isinstance(data, list): return [key for (key, value) in data] elif '_keys' in self.__dict__: return self._keys else: return [] def popitem(self): if self._keys: key = self._keys.pop() value = self[key] del self[key] return (key, value) else: raise KeyError() def setdefault(self, key, failobj=None): dict.setdefault(self, key, failobj) if key not in self._keys: self._keys.append(key) def update(self, data): dict.update(self, data) for key in self.keys(data): if key not in self._keys: self._keys.append(key) def values(self): return map(self.get, self._keys) ###################################################################### # Lazy Sequences ###################################################################### class AbstractLazySequence(object): """ An abstract base class for read-only sequences whose values are computed as needed. Lazy sequences act like tuples -- they can be indexed, sliced, and iterated over; but they may not be modified. The most common application of lazy sequences in NLTK is for I{corpus view} objects, which provide access to the contents of a corpus without loading the entire corpus into memory, by loading pieces of the corpus from disk as needed. The result of modifying a mutable element of a lazy sequence is undefined. In particular, the modifications made to the element may or may not persist, depending on whether and when the lazy sequence caches that element's value or reconstructs it from scratch. Subclasses are required to define two methods: - L{__len__()} - L{iterate_from()}. """ def __len__(self): """ Return the number of tokens in the corpus file underlying this corpus view. """ raise NotImplementedError('should be implemented by subclass') def iterate_from(self, start): """ Return an iterator that generates the tokens in the corpus file underlying this corpus view, starting at the token number C{start}. If C{start>=len(self)}, then this iterator will generate no tokens. """ raise NotImplementedError('should be implemented by subclass') def __getitem__(self, i): """ Return the C{i}th token in the corpus file underlying this corpus view. Negative indices and spans are both supported. """ if isinstance(i, slice): start, stop = slice_bounds(self, i) return LazySubsequence(self, start, stop) else: # Handle negative indices if i < 0: i += len(self) if i < 0: raise IndexError('index out of range') # Use iterate_from to extract it. try: return self.iterate_from(i).next() except StopIteration: raise IndexError('index out of range') def __iter__(self): """Return an iterator that generates the tokens in the corpus file underlying this corpus view.""" return self.iterate_from(0) def count(self, value): """Return the number of times this list contains C{value}.""" return sum(1 for elt in self if elt==value) def index(self, value, start=None, stop=None): """Return the index of the first occurance of C{value} in this list that is greater than or equal to C{start} and less than C{stop}. Negative start & stop values are treated like negative slice bounds -- i.e., they count from the end of the list.""" start, stop = slice_bounds(self, slice(start, stop)) for i, elt in enumerate(islice(self, start, stop)): if elt == value: return i+start raise ValueError('index(x): x not in list') def __contains__(self, value): """Return true if this list contains C{value}.""" return bool(self.count(value)) def __add__(self, other): """Return a list concatenating self with other.""" return LazyConcatenation([self, other]) def __radd__(self, other): """Return a list concatenating other with self.""" return LazyConcatenation([other, self]) def __mul__(self, count): """Return a list concatenating self with itself C{count} times.""" return LazyConcatenation([self] * count) def __rmul__(self, count): """Return a list concatenating self with itself C{count} times.""" return LazyConcatenation([self] * count) _MAX_REPR_SIZE = 60 def __repr__(self): """ @return: A string representation for this corpus view that is similar to a list's representation; but if it would be more than 60 characters long, it is truncated. """ pieces = [] length = 5 for elt in self: pieces.append(repr(elt)) length += len(pieces[-1]) + 2 if length > self._MAX_REPR_SIZE and len(pieces) > 2: return '[%s, ...]' % ', '.join(pieces[:-1]) else: return '[%s]' % ', '.join(pieces) def __cmp__(self, other): """ Return a number indicating how C{self} relates to other. - If C{other} is not a corpus view or a C{list}, return -1. - Otherwise, return C{cmp(list(self), list(other))}. Note: corpus views do not compare equal to tuples containing equal elements. Otherwise, transitivity would be violated, since tuples do not compare equal to lists. """ if not isinstance(other, (AbstractLazySequence, list)): return -1 return cmp(list(self), list(other)) def __hash__(self): """ @raise ValueError: Corpus view objects are unhashable. """ raise ValueError('%s objects are unhashable' % self.__class__.__name__) class LazySubsequence(AbstractLazySequence): """ A subsequence produced by slicing a lazy sequence. This slice keeps a reference to its source sequence, and generates its values by looking them up in the source sequence. """ MIN_SIZE = 100 """The minimum size for which lazy slices should be created. If C{LazySubsequence()} is called with a subsequence that is shorter than C{MIN_SIZE}, then a tuple will be returned instead.""" def __new__(cls, source, start, stop): """ Construct a new slice from a given underlying sequence. The C{start} and C{stop} indices should be absolute indices -- i.e., they should not be negative (for indexing from the back of a list) or greater than the length of C{source}. """ # If the slice is small enough, just use a tuple. if stop-start < cls.MIN_SIZE: return list(islice(source.iterate_from(start), stop-start)) else: return object.__new__(cls) def __init__(self, source, start, stop): self._source = source self._start = start self._stop = stop def __len__(self): return self._stop - self._start def iterate_from(self, start): return islice(self._source.iterate_from(start+self._start), max(0, len(self)-start)) class LazyConcatenation(AbstractLazySequence): """ A lazy sequence formed by concatenating a list of lists. This underlying list of lists may itself be lazy. C{LazyConcatenation} maintains an index that it uses to keep track of the relationship between offsets in the concatenated lists and offsets in the sublists. """ def __init__(self, list_of_lists): self._list = list_of_lists self._offsets = [0] def __len__(self): if len(self._offsets) <= len(self._list): for tok in self.iterate_from(self._offsets[-1]): pass return self._offsets[-1] def iterate_from(self, start_index): if start_index < self._offsets[-1]: sublist_index = bisect.bisect_right(self._offsets, start_index)-1 else: sublist_index = len(self._offsets)-1 index = self._offsets[sublist_index] # Construct an iterator over the sublists. if isinstance(self._list, AbstractLazySequence): sublist_iter = self._list.iterate_from(sublist_index) else: sublist_iter = islice(self._list, sublist_index, None) for sublist in sublist_iter: if sublist_index == (len(self._offsets)-1): assert index+len(sublist) >= self._offsets[-1], ( 'offests not monotonic increasing!') self._offsets.append(index+len(sublist)) else: assert self._offsets[sublist_index+1] == index+len(sublist), ( 'inconsistent list value (num elts)') for value in sublist[max(0, start_index-index):]: yield value index += len(sublist) sublist_index += 1 class LazyMap(AbstractLazySequence): """ A lazy sequence whose elements are formed by applying a given function to each element in one or more underlying lists. The function is applied lazily -- i.e., when you read a value from the list, C{LazyMap} will calculate that value by applying its function to the underlying lists' value(s). C{LazyMap} is essentially a lazy version of the Python primitive function C{map}. In particular, the following two expressions are equivalent: >>> map(f, sequences...) >>> list(LazyMap(f, sequences...)) Like the Python C{map} primitive, if the source lists do not have equal size, then the value C{None} will be supplied for the 'missing' elements. Lazy maps can be useful for conserving memory, in cases where individual values take up a lot of space. This is especially true if the underlying list's values are constructed lazily, as is the case with many corpus readers. A typical example of a use case for this class is performing feature detection on the tokens in a corpus. Since featuresets are encoded as dictionaries, which can take up a lot of memory, using a C{LazyMap} can significantly reduce memory usage when training and running classifiers. """ def __init__(self, function, *lists, **config): """ @param function: The function that should be applied to elements of C{lists}. It should take as many arguments as there are C{lists}. @param lists: The underlying lists. @kwparam cache_size: Determines the size of the cache used by this lazy map. (default=5) """ if not lists: raise TypeError('LazyMap requires at least two args') self._lists = lists self._func = function self._cache_size = config.get('cache_size', 5) if self._cache_size > 0: self._cache = {} else: self._cache = None # If you just take bool() of sum() here _all_lazy will be true just # in case n >= 1 list is an AbstractLazySequence. Presumably this # isn't what's intended. self._all_lazy = sum(isinstance(lst, AbstractLazySequence) for lst in lists) == len(lists) def iterate_from(self, index): # Special case: one lazy sublist if len(self._lists) == 1 and self._all_lazy: for value in self._lists[0].iterate_from(index): yield self._func(value) return # Special case: one non-lazy sublist elif len(self._lists) == 1: while True: try: yield self._func(self._lists[0][index]) except IndexError: return index += 1 # Special case: n lazy sublists elif self._all_lazy: iterators = [lst.iterate_from(index) for lst in self._lists] while True: elements = [] for iterator in iterators: try: elements.append(iterator.next()) except: elements.append(None) if elements == [None] * len(self._lists): return yield self._func(*elements) index += 1 # general case else: while True: try: elements = [lst[index] for lst in self._lists] except IndexError: elements = [None] * len(self._lists) for i, lst in enumerate(self._lists): try: elements[i] = lst[index] except IndexError: pass if elements == [None] * len(self._lists): return yield self._func(*elements) index += 1 def __getitem__(self, index): if isinstance(index, slice): sliced_lists = [lst[index] for lst in self._lists] return LazyMap(self._func, *sliced_lists) else: # Handle negative indices if index < 0: index += len(self) if index < 0: raise IndexError('index out of range') # Check the cache if self._cache is not None and index in self._cache: return self._cache[index] # Calculate the value try: val = self.iterate_from(index).next() except StopIteration: raise IndexError('index out of range') # Update the cache if self._cache is not None: if len(self._cache) > self._cache_size: self._cache.popitem() # discard random entry self._cache[index] = val # Return the value return val def __len__(self): return max(len(lst) for lst in self._lists) class LazyMappedList(Deprecated, LazyMap): """Use LazyMap instead.""" def __init__(self, lst, func): LazyMap.__init__(self, func, lst) class LazyZip(LazyMap): """ A lazy sequence whose elements are tuples, each containing the i-th element from each of the argument sequences. The returned list is truncated in length to the length of the shortest argument sequence. The tuples are constructed lazily -- i.e., when you read a value from the list, C{LazyZip} will calculate that value by forming a C{tuple} from the i-th element of each of the argument sequences. C{LazyZip} is essentially a lazy version of the Python primitive function C{zip}. In particular, the following two expressions are equivalent: >>> zip(sequences...) >>> list(LazyZip(sequences...)) Lazy zips can be useful for conserving memory in cases where the argument sequences are particularly long. A typical example of a use case for this class is combining long sequences of gold standard and predicted values in a classification or tagging task in order to calculate accuracy. By constructing tuples lazily and avoiding the creation of an additional long sequence, memory usage can be significantly reduced. """ def __init__(self, *lists): """ @param lists: the underlying lists @type lists: C{list} of C{list} """ LazyMap.__init__(self, lambda *elts: elts, *lists) def iterate_from(self, index): iterator = LazyMap.iterate_from(self, index) while index < len(self): yield iterator.next() index += 1 return def __len__(self): return min(len(lst) for lst in self._lists) class LazyEnumerate(LazyZip): """ A lazy sequence whose elements are tuples, each ontaining a count (from zero) and a value yielded by underlying sequence. C{LazyEnumerate} is useful for obtaining an indexed list. The tuples are constructed lazily -- i.e., when you read a value from the list, C{LazyEnumerate} will calculate that value by forming a C{tuple} from the count of the i-th element and the i-th element of the underlying sequence. C{LazyEnumerate} is essentially a lazy version of the Python primitive function C{enumerate}. In particular, the following two expressions are equivalent: >>> enumerate(sequence) >>> list(LazyEnumerate(sequence)) Lazy enumerations can be useful for conserving memory in cases where the argument sequences are particularly long. A typical example of a use case for this class is obtaining an indexed list for a long sequence of values. By constructing tuples lazily and avoiding the creation of an additional long sequence, memory usage can be significantly reduced. """ def __init__(self, lst): """ @param lst: the underlying list @type lst: C{list} """ LazyZip.__init__(self, xrange(len(lst)), lst) class LazyMappedChain(Deprecated, LazyConcatenation): """Use LazyConcatenation(LazyMap(func, lists)) instead.""" def __init__(self, lst, func): LazyConcatenation.__init__(self, LazyMap(func, lst)) ###################################################################### # Binary Search in a File ###################################################################### # inherited from pywordnet, by Oliver Steele def binary_search_file(file, key, cache={}, cacheDepth=-1): """ Searches through a sorted file using the binary search algorithm. @type file: file @param file: the file to be searched through. @type key: {string} @param key: the identifier we are searching for. @return: The line from the file with first word key. """ key = key + ' ' keylen = len(key) start = 0 currentDepth = 0 if hasattr(file, 'name'): end = os.stat(file.name).st_size - 1 else: file.seek(0, 2) end = file.tell() - 1 file.seek(0) while start < end: lastState = start, end middle = (start + end) / 2 if cache.get(middle): offset, line = cache[middle] else: line = "" while True: file.seek(max(0, middle - 1)) if middle > 0: file.readline() offset = file.tell() line = file.readline() if line != "": break # at EOF; try to find start of the last line middle = (start + middle)/2 if middle == end -1: return None if currentDepth < cacheDepth: cache[middle] = (offset, line) if offset > end: assert end != middle - 1, "infinite loop" end = middle - 1 elif line[:keylen] == key: return line elif line > key: assert end != middle - 1, "infinite loop" end = middle - 1 elif line < key: start = offset + len(line) - 1 currentDepth += 1 thisState = start, end if lastState == thisState: # Detects the condition where we're searching past the end # of the file, which is otherwise difficult to detect return None return None nltk-2.0~b9/nltk/treetransforms.py0000644000175000017500000003136411211063750017161 0ustar bhavanibhavani# Natural Language Toolkit: Tree Transformations # # Copyright (C) 2005-2007 Oregon Graduate Institute # Author: Nathan Bodenstab # URL: # For license information, see LICENSE.TXT """ A collection of methods for tree (grammar) transformations used in parsing natural language. Although many of these methods are technically grammar transformations (ie. Chomsky Norm Form), when working with treebanks it is much more natural to visualize these modifications in a tree structure. Hence, we will do all transformation directly to the tree itself. Transforming the tree directly also allows us to do parent annotation. A grammar can then be simply induced from the modified tree. The following is a short tutorial on the available transformations. 1. Chomsky Normal Form (binarization) It is well known that any grammar has a Chomsky Normal Form (CNF) equivalent grammar where CNF is defined by every production having either two non-terminals or one terminal on its right hand side. When we have hierarchically structured data (ie. a treebank), it is natural to view this in terms of productions where the root of every subtree is the head (left hand side) of the production and all of its children are the right hand side constituents. In order to convert a tree into CNF, we simply need to ensure that every subtree has either two subtrees as children (binarization), or one leaf node (non-terminal). In order to binarize a subtree with more than two children, we must introduce artificial nodes. There are two popular methods to convert a tree into CNF: left factoring and right factoring. The following example demonstrates the difference between them. Example:: Original Right-Factored Left-Factored A A A / | \ / \ / \ B C D ==> B A| OR A| D / \ / \ C D B C 2. Parent Annotation In addition to binarizing the tree, there are two standard modifications to node labels we can do in the same traversal: parent annotation and Markov order-N smoothing (or sibling smoothing). The purpose of parent annotation is to refine the probabilities of productions by adding a small amount of context. With this simple addition, a CYK (inside-outside, dynamic programming chart parse) can improve from 74% to 79% accuracy. A natural generalization from parent annotation is to grandparent annotation and beyond. The tradeoff becomes accuracy gain vs. computational complexity. We must also keep in mind data sparcity issues. Example:: Original Parent Annotation A A^ / | \ / \ B C D ==> B^ A|^ where ? is the / \ parent of A C^ D^ 3. Markov order-N smoothing Markov smoothing combats data sparcity issues as well as decreasing computational requirements by limiting the number of children included in artificial nodes. In practice, most people use an order 2 grammar. Example:: Original No Smoothing Markov order 1 Markov order 2 etc. __A__ A A A / /|\ \ / \ / \ / \ B C D E F ==> B A| ==> B A| ==> B A| / \ / \ / \ C ... C ... C ... Annotation decisions can be thought about in the vertical direction (parent, grandparent, etc) and the horizontal direction (number of siblings to keep). Parameters to the following functions specify these values. For more information see: Dan Klein and Chris Manning (2003) "Accurate Unlexicalized Parsing", ACL-03. http://www.aclweb.org/anthology/P03-1054 4. Unary Collapsing Collapse unary productions (ie. subtrees with a single child) into a new non-terminal (Tree node). This is useful when working with algorithms that do not allow unary productions, yet you do not wish to lose the parent information. Example:: A | B ==> A+B / \ / \ C D C D """ from nltk.tree import Tree def chomsky_normal_form(tree, factor = "right", horzMarkov = None, vertMarkov = 0, childChar = "|", parentChar = "^"): # assume all subtrees have homogeneous children # assume all terminals have no siblings # A semi-hack to have elegant looking code below. As a result, # any subtree with a branching factor greater than 999 will be incorrectly truncated. if horzMarkov == None: horzMarkov = 999 # Traverse the tree depth-first keeping a list of ancestor nodes to the root. # I chose not to use the tree.treepositions() method since it requires # two traversals of the tree (one to get the positions, one to iterate # over them) and node access time is proportional to the height of the node. # This method is 7x faster which helps when parsing 40,000 sentences. nodeList = [(tree, [tree.node])] while nodeList != []: node, parent = nodeList.pop() if isinstance(node,Tree): # parent annotation parentString = "" originalNode = node.node if vertMarkov != 0 and node != tree and isinstance(node[0],Tree): parentString = "%s<%s>" % (parentChar, "-".join(parent)) node.node += parentString parent = [originalNode] + parent[:vertMarkov - 1] # add children to the agenda before we mess with them for child in node: nodeList.append((child, parent)) # chomsky normal form factorization if len(node) > 2: childNodes = [child.node for child in node] nodeCopy = node.copy() node[0:] = [] # delete the children curNode = node numChildren = len(nodeCopy) for i in range(1,numChildren - 1): if factor == "right": newHead = "%s%s<%s>%s" % (originalNode, childChar, "-".join(childNodes[i:min([i+horzMarkov,numChildren])]),parentString) # create new head newNode = Tree(newHead, []) curNode[0:] = [nodeCopy.pop(0), newNode] else: newHead = "%s%s<%s>%s" % (originalNode, childChar, "-".join(childNodes[max([numChildren-i-horzMarkov,0]):-i]),parentString) newNode = Tree(newHead, []) curNode[0:] = [newNode, nodeCopy.pop()] curNode = newNode curNode[0:] = [child for child in nodeCopy] def un_chomsky_normal_form(tree, expandUnary = True, childChar = "|", parentChar = "^", unaryChar = "+"): # Traverse the tree-depth first keeping a pointer to the parent for modification purposes. nodeList = [(tree,[])] while nodeList != []: node,parent = nodeList.pop() if isinstance(node,Tree): # if the node contains the 'childChar' character it means that # it is an artificial node and can be removed, although we still need # to move its children to its parent childIndex = node.node.find(childChar) if childIndex != -1: nodeIndex = parent.index(node) parent.remove(parent[nodeIndex]) # Generated node was on the left if the nodeIndex is 0 which # means the grammar was left factored. We must insert the children # at the beginning of the parent's children if nodeIndex == 0: parent.insert(0,node[0]) parent.insert(1,node[1]) else: parent.extend([node[0],node[1]]) # parent is now the current node so the children of parent will be added to the agenda node = parent else: parentIndex = node.node.find(parentChar) if parentIndex != -1: # strip the node name of the parent annotation node.node = node.node[:parentIndex] # expand collapsed unary productions if expandUnary == True: unaryIndex = node.node.find(unaryChar) if unaryIndex != -1: newNode = Tree(node.node[unaryIndex + 1:], [i for i in node]) node.node = node.node[:unaryIndex] node[0:] = [newNode] for child in node: nodeList.append((child,node)) def collapse_unary(tree, collapsePOS = False, collapseRoot = False, joinChar = "+"): """ Collapse subtrees with a single child (ie. unary productions) into a new non-terminal (Tree node) joined by 'joinChar'. This is useful when working with algorithms that do not allow unary productions, and completely removing the unary productions would require loss of useful information. The Tree is modified directly (since it is passed by reference) and no value is returned. @param tree: The Tree to be collapsed @type tree: C{Tree} @param collapsePOS: 'False' (default) will not collapse the parent of leaf nodes (ie. Part-of-Speech tags) since they are always unary productions @type collapsePOS: C{boolean} @param collapseRoot: 'False' (default) will not modify the root production if it is unary. For the Penn WSJ treebank corpus, this corresponds to the TOP -> productions. @type collapseRoot: C{boolean} @param joinChar: A string used to connect collapsed node values (default = "+") @type joinChar: C{string} """ if collapseRoot == False and isinstance(tree, Tree) and len(tree) == 1: nodeList = [tree[0]] else: nodeList = [tree] # depth-first traversal of tree while nodeList != []: node = nodeList.pop() if isinstance(node,Tree): if len(node) == 1 and isinstance(node[0], Tree) and (collapsePOS == True or isinstance(node[0,0], Tree)): node.node += joinChar + node[0].node node[0:] = [child for child in node[0]] # since we assigned the child's children to the current node, # evaluate the current node again nodeList.append(node) else: for child in node: nodeList.append(child) ################################################################# # Demonstration ################################################################# def demo(): """ A demonstration showing how each tree transform can be used. """ from nltk.draw.tree import draw_trees from nltk import treetransforms, bracket_parse from copy import deepcopy # original tree from WSJ bracketed text sentence = """(TOP (S (S (VP (VBN Turned) (ADVP (RB loose)) (PP (IN in) (NP (NP (NNP Shane) (NNP Longman) (POS 's)) (NN trading) (NN room))))) (, ,) (NP (DT the) (NN yuppie) (NNS dealers)) (VP (AUX do) (NP (NP (RB little)) (ADJP (RB right)))) (. .)))""" tree = bracket_parse(sentence) # collapse subtrees with only one child collapsedTree = deepcopy(tree) treetransforms.collapse_unary(collapsedTree) # convert the tree to CNF cnfTree = deepcopy(collapsedTree) treetransforms.chomsky_normal_form(cnfTree) # convert the tree to CNF with parent annotation (one level) and horizontal smoothing of order two parentTree = deepcopy(collapsedTree) treetransforms.chomsky_normal_form(parentTree, horzMarkov=2, vertMarkov=1) # convert the tree back to its original form (used to make CYK results comparable) original = deepcopy(parentTree) treetransforms.un_chomsky_normal_form(original) # convert tree back to bracketed text sentence2 = original.pprint() print sentence print sentence2 print "Sentences the same? ", sentence == sentence2 draw_trees(tree, collapsedTree, cnfTree, parentTree, original) if __name__ == '__main__': demo() __all__ = ["chomsky_normal_form", "un_chomsky_normal_form", "collapse_unary"] nltk-2.0~b9/nltk/tree.py0000644000175000017500000016242611363770700015056 0ustar bhavanibhavani# Natural Language Toolkit: Text Trees # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # Nathan Bodenstab (tree transforms) # URL: # For license information, see LICENSE.TXT """ Class for representing hierarchical language structures, such as syntax trees and morphological trees. """ # TODO: add LabelledTree (can be used for dependency trees) import re import string from grammar import Production, Nonterminal from probability import ProbabilisticMixIn from util import slice_bounds ###################################################################### ## Trees ###################################################################### class Tree(list): """ A hierarchical structure. Each C{Tree} represents a single hierarchical grouping of leaves and subtrees. For example, each constituent in a syntax tree is represented by a single C{Tree}. A tree's children are encoded as a C{list} of leaves and subtrees, where a X{leaf} is a basic (non-tree) value; and a X{subtree} is a nested C{Tree}. Any other properties that a C{Tree} defines are known as X{node properties}, and are used to add information about individual hierarchical groupings. For example, syntax trees use a NODE property to label syntactic constituents with phrase tags, such as \"NP\" and\"VP\". Several C{Tree} methods use X{tree positions} to specify children or descendants of a tree. Tree positions are defined as follows: - The tree position M{i} specifies a C{Tree}'s M{i}th child. - The tree position C{()} specifies the C{Tree} itself. - If C{M{p}} is the tree position of descendant M{d}, then C{M{p}+(M{i})} specifies the C{M{i}}th child of M{d}. I.e., every tree position is either a single index C{M{i}}, specifying C{self[M{i}]}; or a sequence C{(M{i1}, M{i2}, ..., M{iN})}, specifying C{self[M{i1}][M{i2}]...[M{iN}]}. """ def __new__(cls, node_or_str=None, children=None): if node_or_str is None: return list.__new__(cls) # used by copy.deepcopy if children is None: if not isinstance(node_or_str, basestring): raise TypeError("%s: Expected a node value and child list " "or a single string" % cls.__name__) return cls.parse(node_or_str) else: if (isinstance(children, basestring) or not hasattr(children, '__iter__')): raise TypeError("%s() argument 2 should be a list, not a " "string" % cls.__name__) return list.__new__(cls, node_or_str, children) def __init__(self, node_or_str, children=None): """ Construct a new tree. This constructor can be called in one of two ways: - C{Tree(node, children)} constructs a new tree with the specified node value and list of children. - C{Tree(s)} constructs a new tree by parsing the string C{s}. It is equivalent to calling the class method C{Tree.parse(s)}. """ # Because __new__ may delegate to Tree.parse(), the __init__ # method may end up getting called more than once (once when # constructing the return value for Tree.parse; and again when # __new__ returns). We therefore check if `children` is None # (which will cause __new__ to call Tree.parse()); if so, then # __init__ has already been called once, so just return. if children is None: return list.__init__(self, children) self.node = node_or_str #//////////////////////////////////////////////////////////// # Comparison operators #//////////////////////////////////////////////////////////// def __eq__(self, other): if not isinstance(other, Tree): return False return self.node == other.node and list.__eq__(self, other) def __ne__(self, other): return not (self == other) def __lt__(self, other): if not isinstance(other, Tree): return False return self.node < other.node or list.__lt__(self, other) def __le__(self, other): if not isinstance(other, Tree): return False return self.node <= other.node or list.__le__(self, other) def __gt__(self, other): if not isinstance(other, Tree): return True return self.node > other.node or list.__gt__(self, other) def __ge__(self, other): if not isinstance(other, Tree): return False return self.node >= other.node or list.__ge__(self, other) #//////////////////////////////////////////////////////////// # Disabled list operations #//////////////////////////////////////////////////////////// def __mul__(self, v): raise TypeError('Tree does not support multiplication') def __rmul__(self, v): raise TypeError('Tree does not support multiplication') def __add__(self, v): raise TypeError('Tree does not support addition') def __radd__(self, v): raise TypeError('Tree does not support addition') #//////////////////////////////////////////////////////////// # Indexing (with support for tree positions) #//////////////////////////////////////////////////////////// def __getitem__(self, index): if isinstance(index, (int, slice)): return list.__getitem__(self, index) else: if len(index) == 0: return self elif len(index) == 1: return self[int(index[0])] else: return self[int(index[0])][index[1:]] def __setitem__(self, index, value): if isinstance(index, (int, slice)): return list.__setitem__(self, index, value) else: if len(index) == 0: raise IndexError('The tree position () may not be ' 'assigned to.') elif len(index) == 1: self[index[0]] = value else: self[index[0]][index[1:]] = value def __delitem__(self, index): if isinstance(index, (int, slice)): return list.__delitem__(self, index) else: if len(index) == 0: raise IndexError('The tree position () may not be deleted.') elif len(index) == 1: del self[index[0]] else: del self[index[0]][index[1:]] #//////////////////////////////////////////////////////////// # Basic tree operations #//////////////////////////////////////////////////////////// def leaves(self): """ @return: a list containing this tree's leaves. The order reflects the order of the leaves in the tree's hierarchical structure. @rtype: C{list} """ leaves = [] for child in self: if isinstance(child, Tree): leaves.extend(child.leaves()) else: leaves.append(child) return leaves def flatten(self): """ @return: a tree consisting of this tree's root connected directly to its leaves, omitting all intervening non-terminal nodes. @rtype: C{Tree} """ return Tree(self.node, self.leaves()) def height(self): """ @return: The height of this tree. The height of a tree containing no children is 1; the height of a tree containing only leaves is 2; and the height of any other tree is one plus the maximum of its children's heights. @rtype: C{int} """ max_child_height = 0 for child in self: if isinstance(child, Tree): max_child_height = max(max_child_height, child.height()) else: max_child_height = max(max_child_height, 1) return 1 + max_child_height def treepositions(self, order='preorder'): """ @param order: One of: C{preorder}, C{postorder}, C{bothorder}, C{leaves}. """ positions = [] if order in ('preorder', 'bothorder'): positions.append( () ) for i, child in enumerate(self): if isinstance(child, Tree): childpos = child.treepositions(order) positions.extend((i,)+p for p in childpos) else: positions.append( (i,) ) if order in ('postorder', 'bothorder'): positions.append( () ) return positions def subtrees(self, filter=None): """ Generate all the subtrees of this tree, optionally restricted to trees matching the filter function. @type filter: C{function} @param filter: the function to filter all local trees """ if not filter or filter(self): yield self for child in self: if isinstance(child, Tree): for subtree in child.subtrees(filter): yield subtree def productions(self): """ Generate the productions that correspond to the non-terminal nodes of the tree. For each subtree of the form (P: C1 C2 ... Cn) this produces a production of the form P -> C1 C2 ... Cn. @rtype: list of C{Production}s """ if not isinstance(self.node, basestring): raise TypeError, 'Productions can only be generated from trees having node labels that are strings' prods = [Production(Nonterminal(self.node), _child_names(self))] for child in self: if isinstance(child, Tree): prods += child.productions() return prods def pos(self): """ @return: a list of tuples containing leaves and pre-terminals (part-of-speech tags). The order reflects the order of the leaves in the tree's hierarchical structure. @rtype: C{list} of C{tuples} """ pos = [] for child in self: if isinstance(child, Tree): pos.extend(child.pos()) else: pos.append((child, self.node)) return pos def leaf_treeposition(self, index): """ @return: The tree position of the C{index}-th leaf in this tree. I.e., if C{tp=self.leaf_treeposition(i)}, then C{self[tp]==self.leaves()[i]}. @raise IndexError: If this tree contains fewer than C{index+1} leaves, or if C{index<0}. """ if index < 0: raise IndexError('index must be non-negative') stack = [(self, ())] while stack: value, treepos = stack.pop() if not isinstance(value, Tree): if index == 0: return treepos else: index -= 1 else: for i in range(len(value)-1, -1, -1): stack.append( (value[i], treepos+(i,)) ) raise IndexError('index must be less than or equal to len(self)') def treeposition_spanning_leaves(self, start, end): """ @return: The tree position of the lowest descendant of this tree that dominates C{self.leaves()[start:end]}. @raise ValueError: if C{end <= start} """ if end <= start: raise ValueError('end must be greater than start') # Find the tree positions of the start & end leaves, and # take the longest common subsequence. start_treepos = self.leaf_treeposition(start) end_treepos = self.leaf_treeposition(end-1) # Find the first index where they mismatch: for i in range(len(start_treepos)): if i == len(end_treepos) or start_treepos[i] != end_treepos[i]: return start_treepos[:i] return start_treepos #//////////////////////////////////////////////////////////// # Transforms #//////////////////////////////////////////////////////////// def chomsky_normal_form(self, factor = "right", horzMarkov = None, vertMarkov = 0, childChar = "|", parentChar = "^"): """ This method can modify a tree in three ways: 1. Convert a tree into its Chomsky Normal Form (CNF) equivalent -- Every subtree has either two non-terminals or one terminal as its children. This process requires the creation of more"artificial" non-terminal nodes. 2. Markov (vertical) smoothing of children in new artificial nodes 3. Horizontal (parent) annotation of nodes @param factor: Right or left factoring method (default = "right") @type factor: C{string} = [left|right] @param horzMarkov: Markov order for sibling smoothing in artificial nodes (None (default) = include all siblings) @type horzMarkov: C{int} | None @param vertMarkov: Markov order for parent smoothing (0 (default) = no vertical annotation) @type vertMarkov: C{int} | None @param childChar: A string used in construction of the artificial nodes, separating the head of the original subtree from the child nodes that have yet to be expanded (default = "|") @type childChar: C{string} @param parentChar: A string used to separate the node representation from its vertical annotation @type parentChar: C{string} """ from treetransforms import chomsky_normal_form chomsky_normal_form(self, factor, horzMarkov, vertMarkov, childChar, parentChar) def un_chomsky_normal_form(self, expandUnary = True, childChar = "|", parentChar = "^", unaryChar = "+"): """ This method modifies the tree in three ways: 1. Transforms a tree in Chomsky Normal Form back to its original structure (branching greater than two) 2. Removes any parent annotation (if it exists) 3. (optional) expands unary subtrees (if previously collapsed with collapseUnary(...) ) @param expandUnary: Flag to expand unary or not (default = True) @type expandUnary: C{boolean} @param childChar: A string separating the head node from its children in an artificial node (default = "|") @type childChar: C{string} @param parentChar: A sting separating the node label from its parent annotation (default = "^") @type parentChar: C{string} @param unaryChar: A string joining two non-terminals in a unary production (default = "+") @type unaryChar: C{string} """ from treetransforms import un_chomsky_normal_form un_chomsky_normal_form(self, expandUnary, childChar, parentChar, unaryChar) def collapse_unary(self, collapsePOS = False, collapseRoot = False, joinChar = "+"): """ Collapse subtrees with a single child (ie. unary productions) into a new non-terminal (Tree node) joined by 'joinChar'. This is useful when working with algorithms that do not allow unary productions, and completely removing the unary productions would require loss of useful information. The Tree is modified directly (since it is passed by reference) and no value is returned. @param collapsePOS: 'False' (default) will not collapse the parent of leaf nodes (ie. Part-of-Speech tags) since they are always unary productions @type collapsePOS: C{boolean} @param collapseRoot: 'False' (default) will not modify the root production if it is unary. For the Penn WSJ treebank corpus, this corresponds to the TOP -> productions. @type collapseRoot: C{boolean} @param joinChar: A string used to connect collapsed node values (default = "+") @type joinChar: C{string} """ from treetransforms import collapse_unary collapse_unary(self, collapsePOS, collapseRoot, joinChar) #//////////////////////////////////////////////////////////// # Convert, copy #//////////////////////////////////////////////////////////// # [classmethod] def convert(cls, val): """ Convert a tree between different subtypes of Tree. C{cls} determines which class will be used to encode the new tree. @type val: L{Tree} @param val: The tree that should be converted. @return: The new C{Tree}. """ if isinstance(val, Tree): children = [cls.convert(child) for child in val] return cls(val.node, children) else: return val convert = classmethod(convert) def copy(self, deep=False): if not deep: return self.__class__(self.node, self) else: return self.__class__.convert(self) def _frozen_class(self): return ImmutableTree def freeze(self, leaf_freezer=None): frozen_class = self._frozen_class() if leaf_freezer is None: newcopy = frozen_class.convert(self) else: newcopy = self.copy(deep=True) for pos in newcopy.treepositions('leaves'): newcopy[pos] = leaf_freezer(newcopy[pos]) newcopy = frozen_class.convert(newcopy) hash(newcopy) # Make sure the leaves are hashable. return newcopy #//////////////////////////////////////////////////////////// # Parsing #//////////////////////////////////////////////////////////// @classmethod def parse(cls, s, brackets='()', parse_node=None, parse_leaf=None, node_pattern=None, leaf_pattern=None, remove_empty_top_bracketing=False): """ Parse a bracketed tree string and return the resulting tree. Trees are represented as nested brackettings, such as:: (S (NP (NNP John)) (VP (V runs))) @type s: C{str} @param s: The string to parse @type brackets: length-2 C{str} @param brackets: The bracket characters used to mark the beginning and end of trees and subtrees. @type parse_node: C{function} @type parse_leaf: C{function} @param parse_node, parse_leaf: If specified, these functions are applied to the substrings of C{s} corresponding to nodes and leaves (respectively) to obtain the values for those nodes and leaves. They should have the following signature: >>> parse_node(str) -> value For example, these functions could be used to parse nodes and leaves whose values should be some type other than string (such as L{FeatStruct }). Note that by default, node strings and leaf strings are delimited by whitespace and brackets; to override this default, use the C{node_pattern} and C{leaf_pattern} arguments. @type node_pattern: C{str} @type leaf_pattern: C{str} @param node_pattern, leaf_pattern: Regular expression patterns used to find node and leaf substrings in C{s}. By default, both nodes patterns are defined to match any sequence of non-whitespace non-bracket characters. @type remove_empty_top_bracketing: C{bool} @param remove_empty_top_bracketing: If the resulting tree has an empty node label, and is length one, then return its single child instead. This is useful for treebank trees, which sometimes contain an extra level of bracketing. @return: A tree corresponding to the string representation C{s}. If this class method is called using a subclass of C{Tree}, then it will return a tree of that type. @rtype: C{Tree} """ if not isinstance(brackets, basestring) or len(brackets) != 2: raise TypeError('brackets must be a length-2 string') if re.search('\s', brackets): raise TypeError('whitespace brackets not allowed') # Construct a regexp that will tokenize the string. open_b, close_b = brackets open_pattern, close_pattern = (re.escape(open_b), re.escape(close_b)) if node_pattern is None: node_pattern = '[^\s%s%s]+' % (open_pattern, close_pattern) if leaf_pattern is None: leaf_pattern = '[^\s%s%s]+' % (open_pattern, close_pattern) token_re = re.compile('%s\s*(%s)?|%s|(%s)' % ( open_pattern, node_pattern, close_pattern, leaf_pattern)) # Walk through each token, updating a stack of trees. stack = [(None, [])] # list of (node, children) tuples for match in token_re.finditer(s): token = match.group() # Beginning of a tree/subtree if token[0] == open_b: if len(stack) == 1 and len(stack[0][1]) > 0: cls._parse_error(s, match, 'end-of-string') node = token[1:].lstrip() if parse_node is not None: node = parse_node(node) stack.append((node, [])) # End of a tree/subtree elif token == close_b: if len(stack) == 1: if len(stack[0][1]) == 0: cls._parse_error(s, match, open_b) else: cls._parse_error(s, match, 'end-of-string') node, children = stack.pop() stack[-1][1].append(cls(node, children)) # Leaf node else: if len(stack) == 1: cls._parse_error(s, match, open_b) if parse_leaf is not None: token = parse_leaf(token) stack[-1][1].append(token) # check that we got exactly one complete tree. if len(stack) > 1: cls._parse_error(s, 'end-of-string', close_b) elif len(stack[0][1]) == 0: cls._parse_error(s, 'end-of-string', open_b) else: assert stack[0][0] is None assert len(stack[0][1]) == 1 tree = stack[0][1][0] # If the tree has an extra level with node='', then get rid of # it. E.g.: "((S (NP ...) (VP ...)))" if remove_empty_top_bracketing and tree.node == '' and len(tree) == 1: tree = tree[0] # return the tree. return tree @classmethod def _parse_error(cls, s, match, expecting): """ Display a friendly error message when parsing a tree string fails. @param s: The string we're parsing. @param match: regexp match of the problem token. @param expecting: what we expected to see instead. """ # Construct a basic error message if match == 'end-of-string': pos, token = len(s), 'end-of-string' else: pos, token = match.start(), match.group() msg = '%s.parse(): expected %r but got %r\n%sat index %d.' % ( cls.__name__, expecting, token, ' '*12, pos) # Add a display showing the error token itsels: s = s.replace('\n', ' ').replace('\t', ' ') offset = pos if len(s) > pos+10: s = s[:pos+10]+'...' if pos > 10: s = '...'+s[pos-10:] offset = 13 msg += '\n%s"%s"\n%s^' % (' '*16, s, ' '*(17+offset)) raise ValueError(msg) #//////////////////////////////////////////////////////////// # Visualization & String Representation #//////////////////////////////////////////////////////////// def draw(self): """ Open a new window containing a graphical diagram of this tree. """ from nltk.draw.tree import draw_trees draw_trees(self) def __repr__(self): childstr = ", ".join(repr(c) for c in self) return '%s(%r, [%s])' % (self.__class__.__name__, self.node, childstr) def __str__(self): return self.pprint() def pprint(self, margin=70, indent=0, nodesep='', parens='()', quotes=False): """ @return: A pretty-printed string representation of this tree. @rtype: C{string} @param margin: The right margin at which to do line-wrapping. @type margin: C{int} @param indent: The indentation level at which printing begins. This number is used to decide how far to indent subsequent lines. @type indent: C{int} @param nodesep: A string that is used to separate the node from the children. E.g., the default value C{':'} gives trees like C{(S: (NP: I) (VP: (V: saw) (NP: it)))}. """ # Try writing it on one line. s = self._pprint_flat(nodesep, parens, quotes) if len(s)+indent < margin: return s # If it doesn't fit on one line, then write it on multi-lines. if isinstance(self.node, basestring): s = '%s%s%s' % (parens[0], self.node, nodesep) else: s = '%s%r%s' % (parens[0], self.node, nodesep) for child in self: if isinstance(child, Tree): s += '\n'+' '*(indent+2)+child.pprint(margin, indent+2, nodesep, parens, quotes) elif isinstance(child, tuple): s += '\n'+' '*(indent+2)+ "/".join(child) elif isinstance(child, basestring) and not quotes: s += '\n'+' '*(indent+2)+ '%s' % child else: s += '\n'+' '*(indent+2)+ '%r' % child return s+parens[1] def pprint_latex_qtree(self): r""" Returns a representation of the tree compatible with the LaTeX qtree package. This consists of the string C{\Tree} followed by the parse tree represented in bracketed notation. For example, the following result was generated from a parse tree of the sentence C{The announcement astounded us}:: \Tree [.I'' [.N'' [.D The ] [.N' [.N announcement ] ] ] [.I' [.V'' [.V' [.V astounded ] [.N'' [.N' [.N us ] ] ] ] ] ] ] See U{http://www.ling.upenn.edu/advice/latex.html} for the LaTeX style file for the qtree package. @return: A latex qtree representation of this tree. @rtype: C{string} """ return r'\Tree ' + self.pprint(indent=6, nodesep='', parens=('[.', ' ]')) def _pprint_flat(self, nodesep, parens, quotes): childstrs = [] for child in self: if isinstance(child, Tree): childstrs.append(child._pprint_flat(nodesep, parens, quotes)) elif isinstance(child, tuple): childstrs.append("/".join(child)) elif isinstance(child, basestring) and not quotes: childstrs.append('%s' % child) else: childstrs.append('%r' % child) if isinstance(self.node, basestring): return '%s%s%s %s%s' % (parens[0], self.node, nodesep, string.join(childstrs), parens[1]) else: return '%s%r%s %s%s' % (parens[0], self.node, nodesep, string.join(childstrs), parens[1]) class ImmutableTree(Tree): def __init__(self, node_or_str, children=None): if children is None: return # see note in Tree.__init__() super(ImmutableTree, self).__init__(node_or_str, children) # Precompute our hash value. This ensures that we're really # immutable. It also means we only have to calculate it once. try: self._hash = hash( (self.node, tuple(self)) ) except (TypeError, ValueError): raise ValueError("ImmutableTree's node value and children " "must be immutable") def __setitem__(self): raise ValueError, 'ImmutableTrees may not be modified' def __setslice__(self): raise ValueError, 'ImmutableTrees may not be modified' def __delitem__(self): raise ValueError, 'ImmutableTrees may not be modified' def __delslice__(self): raise ValueError, 'ImmutableTrees may not be modified' def __iadd__(self): raise ValueError, 'ImmutableTrees may not be modified' def __imul__(self): raise ValueError, 'ImmutableTrees may not be modified' def append(self, v): raise ValueError, 'ImmutableTrees may not be modified' def extend(self, v): raise ValueError, 'ImmutableTrees may not be modified' def pop(self, v=None): raise ValueError, 'ImmutableTrees may not be modified' def remove(self, v): raise ValueError, 'ImmutableTrees may not be modified' def reverse(self): raise ValueError, 'ImmutableTrees may not be modified' def sort(self): raise ValueError, 'ImmutableTrees may not be modified' def __hash__(self): return self._hash def _set_node(self, node): """Set self._node. This will only succeed the first time the node value is set, which should occur in Tree.__init__().""" if hasattr(self, 'node'): raise ValueError, 'ImmutableTrees may not be modified' self._node = node def _get_node(self): return self._node node = property(_get_node, _set_node) ###################################################################### ## Parented trees ###################################################################### class AbstractParentedTree(Tree): """ An abstract base class for L{Tree}s that automatically maintain pointers to their parents. These parent pointers are updated whenever any change is made to a tree's structure. Two subclasses are currently defined: - L{ParentedTree} is used for tree structures where each subtree has at most one parent. This class should be used in cases where there is no"sharing" of subtrees. - L{MultiParentedTree} is used for tree structures where a subtree may have zero or more parents. This class should be used in cases where subtrees may be shared. Subclassing =========== The C{AbstractParentedTree} class redefines all operations that modify a tree's structure to call two methods, which are used by subclasses to update parent information: - L{_setparent()} is called whenever a new child is added. - L{_delparent()} is called whenever a child is removed. """ def __init__(self, node_or_str, children=None): if children is None: return # see note in Tree.__init__() super(AbstractParentedTree, self).__init__(node_or_str, children) # iterate over self, and *not* children, because children # might be an iterator. for i, child in enumerate(self): if isinstance(child, Tree): self._setparent(child, i, dry_run=True) for i, child in enumerate(self): if isinstance(child, Tree): self._setparent(child, i) #//////////////////////////////////////////////////////////// # Parent management #//////////////////////////////////////////////////////////// def _setparent(self, child, index, dry_run=False): """ Update C{child}'s parent pointer to point to self. This method is only called if C{child}'s type is L{Tree}; i.e., it is not called when adding a leaf to a tree. This method is always called before the child is actually added to C{self}'s child list. @type child: L{Tree} @type index: C{int} @param index: The index of C{child} in C{self}. @raise TypeError: If C{child} is a tree with an impropriate type. Typically, if C{child} is a tree, then its type needs to match C{self}'s type. This prevents mixing of different tree types (single-parented, multi-parented, and non-parented). @param dry_run: If true, the don't actually set the child's parent pointer; just check for any error conditions, and raise an exception if one is found. """ raise AssertionError('Abstract base class') def _delparent(self, child, index): """ Update C{child}'s parent pointer to not point to self. This method is only called if C{child}'s type is L{Tree}; i.e., it is not called when removing a leaf from a tree. This method is always called before the child is actually removed from C{self}'s child list. @type child: L{Tree} @type index: C{int} @param index: The index of C{child} in C{self}. """ raise AssertionError('Abstract base class') #//////////////////////////////////////////////////////////// # Methods that add/remove children #//////////////////////////////////////////////////////////// # Every method that adds or removes a child must make # appropriate calls to _setparent() and _delparent(). def __delitem__(self, index): # del ptree[start:stop] if isinstance(index, slice): start, stop = slice_bounds(self, index) # Clear all the children pointers. for i in xrange(start, stop): if isinstance(self[i], Tree): self._delparent(self[i], i) # Delete the children from our child list. super(AbstractParentedTree, self).__delitem__(index) # del ptree[i] elif isinstance(index, int): if index < 0: index += len(self) if index < 0: raise IndexError('index out of range') # Clear the child's parent pointer. if isinstance(self[index], Tree): self._delparent(self[index], index) # Remove the child from our child list. super(AbstractParentedTree, self).__delitem__(index) # del ptree[()] elif len(index) == 0: raise IndexError('The tree position () may not be deleted.') # del ptree[(i,)] elif len(index) == 1: del self[index[0]] # del ptree[i1, i2, i3] else: del self[index[0]][index[1:]] def __setitem__(self, index, value): # ptree[start:stop] = value if isinstance(index, slice): start, stop = slice_bounds(self, index) # make a copy of value, in case it's an iterator if not isinstance(value, (list, tuple)): value = list(value) # Check for any error conditions, so we can avoid ending # up in an inconsistent state if an error does occur. for i, child in enumerate(value): if isinstance(child, Tree): self._setparent(child, start+i, dry_run=True) # clear the child pointers of all parents we're removing for i in xrange(start, stop): if isinstance(self[i], Tree): self._delparent(self[i], i) # set the child pointers of the new children. We do this # after clearing *all* child pointers, in case we're e.g. # reversing the elements in a tree. for i, child in enumerate(value): if isinstance(child, Tree): self._setparent(child, start+i) # finally, update the content of the child list itself. super(AbstractParentedTree, self).__setitem__(index, value) # ptree[i] = value elif isinstance(index, int): if index < 0: index += len(self) if index < 0: raise IndexError('index out of range') # if the value is not changing, do nothing. if value is self[index]: return # Set the new child's parent pointer. if isinstance(value, Tree): self._setparent(value, index) # Remove the old child's parent pointer if isinstance(self[index], Tree): self._delparent(self[index], index) # Update our child list. super(AbstractParentedTree, self).__setitem__(index, value) # ptree[()] = value elif len(index) == 0: raise IndexError('The tree position () may not be assigned to.') # ptree[(i,)] = value elif len(index) == 1: self[index[0]] = value # ptree[i1, i2, i3] = value else: self[index[0]][index[1:]] = value def append(self, child): if isinstance(child, Tree): self._setparent(child, len(self)) super(AbstractParentedTree, self).append(child) def extend(self, children): for child in children: if isinstance(child, Tree): self._setparent(child, len(self)) super(AbstractParentedTree, self).append(child) def insert(self, index, child): # Handle negative indexes. Note that if index < -len(self), # we do *not* raise an IndexError, unlike __getitem__. This # is done for consistency with list.__getitem__ and list.index. if index < 0: index += len(self) if index < 0: index = 0 # Set the child's parent, and update our child list. if isinstance(child, Tree): self._setparent(child, index) super(AbstractParentedTree, self).insert(index, child) def pop(self, index=-1): if index < 0: index += len(self) if index < 0: raise IndexError('index out of range') if isinstance(self[index], Tree): self._delparent(self[index], index) return super(AbstractParentedTree, self).pop(index) # n.b.: like `list`, this is done by equality, not identity! # To remove a specific child, use del ptree[i]. def remove(self, child): index = self.index(child) if isinstance(self[index], Tree): self._delparent(self[index], index) super(AbstractParentedTree, self).remove(child) # We need to implement __getslice__ and friends, even though # they're deprecated, because otherwise list.__getslice__ will get # called (since we're subclassing from list). Just delegate to # __getitem__ etc., but use max(0, start) and max(0, stop) because # because negative indices are already handled *before* # __getslice__ is called; and we don't want to double-count them. if hasattr(list, '__getslice__'): def __getslice__(self, start, stop): return self.__getitem__(slice(max(0, start), max(0, stop))) def __delslice__(self, start, stop): return self.__delitem__(slice(max(0, start), max(0, stop))) def __setslice__(self, start, stop, value): return self.__setitem__(slice(max(0, start), max(0, stop)), value) class ParentedTree(AbstractParentedTree): """ A L{Tree} that automatically maintains parent pointers for single-parented trees. The following read-only property values are automatically updated whenever the structure of a parented tree is modified: L{parent}, L{parent_index}, L{left_sibling}, L{right_sibling}, L{root}, L{treeposition}. Each C{ParentedTree} may have at most one parent. In particular, subtrees may not be shared. Any attempt to reuse a single C{ParentedTree} as a child of more than one parent (or as multiple children of the same parent) will cause a C{ValueError} exception to be raised. C{ParentedTrees} should never be used in the same tree as C{Trees} or C{MultiParentedTrees}. Mixing tree implementations may result in incorrect parent pointers and in C{TypeError} exceptions. """ def __init__(self, node_or_str, children=None): if children is None: return # see note in Tree.__init__() self._parent = None """The parent of this Tree, or C{None} if it has no parent.""" super(ParentedTree, self).__init__(node_or_str, children) def _frozen_class(self): return ImmutableParentedTree #///////////////////////////////////////////////////////////////// # Properties #///////////////////////////////////////////////////////////////// def _get_parent_index(self): if self._parent is None: return None for i, child in enumerate(self._parent): if child is self: return i assert False, 'expected to find self in self._parent!' def _get_left_sibling(self): parent_index = self._get_parent_index() if self._parent and parent_index > 0: return self._parent[parent_index-1] return None # no left sibling def _get_right_sibling(self): parent_index = self._get_parent_index() if self._parent and parent_index < (len(self._parent)-1): return self._parent[parent_index+1] return None # no right sibling def _get_treeposition(self): if self._parent is None: return () else: return (self._parent._get_treeposition() + (self._get_parent_index(),)) def _get_root(self): if self._parent is None: return self else: return self._parent._get_root() parent = property(lambda self: self._parent, doc=""" The parent of this tree, or C{None} if it has no parent.""") parent_index = property(_get_parent_index, doc=""" The index of this tree in its parent. I.e., C{ptree.parent[ptree.parent_index] is ptree}. Note that C{ptree.parent_index} is not necessarily equal to C{ptree.parent.index(ptree)}, since the C{index()} method returns the first child that is I{equal} to its argument.""") left_sibling = property(_get_left_sibling, doc=""" The left sibling of this tree, or C{None} if it has none.""") right_sibling = property(_get_right_sibling, doc=""" The right sibling of this tree, or C{None} if it has none.""") root = property(_get_root, doc=""" The root of this tree. I.e., the unique ancestor of this tree whose parent is C{None}. If C{ptree.parent} is C{None}, then C{ptree} is its own root.""") treeposition = property(_get_treeposition, doc=""" The tree position of this tree, relative to the root of the tree. I.e., C{ptree.root[ptree.treeposition] is ptree}.""") treepos = treeposition # [xx] alias -- which name should we use? #///////////////////////////////////////////////////////////////// # Parent Management #///////////////////////////////////////////////////////////////// def _delparent(self, child, index): # Sanity checks assert isinstance(child, ParentedTree) assert self[index] is child assert child._parent is self # Delete child's parent pointer. child._parent = None def _setparent(self, child, index, dry_run=False): # If the child's type is incorrect, then complain. if not isinstance(child, ParentedTree): raise TypeError('Can not insert a non-ParentedTree '+ 'into a ParentedTree') # If child already has a parent, then complain. if child._parent is not None: raise ValueError('Can not insert a subtree that already ' 'has a parent.') # Set child's parent pointer & index. if not dry_run: child._parent = self class MultiParentedTree(AbstractParentedTree): """ A L{Tree} that automatically maintains parent pointers for multi-parented trees. The following read-only property values are automatically updated whenever the structure of a multi-parented tree is modified: L{parents}, L{parent_indices}, L{left_siblings}, L{right_siblings}, L{roots}, L{treepositions}. Each C{MultiParentedTree} may have zero or more parents. In particular, subtrees may be shared. If a single C{MultiParentedTree} is used as multiple children of the same parent, then that parent will appear multiple times in its C{parents} property. C{MultiParentedTrees} should never be used in the same tree as C{Trees} or C{ParentedTrees}. Mixing tree implementations may result in incorrect parent pointers and in C{TypeError} exceptions. """ def __init__(self, node_or_str, children=None): if children is None: return # see note in Tree.__init__() self._parents = [] """A list of this tree's parents. This list should not contain duplicates, even if a parent contains this tree multiple times.""" super(MultiParentedTree, self).__init__(node_or_str, children) def _frozen_class(self): return ImmutableMultiParentedTree #///////////////////////////////////////////////////////////////// # Properties #///////////////////////////////////////////////////////////////// def _get_parent_indices(self): return [(parent, index) for parent in self._parents for index, child in enumerate(parent) if child is self] def _get_left_siblings(self): return [parent[index-1] for (parent, index) in self._get_parent_indices() if index > 0] def _get_right_siblings(self): return [parent[index+1] for (parent, index) in self._get_parent_indices() if index < (len(parent)-1)] def _get_roots(self): return self._get_roots_helper({}).values() def _get_roots_helper(self, result): if self._parents: for parent in self._parents: parent._get_roots_helper(result) else: result[id(self)] = self return result parents = property(lambda self: list(self._parents), doc=""" The set of parents of this tree. If this tree has no parents, then C{parents} is the empty set. To check if a tree is used as multiple children of the same parent, use the L{parent_indices} property. @type: C{list} of L{MultiParentedTree}""") left_siblings = property(_get_left_siblings, doc=""" A list of all left siblings of this tree, in any of its parent trees. A tree may be its own left sibling if it is used as multiple contiguous children of the same parent. A tree may appear multiple times in this list if it is the left sibling of this tree with respect to multiple parents. @type: C{list} of L{MultiParentedTree}""") right_siblings = property(_get_right_siblings, doc=""" A list of all right siblings of this tree, in any of its parent trees. A tree may be its own right sibling if it is used as multiple contiguous children of the same parent. A tree may appear multiple times in this list if it is the right sibling of this tree with respect to multiple parents. @type: C{list} of L{MultiParentedTree}""") roots = property(_get_roots, doc=""" The set of all roots of this tree. This set is formed by tracing all possible parent paths until trees with no parents are found. @type: C{list} of L{MultiParentedTree}""") def parent_indices(self, parent): """ Return a list of the indices where this tree occurs as a child of C{parent}. If this child does not occur as a child of C{parent}, then the empty list is returned. The following is always true:: for parent_index in ptree.parent_indices(parent): parent[parent_index] is ptree """ if parent not in self._parents: return [] else: return [index for (index, child) in enumerate(parent) if child is self] def treepositions(self, root): """ Return a list of all tree positions that can be used to reach this multi-parented tree starting from C{root}. I.e., the following is always true:: for treepos in ptree.treepositions(root): root[treepos] is ptree """ if self is root: return [()] else: return [treepos+(index,) for parent in self._parents for treepos in parent.treepositions(root) for (index, child) in enumerate(parent) if child is self] #///////////////////////////////////////////////////////////////// # Parent Management #///////////////////////////////////////////////////////////////// def _delparent(self, child, index): # Sanity checks assert isinstance(child, MultiParentedTree) assert self[index] is child assert len([p for p in child._parents if p is self]) == 1 # If the only copy of child in self is at index, then delete # self from child's parent list. for i, c in enumerate(self): if c is child and i != index: break else: child._parents.remove(self) def _setparent(self, child, index, dry_run=False): # If the child's type is incorrect, then complain. if not isinstance(child, MultiParentedTree): raise TypeError('Can not insert a non-MultiParentedTree '+ 'into a MultiParentedTree') # Add self as a parent pointer if it's not already listed. if not dry_run: for parent in child._parents: if parent is self: break else: child._parents.append(self) class ImmutableParentedTree(ImmutableTree, ParentedTree): def __init__(self, node_or_str, children=None): if children is None: return # see note in Tree.__init__() super(ImmutableParentedTree, self).__init__(node_or_str, children) class ImmutableMultiParentedTree(ImmutableTree, MultiParentedTree): def __init__(self, node_or_str, children=None): if children is None: return # see note in Tree.__init__() super(ImmutableMultiParentedTree, self).__init__(node_or_str, children) ###################################################################### ## Probabilistic trees ###################################################################### class ProbabilisticTree(Tree, ProbabilisticMixIn): def __new__(cls, node_or_str, children=None, **prob_kwargs): return super(ProbabilisticTree, cls).__new__( cls, node_or_str, children) def __init__(self, node_or_str, children=None, **prob_kwargs): if children is None: return # see note in Tree.__init__() Tree.__init__(self, node_or_str, children) ProbabilisticMixIn.__init__(self, **prob_kwargs) # We have to patch up these methods to make them work right: def _frozen_class(self): return ImmutableProbabilisticTree def __repr__(self): return '%s (p=%s)' % (Tree.__repr__(self), self.prob()) def __str__(self): return '%s (p=%s)' % (self.pprint(margin=60), self.prob()) def __cmp__(self, other): c = Tree.__cmp__(self, other) if c != 0: return c return cmp(self.prob(), other.prob()) def __eq__(self, other): if not isinstance(other, Tree): return False return Tree.__eq__(self, other) and self.prob()==other.prob() def __ne__(self, other): return not (self == other) def copy(self, deep=False): if not deep: return self.__class__(self.node, self, prob=self.prob()) else: return self.__class__.convert(self) def convert(cls, val): if isinstance(val, Tree): children = [cls.convert(child) for child in val] if isinstance(val, ProbabilisticMixIn): return cls(val.node, children, prob=val.prob()) else: return cls(val.node, children, prob=1.0) else: return val convert = classmethod(convert) class ImmutableProbabilisticTree(ImmutableTree, ProbabilisticMixIn): def __new__(cls, node_or_str, children=None, **prob_kwargs): return super(ImmutableProbabilisticTree, cls).__new__( cls, node_or_str, children) def __init__(self, node_or_str, children=None, **prob_kwargs): if children is None: return # see note in Tree.__init__() ImmutableTree.__init__(self, node_or_str, children) ProbabilisticMixIn.__init__(self, **prob_kwargs) # We have to patch up these methods to make them work right: def _frozen_class(self): return ImmutableProbabilisticTree def __repr__(self): return '%s [%s]' % (Tree.__repr__(self), self.prob()) def __str__(self): return '%s [%s]' % (self.pprint(margin=60), self.prob()) def __cmp__(self, other): c = Tree.__cmp__(self, other) if c != 0: return c return cmp(self.prob(), other.prob()) def __eq__(self, other): if not isinstance(other, Tree): return False return Tree.__eq__(self, other) and self.prob()==other.prob() def __ne__(self, other): return not (self == other) def copy(self, deep=False): if not deep: return self.__class__(self.node, self, prob=self.prob()) else: return self.__class__.convert(self) def convert(cls, val): if isinstance(val, Tree): children = [cls.convert(child) for child in val] if isinstance(val, ProbabilisticMixIn): return cls(val.node, children, prob=val.prob()) else: return cls(val.node, children, prob=1) else: return val convert = classmethod(convert) def _child_names(tree): names = [] for child in tree: if isinstance(child, Tree): names.append(Nonterminal(child.node)) else: names.append(child) return names ###################################################################### ## Parsing ###################################################################### # We should consider deprecating this function: #@deprecated('Use Tree.parse(s, remove_top_empty_bracketing=True) instead.') def bracket_parse(s): """ Parse a treebank string and return a tree. Trees are represented as nested brackettings, e.g. (S (NP (NNP John)) (VP (V runs))). @return: A tree corresponding to the string representation. @rtype: C{tree} @param s: The string to be converted @type s: C{string} """ return Tree.parse(s, remove_empty_top_bracketing=True) def sinica_parse(s): """ Parse a Sinica Treebank string and return a tree. Trees are represented as nested brackettings, as shown in the following example (X represents a Chinese character): S(goal:NP(Head:Nep:XX)|theme:NP(Head:Nhaa:X)|quantity:Dab:X|Head:VL2:X)#0(PERIODCATEGORY) @return: A tree corresponding to the string representation. @rtype: C{tree} @param s: The string to be converted @type s: C{string} """ tokens = re.split(r'([()| ])', s) for i in range(len(tokens)): if tokens[i] == '(': tokens[i-1], tokens[i] = tokens[i], tokens[i-1] # pull nonterminal inside parens elif ':' in tokens[i]: fields = tokens[i].split(':') if len(fields) == 2: # non-terminal tokens[i] = fields[1] else: tokens[i] = "(" + fields[-2] + " " + fields[-1] + ")" elif tokens[i] == '|': tokens[i] = '' treebank_string = string.join(tokens) return bracket_parse(treebank_string) # s = re.sub(r'^#[^\s]*\s', '', s) # remove leading identifier # s = re.sub(r'\w+:', '', s) # remove role tags # return s ###################################################################### ## Demonstration ###################################################################### def demo(): """ A demonstration showing how C{Tree}s and C{Tree}s can be used. This demonstration creates a C{Tree}, and loads a C{Tree} from the L{treebank} corpus, and shows the results of calling several of their methods. """ from nltk import tree # Demonstrate tree parsing. s = '(S (NP (DT the) (NN cat)) (VP (VBD ate) (NP (DT a) (NN cookie))))' t = Tree(s) print "Convert bracketed string into tree:" print t print t.__repr__() print "Display tree properties:" print t.node # tree's constituent type print t[0] # tree's first child print t[1] # tree's second child print t.height() print t.leaves() print t[1] print t[1,1] print t[1,1,0] # Demonstrate tree modification. the_cat = t[0] the_cat.insert(1, tree.bracket_parse('(JJ big)')) print "Tree modification:" print t t[1,1,1] = tree.bracket_parse('(NN cake)') print t print # Tree transforms print "Collapse unary:" t.collapse_unary() print t print "Chomsky normal form:" t.chomsky_normal_form() print t print # Demonstrate probabilistic trees. pt = tree.ProbabilisticTree('x', ['y', 'z'], prob=0.5) print "Probabilistic Tree:" print pt print # Demonstrate parsing of treebank output format. t = tree.bracket_parse(t.pprint()) print "Convert tree to bracketed string and back again:" print t print # Demonstrate LaTeX output print "LaTeX output:" print t.pprint_latex_qtree() print # Demonstrate Productions print "Production output:" print t.productions() print # Demonstrate tree nodes containing objects other than strings t.node = ('test', 3) print t if __name__ == '__main__': demo() __all__ = ['ImmutableProbabilisticTree', 'ImmutableTree', 'ProbabilisticMixIn', 'ProbabilisticTree', 'Tree', 'bracket_parse', 'sinica_parse', 'ParentedTree', 'MultiParentedTree', 'ImmutableParentedTree', 'ImmutableMultiParentedTree'] nltk-2.0~b9/nltk/text.py0000644000175000017500000005476611327451603015110 0ustar bhavanibhavani# Natural Language Toolkit: Texts # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT from math import log import re from nltk.probability import FreqDist, LidstoneProbDist from nltk.probability import ConditionalFreqDist as CFD from nltk.compat import defaultdict from nltk.util import tokenwrap, LazyConcatenation from nltk.model import NgramModel from nltk.metrics import f_measure, BigramAssocMeasures from nltk.collocations import BigramCollocationFinder class ContextIndex(object): """ A bidirectional index between words and their 'contexts' in a text. The context of a word is usually defined to be the words that occur in a fixed window around the word; but other definitions may also be used by providing a custom context function. """ @staticmethod def _default_context(tokens, i): """One left token and one right token, normalized to lowercase""" if i == 0: left = '*START*' else: left = tokens[i-1].lower() if i == len(tokens) - 1: right = '*END*' else: right = tokens[i+1].lower() return (left, right) def __init__(self, tokens, context_func=None, filter=None, key=lambda x:x): self._key = key self._tokens = tokens if not context_func: self._context_func = self._default_context if filter: tokens = [t for t in tokens if filter(t)] self._word_to_contexts = CFD((self._key(w), self._context_func(tokens, i)) for i, w in enumerate(tokens)) self._context_to_words = CFD((self._context_func(tokens, i), self._key(w)) for i, w in enumerate(tokens)) def tokens(self): """ @rtype: C{list} of token @return: The document that this context index was created from. """ return self._tokens def word_similarity_dict(self, word): """ Return a dictionary mapping from words to 'similarity scores,' indicating how often these two words occur in the same context. """ word = self._key(word) word_contexts = set(self._word_to_contexts[word]) scores = {} for w, w_contexts in self._word_to_contexts.items(): scores[w] = f_measure(word_contexts, set(w_contexts)) return scores def similar_words(self, word, n=20): scores = defaultdict(int) for c in self._word_to_contexts[self._key(word)]: for w in self._context_to_words[c]: if w != word: print w, c, self._context_to_words[c][word], self._context_to_words[c][w] scores[w] += self._context_to_words[c][word] * self._context_to_words[c][w] return sorted(scores, key=scores.get)[:n] def common_contexts(self, words, fail_on_unknown=False): """ Find contexts where the specified words can all appear; and return a frequency distribution mapping each context to the number of times that context was used. @param words: The words used to seed the similarity search @type words: C{str} @param fail_on_unknown: If true, then raise a value error if any of the given words do not occur at all in the index. """ words = [self._key(w) for w in words] contexts = [set(self._word_to_contexts[w]) for w in words] empty = [words[i] for i in range(len(words)) if not contexts[i]] common = reduce(set.intersection, contexts) if empty and fail_on_unknown: raise ValueError("The following word(s) were not found:", " ".join(words)) elif not common: # nothing in common -- just return an empty freqdist. return FreqDist() else: fd = FreqDist(c for w in words for c in self._word_to_contexts[w] if c in common) return fd class ConcordanceIndex(object): """ An index that can be used to look up the offset locations at which a given word occurs in a document. """ def __init__(self, tokens, key=lambda x:x): """ Construct a new concordance index. @param tokens: The document (list of tokens) that this concordance index was created from. This list can be used to access the context of a given word occurance. @param key: A function that maps each token to a normalized version that will be used as a key in the index. E.g., if you use C{key=lambda s:s.lower()}, then the index will be case-insensitive. """ self._tokens = tokens """The document (list of tokens) that this concordance index was created from.""" self._key = key """Function mapping each token to an index key (or None).""" self._offsets = defaultdict(list) """Dictionary mapping words (or keys) to lists of offset indices.""" # Initialize the index (self._offsets) for index, word in enumerate(tokens): word = self._key(word) self._offsets[word].append(index) def tokens(self): """ @rtype: C{list} of token @return: The document that this concordance index was created from. """ return self._tokens def offsets(self, word): """ @rtype: C{list} of C{int} @return: A list of the offset positions at which the given word occurs. If a key function was specified for the index, then given word's key will be looked up. """ word = self._key(word) return self._offsets[word] def __repr__(self): return '' % ( len(self._tokens), len(self._offsets)) def print_concordance(self, word, width=75, lines=25): """ Print a concordance for C{word} with the specified context window. @param word: The target word @type word: C{str} @param width: The width of each line, in characters (default=80) @type width: C{int} @param lines: The number of lines to display (default=25) @type lines: C{int} """ half_width = (width - len(word) - 2) / 2 context = width/4 # approx number of words of context offsets = self.offsets(word) if offsets: lines = min(lines, len(offsets)) print "Displaying %s of %s matches:" % (lines, len(offsets)) for i in offsets: if lines <= 0: break left = (' ' * half_width + ' '.join(self._tokens[i-context:i])) right = ' '.join(self._tokens[i+1:i+context]) left = left[-half_width:] right = right[:half_width] print left, word, right lines -= 1 else: print "No matches" class TokenSearcher(object): """ A class that makes it easier to use regular expressions to search over tokenized strings. The tokenized string is converted to a string where tokens are marked with angle brackets -- e.g., C{''}. The regular expression passed to the L{findall()} method is modified to treat angle brackets as nongrouping parentheses, in addition to matching the token boundaries; and to have C{'.'} not match the angle brackets. """ def __init__(self, tokens): self._raw = ''.join('<'+w+'>' for w in tokens) def findall(self, regexp): """ Find instances of the regular expression in the text. The text is a list of tokens, and a regexp pattern to match a single token must be surrounded by angle brackets. E.g. >>> ts.findall("<.*><.*>") ['you rule bro', ['telling you bro; u twizted bro >>> ts.findall("(<.*>)") monied; nervous; dangerous; white; white; white; pious; queer; good; mature; white; Cape; great; wise; wise; butterless; white; fiendish; pale; furious; better; certain; complete; dismasted; younger; brave; brave; brave; brave >>> text9.findall("{3,}") thread through those; the thought that; that the thing; the thing that; that that thing; through these than through; them that the; through the thick; them that they; thought that the @param regexp: A regular expression @type regexp: C{str} """ # preprocess the regular expression regexp = re.sub(r'\s', '', regexp) regexp = re.sub(r'<', '(?:<(?:', regexp) regexp = re.sub(r'>', ')>)', regexp) regexp = re.sub(r'(?]', regexp) # perform the search hits = re.findall(regexp, self._raw) # Sanity check for h in hits: if not h.startswith('<') and h.endswith('>'): raise ValueError('Bad regexp for TokenSearcher.findall') # postprocess the output hits = [h[1:-1].split('><') for h in hits] return hits class Text(object): """ A wrapper around a sequence of simple (string) tokens, which is intended to support initial exploration of texts (via the interactive console). Its methods perform a variety of analyses on the text's contexts (e.g., counting, concordancing, collocation discovery), and display the results. If you wish to write a program which makes use of these analyses, then you should bypass the C{Text} class, and use the appropriate analysis function or class directly instead. C{Text}s are typically initialized from a given document or corpus. E.g.: >>> moby = Text(nltk.corpus.gutenberg.words('melville-moby_dick.txt')) """ # This defeats lazy loading, but makes things faster. This # *shouldnt* be necessary because the corpus view *should* be # doing intelligent caching, but without this it's running slow. # Look into whether the caching is working correctly. _COPY_TOKENS = True def __init__(self, tokens, name=None): """ Create a Text object. @param tokens: The source text. @type tokens: C{sequence} of C{str} """ if self._COPY_TOKENS: tokens = list(tokens) self.tokens = tokens if name: self.name = name elif ']' in tokens[:20]: end = tokens[:20].index(']') self.name = " ".join(map(str, tokens[1:end])) else: self.name = " ".join(map(str, tokens[:8])) + "..." #//////////////////////////////////////////////////////////// # Support item & slice access #//////////////////////////////////////////////////////////// def __getitem__(self, i): if isinstance(i, slice): return self.tokens[i.start:i.stop] else: return self.tokens[i] def __len__(self): return len(self.tokens) #//////////////////////////////////////////////////////////// # Interactive console methods #//////////////////////////////////////////////////////////// def concordance(self, word, width=79, lines=25): """ Print a concordance for C{word} with the specified context window. @seealso: L{ConcordanceIndex} """ if '_concordance_index' not in self.__dict__: print "Building index..." self._concordance_index = ConcordanceIndex(self.tokens, key=lambda s:s.lower()) self._concordance_index.print_concordance(word, width, lines) def collocations(self, num=20, window_size=2): """ Print collocations derived from the text, ignoring stopwords. @seealso: L{find_collocations} @param num: The maximum number of collocations to print. @type num: C{int} @param window_size: The number of tokens spanned by a collocation (default=2) @type window_size: C{int} """ if not ('_collocations' in self.__dict__ and self._num == num and self._window_size == window_size): self._num = num self._window_size = window_size print "Building collocations list" from nltk.corpus import stopwords ignored_words = stopwords.words('english') finder = BigramCollocationFinder.from_words(self.tokens, window_size) finder.apply_freq_filter(2) finder.apply_word_filter(lambda w: len(w) < 3 or w.lower() in ignored_words) bigram_measures = BigramAssocMeasures() self._collocations = finder.nbest(bigram_measures.likelihood_ratio, num) colloc_strings = [w1+' '+w2 for w1, w2 in self._collocations] print tokenwrap(colloc_strings, separator="; ") def count(self, word): """ Count the number of times this word appears in the text. """ return self.tokens.count(word) def index(self, word): """ Find the index of the first occurrence of the word in the text. """ return self.tokens.index(word) def readability(self, method): # code from nltk_contrib.readability raise NotImplementedError def generate(self, length=100): """ Print random text, generated using a trigram language model. @param length: The length of text to generate (default=100) @type length: C{int} @seealso: L{NgramModel} """ if '_trigram_model' not in self.__dict__: print "Building ngram index..." estimator = lambda fdist, bins: LidstoneProbDist(fdist, 0.2) self._trigram_model = NgramModel(3, self, estimator) text = self._trigram_model.generate(length) print tokenwrap(text) def search(self, pattern): """ Search for instances of the regular expression pattern in the text. @seealso: L{TokenSearcher} """ if '_token_searcher' not in self.__dict__: print "Loading data..." self._token_searcher = TokenSearcher(self.tokens) self._token_searcher.findall(pattern) def similar(self, word, num=20): """ Distributional similarity: find other words which appear in the same contexts as the specified word; list most similar words first. @param word: The word used to seed the similarity search @type word: C{str} @param num: The number of words to generate (default=20) @type num: C{int} @seealso: L{ContextIndex.similar_words()} """ if '_word_context_index' not in self.__dict__: print 'Building word-context index...' self._word_context_index = ContextIndex(self.tokens, filter=lambda x:x.isalpha(), key=lambda s:s.lower()) # words = self._word_context_index.similar_words(word, num) word = word.lower() wci = self._word_context_index._word_to_contexts if word in wci.conditions(): contexts = set(wci[word]) fd = FreqDist(w for w in wci.conditions() for c in wci[w] if c in contexts and not w == word) words = fd.keys()[:num] print tokenwrap(words) else: print "No matches" def common_contexts(self, words, num=20): """ Find contexts where the specified words appear; list most frequent common contexts first. @param word: The word used to seed the similarity search @type word: C{str} @param num: The number of words to generate (default=20) @type num: C{int} @seealso: L{ContextIndex.common_contexts()} """ if '_word_context_index' not in self.__dict__: print 'Building word-context index...' self._word_context_index = ContextIndex(self.tokens, key=lambda s:s.lower()) try: fd = self._word_context_index.common_contexts(words, True) if not fd: print "No common contexts were found" else: ranked_contexts = fd.keys()[:num] print tokenwrap(w1+"_"+w2 for w1,w2 in ranked_contexts) except ValueError, e: print e def dispersion_plot(self, words): """ Produce a plot showing the distribution of the words through the text. Requires pylab to be installed. @param words: The words to be plotted @type word: C{str} @seealso: L{nltk.draw.dispersion_plot()} """ from nltk.draw import dispersion_plot dispersion_plot(self, words) def plot(self, *args): """ See documentation for FreqDist.plot() @seealso: L{nltk.prob.FreqDist.plot()} """ self.vocab().plot(*args) def vocab(self): """ @seealso: L{nltk.prob.FreqDist} """ if "_vocab" not in self.__dict__: print "Building vocabulary index..." self._vocab = FreqDist(self) return self._vocab def findall(self, regexp): """ Find instances of the regular expression in the text. The text is a list of tokens, and a regexp pattern to match a single token must be surrounded by angle brackets. E.g. >>> text5.findall("<.*><.*>") you rule bro; telling you bro; u twizted bro >>> text1.findall("(<.*>)") monied; nervous; dangerous; white; white; white; pious; queer; good; mature; white; Cape; great; wise; wise; butterless; white; fiendish; pale; furious; better; certain; complete; dismasted; younger; brave; brave; brave; brave >>> text9.findall("{3,}") thread through those; the thought that; that the thing; the thing that; that that thing; through these than through; them that the; through the thick; them that they; thought that the @param regexp: A regular expression @type regexp: C{str} """ if "_token_searcher" not in self.__dict__: self._token_searcher = TokenSearcher(self) hits = self._token_searcher.findall(regexp) hits = [' '.join(h) for h in hits] print tokenwrap(hits, "; ") #//////////////////////////////////////////////////////////// # Helper Methods #//////////////////////////////////////////////////////////// _CONTEXT_RE = re.compile('\w+|[\.\!\?]') def _context(self, tokens, i): """ One left & one right token, both case-normalied. Skip over non-sentence-final punctuation. Used by the L{ContextIndex} that is created for L{similar()} and L{common_contexts()}. """ # Left context j = i-1 while j>=0 and not self._CONTEXT_RE.match(tokens[j]): j = j-1 if j == 0: left = '*START*' else: left = tokens[j] # Right context j = i+1 while j' % self.name # Prototype only; this approach will be slow to load class TextCollection(Text): """A collection of texts, which can be loaded with list of texts, or with a corpus consisting of one or more texts, and which supports counting, concordancing, collocation discovery, etc. Initialize a TextCollection as follows: >>> gutenberg = TextCollection(nltk.corpus.gutenberg) >>> mytexts = TextCollection([text1, text2, text3]) Iterating over a TextCollection produces all the tokens of all the texts in order. """ def __init__(self, source, name=None): if hasattr(source, 'words'): # bridge to the text corpus reader source = [source.words(f) for f in source.files()] self._texts = source Text.__init__(self, LazyConcatenation(source)) self._idf_cache = {} def tf(self, term, text, method=None): """ The frequency of the term in text. """ return float(text.count(term)) / len(text) def idf(self, term, method=None): """ The number of texts in the corpus divided by the number of texts that the term appears in. If a term does not appear in the corpus, 0.0 is returned. """ # idf values are cached for performance. idf = self._idf_cache.get(term) if idf is None: matches = len(list(True for text in self._texts if term in text)) if not matches: # FIXME Should this raise some kind of error instead? idf = 0.0 else: idf = log(float(len(self._texts)) / matches) self._idf_cache[term] = idf return idf def tf_idf(self, term, text): return self.tf(term, text) * self.idf(term) def demo(): from nltk.corpus import brown text = Text(brown.words(categories='news')) print text print print "Concordance:" text.concordance('news') print print "Distributionally similar words:" text.similar('news') print print "Collocations:" text.collocations() print print "Automatically generated text:" text.generate() print print "Dispersion plot:" text.dispersion_plot(['news', 'report', 'said', 'announced']) print print "Vocabulary plot:" text.plot(50) print print "Indexing:" print "text[3]:", text[3] print "text[3:5]:", text[3:5] print "text.vocab()['news']:", text.vocab()['news'] if __name__ == '__main__': demo() __all__ = ["ContextIndex", "ConcordanceIndex", "TokenSearcher", "Text", "TextCollection"] nltk-2.0~b9/nltk/sourcedstring.py0000644000175000017500000015245411374105240017003 0ustar bhavanibhavani# Natural Language Toolkit: Sourced Strings # # Copyright (C) 2001-2009 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT """ X{Sourced strings} are strings that are annotated with information about the location in a document where they were originally found. Sourced strings are subclassed from Python strings. As a result, they can usually be used anywhere a normal Python string can be used. >>> newt_contents = '''\ ... She turned me into a newt! ... I got better.''' >>> newt_doc = SourcedString(newt_contents, 'newt.txt') >>> print repr(newt_doc) 'She turned me into a newt!\nI got better.'@[0:40] >>> newt = newt_doc.split()[5] # Find the sixth word. >>> print repr(newt) 'newt!'@[21:26] """ import re, sys from nltk.internals import slice_bounds, abstract __all__ = [ 'StringSource', 'ConsecutiveCharStringSource', 'ContiguousCharStringSource', 'SourcedString', 'SourcedStringStream', 'SourcedStringRegexp', 'SimpleSourcedString', 'CompoundSourcedString', 'SimpleSourcedByteString', 'SimpleSourcedUnicodeString', 'CompoundSourcedByteString', 'CompoundSourcedUnicodeString', ] #////////////////////////////////////////////////////////////////////// # String Sources #////////////////////////////////////////////////////////////////////// class StringSource(object): """ A description of the location of a string in a document. Each C{StringSource} consists of a document identifier, along with information about the begin and end offsets of each character in the string. These offsets are typically either byte offsets or character offsets. (Note that for unicode strings, byte offsets and character offsets are not the same thing.) C{StringSource} is an abstract base class. Two concrete subclasses are used depending on the properties of the string whose source is being described: - L{ConsecutiveCharStringSource} describes the source of strings whose characters have consecutive offsets (in particular, byte strings w/ byte offsets; and unicode strings with character offsets). - L{ContiguousCharStringSource} describes the source of strings whose characters are contiguous, but do not necessarily have consecutive offsets (in particular, unicode strings with byte offsets). @ivar docid: An identifier (such as a filename) that specifies which document contains the string. @ivar offsets: A list of offsets specifying the location of each character in the document. The C{i}th character of the string begins at offset C{offsets[i]} and ends at offset C{offsets[i+1]}. The length of the C{offsets} list is one greater than the list of the string described by this C{StringSource}. @ivar begin: The document offset where the string begins. (I.e., the offset of the first character in the string.) C{source.begin} is always equal to C{source.offsets[0]}. @ivar end: The document offset where the string ends. (For character offsets, one plus the offset of the last character; for byte offsets, one plus the offset of the last byte that encodes the last character). C{source.end} is always equal to C{source.offsets[-1]}. """ def __new__(cls, docid, *args, **kwargs): # If the StringSource constructor is called directly, then # choose one of its subclasses to delegate to. if cls is StringSource: if args: raise TypeError("Specifcy either begin and end, or " "offsets, using keyword arguments") if 'begin' in kwargs and 'end' in kwargs and 'offsets' not in kwargs: cls = ConsecutiveCharStringSource elif ('begin' not in kwargs and 'end' not in kwargs and 'offsets' in kwargs): cls = ContiguousCharStringSource else: raise TypeError("Specify either begin and end, or offsets " "(but not both)") # Construct the object. return object.__new__(cls) def __init__(self, docid, **kwargs): """ Create a new C{StringSource}. When the C{StringSource} constructor is called directly, it automatically delegates to one of its two subclasses: - If C{begin} and C{end} are specified, then a L{ConsecutiveCharStringSource} is returned. - If C{offsets} is specified, then a L{ContiguousCharStringSource} is returned. In both cases, the arguments must be specified as keyword arguments (not positional arguments). """ def __getitem__(self, index): """ Return a L{StringSource} describing the location where the specified character was found. In particular, if C{s} is the string that this source describes, then return a L{StringSource} describing the location of C{s[index]}. @raise IndexError: If index is out of range. """ if isinstance(index, slice): start, stop = slice_bounds(self, index) return self.__getslice__(start, stop) else: if index < 0: index += len(self) if index < 0 or index >= len(self): raise IndexError('StringSource index out of range') return self.__getslice__(index, index+1) @abstract def __getslice__(self, start, stop): """ Return a L{StringSource} describing the location where the specified substring was found. In particular, if C{s} is the string that this source describes, then return a L{StringSource} describing the location of C{s[start:stop]}. """ @abstract def __len__(self): """ Return the length of the string described by this C{StringSource}. Note that this may not be equal to C{self.end-self.begin} for unicode strings described using byte offsets. """ def __str__(self): if self.end == self.begin+1: return '@%s[%s]' % (self.docid, self.begin,) else: return '@%s[%s:%s]' % (self.docid, self.begin, self.end) def __cmp__(self, other): return (cmp(self.docid, self.docid) or cmp([(charloc.begin, charloc.end) for charloc in self], [(charloc.begin, charloc.end) for charloc in other])) def __hash__(self): # Cache hash values. if not hasattr(self, '_hash'): self._hash = hash( (self.docid, tuple((charloc.begin, charloc.end) for charloc in self)) ) return self._hash class ConsecutiveCharStringSource(StringSource): """ A L{StringSource} that specifies the source of strings whose characters have consecutive offsets. In particular, the following two properties must hold for all valid indices: - source[i].end == source[i].begin + 1 - source[i].end == source[i+1].begin These properties allow the source to be stored using just a start offset and an end offset (along with a docid). This C{StringSource} can be used to describe byte strings that are indexed using byte offsets or character offsets; or unicode strings that are indexed using character offsets. """ def __init__(self, docid, begin, end): if not isinstance(begin, (int, long)): raise TypeError("begin attribute expected an integer") if not isinstance(end, (int, long)): raise TypeError("end attribute expected an integer") if not end >= begin: raise ValueError("begin must be less than or equal to end") self.docid = docid self.begin = begin self.end = end @property def offsets(self): return tuple(range(self.begin, self.end+1)) def __len__(self): return self.end-self.begin def __getslice__(self, start, stop): start = max(0, min(len(self), start)) stop = max(start, min(len(self), stop)) return ConsecutiveCharStringSource( self.docid, self.begin+start, self.begin+stop) def __cmp__(self, other): if isinstance(other, ConsecutiveCharStringSource): return (cmp(self.docid, other.docid) or cmp(self.begin, other.begin) or cmp(self.end, other.end)) else: return StringSource.__cmp__(self, other) def __repr__(self): return 'StringSource(%r, begin=%r, end=%r)' % ( self.docid, self.begin, self.end) class ContiguousCharStringSource(StringSource): """ A L{StringSource} that specifies the source of strings whose character are contiguous, but do not necessarily have consecutive offsets. In particular, each character's end offset must be equal to the next character's start offset: - source[i].end == source[i+1].begin This property allow the source to be stored using a list of C{len(source)+1} offsets (along with a docid). This C{StringSource} can be used to describe unicode strings that are indexed using byte offsets. """ CONSTRUCTOR_CHECKS_OFFSETS = False def __init__(self, docid, offsets): offsets = tuple(offsets) if len(offsets) == 0: raise ValueError("at least one offset must be specified") if self.CONSTRUCTOR_CHECKS_OFFSETS: for i in range(len(offsets)): if not isinstance(offsets[i], (int,long)): raise TypeError("offsets must be integers") if i>0 and offsets[i-1]>offsets[i]: raise TypeError("offsets must be monotonic increasing") self.docid = docid self.offsets = offsets @property def begin(self): return self.offsets[0] @property def end(self): return self.offsets[-1] def __len__(self): return len(self.offsets)-1 def __getslice__(self, start, stop): start = max(0, min(len(self), start)) stop = max(start, min(len(self), stop)) return ContiguousCharStringSource( self.docid, self.offsets[start:stop+1]) def __cmp__(self, other): if isinstance(other, ConsecutiveCharStringSource): return (cmp(self.docid, other.docid) or cmp(self.offsets, other._offsets)) else: return StringSource.__cmp__(self, other) def __repr__(self): return 'StringSource(%r, offsets=%r)' % (self.docid, self.offsets) #////////////////////////////////////////////////////////////////////// # Base Class for Sourced Strings. #////////////////////////////////////////////////////////////////////// class SourcedString(basestring): """ A string that is annotated with information about the location in a document where it was originally found. Sourced strings are subclassed from Python strings. As a result, they can usually be used anywhere a normal Python string can be used. There are two types of sourced strings: L{SimpleSourcedString}s, which correspond to a single substring of a document; and L{CompoundSourcedString}s, which are constructed by concatenating strings from multiple sources. Each of these types has two concrete subclasses: one for unicode strings (subclassed from ``unicode``), and one for byte strings (subclassed from ``str``). Two sourced strings are considered equal if their contents are equal, even if their sources differ. This fact is important in ensuring that sourced strings act like normal strings. In particular, it allows sourced strings to be used with code that was originally intended to process plain Python strings. If you wish to determine whether two sourced strings came from the same location in the same document, simply compare their L{sources} attributes. If you know that both sourced strings are L{SimpleSourcedStrings}, then you can compare their L{source} attribute instead. String operations that act on sourced strings will preserve location information whenever possible. However, there are a few types of string manipulation that can cause source information to be discarded. The most common examples of operations that will lose source information are: - ``str.join()``, where the joining string is not sourced. - ``str.replace()``, where the original string is not sourced. - String formatting (the ``%`` operator). - Regular expression substitution. @ivar sources: A sorted tuple of C{(index, source)} pairs. Each such pair specifies that the source of C{self[index:index+len(source)]} is C{source}. Any characters for which no source is specified are sourceless (e.g., plain Python characters that were concatenated to a sourced string). When working with simple sourced strings, it's usually easier to use the L{source} attribute instead; however, the C{sources} attribute is defined for both simple and compound sourced strings. """ def __new__(cls, contents, source): # If the SourcedString constructor is called directly, then # choose one of its subclasses to delegate to. if cls is SourcedString: if isinstance(contents, str): cls = SimpleSourcedByteString elif isinstance(contents, unicode): cls = SimpleSourcedUnicodeString else: raise TypeError("Expected 'contents' to be a unicode " "string or a byte string") # Create the new object using the appropriate string class's # __new__, which takes just the contents argument. return cls._stringtype.__new__(cls, contents) _stringtype = None """A class variable, defined by subclasses of L{SourcedString}, determining what type of string this class contains. Its value must be either C{str} or C{unicode}.""" #////////////////////////////////////////////////////////////////////// #{ Splitting & Stripping Methods #////////////////////////////////////////////////////////////////////// def lstrip(self, chars=None): s = self._stringtype.lstrip(self, chars) return self[len(self)-len(s):] def rstrip(self, chars=None): s = self._stringtype.rstrip(self, chars) return self[:len(s)] def strip(self, chars=None): return self.lstrip(chars).rstrip(chars) _WHITESPACE_RE = re.compile(r'\s+') def split(self, sep=None, maxsplit=None): # Check for unicode/bytestring mismatches: if self._mixed_string_types(sep, maxsplit): return self._decode_and_call('split', sep, maxsplit) # Use a regexp to split self. if sep is None: sep_re = self._WHITESPACE_RE else: sep_re = re.compile(re.escape(sep)) if maxsplit is None: return sep_re.split(self) else: return sep_re.split(self, maxsplit) def rsplit(self, sep=None, maxsplit=None): # Check for unicode/bytestring mismatches: if self._mixed_string_types(sep, maxsplit): return self._decode_and_call('rsplit', sep, maxsplit) # Split on whitespace use a regexp. if sep is None: seps = list(self._WHITESPACE_RE.finditer(self)) if maxsplit: seps = seps[-maxsplit:] if not seps: return [self] result = [self[:seps[0].start()]] for i in range(1, len(seps)): result.append(self[seps[i-1].end():seps[i].start()]) result.append(self[seps[-1].end():]) return result # Split on a given string: use rfind. else: result = [] piece_end = len(self) while maxsplit != 0: sep_pos = self.rfind(sep, 0, piece_end) if sep_pos < 0: break result.append(self[sep_pos+len(sep):piece_end]) piece_end = sep_pos if maxsplit is not None: maxsplit -= 1 if piece_end > 0: result.append(self[:piece_end]) return result[::-1] def partition(self, sep): head, sep, tail = self._stringtype.partition(self, sep) i, j = len(head), len(head)+len(sep) return (self[:i], self[i:j], self[j:]) def rpartition(self, sep): head, sep, tail = self._stringtype.rpartition(self, sep) i, j = len(head), len(head)+len(sep) return (self[:i], self[i:j], self[j:]) _NEWLINE_RE = re.compile(r'\n') _LINE_RE = re.compile(r'.*\n?') def splitlines(self, keepends=False): if keepends: return self._LINE_RE.findall(self) else: return self._NEWLINE_RE.split(self) #////////////////////////////////////////////////////////////////////// #{ String Concatenation Methods #////////////////////////////////////////////////////////////////////// @staticmethod def concat(substrings): """ Return a sourced string formed by concatenating the given list of substrings. Adjacent substrings will be merged when possible. Depending on the types and values of the supplied substrings, the concatenated string's value may be a Python string (C{str} or C{unicode}), a L{SimpleSourcedString}, or a L{CompoundSourcedString}. """ # Flatten nested compound sourced strings, and merge adjacent # strings where possible: merged = [] for substring in substrings: SourcedString.__add_substring_to_list(substring, merged) # Return the concatenated string. if len(merged) == 0: return '' elif len(merged) == 1: return merged[0] else: return CompoundSourcedString(merged) def __add__(self, other): return SourcedString.concat([self, other]) def __radd__(self, other): return SourcedString.concat([other, self]) def __mul__(self, other): if other <= 0: return self._stringtype('') else: result = self for i in range(1, other): result += self return result def __rmul__(self, other): return self.__mul__(other) def join(self, sequence): seq_iter = iter(sequence) # Add the first element; but if sequence is empty, return an # empty string. try: s = seq_iter.next() except StopIteration: return self._stringtype('') # Add the remaining elements, separated by self. for elt in seq_iter: s += self s += elt return s @staticmethod def __add_substring_to_list(substring, result): """ Helper for L{concat()}: add C{substring} to the end of the list of substrings in C{result}. If C{substring} is compound, then add its own substrings instead. Merge adjacent substrings whenever possible. Discard empty un-sourced substrings. """ # Flatten nested compound sourced strings. if isinstance(substring, CompoundSourcedString): for s in substring.substrings: SourcedString.__add_substring_to_list(s, result) # Discard empty Python substrings. elif len(substring) == 0 and not isinstance(substring, SourcedString): pass # discard. # Merge adjacent simple sourced strings (when possible). elif (result and isinstance(result[-1], SimpleSourcedString) and isinstance(substring, SimpleSourcedString) and result[-1].end == substring.begin and result[-1].docid == substring.docid): result[-1] = SourcedString.__merge_simple_substrings( result[-1], substring) # Merge adjacent Python strings. elif (result and not isinstance(result[-1], SourcedString) and not isinstance(substring, SourcedString)): result[-1] += substring # All other strings just get appended to the result list. else: result.append(substring) @staticmethod def __merge_simple_substrings(lhs, rhs): """ Helper for L{__add_substring_to_list()}: Merge C{lhs} and C{rhs} into a single simple sourced string, and return it. """ contents = lhs._stringtype.__add__(lhs, rhs) if (isinstance(lhs.source, ConsecutiveCharStringSource) and isinstance(rhs.source, ConsecutiveCharStringSource)): source = ConsecutiveCharStringSource( lhs.source.docid, lhs.source.begin, rhs.source.end) else: source = ContiguousCharStringSource( lhs.source.docid, lhs.source.offsets+rhs.source.offsets[1:]) return SourcedString(contents, source) #////////////////////////////////////////////////////////////////////// #{ Justification Methods #////////////////////////////////////////////////////////////////////// def center(self, width, fillchar=' '): return (fillchar * ((width-len(self))/2) + self + fillchar * ((width-len(self)+1)/2)) def ljust(self, width, fillchar=' '): return self + fillchar * (width-len(self)) def rjust(self, width, fillchar=' '): return fillchar * (width-len(self)) + self def zfill(self, width): return self.rjust(width, '0') #////////////////////////////////////////////////////////////////////// #{ Replacement Methods #////////////////////////////////////////////////////////////////////// # [xx] There's no reason in principle why this can't preserve # location information. But for now, it doesn't. def __mod__(self, other): return self._stringtype.__mod__(self, other) def replace(self, old, new, count=0): # Check for unicode/bytestring mismatches: if self._mixed_string_types(old, new, count): return self._decode_and_call('replace', old, new, count) # Use a regexp to find all occurences of old, and replace them w/ new. result = '' pos = 0 for match in re.finditer(re.escape(old), self): result += self[pos:match.start()] result += new pos = match.end() result += self[pos:] return result def expandtabs(self, tabsize=8): if len(self) == 0: return self pieces = re.split(r'([\t\n])', self) result = '' offset = 0 for piece in pieces: if piece == '\t': spaces = 8 - (offset % tabsize) # Each inserted space's source is the same as the # source of the tab character that generated it. result += spaces * SourcedString(' ', piece.source) offset = 0 else: result += piece if piece == '\n': offset = 0 else: offset += len(piece) return result def translate(self, table, deletechars=''): # Note: str.translate() and unicode.translate() have # different interfaces. if isinstance(self, unicode): if deletechars: raise TypeError('The unicode version of translate() does not ' 'accept the deletechars parameter') return SourcedString.concat( [SourcedString(table.get(c,c), c.source) for c in self if table.get(c,c) is not None]) else: if len(table) != 256: raise ValueError('translation table must be 256 characters long') return SourcedString.concat( [SourcedString(table[ord(c)], c.source) for c in self if c not in deletechars]) #////////////////////////////////////////////////////////////////////// #{ Unicode #////////////////////////////////////////////////////////////////////// # Unicode string -> byte string def encode(self, encoding=None, errors='strict'): if encoding is None: encoding = sys.getdefaultencoding() if isinstance(self, str): return self.decode().encode(encoding, errors) # Encode characters one at a time. result = [] for i, char in enumerate(self): char_bytes = self._stringtype.encode(char, encoding, errors) for char_byte in char_bytes: if isinstance(char, SimpleSourcedString): result.append(SourcedString(char_byte, char.source)) else: assert not isinstance(char, CompoundSourcedString) result.append(char_byte) return SourcedString.concat(result) # Byte string -> unicode string. def decode(self, encoding=None, errors='strict'): if encoding is None: encoding = sys.getdefaultencoding() if isinstance(self, unicode): return self.encode().decode(encoding, errors) # Decode self into a plain unicode string. unicode_chars = self._stringtype.decode(self, encoding, errors) # Special case: if the resulting string has the same length # that the source string does, then we can safely assume that # each character is encoded with one byte; so we can just # reuse our source. if len(unicode_chars) == len(self): return self._decode_one_to_one(unicode_chars) # Otherwise: re-encode the characters, one at a time, to # determine how long their encodings are. result = [] first_byte = 0 for unicode_char in unicode_chars: char_width = len(unicode_char.encode(encoding, errors)) last_byte = first_byte + char_width - 1 if (isinstance(self[first_byte], SourcedString) and isinstance(self[last_byte], SourcedString)): begin = self[first_byte].begin end = self[last_byte].end if end-begin == 1: source = StringSource(docid=self[first_byte].docid, begin=begin, end=end) else: source = StringSource(docid=self[first_byte].docid, offsets=[begin, end]) result.append(SourcedString(unicode_char, source)) else: result.append(unicode_char) # First byte of the next char is 1+last byte of this char. first_byte = last_byte+1 if last_byte+1 != len(self): raise AssertionError("SourcedString.decode() does not support " "encodings that are not symmetric.") return SourcedString.concat(result) @abstract def _decode_one_to_one(unicode_chars): """ Helper for L{self.decode()}. Returns a unicode-decoded version of this L{SourcedString}. C{unicode_chars} is the unicode-decoded contents of this L{SourcedString}. This is used in the special case where the decoded string has the same length that the source string does. As a result, we can safely assume that each character is encoded with one byte; so we can just reuse our source. E.g., this will happen when decoding an ASCII string with utf-8. """ def _mixed_string_types(self, *args): """ Return true if the list (self,)+args contains at least one unicode string and at least one byte string. (If this is the case, then all byte strings should be converted to unicode by calling decode() before the operation is performed. You can do this automatically using L{_decode_and_call()}. """ any_unicode = isinstance(self, unicode) any_bytestring = isinstance(self, str) for arg in args: any_unicode = any_unicode or isinstance(arg, unicode) any_bytestring = any_bytestring or isinstance(arg, str) return any_unicode and any_bytestring def _decode_and_call(self, op, *args): """ If self or any of the values in args is a byte string, then convert it to unicode by calling its decode() method. Then return the result of calling self.op(*args). C{op} is specified using a string, because if C{self} is a byte string, then it will change type when it is decoded. """ # Make sure all args are decoded to unicode. args = list(args) for i in range(len(args)): if isinstance(args[i], str): args[i] = args[i].decode() # Make sure self is decoded to unicode. if isinstance(self, str): self = self.decode() # Retry the operation. method = getattr(self, op) return method(*args) #////////////////////////////////////////////////////////////////////// #{ Display #////////////////////////////////////////////////////////////////////// def pprint(self, vertical=False, wrap=70): """ Return a string containing a pretty-printed display of this sourced string. @param vertical: If true, then the returned display string will have vertical orientation, rather than the default horizontal orientation. @param wrap: Controls when the pretty-printed output is wrapped to the next line. If C{wrap} is an integer, then lines are wrapped when they become longer than C{wrap}. If C{wrap} is a string, then lines are wrapped immediately following that string. If C{wrap} is C{None}, then lines are never wrapped. """ if len(self) == 0: return '[Empty String]' if vertical == 1: return self._pprint_vertical() # special-cased max_digits = len(str(max(max(getattr(c, 'begin', 0), getattr(c, 'end', 0)) for c in self))) if not isinstance(wrap, (basestring, int, long, type(None))): raise TypeError("Expected wrap to be a sring, int, or None.") result = [] prev_offset = None # most recently displayed offset. prev_docid = None docid_line = '' output_lines = [''] * (max_digits+2) for pos, char in enumerate(self): char_begin = getattr(char, 'begin', None) char_end = getattr(char, 'end', None) char_docid = getattr(char, 'docid', None) # If the docid changed, then display the docid for the # previous segment. if char_docid != prev_docid: width = len(output_lines[0]) - len(docid_line) docid_line += self._pprint_docid(width, prev_docid) prev_docid = char_docid # Put a cap on the beginning of sourceless strings elif not output_lines[0] and char_begin is None: self._pprint_offset(' ', output_lines) # Display the character. if char_begin != prev_offset: self._pprint_offset(char_begin, output_lines) self._pprint_char(char, output_lines) self._pprint_offset(char_end, output_lines) prev_offset = char_end # Decide whether we're at the end of the line or not. line_len = len(output_lines[0]) if ( (isinstance(wrap, basestring) and self[max(0,pos-len(wrap)+1):pos+1] == wrap) or (isinstance(wrap, (int,long)) and line_len>=wrap) or pos == len(self)-1): # Put a cap on the end of sourceless strings if char_end is None: self._pprint_offset(' ', output_lines) # Filter out any empty output lines. output_lines = [l for l in output_lines if l.strip()] # Draw the docid line width = len(output_lines[0]) - len(docid_line) docid_line += self._pprint_docid(width, prev_docid) result.append(docid_line) # Draw the output lines for output_line in reversed(output_lines): result.append(output_line) result.append(output_lines[1]) # Reset variables for the next line. prev_offset = None prev_docid = None docid_line = '' output_lines = [''] * (max_digits+2) return '\n'.join(result) def _pprint_vertical(self): result = [] prev_offset = None max_digits = len(str(max(max(getattr(c, 'begin', 0), getattr(c, 'end', 0)) for c in self))) for pos, char in enumerate(self): char_begin = getattr(char, 'begin', None) char_end = getattr(char, 'end', None) char_docid = getattr(char, 'docid', None) if char_begin is None: assert char_end is None if pos == 0: result.append('+-----+') result.append(':%s:' % self._pprint_char_repr(char).center(5)) if pos == len(self)-1: result.append('+-----+') prev_offset = None else: if char_begin != prev_offset: result.append('+-----+ %s [%s]' % ( str(char_begin).rjust(max_digits), char_docid)) result.append('|%s| %s [%s]' % ( self._pprint_char_repr(char).center(5), ' '*max_digits, char_docid)) result.append('+-----+ %s [%s]' % ( str(char_end).rjust(max_digits), char_docid)) prev_offset = char_end return '\n'.join(result) _PPRINT_CHAR_REPRS = {'\n': r'\n', '\r': r'\r', '\a': r'\a', '\t': r'\t'} def _pprint_docid(self, width, docid): if docid is None: return ' '*width else: return '[%s]' % (docid[:width-2].center(width-2, '=')) def _pprint_char_repr(self, char): # Decide how to represent this character. if 32 <= ord(char) <= 127: return str(char) elif char in self._PPRINT_CHAR_REPRS: return self._PPRINT_CHAR_REPRS[char] elif isinstance(char, str): return r'\x%02x' % ord(char) else: return r'\u%04x' % ord(char) def _pprint_char(self, char, output_lines): """Helper for L{pprint()}: add a character to the pretty-printed output.""" char_repr = self._pprint_char_repr(char) output_lines[0] += char_repr # Add fillers to the offset lines. output_lines[1] += '-'*len(char_repr) for i in range(2, len(output_lines)): output_lines[i] += ' '*len(char_repr) def _pprint_offset(self, offset, output_lines): """Helper for L{pprint()}: add an offset marker to the pretty-printed output.""" if offset is None: return output_lines[0] += '|' output_lines[1] += '+' offset_rep = str(offset).rjust(len(output_lines)-2) for digit in range(len(offset_rep)): output_lines[-digit-1] += offset_rep[digit] #////////////////////////////////////////////////////////////////////// # Simple Sourced String #////////////////////////////////////////////////////////////////////// class SimpleSourcedString(SourcedString): """ A single substring of a document, annotated with information about the location in the document where it was originally found. See L{SourcedString} for more information. """ def __new__(cls, contents, source): # If the SimpleSourcedString constructor is called directly, # then choose one of its subclasses to delegate to. if cls is SimpleSourcedString: if isinstance(contents, str): cls = SimpleSourcedByteString elif isinstance(contents, unicode): cls = SimpleSourcedUnicodeString else: raise TypeError("Expected 'contents' to be a unicode " "string or a byte string") # Create the new object using the appropriate string class's # __new__, which takes just the contents argument. return cls._stringtype.__new__(cls, contents) def __init__(self, contents, source): """ Construct a new sourced string. @param contents: The string contents of the new sourced string. @type contents: C{str} or C{unicode} @param source: The source for the new string. If C{source} is a string, then it is used to automatically construct a new L{ConsecutiveCharStringSource} with a begin offset of C{0} and an end offset of C{len(contents)}. Otherwise, C{source} shoulde be a L{StringSource} whose length matches the length of C{contents}. """ if not isinstance(source, StringSource): source = ConsecutiveCharStringSource(source, 0, len(contents)) elif len(source) != len(contents): raise ValueError("Length of source (%d) must match length of " "contents (%d)" % (len(source), len(contents))) self.source = source """A L{StringLocation} specifying the location where this string occured in the source document.""" @property def begin(self): """ The document offset where the string begins. (I.e., the offset of the first character in the string.)""" return self.source.begin @property def end(self): """The document offset where the string ends. (For character offsets, one plus the offset of the last character; for byte offsets, one plus the offset of the last byte that encodes the last character).""" return self.source.end @property def docid(self): """ An identifier (such as a filename) that specifies the document where the string was found. """ return self.source.docid @property def sources(self): return ((0, self.source),) def __repr__(self): if self.end == self.begin+1: source_repr = '@[%s]' % (self.begin,) else: source_repr = '@[%s:%s]' % (self.begin, self.end) return self._stringtype.__repr__(self) + source_repr def __getitem__(self, index): result = self._stringtype.__getitem__(self, index) if isinstance(index, slice): if index.step not in (None, 1): return result else: start, stop = slice_bounds(self, index) return self.__getslice__(start, stop) else: return SourcedString(result, self.source[index]) def __getslice__(self, start, stop): # Negative indices get handled *before* __getslice__ is # called. Restrict start/stop to be within the range of the # string, to prevent negative indices from being adjusted # twice. start = max(0, min(len(self), start)) stop = max(start, min(len(self), stop)) return SourcedString( self._stringtype.__getslice__(self, start, stop), self.source[start:stop]) def capitalize(self): result = self._stringtype.capitalize(self) return SourcedString(result, self.source) def lower(self): result = self._stringtype.lower(self) return SourcedString(result, self.source) def upper(self): result = self._stringtype.upper(self) return SourcedString(result, self.source) def swapcase(self): result = self._stringtype.swapcase(self) return SourcedString(result, self.source) def title(self): result = self._stringtype.title(self) return SourcedString(result, self.source) def _decode_one_to_one(self, unicode_chars): return SourcedString(unicode_chars, self.source) #////////////////////////////////////////////////////////////////////// # Compound Sourced String #////////////////////////////////////////////////////////////////////// class CompoundSourcedString(SourcedString): """ A string constructed by concatenating substrings from multiple sources, and annotated with information about the locations where those substrings were originally found. See L{SourcedString} for more information. @ivar substrings: The tuple of substrings that compose this compound sourced string. Every compound sourced string is required to have at least two substrings; and the substrings themselves may never be CompoundSourcedStrings. """ def __new__(cls, substrings): # If the CompoundSourcedString constructor is called directly, # then choose one of its subclasses to delegate to. if cls is CompoundSourcedString: # Decide whether to use a unicode string or a byte string. use_unicode = sum(1 for substring in substrings if isinstance(substring, unicode)) if use_unicode: cls = CompoundSourcedUnicodeString else: cls = CompoundSourcedByteString # Build the concatenated string using str.join(), which will # return a str or unicode object; never a sourced string. contents = ''.join(substrings) # Create the new object using the appropriate string class's # __new__, which takes just the contents argument. return cls._stringtype.__new__(cls, contents) def __init__(self, substrings): """ Construct a new compound sourced string that combines the given list of substrings. Typically, compound sourced strings should not be constructed directly; instead, use L{SourcedString.concat()}, which flattens nested compound sourced strings, and merges adjacent substrings when possible. @raise ValueError: If C{len(substrings) < 2} @raise ValueError: If C{substrings} contains any C{CompoundSourcedString}s. """ if len(substrings) < 2: raise ValueError("CompoundSourcedString requires at least " "two substrings") # Don't nest compound sourced strings. for substring in substrings: if isinstance(substring, CompoundSourcedString): raise ValueError("substrings may not contain " "CompoundSourcedStrings.") self.substrings = tuple(substrings) @property def sources(self): index = 0 source_list = [] for substring in self.substrings: if isinstance(substring, SourcedString): source_list.append( (index, substring.source) ) index += len(substring) return tuple(source_list) def __repr__(self): sources = [self._source_repr(s) for s in self.substrings] source_str = '@[%s]' % ','.join(sources) return self._stringtype.__repr__(self) + source_str def _source_repr(self, substring): if isinstance(substring, SimpleSourcedString): return '%s:%s' % (substring.begin, substring.end) else: return '...' def __getitem__(self, index): if isinstance(index, slice): if index.step not in (None, 1): return self._stringtype.__getitem__(self, index) else: start, stop = slice_bounds(self, index) return self.__getslice__(start, stop) else: if index < 0: index += len(self) if index < 0 or index >= len(self): raise IndexError('StringSource index out of range') return self.__getslice__(index, index+1) def __getslice__(self, start, stop): # Bounds checking. start = max(0, min(len(self), start)) stop = max(start, min(len(self), stop)) # Construct a source list for the resulting string. result_substrings = [] offset = 0 for substring in self.substrings: if offset+len(substring) > start: s, e = max(0, start-offset), stop-offset result_substrings.append(substring[s:e]) offset += len(substring) if offset >= stop: break # Concatentate the resulting substrings. if len(result_substrings) == 0: return '' elif len(result_substrings) == 1: return result_substrings[0] else: return SourcedString.concat(result_substrings) def capitalize(self): return SourcedString.concat([s.capitalize() for s in self.substrings]) def lower(self): return SourcedString.concat([s.lower() for s in self.substrings]) def upper(self): return SourcedString.concat([s.upper() for s in self.substrings]) def swapcase(self): return SourcedString.concat([s.swapcase() for s in self.substrings]) def title(self): return SourcedString.concat([s.title() for s in self.substrings]) def encode(self, encoding=None, errors='strict'): return SourcedString.concat([s.encode(encoding, errors) for s in self.substrings]) def _decode_one_to_one(self, unicode_chars): index = 0 result = [] for substring in self.substrings: decoded_substring = unicode_chars[index:index+len(substring)] if isinstance(substring, SourcedString): result.append(SourcedString(decoded_substring, substring.source)) else: result.append(decoded_substring) index += len(substring) return SourcedString.concat(result) #////////////////////////////////////////////////////////////////////// # Concrete Sourced String Classes #////////////////////////////////////////////////////////////////////// class SimpleSourcedByteString(SimpleSourcedString, str): _stringtype = str class SimpleSourcedUnicodeString(SimpleSourcedString, unicode): _stringtype = unicode class CompoundSourcedByteString(CompoundSourcedString, str): _stringtype = str class CompoundSourcedUnicodeString(CompoundSourcedString, unicode): _stringtype = unicode def __init__(self, substrings): # If any substrings have type 'str', then decode them to unicode. for i in range(len(substrings)): if not isinstance(substrings[i], unicode): substrings[i] = substrings[i].decode() CompoundSourcedString.__init__(self, substrings) #////////////////////////////////////////////////////////////////////// # Sourced String Regexp #////////////////////////////////////////////////////////////////////// _original_re_compile = re.compile _original_re_sub = re.sub _original_re_subn = re.subn class SourcedStringRegexp(object): """ Wrapper for regexp pattern objects that cause the L{sub} and L{subn} methods to return sourced strings. """ def __init__(self, pattern, flags=0): if isinstance(pattern, basestring): pattern = _original_re_compile(pattern, flags) self.pattern = pattern def __getattr__(self, attr): return getattr(self.pattern, attr) def subn(self, repl, string, count=0): if (isinstance(repl, SourcedString) or isinstance(string, SourcedString)): result = '' pos = 0 n = 0 for match in self.pattern.finditer(string): result += string[pos:match.start()] result += repl pos = match.end() n += 1 if count and n==count: break result += string[pos:] return result, n else: return self.pattern.subn(repl, string, count) def sub(self, repl, string, count=0): return self.subn(repl, string, count)[0] @staticmethod def patch_re_module(): """ Modify the standard C{re} module by installing new versions of the functions C{re.compile}, C{re.sub}, and C{re.subn}, causing regular expression substitutions to return C{SourcedString}s when called with C{SourcedString}s arguments. Use this function only if necessary: it potentially affects all Python modules that use regular expressions! """ def new_re_sub(pattern, repl, string, count=0): return re.compile(pattern).sub(repl, string, count) def new_re_subn(pattern, repl, string, count=0): return re.compile(pattern).subn(repl, string, count) re.compile = SourcedStringRegexp re.sub = new_re_sub re.subn = new_re_subn @staticmethod def unpatch_re_module(): """ Restore the standard C{re} module to its original state (undoing the work that was done by L{patch_re_module()}). """ re.compile = _original_re_compile re.sub = _original_re_sub re.subn = _original_re_subn #////////////////////////////////////////////////////////////////////// # Sourced String Stream #////////////////////////////////////////////////////////////////////// class SourcedStringStream(object): """ Wrapper for a read-only stream that causes C{read()} (and related methods) to return L{sourced string }s. L{seek()} and L{tell()} are supported, but (currently) there are some restrictions on the values that may be passed to L{seek()}. """ def __init__(self, stream, docid=None, byte_offsets=False): self.stream = stream """The underlying stream.""" self.docid = docid """The docid attribute for sourced strings""" self.charpos = 0 """The current character (not byte) position""" assert not byte_offsets, 'not supported yet!' #///////////////////////////////////////////////////////////////// # Read methods #///////////////////////////////////////////////////////////////// def read(self, size=None): if size is None: return self._sourced_string(self.stream.read()) else: return self._sourced_string(self.stream.read(size)) def readline(self, size=None): if size is None: return self._sourced_string(self.stream.readline()) else: return self._sourced_string(self.stream.readline(size)) def readlines(self, sizehint=None, keepends=True): """ Read this file's contents, decode them using this reader's encoding, and return it as a list of unicode lines. @rtype: C{list} of C{unicode} @param sizehint: Ignored. @param keepends: If false, then strip newlines. """ return self.read().splitlines(keepends) def next(self): """Return the next decoded line from the underlying stream.""" line = self.readline() if line: return line else: raise StopIteration def __iter__(self): """Return self""" return self def xreadlines(self): """Return self""" return self def _sourced_string(self, contents): """Turn the given string into an sourced string, and update charpos.""" # [xx] currently we only support character offsets, not byte # offsets! source = ConsecutiveCharStringSource(self.docid, self.charpos, self.charpos+len(contents)) self.charpos += len(contents) return SourcedString(contents, source) #///////////////////////////////////////////////////////////////// # Pass-through methods & properties #///////////////////////////////////////////////////////////////// closed = property(lambda self: self.stream.closed, doc=""" True if the underlying stream is closed.""") name = property(lambda self: self.stream.name, doc=""" The name of the underlying stream.""") mode = property(lambda self: self.stream.mode, doc=""" The mode of the underlying stream.""") def close(self): """Close the underlying stream.""" self.stream.close() #///////////////////////////////////////////////////////////////// # Seek and tell #///////////////////////////////////////////////////////////////// class SourcedStringStreamPos(int): def __new__(cls, bytepos, charpos): self = int.__new__(cls, bytepos) self.charpos = charpos return self def seek(self, offset, whence=0): if whence == 0: if isinstance(offset, self.SourcedStringStreamPos): self.stream.seek(offset) self.charpos = offset.charpos elif offset == 0: self.stream.seek(0) self.charpos = 0 else: raise TypeError('seek() must be called with a value that ' 'was returned by tell().') elif whence == 1: raise TypeError('Relative seek not supported for ' 'SourcedStringStream.') elif whence == 2: raise TypeError('Seek-from-end not supported for ' 'SourcedStringStream.') else: raise ValueError('Bad whence value %r' % whence) def tell(self): bytepos = self.stream.tell() return self.SourcedStringStreamPos(bytepos, self.charpos) nltk-2.0~b9/nltk/probability.py0000644000175000017500000025243611423114517016433 0ustar bhavanibhavani# -*- coding: utf-8 -*- # Natural Language Toolkit: Probability and Statistics # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird (additions) # Trevor Cohn (additions) # Peter Ljunglöf (additions) # Liang Dong (additions) # Geoffrey Sampson (additions) # # URL: # For license information, see LICENSE.TXT # # $Id: probability.py 8601 2010-07-25 17:43:56Z stevenbird1 $ _NINF = float('-1e300') """ Classes for representing and processing probabilistic information. The L{FreqDist} class is used to encode X{frequency distributions}, which count the number of times that each outcome of an experiment occurs. The L{ProbDistI} class defines a standard interface for X{probability distributions}, which encode the probability of each outcome for an experiment. There are two types of probability distribution: - X{derived probability distributions} are created from frequency distributions. They attempt to model the probability distribution that generated the frequency distribution. - X{analytic probability distributions} are created directly from parameters (such as variance). The L{ConditionalFreqDist} class and L{ConditionalProbDistI} interface are used to encode conditional distributions. Conditional probability distributions can be derived or analytic; but currently the only implementation of the C{ConditionalProbDistI} interface is L{ConditionalProbDist}, a derived distribution. """ import math import random import warnings from operator import itemgetter from itertools import imap, islice from nltk.compat import all ##////////////////////////////////////////////////////// ## Frequency Distributions ##////////////////////////////////////////////////////// # [SB] inherit from defaultdict? # [SB] for NLTK 3.0, inherit from collections.Counter? class FreqDist(dict): """ A frequency distribution for the outcomes of an experiment. A frequency distribution records the number of times each outcome of an experiment has occurred. For example, a frequency distribution could be used to record the frequency of each word type in a document. Formally, a frequency distribution can be defined as a function mapping from each sample to the number of times that sample occurred as an outcome. Frequency distributions are generally constructed by running a number of experiments, and incrementing the count for a sample every time it is an outcome of an experiment. For example, the following code will produce a frequency distribution that encodes how often each word occurs in a text: >>> fdist = FreqDist() >>> for word in tokenize.whitespace(sent): ... fdist.inc(word.lower()) An equivalent way to do this is with the initializer: >>> fdist = FreqDist(word.lower() for word in tokenize.whitespace(sent)) """ def __init__(self, samples=None): """ Construct a new frequency distribution. If C{samples} is given, then the frequency distribution will be initialized with the count of each object in C{samples}; otherwise, it will be initialized to be empty. In particular, C{FreqDist()} returns an empty frequency distribution; and C{FreqDist(samples)} first creates an empty frequency distribution, and then calls C{update} with the list C{samples}. @param samples: The samples to initialize the frequency distribution with. @type samples: Sequence """ dict.__init__(self) self._N = 0 self._reset_caches() if samples: self.update(samples) def inc(self, sample, count=1): """ Increment this C{FreqDist}'s count for the given sample. @param sample: The sample whose count should be incremented. @type sample: any @param count: The amount to increment the sample's count by. @type count: C{int} @rtype: None @raise NotImplementedError: If C{sample} is not a supported sample type. """ if count == 0: return self[sample] = self.get(sample,0) + count def __setitem__(self, sample, value): """ Set this C{FreqDist}'s count for the given sample. @param sample: The sample whose count should be incremented. @type sample: any hashable object @param count: The new value for the sample's count @type count: C{int} @rtype: None @raise TypeError: If C{sample} is not a supported sample type. """ self._N += (value - self.get(sample, 0)) dict.__setitem__(self, sample, value) # Invalidate the caches self._reset_caches() def N(self): """ @return: The total number of sample outcomes that have been recorded by this C{FreqDist}. For the number of unique sample values (or bins) with counts greater than zero, use C{FreqDist.B()}. @rtype: C{int} """ return self._N def B(self): """ @return: The total number of sample values (or X{bins}) that have counts greater than zero. For the total number of sample outcomes recorded, use C{FreqDist.N()}. (FreqDist.B() is the same as len(FreqDist).) @rtype: C{int} """ return len(self) # deprecate this -- use keys() instead? def samples(self): """ @return: A list of all samples that have been recorded as outcomes by this frequency distribution. Use C{count()} to determine the count for each sample. @rtype: C{list} """ return self.keys() def hapaxes(self): """ @return: A list of all samples that occur once (hapax legomena) @rtype: C{list} """ return [item for item in self if self[item] == 1] def Nr(self, r, bins=None): """ @return: The number of samples with count r. @rtype: C{int} @type r: C{int} @param r: A sample count. @type bins: C{int} @param bins: The number of possible sample outcomes. C{bins} is used to calculate Nr(0). In particular, Nr(0) is C{bins-self.B()}. If C{bins} is not specified, it defaults to C{self.B()} (so Nr(0) will be 0). """ if r < 0: raise IndexError, 'FreqDist.Nr(): r must be non-negative' # Special case for Nr(0): if r == 0: if bins is None: return 0 else: return bins-self.B() # We have to search the entire distribution to find Nr. Since # this is an expensive operation, and is likely to be used # repeatedly, cache the results. if self._Nr_cache is None: self._cache_Nr_values() if r >= len(self._Nr_cache): return 0 return self._Nr_cache[r] def _cache_Nr_values(self): Nr = [0] for sample in self: c = self.get(sample, 0) if c >= len(Nr): Nr += [0]*(c+1-len(Nr)) Nr[c] += 1 self._Nr_cache = Nr def count(self, sample): """ Return the count of a given sample. The count of a sample is defined as the number of times that sample outcome was recorded by this C{FreqDist}. Counts are non-negative integers. This method has been replaced by conventional dictionary indexing; use fd[item] instead of fd.count(item). @return: The count of a given sample. @rtype: C{int} @param sample: the sample whose count should be returned. @type sample: any. """ raise AttributeError, "Use indexing to look up an entry in a FreqDist, e.g. fd[item]" def _cumulative_frequencies(self, samples=None): """ Return the cumulative frequencies of the specified samples. If no samples are specified, all counts are returned, starting with the largest. @return: The cumulative frequencies of the given samples. @rtype: C{list} of C{float} @param samples: the samples whose frequencies should be returned. @type sample: any. """ cf = 0.0 if not samples: samples = self.keys() for sample in samples: cf += self[sample] yield cf # slightly odd nomenclature freq() if FreqDist does counts and ProbDist does probs, # here, freq() does probs def freq(self, sample): """ Return the frequency of a given sample. The frequency of a sample is defined as the count of that sample divided by the total number of sample outcomes that have been recorded by this C{FreqDist}. The count of a sample is defined as the number of times that sample outcome was recorded by this C{FreqDist}. Frequencies are always real numbers in the range [0, 1]. @return: The frequency of a given sample. @rtype: float @param sample: the sample whose frequency should be returned. @type sample: any """ if self._N is 0: return 0 return float(self[sample]) / self._N def max(self): """ Return the sample with the greatest number of outcomes in this frequency distribution. If two or more samples have the same number of outcomes, return one of them; which sample is returned is undefined. If no outcomes have occurred in this frequency distribution, return C{None}. @return: The sample with the maximum number of outcomes in this frequency distribution. @rtype: any or C{None} """ if self._max_cache is None: best_sample = None best_count = -1 for sample in self: if self[sample] > best_count: best_sample = sample best_count = self[sample] self._max_cache = best_sample return self._max_cache def plot(self, *args, **kwargs): """ Plot samples from the frequency distribution displaying the most frequent sample first. If an integer parameter is supplied, stop after this many samples have been plotted. If two integer parameters m, n are supplied, plot a subset of the samples, beginning with m and stopping at n-1. For a cumulative plot, specify cumulative=True. (Requires Matplotlib to be installed.) @param title: The title for the graph @type title: C{str} @param cumulative: A flag to specify whether the plot is cumulative (default = False) @type title: C{bool} @param num: The maximum number of samples to plot (default=50). Specify num=0 to get all samples (slow). @type num: C{int} """ try: import pylab except ImportError: raise ValueError('The plot function requires the matplotlib package.' 'See http://matplotlib.sourceforge.net/') if len(args) == 0: args = [len(self)] samples = list(islice(self, *args)) cumulative = _get_kwarg(kwargs, 'cumulative', False) if cumulative: freqs = list(self._cumulative_frequencies(samples)) ylabel = "Cumulative Counts" else: freqs = [self[sample] for sample in samples] ylabel = "Counts" # percents = [f * 100 for f in freqs] only in ProbDist? pylab.grid(True, color="silver") if not "linewidth" in kwargs: kwargs["linewidth"] = 2 pylab.plot(freqs, **kwargs) pylab.xticks(range(len(samples)), [str(s) for s in samples], rotation=90) if "title" in kwargs: pylab.title(kwargs["title"]) pylab.xlabel("Samples") pylab.ylabel(ylabel) pylab.show() def tabulate(self, *args, **kwargs): """ Tabulate the given samples from the frequency distribution (cumulative), displaying the most frequent sample first. (Requires Matplotlib to be installed.) @param samples: The samples to plot (default is all samples) @type samples: C{list} @param title: The title for the graph @type title: C{str} @param num: The maximum number of samples to plot (default=50). Specify num=0 to get all samples (slow). @type num: C{int} """ if len(args) == 0: args = [len(self)] samples = list(islice(self, *args)) cumulative = _get_kwarg(kwargs, 'cumulative', False) if cumulative: freqs = list(self._cumulative_frequencies(samples)) else: freqs = [self[sample] for sample in samples] # percents = [f * 100 for f in freqs] only in ProbDist? for i in range(len(samples)): print "%4s" % str(samples[i]), print for i in range(len(samples)): print "%4d" % freqs[i], print def sorted_samples(self): raise AttributeError, "Use FreqDist.keys(), or iterate over the FreqDist to get its samples in sorted order (most frequent first)" def sorted(self): raise AttributeError, "Use FreqDist.keys(), or iterate over the FreqDist to get its samples in sorted order (most frequent first)" def _sort_keys_by_value(self): if not self._item_cache: self._item_cache = sorted(dict.items(self), key=lambda x:(-x[1], x[0])) def keys(self): """ Return the samples sorted in decreasing order of frequency. @return: A list of samples, in sorted order @rtype: C{list} of any """ self._sort_keys_by_value() return map(itemgetter(0), self._item_cache) def values(self): """ Return the samples sorted in decreasing order of frequency. @return: A list of samples, in sorted order @rtype: C{list} of any """ self._sort_keys_by_value() return map(itemgetter(1), self._item_cache) def items(self): """ Return the items sorted in decreasing order of frequency. @return: A list of items, in sorted order @rtype: C{list} of C{tuple} """ self._sort_keys_by_value() return self._item_cache[:] def __iter__(self): """ Return the samples sorted in decreasing order of frequency. @return: An iterator over the samples, in sorted order @rtype: C{iter} """ return iter(self.keys()) def iterkeys(self): """ Return the samples sorted in decreasing order of frequency. @return: An iterator over the samples, in sorted order @rtype: C{iter} """ return iter(self.keys()) def itervalues(self): """ Return the values sorted in decreasing order. @return: An iterator over the values, in sorted order @rtype: C{iter} """ return iter(self.values()) def iteritems(self): """ Return the items sorted in decreasing order of frequency. @return: An iterator over the items, in sorted order @rtype: C{iter} of any """ self._sort_keys_by_value() return iter(self._item_cache) # sort the supplied samples # if samples: # items = [(sample, self[sample]) for sample in set(samples)] def copy(self): """ Create a copy of this frequency distribution. @return: A copy of this frequency distribution object. @rtype: C{FreqDist} """ return self.__class__(self) def update(self, samples): """ Update the frequency distribution with the provided list of samples. This is a faster way to add multiple samples to the distribution. @param samples: The samples to add. @type samples: C{list} """ try: sample_iter = samples.iteritems() except: sample_iter = imap(lambda x: (x,1), samples) for sample, count in sample_iter: self.inc(sample, count=count) def pop(self, other): self._reset_caches() return dict.pop(self, other) def popitem(self, other): self._reset_caches() return dict.popitem(self, other) def _reset_caches(self): self._Nr_cache = None self._max_cache = None self._item_cache = None def __add__(self, other): clone = self.copy() clone.update(other) return clone def __eq__(self, other): if not isinstance(other, FreqDist): return False return self.items() == other.items() # items are already sorted def __ne__(self, other): return not (self == other) def __le__(self, other): if not isinstance(other, FreqDist): return False return set(self).issubset(other) and all(self[key] <= other[key] for key in self) def __lt__(self, other): if not isinstance(other, FreqDist): return False return self <= other and self != other def __ge__(self, other): if not isinstance(other, FreqDist): return False return other <= self def __gt__(self, other): if not isinstance(other, FreqDist): return False return other < self def __repr__(self): """ @return: A string representation of this C{FreqDist}. @rtype: string """ return '' % self.N() def __str__(self): """ @return: A string representation of this C{FreqDist}. @rtype: string """ items = ['%r: %r' % (s, self[s]) for s in self] return '' % ', '.join(items) def __getitem__(self, sample): return self.get(sample, 0) ##////////////////////////////////////////////////////// ## Probability Distributions ##////////////////////////////////////////////////////// class ProbDistI(object): """ A probability distribution for the outcomes of an experiment. A probability distribution specifies how likely it is that an experiment will have any given outcome. For example, a probability distribution could be used to predict the probability that a token in a document will have a given type. Formally, a probability distribution can be defined as a function mapping from samples to nonnegative real numbers, such that the sum of every number in the function's range is 1.0. C{ProbDist}s are often used to model the probability distribution of the experiment used to generate a frequency distribution. """ SUM_TO_ONE = True """True if the probabilities of the samples in this probability distribution will always sum to one.""" def __init__(self): if self.__class__ == ProbDistI: raise AssertionError, "Interfaces can't be instantiated" def prob(self, sample): """ @return: the probability for a given sample. Probabilities are always real numbers in the range [0, 1]. @rtype: float @param sample: The sample whose probability should be returned. @type sample: any """ raise AssertionError() def logprob(self, sample): """ @return: the base 2 logarithm of the probability for a given sample. Log probabilities range from negitive infinity to zero. @rtype: float @param sample: The sample whose probability should be returned. @type sample: any """ # Default definition, in terms of prob() p = self.prob(sample) if p == 0: # Use some approximation to infinity. What this does # depends on your system's float implementation. return _NINF else: return math.log(p, 2) def max(self): """ @return: the sample with the greatest probability. If two or more samples have the same probability, return one of them; which sample is returned is undefined. @rtype: any """ raise AssertionError() # deprecate this (use keys() instead?) def samples(self): """ @return: A list of all samples that have nonzero probabilities. Use C{prob} to find the probability of each sample. @rtype: C{list} """ raise AssertionError() # cf self.SUM_TO_ONE def discount(self): """ @return: The ratio by which counts are discounted on average: c*/c @rtype: C{float} """ return 0.0 # Subclasses should define more efficient implementations of this, # where possible. def generate(self): """ @return: A randomly selected sample from this probability distribution. The probability of returning each sample C{samp} is equal to C{self.prob(samp)}. """ p = random.random() for sample in self.samples(): p -= self.prob(sample) if p <= 0: return sample # allow for some rounding error: if p < .0001: return sample # we *should* never get here if self.SUM_TO_ONE: warnings.warn("Probability distribution %r sums to %r; generate()" " is returning an arbitrary sample." % (self, 1-p)) return random.choice(list(self.samples())) class UniformProbDist(ProbDistI): """ A probability distribution that assigns equal probability to each sample in a given set; and a zero probability to all other samples. """ def __init__(self, samples): """ Construct a new uniform probability distribution, that assigns equal probability to each sample in C{samples}. @param samples: The samples that should be given uniform probability. @type samples: C{list} @raise ValueError: If C{samples} is empty. """ if len(samples) == 0: raise ValueError('A Uniform probability distribution must '+ 'have at least one sample.') self._sampleset = set(samples) self._prob = 1.0/len(self._sampleset) self._samples = list(self._sampleset) def prob(self, sample): if sample in self._sampleset: return self._prob else: return 0 def max(self): return self._samples[0] def samples(self): return self._samples def __repr__(self): return '' % len(self._sampleset) class DictionaryProbDist(ProbDistI): """ A probability distribution whose probabilities are directly specified by a given dictionary. The given dictionary maps samples to probabilities. """ def __init__(self, prob_dict=None, log=False, normalize=False): """ Construct a new probability distribution from the given dictionary, which maps values to probabilities (or to log probabilities, if C{log} is true). If C{normalize} is true, then the probability values are scaled by a constant factor such that they sum to 1. """ self._prob_dict = prob_dict.copy() self._log = log # Normalize the distribution, if requested. if normalize: if log: value_sum = sum_logs(self._prob_dict.values()) if value_sum <= _NINF: logp = math.log(1.0/len(prob_dict), 2) for x in prob_dict.keys(): self._prob_dict[x] = logp else: for (x, p) in self._prob_dict.items(): self._prob_dict[x] -= value_sum else: value_sum = sum(self._prob_dict.values()) if value_sum == 0: p = 1.0/len(prob_dict) for x in prob_dict: self._prob_dict[x] = p else: norm_factor = 1.0/value_sum for (x, p) in self._prob_dict.items(): self._prob_dict[x] *= norm_factor def prob(self, sample): if self._log: if sample not in self._prob_dict: return 0 else: return 2**(self._prob_dict[sample]) else: return self._prob_dict.get(sample, 0) def logprob(self, sample): if self._log: return self._prob_dict.get(sample, _NINF) else: if sample not in self._prob_dict: return _NINF elif self._prob_dict[sample] == 0: return _NINF else: return math.log(self._prob_dict[sample], 2) def max(self): if not hasattr(self, '_max'): self._max = max((p,v) for (v,p) in self._prob_dict.items())[1] return self._max def samples(self): return self._prob_dict.keys() def __repr__(self): return '' % len(self._prob_dict) class MLEProbDist(ProbDistI): """ The maximum likelihood estimate for the probability distribution of the experiment used to generate a frequency distribution. The X{maximum likelihood estimate} approximates the probability of each sample as the frequency of that sample in the frequency distribution. """ def __init__(self, freqdist): """ Use the maximum likelihood estimate to create a probability distribution for the experiment used to generate C{freqdist}. @type freqdist: C{FreqDist} @param freqdist: The frequency distribution that the probability estimates should be based on. """ self._freqdist = freqdist def freqdist(self): """ @return: The frequency distribution that this probability distribution is based on. @rtype: C{FreqDist} """ return self._freqdist def prob(self, sample): return self._freqdist.freq(sample) def max(self): return self._freqdist.max() def samples(self): return self._freqdist.keys() def __repr__(self): """ @rtype: C{string} @return: A string representation of this C{ProbDist}. """ return '' % self._freqdist.N() class LidstoneProbDist(ProbDistI): """ The Lidstone estimate for the probability distribution of the experiment used to generate a frequency distribution. The C{Lidstone estimate} is paramaterized by a real number M{gamma}, which typically ranges from 0 to 1. The X{Lidstone estimate} approximates the probability of a sample with count M{c} from an experiment with M{N} outcomes and M{B} bins as M{(c+gamma)/(N+B*gamma)}. This is equivalant to adding M{gamma} to the count for each bin, and taking the maximum likelihood estimate of the resulting frequency distribution. """ SUM_TO_ONE = False def __init__(self, freqdist, gamma, bins=None): """ Use the Lidstone estimate to create a probability distribution for the experiment used to generate C{freqdist}. @type freqdist: C{FreqDist} @param freqdist: The frequency distribution that the probability estimates should be based on. @type gamma: C{float} @param gamma: A real number used to paramaterize the estimate. The Lidstone estimate is equivalant to adding M{gamma} to the count for each bin, and taking the maximum likelihood estimate of the resulting frequency distribution. @type bins: C{int} @param bins: The number of sample values that can be generated by the experiment that is described by the probability distribution. This value must be correctly set for the probabilities of the sample values to sum to one. If C{bins} is not specified, it defaults to C{freqdist.B()}. """ if (bins == 0) or (bins is None and freqdist.N() == 0): name = self.__class__.__name__[:-8] raise ValueError('A %s probability distribution ' % name + 'must have at least one bin.') if (bins is not None) and (bins < freqdist.B()): name = self.__class__.__name__[:-8] raise ValueError('\nThe number of bins in a %s distribution ' % name + '(%d) must be greater than or equal to\n' % bins + 'the number of bins in the FreqDist used ' + 'to create it (%d).' % freqdist.N()) self._freqdist = freqdist self._gamma = float(gamma) self._N = self._freqdist.N() if bins is None: bins = freqdist.B() self._bins = bins self._divisor = self._N + bins * gamma if self._divisor == 0.0: # In extreme cases we force the probability to be 0, # which it will be, since the count will be 0: self._gamma = 0 self._divisor = 1 def freqdist(self): """ @return: The frequency distribution that this probability distribution is based on. @rtype: C{FreqDist} """ return self._freqdist def prob(self, sample): c = self._freqdist[sample] return (c + self._gamma) / self._divisor def max(self): # For Lidstone distributions, probability is monotonic with # frequency, so the most probable sample is the one that # occurs most frequently. return self._freqdist.max() def samples(self): return self._freqdist.keys() def discount(self): gb = self._gamma * self._bins return gb / (self._N + gb) def __repr__(self): """ @rtype: C{string} @return: A string representation of this C{ProbDist}. """ return '' % self._freqdist.N() class LaplaceProbDist(LidstoneProbDist): """ The Laplace estimate for the probability distribution of the experiment used to generate a frequency distribution. The X{Lidstone estimate} approximates the probability of a sample with count M{c} from an experiment with M{N} outcomes and M{B} bins as M{(c+1)/(N+B)}. This is equivalant to adding one to the count for each bin, and taking the maximum likelihood estimate of the resulting frequency distribution. """ def __init__(self, freqdist, bins=None): """ Use the Laplace estimate to create a probability distribution for the experiment used to generate C{freqdist}. @type freqdist: C{FreqDist} @param freqdist: The frequency distribution that the probability estimates should be based on. @type bins: C{int} @param bins: The number of sample values that can be generated by the experiment that is described by the probability distribution. This value must be correctly set for the probabilities of the sample values to sum to one. If C{bins} is not specified, it defaults to C{freqdist.B()}. """ LidstoneProbDist.__init__(self, freqdist, 1, bins) def __repr__(self): """ @rtype: C{string} @return: A string representation of this C{ProbDist}. """ return '' % self._freqdist.N() class ELEProbDist(LidstoneProbDist): """ The expected likelihood estimate for the probability distribution of the experiment used to generate a frequency distribution. The X{expected likelihood estimate} approximates the probability of a sample with count M{c} from an experiment with M{N} outcomes and M{B} bins as M{(c+0.5)/(N+B/2)}. This is equivalant to adding 0.5 to the count for each bin, and taking the maximum likelihood estimate of the resulting frequency distribution. """ def __init__(self, freqdist, bins=None): """ Use the expected likelihood estimate to create a probability distribution for the experiment used to generate C{freqdist}. @type freqdist: C{FreqDist} @param freqdist: The frequency distribution that the probability estimates should be based on. @type bins: C{int} @param bins: The number of sample values that can be generated by the experiment that is described by the probability distribution. This value must be correctly set for the probabilities of the sample values to sum to one. If C{bins} is not specified, it defaults to C{freqdist.B()}. """ LidstoneProbDist.__init__(self, freqdist, 0.5, bins) def __repr__(self): """ @rtype: C{string} @return: A string representation of this C{ProbDist}. """ return '' % self._freqdist.N() class HeldoutProbDist(ProbDistI): """ The heldout estimate for the probability distribution of the experiment used to generate two frequency distributions. These two frequency distributions are called the "heldout frequency distribution" and the "base frequency distribution." The X{heldout estimate} uses uses the X{heldout frequency distribution} to predict the probability of each sample, given its frequency in the X{base frequency distribution}. In particular, the heldout estimate approximates the probability for a sample that occurs M{r} times in the base distribution as the average frequency in the heldout distribution of all samples that occur M{r} times in the base distribution. This average frequency is M{Tr[r]/(Nr[r]*N)}, where: - M{Tr[r]} is the total count in the heldout distribution for all samples that occur M{r} times in the base distribution. - M{Nr[r]} is the number of samples that occur M{r} times in the base distribution. - M{N} is the number of outcomes recorded by the heldout frequency distribution. In order to increase the efficiency of the C{prob} member function, M{Tr[r]/(Nr[r]*N)} is precomputed for each value of M{r} when the C{HeldoutProbDist} is created. @type _estimate: C{list} of C{float} @ivar _estimate: A list mapping from M{r}, the number of times that a sample occurs in the base distribution, to the probability estimate for that sample. C{_estimate[M{r}]} is calculated by finding the average frequency in the heldout distribution of all samples that occur M{r} times in the base distribution. In particular, C{_estimate[M{r}]} = M{Tr[r]/(Nr[r]*N)}. @type _max_r: C{int} @ivar _max_r: The maximum number of times that any sample occurs in the base distribution. C{_max_r} is used to decide how large C{_estimate} must be. """ SUM_TO_ONE = False def __init__(self, base_fdist, heldout_fdist, bins=None): """ Use the heldout estimate to create a probability distribution for the experiment used to generate C{base_fdist} and C{heldout_fdist}. @type base_fdist: C{FreqDist} @param base_fdist: The base frequency distribution. @type heldout_fdist: C{FreqDist} @param heldout_fdist: The heldout frequency distribution. @type bins: C{int} @param bins: The number of sample values that can be generated by the experiment that is described by the probability distribution. This value must be correctly set for the probabilities of the sample values to sum to one. If C{bins} is not specified, it defaults to C{freqdist.B()}. """ self._base_fdist = base_fdist self._heldout_fdist = heldout_fdist # The max number of times any sample occurs in base_fdist. self._max_r = base_fdist[base_fdist.max()] # Calculate Tr, Nr, and N. Tr = self._calculate_Tr() Nr = [base_fdist.Nr(r, bins) for r in range(self._max_r+1)] N = heldout_fdist.N() # Use Tr, Nr, and N to compute the probability estimate for # each value of r. self._estimate = self._calculate_estimate(Tr, Nr, N) def _calculate_Tr(self): """ @return: the list M{Tr}, where M{Tr[r]} is the total count in C{heldout_fdist} for all samples that occur M{r} times in C{base_fdist}. @rtype: C{list} of C{float} """ Tr = [0.0] * (self._max_r+1) for sample in self._heldout_fdist: r = self._base_fdist[sample] Tr[r] += self._heldout_fdist[sample] return Tr def _calculate_estimate(self, Tr, Nr, N): """ @return: the list M{estimate}, where M{estimate[r]} is the probability estimate for any sample that occurs M{r} times in the base frequency distribution. In particular, M{estimate[r]} is M{Tr[r]/(N[r]*N)}. In the special case that M{N[r]=0}, M{estimate[r]} will never be used; so we define M{estimate[r]=None} for those cases. @rtype: C{list} of C{float} @type Tr: C{list} of C{float} @param Tr: the list M{Tr}, where M{Tr[r]} is the total count in the heldout distribution for all samples that occur M{r} times in base distribution. @type Nr: C{list} of C{float} @param Nr: The list M{Nr}, where M{Nr[r]} is the number of samples that occur M{r} times in the base distribution. @type N: C{int} @param N: The total number of outcomes recorded by the heldout frequency distribution. """ estimate = [] for r in range(self._max_r+1): if Nr[r] == 0: estimate.append(None) else: estimate.append(Tr[r]/(Nr[r]*N)) return estimate def base_fdist(self): """ @return: The base frequency distribution that this probability distribution is based on. @rtype: C{FreqDist} """ return self._base_fdist def heldout_fdist(self): """ @return: The heldout frequency distribution that this probability distribution is based on. @rtype: C{FreqDist} """ return self._heldout_fdist def samples(self): return self._base_fdist.keys() def prob(self, sample): # Use our precomputed probability estimate. r = self._base_fdist[sample] return self._estimate[r] def max(self): # Note: the Heldout estimation is *not* necessarily monotonic; # so this implementation is currently broken. However, it # should give the right answer *most* of the time. :) return self._base_fdist.max() def discount(self): raise NotImplementedError() def __repr__(self): """ @rtype: C{string} @return: A string representation of this C{ProbDist}. """ s = '' return s % (self._base_fdist.N(), self._heldout_fdist.N()) class CrossValidationProbDist(ProbDistI): """ The cross-validation estimate for the probability distribution of the experiment used to generate a set of frequency distribution. The X{cross-validation estimate} for the probability of a sample is found by averaging the held-out estimates for the sample in each pair of frequency distributions. """ SUM_TO_ONE = False def __init__(self, freqdists, bins): """ Use the cross-validation estimate to create a probability distribution for the experiment used to generate C{freqdists}. @type freqdists: C{list} of C{FreqDist} @param freqdists: A list of the frequency distributions generated by the experiment. @type bins: C{int} @param bins: The number of sample values that can be generated by the experiment that is described by the probability distribution. This value must be correctly set for the probabilities of the sample values to sum to one. If C{bins} is not specified, it defaults to C{freqdist.B()}. """ self._freqdists = freqdists # Create a heldout probability distribution for each pair of # frequency distributions in freqdists. self._heldout_probdists = [] for fdist1 in freqdists: for fdist2 in freqdists: if fdist1 is not fdist2: probdist = HeldoutProbDist(fdist1, fdist2, bins) self._heldout_probdists.append(probdist) def freqdists(self): """ @rtype: C{list} of C{FreqDist} @return: The list of frequency distributions that this C{ProbDist} is based on. """ return self._freqdists def samples(self): # [xx] nb: this is not too efficient return set(sum([fd.keys() for fd in self._freqdists], [])) def prob(self, sample): # Find the average probability estimate returned by each # heldout distribution. prob = 0.0 for heldout_probdist in self._heldout_probdists: prob += heldout_probdist.prob(sample) return prob/len(self._heldout_probdists) def discount(self): raise NotImplementedError() def __repr__(self): """ @rtype: C{string} @return: A string representation of this C{ProbDist}. """ return '' % len(self._freqdists) class WittenBellProbDist(ProbDistI): """ The Witten-Bell estimate of a probability distribution. This distribution allocates uniform probability mass to as yet unseen events by using the number of events that have only been seen once. The probability mass reserved for unseen events is equal to: - M{T / (N + T)} where M{T} is the number of observed event types and M{N} is the total number of observed events. This equates to the maximum likelihood estimate of a new type event occuring. The remaining probability mass is discounted such that all probability estimates sum to one, yielding: - M{p = T / Z (N + T)}, if count = 0 - M{p = c / (N + T)}, otherwise """ def __init__(self, freqdist, bins=None): """ Creates a distribution of Witten-Bell probability estimates. This distribution allocates uniform probability mass to as yet unseen events by using the number of events that have only been seen once. The probability mass reserved for unseen events is equal to: - M{T / (N + T)} where M{T} is the number of observed event types and M{N} is the total number of observed events. This equates to the maximum likelihood estimate of a new type event occuring. The remaining probability mass is discounted such that all probability estimates sum to one, yielding: - M{p = T / Z (N + T)}, if count = 0 - M{p = c / (N + T)}, otherwise The parameters M{T} and M{N} are taken from the C{freqdist} parameter (the C{B()} and C{N()} values). The normalising factor M{Z} is calculated using these values along with the C{bins} parameter. @param freqdist: The frequency counts upon which to base the estimation. @type freqdist: C{FreqDist} @param bins: The number of possible event types. This must be at least as large as the number of bins in the C{freqdist}. If C{None}, then it's assumed to be equal to that of the C{freqdist} @type bins: C{Int} """ assert bins == None or bins >= freqdist.B(),\ 'Bins parameter must not be less than freqdist.B()' if bins == None: bins = freqdist.B() self._freqdist = freqdist self._T = self._freqdist.B() self._Z = bins - self._freqdist.B() self._N = self._freqdist.N() # self._P0 is P(0), precalculated for efficiency: if self._N==0: # if freqdist is empty, we approximate P(0) by a UniformProbDist: self._P0 = 1.0 / self._Z else: self._P0 = self._T / float(self._Z * (self._N + self._T)) def prob(self, sample): # inherit docs from ProbDistI c = self._freqdist[sample] if c == 0: return self._P0 else: return c / float(self._N + self._T) def max(self): return self._freqdist.max() def samples(self): return self._freqdist.keys() def freqdist(self): return self._freqdist def discount(self): raise NotImplementedError() def __repr__(self): """ @rtype: C{string} @return: A string representation of this C{ProbDist}. """ return '' % self._freqdist.N() ##////////////////////////////////////////////////////// ## Good-Turing Probablity Distributions ##////////////////////////////////////////////////////// # Good-Turing frequency estimation was contributed by Alan Turing and # his statistical assistant I.J. Good, during their collaboration in # the WWII. It is a statistical technique for predicting the # probability of occurrence of objects belonging to an unknown number # of species, given past observations of such objects and their # species. (In drawing balls from an urn, the 'objects' would be balls # and the 'species' would be the distinct colors of the balls (finite # but unknown in number). # # The situation frequency zero is quite common in the original # Good-Turing estimation. Bill Gale and Geoffrey Sampson present a # simple and effective approach, Simple Good-Turing. As a smoothing # curve they simply use a power curve: # # Nr = a*r^b (with b < -1 to give the appropriate hyperbolic # relationsihp) # # They estimate a and b by simple linear regression technique on the # logarithmic form of the equation: # # log Nr = a + b*log(r) # # However, they suggest that such a simple curve is probably only # appropriate for high values of r. For low values of r, they use the # measured Nr directly. (see M&S, p.213) # # Gale and Sampson propose to use r while the difference between r and # r* is 1.96 greather than the standar deviation, and switch to r* if # it is less or equal: # # |r - r*| > 1.96 * sqrt((r + 1)^2 (Nr+1 / Nr^2) (1 + Nr+1 / Nr)) # # The 1.96 coefficient correspond to a 0.05 significance criterion, # some implementations can use a coefficient of 1.65 for a 0.1 # significance criterion. # class GoodTuringProbDist(ProbDistI): """ The Good-Turing estimate of a probability distribution. This method calculates the probability mass to assign to events with zero or low counts based on the number of events with higher counts. It does so by using the smoothed count M{c*}: - M{c* = (c + 1) N(c + 1) / N(c)} for c >= 1 - M{things with frequency zero in training} = N(1) for c == 0 where M{c} is the original count, M{N(i)} is the number of event types observed with count M{i}. We can think the count of unseen as the count of frequency one. (see Jurafsky & Martin 2nd Edition, p101) """ def __init__(self, freqdist, bins=None): """ @param freqdist: The frequency counts upon which to base the estimation. @type freqdist: C{FreqDist} @param bins: The number of possible event types. This must be at least as large as the number of bins in the C{freqdist}. If C{None}, then it's assumed to be equal to that of the C{freqdist} @type bins: C{Int} """ assert bins == None or bins >= freqdist.B(),\ 'Bins parameter must not be less than freqdist.B()' if bins == None: bins = freqdist.B() self._freqdist = freqdist self._bins = bins def prob(self, sample): count = self._freqdist[sample] # unseen sample's frequency (count zero) uses frequency one's if count == 0 and self._freqdist.N() != 0: p0 = 1.0 * self._freqdist.Nr(1) / self._freqdist.N() if self._bins == self._freqdist.B(): p0 = 0.0 else: p0 = p0 / (1.0 * self._bins - self._freqdist.B()) nc = self._freqdist.Nr(count) ncn = self._freqdist.Nr(count + 1) # avoid divide-by-zero errors for sparse datasets if nc == 0 or self._freqdist.N() == 0: return 0 return 1.0 * (count + 1) * ncn / (nc * self._freqdist.N()) def max(self): return self._freqdist.max() def samples(self): return self._freqdist.keys() def discount(self): """ @return: The probability mass transferred from the seen samples to the unseen samples. @rtype: C{float} """ return 1.0 * self._freqdist.Nr(1) / self._freqdist.N() def freqdist(self): return self._freqdist def __repr__(self): """ @rtype: C{string} @return: A string representation of this C{ProbDist}. """ return '' % self._freqdist.N() ##////////////////////////////////////////////////////// ## Simple Good-Turing Probablity Distributions ##////////////////////////////////////////////////////// class SimpleGoodTuringProbDist(ProbDistI): """ SimpleGoodTuring ProbDist approximates from frequency to freqency of frequency into a linear line under log space by linear regression. Details of Simple Good-Turing algorithm can be found in: (1) Bill Gale and Geoffrey Sampson's joint paper "Good Turing Smoothing Without Tear", published in Journal of Quantitative Linguistics, vol. 2 pp. 217-237, 1995 (2) Jurafsky & Martin's Book "Speech and Language Processing" 2e Chap 4.5 p103 (log(Nc) = a + b*log(c)) (3) Website maintained by Geoffrey Sampson: http://www.grsampson.net/RGoodTur.html Given a set of pair (xi, yi), where the xi denotes the freqency and yi denotes the freqency of freqency, we want to minimize their square variation. E(x) and E(y) represent the mean of xi and yi. -Slope: b = sigma ((xi-E(x)*(yi-E(y))) / sigma ((xi-E(x))*(xi-E(x))) -Intercept: a = E(y)- b * E(x) """ def __init__(self, freqdist, bins=None): """ @param freqdist: The frequency counts upon which to base the estimation. @type freqdist: C{FreqDist} @param bins: The number of possible event types. This must be at least as large as the number of bins in the C{freqdist}. If C{None}, then it's assumed to be equal to that of the C{freqdist} @type bins: C{Int} """ assert bins == None or bins >= freqdist.B(),\ 'Bins parameter must not be less than freqdist.B()' if bins == None: bins = freqdist.B() self._freqdist = freqdist self._bins = bins r, nr = self._r_Nr() self.find_best_fit(r, nr) self._switch(r, nr) self._renormalize(r, nr) def _r_Nr(self): """ Split the frequency distribution in two list (r, Nr), where Nr(r) > 0 """ r, nr = [], [] b, i = 0, 0 while b != self._freqdist.B(): nr_i = self._freqdist.Nr(i) if nr_i > 0: b += nr_i r.append(i) nr.append(nr_i) i += 1 return (r, nr) def find_best_fit(self, r, nr): """ Use simple linear regression to tune parameters self._slope and self._intercept in the log-log space based on count and Nr(count) (Work in log space to avoid floating point underflow.) """ # For higher sample frequencies the data points becomes horizontal # along line Nr=1. To create a more evident linear model in log-log # space, we average positive Nr values with the surrounding zero # values. (Church and Gale, 1991) if not r or not nr: # Empty r or nr? return zr = [] for j in range(len(r)): i = r[j-1] if j > 0 else 0 k = r[j+1] if j != len(r) - 1 else 2 * r[j] - i zr_ = 2.0 * nr[j] / (k - i) zr.append(zr_) log_r = [math.log(i) for i in r] log_zr = [math.log(i) for i in zr] xy_cov = x_var = 0.0 x_mean = 1.0 * sum(log_r) / len(log_r) y_mean = 1.0 * sum(log_zr) / len(log_zr) for (x, y) in zip(log_r, log_zr): xy_cov += (x - x_mean) * (y - y_mean) x_var += (x - x_mean)**2 self._slope = xy_cov / x_var if x_var != 0 else 0.0 self._intercept = y_mean - self._slope * x_mean def _switch(self, r, nr): """ Calculate the r frontier where we must switch from Nr to Sr when estimating E[Nr]. """ for i, r_ in enumerate(r): if len(r) == i + 1 or r[i+1] != r_ + 1: # We are at the end of r, or there is a gap in r self._switch_at = r_ break Sr = self.smoothedNr smooth_r_star = (r_ + 1) * Sr(r_+1) / Sr(r_) unsmooth_r_star = 1.0 * (r_ + 1) * nr[i+1] / nr[i] std = math.sqrt(self._variance(r_, nr[i], nr[i+1])) if abs(unsmooth_r_star-smooth_r_star) <= 1.96 * std: self._switch_at = r_ break def _variance(self, r, nr, nr_1): r = float(r) nr = float(nr) nr_1 = float(nr_1) return (r + 1.0)**2 * (nr_1 / nr**2) * (1.0 + nr_1 / nr) def _renormalize(self, r, nr): """ It is necessary to renormalize all the probability estimates to ensure a proper probability distribution results. This can be done by keeping the estimate of the probability mass for unseen items as N(1)/N and renormalizing all the estimates for previously seen items (as Gale and Sampson (1995) propose). (See M&S P.213, 1999) """ prob_cov = 0.0 for r_, nr_ in zip(r, nr): prob_cov += nr_ * self._prob_measure(r_) if prob_cov: self._renormal = (1 - self._prob_measure(0)) / prob_cov def smoothedNr(self, r): """ @return: The number of samples with count r. @rtype: C{float} @param r: The amount of freqency. @type r: C{int} """ # Nr = a*r^b (with b < -1 to give the appropriate hyperbolic # relationship) # Estimate a and b by simple linear regression technique on # the logarithmic form of the equation: log Nr = a + b*log(r) return math.exp(self._intercept + self._slope * math.log(r)) def prob(self, sample): """ @param sample: sample of the event @type sample: C{string} @return: The sample's probability. @rtype: C{float} """ count = self._freqdist[sample] p = self._prob_measure(count) if count == 0: if self._bins == self._freqdist.B(): p = 0.0 else: p = p / (1.0 * self._bins - self._freqdist.B()) else: p = p * self._renormal return p def _prob_measure(self, count): if count == 0 and self._freqdist.N() == 0 : return 1.0 elif count == 0 and self._freqdist.N() != 0: return 1.0 * self._freqdist.Nr(1) / self._freqdist.N() if self._switch_at > count: Er_1 = 1.0 * self._freqdist.Nr(count+1) Er = 1.0 * self._freqdist.Nr(count) else: Er_1 = self.smoothedNr(count+1) Er = self.smoothedNr(count) r_star = (count + 1) * Er_1 / Er return r_star / self._freqdist.N() def check(self): prob_sum = 0.0 for i in range(0, len(self._Nr)): prob_sum += self._Nr[i] * self._prob_measure(i) / self._renormal print "Probability Sum:", prob_sum #assert prob_sum != 1.0, "probability sum should be one!" def discount(self): """ This function returns the total mass of probability transfers from the seen samples to the unseen samples. """ return 1.0 * self.smoothedNr(1) / self._freqdist.N() def max(self): return self._freqdist.max() def samples(self): return self._freqdist.keys() def freqdist(self): return self._freqdist def __repr__(self): """ @rtype: C{string} @return: A string representation of this C{ProbDist}. """ return ''\ % self._freqdist.N() class MutableProbDist(ProbDistI): """ An mutable probdist where the probabilities may be easily modified. This simply copies an existing probdist, storing the probability values in a mutable dictionary and providing an update method. """ def __init__(self, prob_dist, samples, store_logs=True): """ Creates the mutable probdist based on the given prob_dist and using the list of samples given. These values are stored as log probabilities if the store_logs flag is set. @param prob_dist: the distribution from which to garner the probabilities @type prob_dist: ProbDist @param samples: the complete set of samples @type samples: sequence of any @param store_logs: whether to store the probabilities as logarithms @type store_logs: bool """ try: import numpy except ImportError: print "Error: Please install numpy; for instructions see http://www.nltk.org/" exit() self._samples = samples self._sample_dict = dict((samples[i], i) for i in range(len(samples))) self._data = numpy.zeros(len(samples), numpy.float64) for i in range(len(samples)): if store_logs: self._data[i] = prob_dist.logprob(samples[i]) else: self._data[i] = prob_dist.prob(samples[i]) self._logs = store_logs def samples(self): # inherit documentation return self._samples def prob(self, sample): # inherit documentation i = self._sample_dict.get(sample) if i != None: if self._logs: return 2**(self._data[i]) else: return self._data[i] else: return 0.0 def logprob(self, sample): # inherit documentation i = self._sample_dict.get(sample) if i != None: if self._logs: return self._data[i] else: return math.log(self._data[i], 2) else: return float('-inf') def update(self, sample, prob, log=True): """ Update the probability for the given sample. This may cause the object to stop being the valid probability distribution - the user must ensure that they update the sample probabilities such that all samples have probabilities between 0 and 1 and that all probabilities sum to one. @param sample: the sample for which to update the probability @type sample: C{any} @param prob: the new probability @type prob: C{float} @param log: is the probability already logged @type log: C{bool} """ i = self._sample_dict.get(sample) assert i != None if self._logs: if log: self._data[i] = prob else: self._data[i] = math.log(prob, 2) else: if log: self._data[i] = 2**(prob) else: self._data[i] = prob ##////////////////////////////////////////////////////// ## Probability Distribution Operations ##////////////////////////////////////////////////////// def log_likelihood(test_pdist, actual_pdist): if (not isinstance(test_pdist, ProbDistI) or not isinstance(actual_pdist, ProbDistI)): raise ValueError('expected a ProbDist.') # Is this right? return sum(actual_pdist.prob(s) * math.log(test_pdist.prob(s), 2) for s in actual_pdist.keys()) def entropy(pdist): probs = [pdist.prob(s) for s in pdist.samples()] return -sum([p * math.log(p,2) for p in probs]) ##////////////////////////////////////////////////////// ## Conditional Distributions ##////////////////////////////////////////////////////// class ConditionalFreqDist(object): """ A collection of frequency distributions for a single experiment run under different conditions. Conditional frequency distributions are used to record the number of times each sample occurred, given the condition under which the experiment was run. For example, a conditional frequency distribution could be used to record the frequency of each word (type) in a document, given its length. Formally, a conditional frequency distribution can be defined as a function that maps from each condition to the C{FreqDist} for the experiment under that condition. The frequency distribution for each condition is accessed using the indexing operator: >>> cfdist[3] >>> cfdist[3].freq('the') 0.4 >>> cfdist[3]['dog'] 2 When the indexing operator is used to access the frequency distribution for a condition that has not been accessed before, C{ConditionalFreqDist} creates a new empty C{FreqDist} for that condition. Conditional frequency distributions are typically constructed by repeatedly running an experiment under a variety of conditions, and incrementing the sample outcome counts for the appropriate conditions. For example, the following code will produce a conditional frequency distribution that encodes how often each word type occurs, given the length of that word type: >>> cfdist = ConditionalFreqDist() >>> for word in tokenize.whitespace(sent): ... condition = len(word) ... cfdist[condition].inc(word) An equivalent way to do this is with the initializer: >>> cfdist = ConditionalFreqDist((len(word), word) for word in tokenize.whitespace(sent)) """ def __init__(self, cond_samples=None): """ Construct a new empty conditional frequency distribution. In particular, the count for every sample, under every condition, is zero. @param cond_samples: The samples to initialize the conditional frequency distribution with @type cond_samples: Sequence of (condition, sample) tuples """ self._fdists = {} if cond_samples: for (cond, sample) in cond_samples: self[cond].inc(sample) def __getitem__(self, condition): """ @return: The frequency distribution that encodes the frequency of each sample outcome, given that the experiment was run under the given condition. If the frequency distribution for the given condition has not been accessed before, then this will create a new empty C{FreqDist} for that condition. @rtype: C{FreqDist} @param condition: The condition under which the experiment was run. @type condition: any """ # Create the conditioned freq dist, if it doesn't exist if condition not in self._fdists: self._fdists[condition] = FreqDist() return self._fdists[condition] def conditions(self): """ @return: A list of the conditions that have been accessed for this C{ConditionalFreqDist}. Use the indexing operator to access the frequency distribution for a given condition. Note that the frequency distributions for some conditions may contain zero sample outcomes. @rtype: C{list} """ return sorted(self._fdists.keys()) def __len__(self): """ @return: The number of conditions that have been accessed for this C{ConditionalFreqDist}. @rtype: C{int} """ return len(self._fdists) def N(self): """ @return: The total number of sample outcomes that have been recorded by this C{ConditionalFreqDist}. @rtype: C{int} """ return sum(fdist.N() for fdist in self._fdists.values()) def plot(self, *args, **kwargs): """ Plot the given samples from the conditional frequency distribution. For a cumulative plot, specify cumulative=True. (Requires Matplotlib to be installed.) @param samples: The samples to plot @type samples: C{list} @param title: The title for the graph @type title: C{str} @param conditions: The conditions to plot (default is all) @type conditions: C{list} """ try: import pylab except ImportError: raise ValueError('The plot function requires the matplotlib package.' 'See http://matplotlib.sourceforge.net/') cumulative = _get_kwarg(kwargs, 'cumulative', False) conditions = _get_kwarg(kwargs, 'conditions', self.conditions()) title = _get_kwarg(kwargs, 'title', '') samples = _get_kwarg(kwargs, 'samples', sorted(set(v for c in conditions for v in self[c]))) # this computation could be wasted if not "linewidth" in kwargs: kwargs["linewidth"] = 2 for condition in conditions: if cumulative: freqs = list(self[condition]._cumulative_frequencies(samples)) ylabel = "Cumulative Counts" legend_loc = 'lower right' else: freqs = [self[condition][sample] for sample in samples] ylabel = "Counts" legend_loc = 'upper right' # percents = [f * 100 for f in freqs] only in ConditionalProbDist? kwargs['label'] = condition pylab.plot(freqs, *args, **kwargs) pylab.legend(loc=legend_loc) pylab.grid(True, color="silver") pylab.xticks(range(len(samples)), [str(s) for s in samples], rotation=90) if title: pylab.title(title) pylab.xlabel("Samples") pylab.ylabel(ylabel) pylab.show() def tabulate(self, *args, **kwargs): """ Tabulate the given samples from the conditional frequency distribution. @param samples: The samples to plot @type samples: C{list} @param title: The title for the graph @type title: C{str} @param conditions: The conditions to plot (default is all) @type conditions: C{list} """ cumulative = _get_kwarg(kwargs, 'cumulative', False) conditions = _get_kwarg(kwargs, 'conditions', self.conditions()) samples = _get_kwarg(kwargs, 'samples', sorted(set(v for c in conditions for v in self[c]))) # this computation could be wasted condition_size = max(len(str(c)) for c in conditions) print ' ' * condition_size, for s in samples: print "%4s" % str(s), print for c in conditions: print "%*s" % (condition_size, str(c)), if cumulative: freqs = list(self[c]._cumulative_frequencies(samples)) else: freqs = [self[c][sample] for sample in samples] for f in freqs: print "%4d" % f, print def __eq__(self, other): if not isinstance(other, ConditionalFreqDist): return False return self.conditions() == other.conditions() \ and all(self[c] == other[c] for c in self.conditions()) # conditions are already sorted def __ne__(self, other): return not (self == other) def __le__(self, other): if not isinstance(other, ConditionalFreqDist): return False return set(self.conditions()).issubset(other.conditions()) \ and all(self[c] <= other[c] for c in self.conditions()) def __lt__(self, other): if not isinstance(other, ConditionalFreqDist): return False return self <= other and self != other def __ge__(self, other): if not isinstance(other, ConditionalFreqDist): return False return other <= self def __gt__(self, other): if not isinstance(other, ConditionalFreqDist): return False return other < self def __repr__(self): """ @return: A string representation of this C{ConditionalProbDist}. @rtype: C{string} """ return '' % self.__len__() def __repr__(self): """ @return: A string representation of this C{ConditionalFreqDist}. @rtype: C{string} """ n = len(self._fdists) return '' % n class ConditionalProbDistI(object): """ A collection of probability distributions for a single experiment run under different conditions. Conditional probability distributions are used to estimate the likelihood of each sample, given the condition under which the experiment was run. For example, a conditional probability distribution could be used to estimate the probability of each word type in a document, given the length of the word type. Formally, a conditional probability distribution can be defined as a function that maps from each condition to the C{ProbDist} for the experiment under that condition. """ def __init__(self): raise AssertionError, 'ConditionalProbDistI is an interface' def __getitem__(self, condition): """ @return: The probability distribution for the experiment run under the given condition. @rtype: C{ProbDistI} @param condition: The condition whose probability distribution should be returned. @type condition: any """ raise AssertionError def __len__(self): """ @return: The number of conditions that are represented by this C{ConditionalProbDist}. @rtype: C{int} """ raise AssertionError def conditions(self): """ @return: A list of the conditions that are represented by this C{ConditionalProbDist}. Use the indexing operator to access the probability distribution for a given condition. @rtype: C{list} """ raise AssertionError # For now, this is the only implementation of ConditionalProbDistI; # but we would want a different implementation if we wanted to build a # conditional probability distribution analytically (e.g., a gaussian # distribution), rather than basing it on an underlying frequency # distribution. class ConditionalProbDist(ConditionalProbDistI): """ A conditional probability distribution modelling the experiments that were used to generate a conditional frequency distribution. A C{ConditoinalProbDist} is constructed from a C{ConditionalFreqDist} and a X{C{ProbDist} factory}: - The B{C{ConditionalFreqDist}} specifies the frequency distribution for each condition. - The B{C{ProbDist} factory} is a function that takes a condition's frequency distribution, and returns its probability distribution. A C{ProbDist} class's name (such as C{MLEProbDist} or C{HeldoutProbDist}) can be used to specify that class's constructor. The first argument to the C{ProbDist} factory is the frequency distribution that it should model; and the remaining arguments are specified by the C{factory_args} parameter to the C{ConditionalProbDist} constructor. For example, the following code constructs a C{ConditionalProbDist}, where the probability distribution for each condition is an C{ELEProbDist} with 10 bins: >>> cpdist = ConditionalProbDist(cfdist, ELEProbDist, 10) >>> print cpdist['run'].max() 'NN' >>> print cpdist['run'].prob('NN') 0.0813 """ def __init__(self, cfdist, probdist_factory, *factory_args, **factory_kw_args): """ Construct a new conditional probability distribution, based on the given conditional frequency distribution and C{ProbDist} factory. @type cfdist: L{ConditionalFreqDist} @param cfdist: The C{ConditionalFreqDist} specifying the frequency distribution for each condition. @type probdist_factory: C{class} or C{function} @param probdist_factory: The function or class that maps a condition's frequency distribution to its probability distribution. The function is called with the frequency distribution as its first argument, C{factory_args} as its remaining arguments, and C{factory_kw_args} as keyword arguments. @type factory_args: (any) @param factory_args: Extra arguments for C{probdist_factory}. These arguments are usually used to specify extra properties for the probability distributions of individual conditions, such as the number of bins they contain. @type factory_kw_args: (any) @param factory_kw_args: Extra keyword arguments for C{probdist_factory}. """ self._probdist_factory = probdist_factory self._cfdist = cfdist self._factory_args = factory_args self._factory_kw_args = factory_kw_args self._pdists = {} for c in cfdist.conditions(): pdist = probdist_factory(cfdist[c], *factory_args, **factory_kw_args) self._pdists[c] = pdist def __contains__(self, condition): return condition in self._pdists def __getitem__(self, condition): if condition not in self._pdists: # If it's a condition we haven't seen, create a new prob # dist from the empty freq dist. Typically, this will # give a uniform prob dist. pdist = self._probdist_factory(FreqDist(), *self._factory_args, **self._factory_kw_args) self._pdists[condition] = pdist return self._pdists[condition] def conditions(self): return self._pdists.keys() def __len__(self): return len(self._pdists) class DictionaryConditionalProbDist(ConditionalProbDistI): """ An alternative ConditionalProbDist that simply wraps a dictionary of ProbDists rather than creating these from FreqDists. """ def __init__(self, probdist_dict): """ @param probdist_dict: a dictionary containing the probdists indexed by the conditions @type probdist_dict: dict any -> probdist """ self._dict = probdist_dict def __getitem__(self, condition): # inherit documentation # this will cause an exception for unseen conditions return self._dict[condition] def conditions(self): # inherit documentation return self._dict.keys() ##////////////////////////////////////////////////////// ## Adding in log-space. ##////////////////////////////////////////////////////// # If the difference is bigger than this, then just take the bigger one: _ADD_LOGS_MAX_DIFF = math.log(1e-30, 2) def add_logs(logx, logy): """ Given two numbers C{logx}=M{log(x)} and C{logy}=M{log(y)}, return M{log(x+y)}. Conceptually, this is the same as returning M{log(2**(C{logx})+2**(C{logy}))}, but the actual implementation avoids overflow errors that could result from direct computation. """ if (logx < logy + _ADD_LOGS_MAX_DIFF): return logy if (logy < logx + _ADD_LOGS_MAX_DIFF): return logx base = min(logx, logy) return base + math.log(2**(logx-base) + 2**(logy-base), 2) def sum_logs(logs): if len(logs) == 0: # Use some approximation to infinity. What this does # depends on your system's float implementation. return _NINF else: return reduce(add_logs, logs[1:], logs[0]) ##////////////////////////////////////////////////////// ## Probabilistic Mix-in ##////////////////////////////////////////////////////// class ProbabilisticMixIn(object): """ A mix-in class to associate probabilities with other classes (trees, rules, etc.). To use the C{ProbabilisticMixIn} class, define a new class that derives from an existing class and from ProbabilisticMixIn. You will need to define a new constructor for the new class, which explicitly calls the constructors of both its parent classes. For example: >>> class A: ... def __init__(self, x, y): self.data = (x,y) ... >>> class ProbabilisticA(A, ProbabilisticMixIn): ... def __init__(self, x, y, **prob_kwarg): ... A.__init__(self, x, y) ... ProbabilisticMixIn.__init__(self, **prob_kwarg) See the documentation for the ProbabilisticMixIn L{constructor<__init__>} for information about the arguments it expects. You should generally also redefine the string representation methods, the comparison methods, and the hashing method. """ def __init__(self, **kwargs): """ Initialize this object's probability. This initializer should be called by subclass constructors. C{prob} should generally be the first argument for those constructors. @kwparam prob: The probability associated with the object. @type prob: C{float} @kwparam logprob: The log of the probability associated with the object. @type logprob: C{float} """ if 'prob' in kwargs: if 'logprob' in kwargs: raise TypeError('Must specify either prob or logprob ' '(not both)') else: ProbabilisticMixIn.set_prob(self, kwargs['prob']) elif 'logprob' in kwargs: ProbabilisticMixIn.set_logprob(self, kwargs['logprob']) else: self.__prob = self.__logprob = None def set_prob(self, prob): """ Set the probability associated with this object to C{prob}. @param prob: The new probability @type prob: C{float} """ self.__prob = prob self.__logprob = None def set_logprob(self, logprob): """ Set the log probability associated with this object to C{logprob}. I.e., set the probability associated with this object to C{2**(logprob)}. @param logprob: The new log probability @type logprob: C{float} """ self.__logprob = logprob self.__prob = None def prob(self): """ @return: The probability associated with this object. @rtype: C{float} """ if self.__prob is None: if self.__logprob is None: return None self.__prob = 2**(self.__logprob) return self.__prob def logprob(self): """ @return: C{log(p)}, where C{p} is the probability associated with this object. @rtype: C{float} """ if self.__logprob is None: if self.__prob is None: return None self.__logprob = math.log(self.__prob, 2) return self.__logprob class ImmutableProbabilisticMixIn(ProbabilisticMixIn): def set_prob(self, prob): raise ValueError, '%s is immutable' % self.__class__.__name__ def set_logprob(self, prob): raise ValueError, '%s is immutable' % self.__class__.__name__ ## Helper function for processing keyword arguments def _get_kwarg(kwargs, key, default): if key in kwargs: arg = kwargs[key] del kwargs[key] else: arg = default return arg ##////////////////////////////////////////////////////// ## Demonstration ##////////////////////////////////////////////////////// def _create_rand_fdist(numsamples, numoutcomes): """ Create a new frequency distribution, with random samples. The samples are numbers from 1 to C{numsamples}, and are generated by summing two numbers, each of which has a uniform distribution. """ import random fdist = FreqDist() for x in range(numoutcomes): y = (random.randint(1, (1+numsamples)/2) + random.randint(0, numsamples/2)) fdist.inc(y) return fdist def _create_sum_pdist(numsamples): """ Return the true probability distribution for the experiment C{_create_rand_fdist(numsamples, x)}. """ fdist = FreqDist() for x in range(1, (1+numsamples)/2+1): for y in range(0, numsamples/2+1): fdist.inc(x+y) return MLEProbDist(fdist) def demo(numsamples=6, numoutcomes=500): """ A demonstration of frequency distributions and probability distributions. This demonstration creates three frequency distributions with, and uses them to sample a random process with C{numsamples} samples. Each frequency distribution is sampled C{numoutcomes} times. These three frequency distributions are then used to build six probability distributions. Finally, the probability estimates of these distributions are compared to the actual probability of each sample. @type numsamples: C{int} @param numsamples: The number of samples to use in each demo frequency distributions. @type numoutcomes: C{int} @param numoutcomes: The total number of outcomes for each demo frequency distribution. These outcomes are divided into C{numsamples} bins. @rtype: C{None} """ # Randomly sample a stochastic process three times. fdist1 = _create_rand_fdist(numsamples, numoutcomes) fdist2 = _create_rand_fdist(numsamples, numoutcomes) fdist3 = _create_rand_fdist(numsamples, numoutcomes) # Use our samples to create probability distributions. pdists = [ MLEProbDist(fdist1), LidstoneProbDist(fdist1, 0.5, numsamples), HeldoutProbDist(fdist1, fdist2, numsamples), HeldoutProbDist(fdist2, fdist1, numsamples), CrossValidationProbDist([fdist1, fdist2, fdist3], numsamples), GoodTuringProbDist(fdist1), SimpleGoodTuringProbDist(fdist1), SimpleGoodTuringProbDist(fdist1, 7), _create_sum_pdist(numsamples), ] # Find the probability of each sample. vals = [] for n in range(1,numsamples+1): vals.append(tuple([n, fdist1.freq(n)] + [pdist.prob(n) for pdist in pdists])) # Print the results in a formatted table. print ('%d samples (1-%d); %d outcomes were sampled for each FreqDist' % (numsamples, numsamples, numoutcomes)) print '='*9*(len(pdists)+2) FORMATSTR = ' FreqDist '+ '%8s '*(len(pdists)-1) + '| Actual' print FORMATSTR % tuple(`pdist`[1:9] for pdist in pdists[:-1]) print '-'*9*(len(pdists)+2) FORMATSTR = '%3d %8.6f ' + '%8.6f '*(len(pdists)-1) + '| %8.6f' for val in vals: print FORMATSTR % val # Print the totals for each column (should all be 1.0) zvals = zip(*vals) def sum(lst): return reduce(lambda x,y:x+y, lst, 0) sums = [sum(val) for val in zvals[1:]] print '-'*9*(len(pdists)+2) FORMATSTR = 'Total ' + '%8.6f '*(len(pdists)) + '| %8.6f' print FORMATSTR % tuple(sums) print '='*9*(len(pdists)+2) # Display the distributions themselves, if they're short enough. if len(`str(fdist1)`) < 70: print ' fdist1:', str(fdist1) print ' fdist2:', str(fdist2) print ' fdist3:', str(fdist3) print print 'Generating:' for pdist in pdists: fdist = FreqDist(pdist.generate() for i in range(5000)) print '%20s %s' % (pdist.__class__.__name__[:20], str(fdist)[:55]) print def gt_demo(): from nltk import corpus emma_words = corpus.gutenberg.words('austen-emma.txt') fd = FreqDist(emma_words) gt = GoodTuringProbDist(fd) sgt = SimpleGoodTuringProbDist(fd) katz = SimpleGoodTuringProbDist(fd, 7) print '%18s %8s %12s %14s %12s' \ % ("word", "freqency", "GoodTuring", "SimpleGoodTuring", "Katz-cutoff" ) for key in fd.keys(): print '%18s %8d %12e %14e %12e' \ % (key, fd[key], gt.prob(key), sgt.prob(key), katz.prob(key)) if __name__ == '__main__': demo(6, 10) demo(5, 5000) gt_demo() __all__ = ['ConditionalFreqDist', 'ConditionalProbDist', 'ConditionalProbDistI', 'CrossValidationProbDist', 'DictionaryConditionalProbDist', 'DictionaryProbDist', 'ELEProbDist', 'FreqDist', 'GoodTuringProbDist', 'SimpleGoodTuringProbDist', 'HeldoutProbDist', 'ImmutableProbabilisticMixIn', 'LaplaceProbDist', 'LidstoneProbDist', 'MLEProbDist', 'MutableProbDist', 'ProbDistI', 'ProbabilisticMixIn', 'UniformProbDist', 'WittenBellProbDist', 'add_logs', 'log_likelihood', 'sum_logs', 'entropy'] nltk-2.0~b9/nltk/olac.py0000644000175000017500000000211611327451603015020 0ustar bhavanibhavani# Natural Language Toolkit: Support for OLAC Metadata # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT from StringIO import StringIO def read_olac(xml): """ Read an OLAC XML record and return a list of attributes. @param xml: XML string for conversion @type xml: C{string} @rtype: C{list} of C{tuple} """ from lxml import etree root = etree.parse(StringIO(xml)).getroot() return [(element.tag, element.attrib, element.text) for element in root.getchildren()] def pprint_olac(xml): for tag, attrib, text in read_olac(xml): print "%-12s" % tag + ':', if text: print text, if attrib: print "(%s=%s)" % (attrib['type'], attrib['code']), print def demo(): from lxml import etree import nltk.data file = nltk.data.find('corpora/treebank/olac.xml') xml = open(file).read() pprint_olac(xml) if __name__ == '__main__': demo() __all__ = ['read_olac', 'pprint_olac'] nltk-2.0~b9/nltk/nltk.jar0000644000175000017500000003171111144536574015211 0ustar bhavanibhavaniPK½lê8 META-INF/þÊPKPK½lê8META-INF/MANIFEST.MFóMÌËLK-.Ñ K-*ÎÌϳR0Ô3àår.JM,IMÑuª ˜êĘ+h—æ)øf&åW—¤æ+xæ%ëiòrñrPKIgݵGGPK½lê8org/nltk/mallet/RunCRF.class•WYcWþ®µÌh<¶l9rª¤!JšEvb« Å9-M»ˆÊKc§TIÓ2–®U9ÒHÕŒÜ8mHI ´”½e„%ì4v%MÂZ(k²Cùü€sglKÆ[£‡{îœ9û6G/ÿç…ËnÂ?¼ øÐƒw*ØMãRÒ ¼x@pB<(h³>ìŤŒ£â!'#/AWЊ‚Œ¢€I( h(ÂGY”€+ᘂi—ðˆ‚M(HxTÁfœð.!-xL`Þ-ŽSâx\Áiœ‘ðÁýD#"8%á½2Þ'Ìz¿Œ'|JÁ.aÞð´ ø ðŒLþ>,ŽHø¨‚áã žÁ³BÑ'd|RÁ§ði Ÿ‘ðYŸ“qVÆçòí 2ÎÉø¢„/Éø²„¯ˆ°œ—ðUo®ÉðÆĤ6¥EËf6¸¬ž‰&ìw½ ¾ñœ¦ÍeuΪ#,ñ ?ÑL“—t¢óç ižÈæøpÑÌt·x`PãºÎK}9Í0¸Á°'ÁÓåh9OÑ”Ík¹7£ãšÁ¹}…|^ÓÓŽmB o6¹aÖËnNÕ“‘Xw"k˜×*]ðtïÞ¬ž5ocpE:î!Q}ä 9” Ÿ‡Êùq^ÓÆ…'î¼–%Ýí‘ÃN(0™è¨Y¢xõ F¥ÿXŠÏ$ïMåæÄ*£…r)Åp4(ë}º…qŠSñ5|]Å7ðM†­5áq2?£åö•2å<×Íù ëÿß‚;ÊÙ\ZdÓPçÇŠ$qˆˆ´a;a[‰àK°A6Äbnó°˜×Õ#µ´ôÈ3hè´àêñ}—Ï"BÏn î*<É*¼É ¤€lÁ×ôí!`A© ñÊ ÔçÑôÖÕQš7þ¥oZÄ›*Z“ÎÛ 1%¤Ú,¬‹5VL†”P£…ö ÖÇÔjẫżâ¶!äµ°1æ y\®¯bSÒÑ]Á«Øœ y*ÇÜ!R´%¹{[ÝÜ`a›«§)Ø" ¶ŸÃ™;Øda‡…W‰5‡¼¡f 1È ´6Ûà,:ªØµàõnrW°uºéò[ˆÚÞßxÅÕÓl™c:‡3¸)ä¶\žXx£…›O·°óÿ}ðÞtº‰î×Rô ¶Ó‡›²î£œ«”u?†À0Ú)ã›1Š”ñ.Êö]Ä1F©Bør}˜`€n{1@AóÑߨ ˜ïPK©4‰2ÄPK½lê8-org/nltk/mallet/CRFInfo$WeightGroupInfo.classuPÑJA=ãÚnn[n–™ÐK!dm=½† õ<Ú¸Ž®³1;ýVôÐôQÑ z¸wΙsçž{¿¾?>\ ^‚ƒÐGÛv<ì1_†JoÆŸy”pGC£¥Š/ªÁM®ÅP$bldª"/ ®™Ê¬qÎPï¥:ŽTbæÑ‚'‰0Q{ÐéªIJÅî•TÒ\34WªNîÉ@;}$åžTâ6_Œ„¾ã£„˜æz,:Ò‚à·äÌÚ °ŽR5û Ç+¾o<OÍNó'‹©É?&è*%t;áY&2†ðo ýÑŒ†fpšÖemEÒF‹´\ kpéÆàÙ†ÖåÓ,Ø :Bà·NßÀZï(¼.5›]Ê@ŸÞ·(PFÕ*‰uPÁ.ØPKH·´0ÉPKkê8 org/nltk/mallet/TrainCRF$1.classuŒA Â0Eÿh5Z+v)®\¸6ô Å‚ Ô ÄjkL ‰Î…ðPbЏt†ùŸóþëýxÈ00†˜ïÍÍ–²¨•$ŒVÔ:ßËFÜa²Ò¥2®ÖÕFú³91Œ3c+®•¿ð«PJzþƒ„d­µ´¹ÎIGHÛ*®„®øöØÈÒ¦ÿ0|ßé†EHzÁ#ôƒÃÅá‡Ú”>PK8“Åp ÏPKkê8(org/nltk/mallet/TrainCRF$MyHandler.classuQÁRA}³!Ùd]H FAVDM<¸Å•PˆVE±rŸqÌX³»Vñ7úxÀ*~€eÑ¢P™CwOO¿~=¯ÿüýõÀZ|4”Ѩà.æÊ˜p ÎÜ÷±äc™¡´!µÌ^0𭆩ކjWjñ6ö…Ýç}EÿKÞW2ýÈ5»ŸøWç™T±2I"uwM²'´[>"0„¯µ¶£xšŠ”a¹Ù56‰µÊ>ÇC®”Èâ}Ë¥îìm­®µyÐ3¹=[ÒNÿ{|îØBTøxb*/_m¾ß^vw}¬†xŒ'!žº‚&ÃÊD–7G;\”°ÔàJ]ÿNÇèÔ(ñ¿daòä µ^qÂõŽÒL ILa Õ¸Jšø•:ëeVða›aö†´SØÝ”&ÜXá‹–¥“‘>Å*w˜Ÿ4 "Úxîà9Éh ·è¶Džv‚ⳟðN1ÚÙÒ(yLv3ãÒ˜€ùêêSg(’?Cé4C½âùFñ÷ˆ§~…Çó°ë<fÉ{¸ƒÛäëU±ÔÊ4ö" 8PK Н¼PK½lê8org/nltk/mallet/TrainCRF.classÅX{|S÷uÿ|¥ë ƒi!EØ–Eh ©’cƒˆ0`Ëárm]Ûôâê Ò¤¯$ëºG·¶4kÒ­]ÚÕkº´)0™DIHוô•õ½­ë¶vïuݺG÷h·µÙ9W˶ æ¯áçþ~çw~çwÞ¿óÓöüKn¡GUœÅ9ïW±+ø€ Ωxðë^܃ßPñ!|XÁoªPñ”‚xðQ¿…©Ð0)ÈßVðqËñ´BF¿#£gdôI}ªÏâÓ8 .*ø]/ÖaJÀ%Ï x^@QÀ ²óEÙù’€Ë2}YFŸ‘ÑïÉè³2ú}}NFW¼¢àó*B8çÁäûE_ðemxUhÿ@ÁW„ük^lÇ×U|ߔєà¾%à…êdôÇ¢ö·ü‰hømßQñ§ø3®¢[0Ýø®)|OÁ_¨Øƒs2ùK§ñWþZÁßxð·*¢rt çEïÁ÷eòü@Á?ªˆáqþI¾?TðÏòý•%øWÃø7?¿«Lþ þSÁ©8ŽÇUÃüD(^•É øÿëÁOüLÅ8~,à5Ä‚#Rh‘J‹É¥ÛC R<äQ‘!¯‡T5*¤yh‰BKYrZ¦Pa™méfº×L²¶™I\2!h‘tÚ°º“z.gäÛ¢F"ΧxÉ…Sz2iØáa=g„ó¶™ wgR)=(1Ù$,:™y*“0’µÌ—ŽÔÒ1_WÔÌÙ×Ë^ö0û›D8Ëá´ÛÌéÃI#A £„†;Ì´ißIXÜr˜éf9Xœ¨™6úò©aÊ 1Á;bºmt÷÷¢Áè ý´63aGühÆ §“öÉŠ4LIf:·Ì/ëhÎ!»•…S{&FŒ’– -'4'õa#Ùa«Ž8Ø›#œŸ—ý@ÖÈ…»’ÙqÞhw^‹0’ÎÙzzÄplSÒ$©§Ç¶e¦Ç:· ±]\)v6aUp¨Åa…š ž;F’eã©™¼5b”âaIL…•k—Åc¬T­Åú =aX¼B+ZIˆ]ÕLf*›4búؘamª iÛ`E¶õ²kò–q˜í•a쩼` - 'VȧÑ*Z­Ñòk´–Ö)´^£èu„Öë°§èt#áöy·dͬ6mÃÒùô°„Ù+“ÏFÊ‚ûؽ¹–M 4Ú@…ßMm¢› ®LBýz6SP£-Ô¢Q+µi¢v´U¡[4ÚFoPèV¶Ón£Û™í<Ñ»iÀ怗Gu¬ÑiFrD§€;h§Fw ¸‹Þ¤QíÒ¨[ŽÝ- ‡z5Ú#£½2ŠÈhŸŒî–Q”8›ÖNÇW>m›)£š„Õ³ƒoWÞLrìÖ ¦‰,;ÑHr"C+?"®eÓí§>4:H‡Ä,ý ÐVÂæù´=b˜cãvÉŽÎËfa4ŠIh JhÖèÝ£QœŽj4D÷jtŸèsŒŽkt?é\M8‰4¦…bçök„Q9.+©Ñ(ix!¬™¶BW¥ŽõXV†Sè­x»Fãdjt‚V6NSFø€1=ÙeåSû5F]Vc:Ý hxkt’’ìžÊJ(äüÐ('l 7íÑð<<“©ÚÓŸs¾SˆÏšÙðQ3[¾#F¬ÑÉVlŸH%5J‘øvWh”¡¬øì”FqM´5ÊÓi ?wn˜s¼ÍÛ†žªØª\hj—„ã®hsö÷¤më&ˆÁYzP£7KÁZ";úµç$ÞÖVø>ÁêÏàLÛï-xL£·ÊÞ5³5«TD…Þ¦ÑÛiaëõ^’\d§ýÁÅWìÝ×µŸm~SlÜ6­§x±6#bN˜@B·õvQþÌbÚa„„ìc¡ÒáÐÐlgɤí2ZïµW” œ1“É€žHf˜*`gX3ç0i¿NMKwü ç–EN±òÝ~µ:<—‘èˉÓ4%·Uâ§}»7“O'j’cÅô–ìòYW¾ô [|kq^^ýB8È€ÎvŒÒ°Z™#—cÓóM<*§-«,V.z¶Hð*Ƭ®€s0aŒêù¤•ö‚oä¹;—Dz#4&3™“RÆç}0:ÛÔ["Ìï*mKUsÇš«§óÒ2ÆŒ‰ðAÝæK‘m¯p'˜u"6¬ÓŒDçÙØ9MÏ^+µó;6^ÌÁLØy-™«×·Is¨\ߎ&w/Hç…Ú¥iLÏs$èéúeêN_ÃW+Öp¬ì)¯´ÌŒ5½ì îv6rHçy§‡Œ= Æ÷o)Èæ¬¸‚Ù³$W¹çK‰ØTc¯r+½t&†+‘Yí]Vk½Q5J¥û›e¿Žë¹>cÂvzoi9ÓÎde…Im,É*W7;é8MÛc9Û;j¦+㦄‘³y*Jõ1-?VÔi^¹:ýu™¤ñŒsË—g+‡êîðp\8áPܽ»Î®z¨º½4›Ô±ú@¥}!ø{£Ñøñî}}=ݱžÝÇbýƒÝ±Á~®ö7VdÉõf,'•så§‚‘èrž [¯ùT˜ñ ÖìíŠöÖ?ï¦ÚóöêÉѺgnŒííïé9~h°«?ÖÓ?PŸUk-«Ø¸e‡òºÅñP—eó®H´kWOt –ņZ»ÌºôlÖH'®Z.æv”ìæõœWYöØ™jvŒVãÄWï@6ïŠ33[Érrq-wªÇtQõVšÀdéí×/EŠ^hïXÞÆ=^é½ÚkeReùëxAÎW%¯yÝÌDžö¬f©•ÎÕ»,*f*—¦¹šlF®ûT§–¥ô‰R±)ý~àv:"‹חÿÄÒ?Tj´aõœÖ“y§Fœ2™3ÏNÍd“5:1TbJh“^  ¸ì§{«Íì@ßÌ\©–é¥ÙRëRýÙ¦þ/,ºšå¡=˜ÓÇø·èÈߥ3Û,N:>ÍiÐE»«ßþµí¼HRI¯ÊË œxKõÔš ÌbM°î‚D—ÂÜ*ŽØWig²¯øÃ…ÈÚ%*û§ùÕ¾†Oå6,+»n~a™Ø}Æ2å6ró\IfrFyk£³XéwëvfÂb4™Ïó×ÙÊi:Måü”Çžã Õ´¤ª–ŸœRxó¼p²–¥C\çg¤¹˜£³~5[À´»ŽÛ‡®çG@æ‚ Há,ÖÊc—Gkå5ë|ù 4­‘§6ÏÝ ùÅŠáÏñìþ’`[¦@Ÿæá äZxåMŠEBºh5ϼLð`‹âL¾x®øÜ}!W ¡”Ö‹ðà=R„M¡±Ãåw¡ÅÛš—\ÂR×–ÐTÄòxˆ¦ÐÜáö»yÇŠV:_«ø³ú öËç9¬!t4øžƒŸðl—ÑZÂËXסøÝ~å"Ö ¸AÀëÜ( `C'_{e§˜ÓMç±éÃXÂ[XΛ'q_ ÷ú*nó$"%\°ŠÛ2‰í%\K×:‰ E´Å‹±ÞíÍá¶ M·°m o¸Ì³[¯`Ÿ|滈_Àô6¶Ë%ÜÞáá•ÞX@‡ŸØÉâË©DÄÜî(`ç¿ûÙªƒÞ… ;Ø!wb9º° =ŒÙ‹6Ü»pû0À1Ü‹#Á=H#Ž åÂ$ŽáiÇ3¸SÐñ ãKLõ-$ð]ŒâûÃ0ŽŸâ­Cš6"C!dé6œ¢näh§igÈă4‡ð NÜ~‘ÿÿ~¹YäeœÆ+Ï´pç—ÿ>‡»u¸ïhð5´ð¦§p²Í×P@ÛC)`×Ëèîð¸vx}^1Àî§ÐÛê÷ø¼®D=U;TgZž4žGïðùÔ'áñ5~Eìa§ì½ìwûÔm¾FWì/M¾Vx¤á{BWpskHö5,Þ¡ø‘é)43mÃ6ŸÂ´ Sý ÖÀûÐÌðQ΄ÇÐÄ ­gì&Άv6}'«¼Õ=ÂêfXá Æ=Ì4à½Lõ>¼çð$Þò÷i<ÁNz7~¥”Eô>N@•¿M=HKûv®oyéCØÌ3±»‹ˆVãk?ÇWËúm ðê»|.âà+XYCÜ|¨´Ò?we@VŠˆÅK«Sì 5.àHgå=ñÄ4t´Ã‰î¡w¾àéh(!ýì–{”Žó»/á¾¥ˆc,Øñ*¯û9t8l=E Ç›Oa¤€„ß# qßhye¬´RÄxÜÙáõ{ÅÛ'üìÉ“eÊ’ÓµÇb[a{}Œ-õq¶þÓXOàV|;ñ)ìÆ³èÃy¶þ䋨—pϱžgñ8^àÝ—™ê³¸‚WðM|ßÁð=|?Ä«ø ¾ÂÅí«äÆ×HÅ×i¾Q á_…ï©LJ¥Þñ¿£+* ý×$\‹/ ·ˆLœ‘ÍÙæS.j¶¸šå.à@k;Ø| Û_Äéxó÷‹˜ˆ/f‹Ä]lBvֺ͙™Ï—Š»lÆÍ¬i+[ཌ[Äõ[8r2kof©Nó÷mû5'¾ÖqŠ˜Óvþ~ôPKÇM` ïPK½lê8'org/nltk/mallet/CRFInfo$StateInfo.classuQMO1}YV\¿?QЉ«‰7 ‘„`"Æ‹§.¨–n²[õwy &üþ(㔬у:ï½æÍt¦óùõþàÕ<2XqŪek6¬ç°™C…aJó¡`˜o=ðgî+®û~ÇDR÷O RK#¹ª‡±a` ùžÔ?Ú»±!id¨ÛT$f(ÝýWÆQ<*±^„ìL¢ÊwÿfdÍ@ÆÕ#†•Võ}­Ì£?äJ ãׯMÝ ­é̶wΰ¾7ѵKÖÃ{šp¶%µh? Ýð@ÑÓ Ÿ¢®hH+Ü$åжãbó.JØq‘‡ã¢'‡]†Ê„‡ªðŒþèw›Z‹¨®xÛq½ßY¯‚Ñ¥_LïÙ—&ÔE…v•¥-æÀèLc¶£1t13Æb‚³ðϳ3Œ5íeŠ ¤NFа\;ÕÞªGHÛ0õ:v/R,’ hÐK—Ä›”±D:…el:äÊ`[`ßPK]˜òãpdPK½lê8org/nltk/mallet/CRFInfo.classµY |ÔÕñŸÙl²¿l~° в(BÈA•K“@J”ÛMò#,nvãÁoð¾ð.©Á$F<°í_ëÕZ«¶¶Þ÷YoQìwÞï·G6¥þÛðqÞïͼ73ofÞ̼õ‰½÷?DD‡Ù2´•geR;ëúiÜ_Æl9p (`€\ƒ 0T@ž€ýì/`˜€œ| àvò>(‹æ‘Nqò(-¸|c²ÈÍY\ÈEŠ<6‹KøPºœÔȇɖquËx¸F÷Ëx„FÏj|¤ÐÇk|”ÆGk|ŒÆ^Æ4ž/ã9ØB9x±“æñÙ¼4_'jì‘IƒL5n’ÑÐx™ ›5^.;½^!ø“øÜ"£_ã€Æ­Ÿ,gŠ˜ÆaÙqð)N ð’LHX)c›Æ«4>UδZ˜®qòi¼ÖÁë4>]ã32Ö}JêÏÉgòYböõÂ÷lÏÑø\'ŸÇç ²@À_˜Eå¼^ã oÔø"Yz±Æ—h|©Æ—9ùr¾Bã+EÖU_­ñ5b£M_›Å×ñõnÐøFouo–õ¿Öx³Æ·h|«Æ·i|»ÆíÿFã;4¾Sãßj¼ÅÁwÉêÉ™|7o•¯mN¾G¢g;ïÓß+ ÃÁLÙÇÞæåáiÁ@¤µÊ¿,À¤WùýF°Üç …ŒSf]Ø6LR†/ÐÜl™ö«^á9ÅS {}%‚óú›Kªm"SZ$èer™K|êÂA¬)§Ù …¼ÿ_`¥Ñ„Ó.‹ø|«ÊðDcX výBQOT{C`›“ÀJ0ÅYÞf¿Ç4БIäIÕ`s‰ß>©¤‚ŒpIym¥póðÄ)à1peψ0…MÜgnI¥x愃¨ ¾ƒGëWµB½Ì–@“á«ôúð=p™¡”žj„qÞ@°ÆÓl?OSSXPéÇ”…y…¿ÉšeLòú½á)L¹ù½#mÌ<&{9 À«½~£&ÒÒ`ë= "ÍYÑÖh´ªØrp—ƒïsp7Ük±ñJD)ÅbhWåo@‘ áiQ¤ôVO0d¨%bˆ•‡7–4ZJ*|F‹á«%N,6µ•k›0MècÏ>8çFõbN¯u=&¦f·ÜDËá—à³%"Åtܪqr ¸›°é×1}1K‘02±«4ŒYCßçìíýTœô§*?â¹`Ÿ™ábgÇöN DT íûv¤·lI¥ÁpEKkxU½ÑV1›¸½‘ªœÑ‚dETLÊuàè´Ö©£¤\$)ÓZTø ¿… LטzJµ"•5b‹uè²UõžfÓ³SöÝC©ÖáVùVD$ÛŒNq§«R*âhò†Z}žU=.ël/«½ÅãÅùç/L•*|?r÷¤FŸ•PÒòÕí­ D‚†™ t+®ÇÊvþʤäcuê Nºè>ÌféÔ-³ûi§N x@Àïè÷:ýþO§G…úýQ§Ç ÑoÒ[~Iç—a_þƒ’Š÷-ý+“J{¿éß²Ø\§‡h':úåAØAõ3‚ª-‰ŠVæÎw!zøï⧃#!ÃâÝ“”2SBEn ìn„—3øÙŠ ¡ý ‹>Ô¹œÐfÉðz|å9ægô/=î™Ë¼~ ËM‚È ÇK’NoÓ;¸Aq”Îÿà¨ɮÐùUnÑù5neS\N_ç7À¼É…!Ð štŸÙ&¦- êü&¿¥Ó3bÿ¬„øÐé=zydëÌjRHç·åà™ÑÈ€@78ïèü.¿çà÷uþ€?dרl1*ÁO8!ÂûK„#¢#†b‚ø,ôú—9ø#?æOtþTnâgqû$$bÎí`üsÞ­óü%LS*‘ã€6pÃ/¢ræLô¡ÞVŸ!ßnüú8ÛWŽ,g‚%Ü ñ½ØÁ_ëü\·!nw´?‡³Aw»'«“‹ëv[ ®[yÀ­þ&»GƒËh¿ã=Lcn·Ùò¸G¹º ×¦øsð÷:ÿÀ{uþцóÛ…!¸-ÔéGÜw‹VÜùVºoˆ›[äVÇð9¨ú+ž¢¯”œ írŠÜ‰©c2Žm+ÂÛbl ÕÌhp›äTªBw~4B&—`k ¶é#º6›nK³Ùuf¶9lé[†ŽQÓiÝ«Ó#r®Â¬†pl'¨¶’¶_‰j.ƒ!¼ dDrXæmŽ˜ï¡„Ø*òeyÛêJOH Œõ­³za•Ðl~o¹SUÛÍTYé‘ÜŠ›å7VVù‘ZÔÓ­(ß,Éû²¥Ú…ÍID¦Cö…6øÙEª)û™N#º ¥ÂCI46Ñ™U1áèü¾=ñͦù)›”QÉo‹¾”*îãÁÑ×zéË—ãøÒ©…¤Ó«³ž†ƒRáÍ»Úð7K}@ヶÑî -Ò,Võæ51©0Ö¨'V–´öø2›À ãäˆÇJz˜™A®ÚÍsAU³?4Ê=!£7–fxZ[ SÜ0?ÝæÇÃE L2pχ+މw$Žòž*ÝrOÈlí#ò4–ßדF5’ƒâmo(¡ïýùÀKpDNB1“í°”S¥*kb•1k6haв(½F«ÏÛG£<&åŽ\+}Õ¡¨gx­üúdÙ¨—~²ñw”2ب}r ö ˆãðj1ÔÏHšºÊÑGLʧWBžŒ>Š²Ô®„'RÊG—nå<©”p7(-=´ˆ=‹tÅ/6•;Ué †Ì‹eÞé÷ ÃgÝšXªMxu ‘n•™Ï/!5õ'#Õ«~é’Öup4{¨`­²ð’9{c!‘[£^m¸· ¤ñU“ž)Èr¨ü^!úôlÁV…ÔMO DÄ)_O(µñ ê7AD»Ñ)‰´à»¤1½% ¸Ç¦†3¢Úú¥Påë{lß±’ê—DAí´•ˆ†’S*'I‹„—ñèRc·5dzK¨1S^`j|ȶÆ]Öøˆµï35≦ÆG-~x¨©ñqkÄsMèôÿ„oý_Ï$Ìÿ‚¯g-¾ÏY|ž·ÆÝÖþÔ8ÿá…ƒ=/áë J#;Æ•]Ä.[¥dw½À•ÞAø‹rl'­ƒ2 ¸ƒœ²èú è_ÖAÙrd:@€K¦ *(Óû(wfQ' ®)(.ì¤!Ý4”¡]Þ=J±—Ð`ÀùPf L>„²iÊ¥iDSåS1þ¥qTBGÑ¡4‰£Rš@•4™fÐq4›Êi.MŸ´”Q#-¦åø:…N¤¿³†ƒþ^¡¨ãÿÓ:þÛÖñOý_¿“öëeýû²@ãÁMÔ@Ëð¯™N"/­¢),ðªe—- ÿwèyòa½O>ç$òÿNÞJKèdØ0îuê×èuHÄ©mg@–â)즘f¦M9 ¨›dÚÌÛŠè¦á6š`ϳ?BîM|až½›F0MHÏKwÔIo¹ òì»ÈÝE#aªv6@9$‰2 ækç ŒN¢äâí|(c%Û2!#/ÃUÐI…›`b1g;ͦÈÂØ¦˜b “®0‡3ÖÂd(Ìp`J,ŒCaúí¤Cç»ë¢q·Sd®dg›²;éˆvº Ø#{bÇ·ÓùÀÕûlÚ)ÊÑŠ’kQ\ÇtÑø¾ê@›˜´k¢ŽeroÊ (-*î¤)ó»èX×qTš—ÐIe¦ÒÄ囆yö.šº>Û÷Vǃèn*\ ·®A†àYK#içtÍ3i…Ykq6µÑ9€çÒ%t]KçÓfº€¶Ð…Èõ¹7"_„ {12å%›Ké}ºœ¾ +˜éJvÒU<„®f7]ÃÅ´‰'е<®ãyt=7Ò ÜJ7òit_@7óFÄÔt o¡[ù^ºï§5¼ ÕEr¯ŽXg§ Œ;©ǯì i½"“Z“"“f$FæteÓ¼‚{iZÔ{UÝô+ÿK,ûãaÔM3ÆY$zZA¡«î­)vÍT‘ÕÏÊ pi±«&†š¥P£Š]³c¨9 5°ØU«PN êj(XvS½6Yñ9WŠû4/ÝIYðŒ‹îBJ¸›F£.—Ð6:šî¡2ÚN'ÀcaÔæ•È)ëP›Ïƺ Q“¯F5¼5ùVÔã»àÑ.Ôá]ø÷êïs¨½¯ ú¾Úûªï§ðözŒÓUÅ}“Þ²üÒ`¦ úx'Í›yüÌ¢‚Â.:¡ƒæºXš÷ε ‹vТB×âDìb…]bº²&m¼=×^l9óúâ\»éM8Oyó”¼tÓ›’˜7¤¨ /=šgµÓlP÷¦,i§ñ ,U”“€œqb*Çgôt|zÔñéâx;¿´h[Ìþ è]C&º zŒÐcäÃz‡£·˜‚"RÛQK-ÆjéÖTÛËžt{ŠìM(3[v;éݰënXöEXýUzðEúÞSEøËÞvJ‡,¢m….O75ب¦È´×ƒÔØAM‰#6YÖAÍ=LŽšf˜^t/5åÚq@×rDo¦†9õÊT>ŽÄ‡2úŠN:i‚+šsíyއ §.m¼–«á{3 5ñãdš««[¯ÁXo+“Ø2Êü§øåü ”¯/Q°¾µ¾E©Úƒõy™(„<²†mt>§!§Ø‘Òè¬ÛÊoïF3l¤2mˆgßšZb™ÁÍ þÿOfˆŸvõǧ“²X'÷ƒ2ýi4gS çÐÑ<€ÊØE'ð s.­äÁ´Yól¬»ó°û=äTó”å8eFh{Ö»g'Ë5 ªªJL—ô,<Œ2ù>öp£Ÿùe2tƒMª?vKMŠïÌ ù¹3Ôèckõö@Ãx"l< R?GP›RgB¦Ø¸ñ¸N:ºÓ]g@ðµt`¢¤3{Úìž$cG¸”Џ ¬¾¤¯X‹C‘(%ÚÏLÖ>§²•kõ´U¢£öšÖ_ãj¥fݶå°žÖÕ`=¬¿¥ïú`}Ö/a=¬kÁº¬÷Ð÷k¿eëØz½¼¡xe›¸«Iæ?')dÕ²]TÞMçr¯È=žFò:–BÖ´×’½ÑŠ.Ä»äd3Ï& :¯gDGUì_œÍÅÛן½T$„aTýT7´”4>ñáA|4ÐxtV¥l@µÕÿ°ÕJ1‹À/¶™ö³¸ ã &ãêöß,L>©¬WÐp> ßÌ6ëU°Q/‚¿/Œj¹<7Ó„ßE&¿Nº8F¾Ä$Û;éÒ$Êe&Eª­ëòØF³§ºÉ»Ðu%æ0èUŒþ1W‹_æÕÄ8\#¢“M˜ßKóôÈÜE×F9_Ûv½¹rQT¥8哲×1•Ìʉ‰wc¡ë&Ù‘,õs\¢äAJrœóÍæ2ÃZvy\+ׯÓÆ§ç¦›ÕRŠ ­ïÜt,¿.N²7m¦ÌB×f ×§£Ð<]躪»¤}ïn)`QëÝ·ÍŠZ/d=0>¹3Ýfª}o£ýÇ»ã j- ÇgQ¦c8‚B¶’¸–ó*:™O¥Õ¼š¶óz ­þ¼–¾áu´—OçQ|Oç3¹†Ïâ%¼ž}|6¯æs€=‹Ïåóx#¯åËù¾Š70ž|;_Ä[ùbîæKø%¾”¿u_‰<Ævë•~¸uÿî¤öùiãºè7Õ…ÛéŽNº39£Þ€òp#’öMêùkG|§#Ø3ÐWD³”$Ü™ÔÞI¿½¶ì »ÒvfßA³ÒwМŒTçØA –P׎UBƒhÃѨ¨£¬ÉPœI÷¨ºl£7Û°sÆíÄÿPKÇ€hóÀ*PK½lê8 META-INF/þÊPK½lê8IgݵGG=META-INF/MANIFEST.MFPK½lê8©4‰2ÄÆorg/nltk/mallet/RunCRF.classPK½lê8H·´0É-Ôorg/nltk/mallet/CRFInfo$WeightGroupInfo.classPKkê88“Åp Ï _ org/nltk/mallet/TrainCRF$1.classPKkê8 Н¼(M org/nltk/mallet/TrainCRF$MyHandler.classPK½lê8ÇM` ïR org/nltk/mallet/TrainCRF.classPK½lê8]˜òãpd'þorg/nltk/mallet/CRFInfo$StateInfo.classPK½lê8Ç€hóÀ*Ãorg/nltk/mallet/CRFInfo.classPK ²1nltk-2.0~b9/nltk/lazyimport.py0000644000175000017500000001076611211063745016324 0ustar bhavanibhavani# This module is from mx/DateTime/LazyModule.py and is # distributed under the terms of the eGenix.com Public License Agreement # http://www.egenix.com/products/eGenix.com-Public-License-1.1.0.pdf """ Helper to enable simple lazy module import. 'Lazy' means the actual import is deferred until an attribute is requested from the module's namespace. This has the advantage of allowing all imports to be done at the top of a script (in a prominent and visible place) without having a great impact on startup time. Copyright (c) 1999-2005, Marc-Andre Lemburg; mailto:mal@lemburg.com See the documentation for further information on copyrights, or contact the author. All Rights Reserved. """ ### Constants _debug = 0 ### class LazyModule: """ Lazy module class. Lazy modules are imported into the given namespaces whenever a non-special attribute (there are some attributes like __doc__ that class instances handle without calling __getattr__) is requested. The module is then registered under the given name in locals usually replacing the import wrapper instance. The import itself is done using globals as global namespace. Example of creating a lazy load module: ISO = LazyModule('ISO',locals(),globals()) Later, requesting an attribute from ISO will load the module automatically into the locals() namespace, overriding the LazyModule instance: t = ISO.Week(1998,1,1) """ # Flag which inidicates whether the LazyModule is initialized or not __lazymodule_init = 0 # Name of the module to load __lazymodule_name = '' # Flag which indicates whether the module was loaded or not __lazymodule_loaded = 0 # Locals dictionary where to register the module __lazymodule_locals = None # Globals dictionary to use for the module import __lazymodule_globals = None def __init__(self, name, locals, globals=None): """ Create a LazyModule instance wrapping module name. The module will later on be registered in locals under the given module name. globals is optional and defaults to locals. """ self.__lazymodule_locals = locals if globals is None: globals = locals self.__lazymodule_globals = globals mainname = globals.get('__name__', '') if mainname: self.__name__ = mainname + '.' + name self.__lazymodule_name = name else: self.__name__ = self.__lazymodule_name = name self.__lazymodule_init = 1 def __lazymodule_import(self): """ Import the module now. """ # Load and register module name = self.__lazymodule_name if self.__lazymodule_loaded: return self.__lazymodule_locals[name] if _debug: print 'LazyModule: Loading module %r' % name self.__lazymodule_locals[name] \ = module \ = __import__(name, self.__lazymodule_locals, self.__lazymodule_globals, '*') # Fill namespace with all symbols from original module to # provide faster access. self.__dict__.update(module.__dict__) # Set import flag self.__dict__['__lazymodule_loaded'] = 1 if _debug: print 'LazyModule: Module %r loaded' % name return module def __getattr__(self, name): """ Import the module on demand and get the attribute. """ if self.__lazymodule_loaded: raise AttributeError, name if _debug: print 'LazyModule: ' \ 'Module load triggered by attribute %r read access' % name module = self.__lazymodule_import() return getattr(module, name) def __setattr__(self, name, value): """ Import the module on demand and set the attribute. """ if not self.__lazymodule_init: self.__dict__[name] = value return if self.__lazymodule_loaded: self.__lazymodule_locals[self.__lazymodule_name] = value self.__dict__[name] = value return if _debug: print 'LazyModule: ' \ 'Module load triggered by attribute %r write access' % name module = self.__lazymodule_import() setattr(module, name, value) def __repr__(self): return "" % self.__name__ nltk-2.0~b9/nltk/internals.py0000644000175000017500000007132211374105240016101 0ustar bhavanibhavani# Natural Language Toolkit: Internal utility functions # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT import subprocess import os import os.path import re import warnings import textwrap import types import sys import stat from nltk import __file__ # Use the c version of ElementTree, which is faster, if possible: try: from xml.etree import cElementTree as ElementTree except ImportError: from nltk.etree import ElementTree ###################################################################### # Regular Expression Processing ###################################################################### def convert_regexp_to_nongrouping(pattern): """ Convert all grouping parenthases in the given regexp pattern to non-grouping parenthases, and return the result. E.g.: >>> convert_regexp_to_nongrouping('ab(c(x+)(z*))?d') 'ab(?:c(?:x+)(?:z*))?d' @type pattern: C{str} @rtype: C{str} """ # Sanity check: back-references are not allowed! for s in re.findall(r'\\.|\(\?P=', pattern): if s[1] in '0123456789' or s == '(?P=': raise ValueError('Regular expressions with back-references ' 'are not supported: %r' % pattern) # This regexp substitution function replaces the string '(' # with the string '(?:', but otherwise makes no changes. def subfunc(m): return re.sub('^\((\?P<[^>]*>)?$', '(?:', m.group()) # Scan through the regular expression. If we see any backslashed # characters, ignore them. If we see a named group, then # replace it with "(?:". If we see any open parens that are part # of an extension group, ignore those too. But if we see # any other open paren, replace it with "(?:") return re.sub(r'''(?x) \\. | # Backslashed character \(\?P<[^>]*> | # Named group \(\? | # Extension group \( # Grouping parenthasis''', subfunc, pattern) ########################################################################## # Java Via Command-Line ########################################################################## _java_bin = None _java_options = [] # [xx] add classpath option to config_java? def config_java(bin=None, options=None): """ Configure nltk's java interface, by letting nltk know where it can find the C{java} binary, and what extra options (if any) should be passed to java when it is run. @param bin: The full path to the C{java} binary. If not specified, then nltk will search the system for a C{java} binary; and if one is not found, it will raise a C{LookupError} exception. @type bin: C{string} @param options: A list of options that should be passed to the C{java} binary when it is called. A common value is C{['-Xmx512m']}, which tells the C{java} binary to increase the maximum heap size to 512 megabytes. If no options are specified, then do not modify the options list. @type options: C{list} of C{string} """ global _java_bin, _java_options _java_bin = find_binary('java', bin, env_vars=['JAVAHOME', 'JAVA_HOME']) if options is not None: if isinstance(options, basestring): options = options.split() _java_options = list(options) def java(cmd, classpath=None, stdin=None, stdout=None, stderr=None, blocking=True): """ Execute the given java command, by opening a subprocess that calls C{java}. If java has not yet been configured, it will be configured by calling L{config_java()} with no arguments. @param cmd: The java command that should be called, formatted as a list of strings. Typically, the first string will be the name of the java class; and the remaining strings will be arguments for that java class. @type cmd: C{list} of C{string} @param classpath: A C{':'} separated list of directories, JAR archives, and ZIP archives to search for class files. @type classpath: C{string} @param stdin, stdout, stderr: Specify the executed programs' standard input, standard output and standard error file handles, respectively. Valid values are C{subprocess.PIPE}, an existing file descriptor (a positive integer), an existing file object, and C{None}. C{subprocess.PIPE} indicates that a new pipe to the child should be created. With C{None}, no redirection will occur; the child's file handles will be inherited from the parent. Additionally, stderr can be C{subprocess.STDOUT}, which indicates that the stderr data from the applications should be captured into the same file handle as for stdout. @param blocking: If C{false}, then return immediately after spawning the subprocess. In this case, the return value is the C{Popen} object, and not a C{(stdout, stderr)} tuple. @return: If C{blocking=True}, then return a tuple C{(stdout, stderr)}, containing the stdout and stderr outputs generated by the java command if the C{stdout} and C{stderr} parameters were set to C{subprocess.PIPE}; or C{None} otherwise. If C{blocking=False}, then return a C{subprocess.Popen} object. @raise OSError: If the java command returns a nonzero return code. """ if stdin == 'pipe': stdin = subprocess.PIPE if stdout == 'pipe': stdout = subprocess.PIPE if stderr == 'pipe': stderr = subprocess.PIPE if isinstance(cmd, basestring): raise TypeError('cmd should be a list of strings') # Make sure we know where a java binary is. if _java_bin is None: config_java() # Set up the classpath. if classpath is None: classpath = NLTK_JAR else: classpath += ':' + NLTK_JAR # Construct the full command string. cmd = list(cmd) cmd = ['-cp', classpath] + cmd cmd = [_java_bin] + _java_options + cmd # Call java via a subprocess p = subprocess.Popen(cmd, stdin=stdin, stdout=stdout, stderr=stderr) if not blocking: return p (stdout, stderr) = p.communicate() # Check the return code. if p.returncode != 0: print stderr raise OSError('Java command failed!') return (stdout, stderr) #: The location of the NLTK jar file, which is used to communicate #: with external Java packages (such as Mallet) that do not have #: a sufficiently powerful native command-line interface. NLTK_JAR = os.path.abspath(os.path.join(os.path.split(__file__)[0], 'nltk.jar')) if 0: #config_java(options='-Xmx512m') # Write: #java('weka.classifiers.bayes.NaiveBayes', # ['-d', '/tmp/names.model', '-t', '/tmp/train.arff'], # classpath='/Users/edloper/Desktop/weka/weka.jar') # Read: (a,b) = java(['weka.classifiers.bayes.NaiveBayes', '-l', '/tmp/names.model', '-T', '/tmp/test.arff', '-p', '0'],#, '-distribution'], classpath='/Users/edloper/Desktop/weka/weka.jar') ###################################################################### # Parsing ###################################################################### class ParseError(ValueError): """ Exception raised by parse_* functions when they fail. @param position: The index in the input string where an error occured. @param expected: What was expected when an error occured. """ def __init__(self, expected, position): ValueError.__init__(self, expected, position) self.expected = expected self.position = position def __str__(self): return 'Expected %s at %s' % (self.expected, self.position) _STRING_START_RE = re.compile(r"[uU]?[rR]?(\"\"\"|\'\'\'|\"|\')") def parse_str(s, start_position): """ If a Python string literal begins at the specified position in the given string, then return a tuple C{(val, end_position)} containing the value of the string literal and the position where it ends. Otherwise, raise a L{ParseError}. """ # Read the open quote, and any modifiers. m = _STRING_START_RE.match(s, start_position) if not m: raise ParseError('open quote', start_position) quotemark = m.group(1) # Find the close quote. _STRING_END_RE = re.compile(r'\\|%s' % quotemark) position = m.end() while True: match = _STRING_END_RE.search(s, position) if not match: raise ParseError('close quote', position) if match.group(0) == '\\': position = match.end()+1 else: break # Parse it, using eval. Strings with invalid escape sequences # might raise ValueEerror. try: return eval(s[start_position:match.end()]), match.end() except ValueError, e: raise ParseError('valid string (%s)' % e, start) _PARSE_INT_RE = re.compile(r'-?\d+') def parse_int(s, start_position): """ If an integer begins at the specified position in the given string, then return a tuple C{(val, end_position)} containing the value of the integer and the position where it ends. Otherwise, raise a L{ParseError}. """ m = _PARSE_INT_RE.match(s, start_position) if not m: raise ParseError('integer', start_position) return int(m.group()), m.end() _PARSE_NUMBER_VALUE = re.compile(r'-?(\d*)([.]?\d*)?') def parse_number(s, start_position): """ If an integer or float begins at the specified position in the given string, then return a tuple C{(val, end_position)} containing the value of the number and the position where it ends. Otherwise, raise a L{ParseError}. """ m = _PARSE_NUMBER_VALUE.match(s, start_position) if not m or not (m.group(1) or m.group(2)): raise ParseError('number', start_position) if m.group(2): return float(m.group()), m.end() else: return int(m.group()), m.end() ###################################################################### # Check if a method has been overridden ###################################################################### def overridden(method): """ @return: True if C{method} overrides some method with the same name in a base class. This is typically used when defining abstract base classes or interfaces, to allow subclasses to define either of two related methods: >>> class EaterI: ... '''Subclass must define eat() or batch_eat().''' ... def eat(self, food): ... if overridden(self.batch_eat): ... return self.batch_eat([food])[0] ... else: ... raise NotImplementedError() ... def batch_eat(self, foods): ... return [self.eat(food) for food in foods] @type method: instance method """ # [xx] breaks on classic classes! if isinstance(method, types.MethodType) and method.im_class is not None: name = method.__name__ funcs = [cls.__dict__[name] for cls in _mro(method.im_class) if name in cls.__dict__] return len(funcs) > 1 else: raise TypeError('Expected an instance method.') def _mro(cls): """ Return the I{method resolution order} for C{cls} -- i.e., a list containing C{cls} and all its base classes, in the order in which they would be checked by C{getattr}. For new-style classes, this is just cls.__mro__. For classic classes, this can be obtained by a depth-first left-to-right traversal of C{__bases__}. """ if isinstance(cls, type): return cls.__mro__ else: mro = [cls] for base in cls.__bases__: mro.extend(_mro(base)) return mro ###################################################################### # Deprecation decorator & base class ###################################################################### # [xx] dedent msg first if it comes from a docstring. def _add_epytext_field(obj, field, message): """Add an epytext @field to a given object's docstring.""" indent = '' # If we already have a docstring, then add a blank line to separate # it from the new field, and check its indentation. if obj.__doc__: obj.__doc__ = obj.__doc__.rstrip()+'\n\n' indents = re.findall(r'(?<=\n)[ ]+(?!\s)', obj.__doc__.expandtabs()) if indents: indent = min(indents) # If we don't have a docstring, add an empty one. else: obj.__doc__ = '' obj.__doc__ += textwrap.fill('@%s: %s' % (field, message), initial_indent=indent, subsequent_indent=indent+' ') def deprecated(message): """ A decorator used to mark functions as deprecated. This will cause a warning to be printed the when the function is used. Usage: >>> @deprecated('Use foo() instead') >>> def bar(x): ... print x/10 """ def decorator(func): msg = ("Function %s() has been deprecated. %s" % (func.__name__, message)) msg = '\n' + textwrap.fill(msg, initial_indent=' ', subsequent_indent=' ') def newFunc(*args, **kwargs): warnings.warn(msg, category=DeprecationWarning, stacklevel=2) return func(*args, **kwargs) # Copy the old function's name, docstring, & dict newFunc.__dict__.update(func.__dict__) newFunc.__name__ = func.__name__ newFunc.__doc__ = func.__doc__ newFunc.__deprecated__ = True # Add a @deprecated field to the docstring. _add_epytext_field(newFunc, 'deprecated', message) return newFunc return decorator class Deprecated(object): """ A base class used to mark deprecated classes. A typical usage is to alert users that the name of a class has changed: >>> class OldClassName(Deprecated, NewClassName): ... "Use NewClassName instead." The docstring of the deprecated class will be used in the deprecation warning message. """ def __new__(cls, *args, **kwargs): # Figure out which class is the deprecated one. dep_cls = None for base in _mro(cls): if Deprecated in base.__bases__: dep_cls = base; break assert dep_cls, 'Unable to determine which base is deprecated.' # Construct an appropriate warning. doc = dep_cls.__doc__ or ''.strip() # If there's a @deprecated field, strip off the field marker. doc = re.sub(r'\A\s*@deprecated:', r'', doc) # Strip off any indentation. doc = re.sub(r'(?m)^\s*', '', doc) # Construct a 'name' string. name = 'Class %s' % dep_cls.__name__ if cls != dep_cls: name += ' (base class for %s)' % cls.__name__ # Put it all together. msg = '%s has been deprecated. %s' % (name, doc) # Wrap it. msg = '\n' + textwrap.fill(msg, initial_indent=' ', subsequent_indent=' ') warnings.warn(msg, category=DeprecationWarning, stacklevel=2) # Do the actual work of __new__. return object.__new__(cls, *args, **kwargs) ########################################################################## # COUNTER, FOR UNIQUE NAMING ########################################################################## class Counter: """ A counter that auto-increments each time its value is read. """ def __init__(self, initial_value=0): self._value = initial_value def get(self): self._value += 1 return self._value ########################################################################## # Search for binaries ########################################################################## def find_binary(name, path_to_bin=None, env_vars=(), searchpath=(), binary_names=None, url=None, verbose=True): """ Search for the binary for a program that is used by nltk. @param name: The name of the program @param path_to_bin: The user-supplied binary location, or None. @param env_vars: A list of environment variable names to check @param binary_names: A list of alternative binary names to check. @param searchpath: List of directories to search. """ if binary_names is None: binary_names = [name] assert isinstance(name, basestring) assert not isinstance(binary_names, basestring) assert not isinstance(searchpath, basestring) if isinstance(env_vars, basestring): env_vars = env_vars.split() # If an explicit bin was given, then check it, and return it if # it's present; otherwise, complain. if path_to_bin is not None: if os.path.isfile(path_to_bin): return path_to_bin for bin in binary_names: if os.path.isfile(os.path.join(path_to_bin, bin)): return os.path.join(path_to_bin, bin) if os.path.isfile(os.path.join(path_to_bin, 'bin', bin)): return os.path.join(path_to_bin, 'bin', bin) raise ValueError('Could not find %s binary at %s' % (name, path_to_bin)) # Check environment variables for env_var in env_vars: if env_var in os.environ: path_to_bin = os.environ[env_var] if os.path.isfile(path_to_bin): if verbose: print '[Found %s: %s]' % (name, path_to_bin) return os.environ[env_var] else: for bin_name in binary_names: path_to_bin = os.path.join(os.environ[env_var], bin_name) if os.path.isfile(path_to_bin): if verbose: print '[Found %s: %s]'%(name, path_to_bin) return path_to_bin path_to_bin = os.path.join(os.environ[env_var], 'bin', bin_name) if os.path.isfile(path_to_bin): if verbose: print '[Found %s: %s]'%(name, path_to_bin) return path_to_bin # Check the path list. for directory in searchpath: for bin in binary_names: path_to_bin = os.path.join(directory, bin) if os.path.isfile(path_to_bin): return path_to_bin # If we're on a POSIX system, then try using the 'which' command # to find the binary. if os.name == 'posix': for bin in binary_names: try: p = subprocess.Popen(['which', bin], stdout=subprocess.PIPE) stdout, stderr = p.communicate() path = stdout.strip() if path.endswith(bin) and os.path.exists(path): if verbose: print '[Found %s: %s]' % (name, path) return path except KeyboardInterrupt, SystemExit: raise except: pass msg = ("NLTK was unable to find the %s executable! Use " "config_%s()" % (name, name)) if env_vars: msg += ' or set the %s environment variable' % env_vars[0] msg = textwrap.fill(msg+'.', initial_indent=' ', subsequent_indent=' ') msg += "\n\n >>> config_%s('/path/to/%s')" % (name, name) if searchpath: msg += '\n\n Searched in:' msg += ''.join('\n - %s' % d for d in searchpath) if url: msg += ('\n\n For more information, on %s, see:\n <%s>' % (name, url)) div = '='*75 raise LookupError('\n\n%s\n%s\n%s' % (div, msg, div)) ########################################################################## # Import Stdlib Module ########################################################################## def import_from_stdlib(module): """ When python is run from within the nltk/ directory tree, the current directory is included at the beginning of the search path. Unfortunately, that means that modules within nltk can sometimes shadow standard library modules. As an example, the stdlib 'inspect' module will attempt to import the stdlib 'tokenzie' module, but will instead end up importing NLTK's 'tokenize' module instead (causing the import to fail). """ old_path = sys.path sys.path = [d for d in sys.path if d not in ('', '.')] m = __import__(module) sys.path = old_path return m ########################################################################## # Abstract declaration ########################################################################## def abstract(func): """ A decorator used to mark methods as abstract. I.e., methods that are marked by this decorator must be overridden by subclasses. If an abstract method is called (either in the base class or in a subclass that does not override the base class method), it will raise C{NotImplementedError}. """ # Avoid problems caused by nltk.tokenize shadowing the stdlib tokenize: inspect = import_from_stdlib('inspect') # Read the function's signature. args, varargs, varkw, defaults = inspect.getargspec(func) # Create a new function with the same signature (minus defaults) # that raises NotImplementedError. msg = '%s is an abstract method.' % func.__name__ signature = inspect.formatargspec(args, varargs, varkw, ()) exec ('def newfunc%s: raise NotImplementedError(%r)' % (signature, msg)) # Substitute in the defaults after-the-fact, since eval(repr(val)) # may not work for some default values. newfunc.func_defaults = func.func_defaults # Copy the name and docstring newfunc.__name__ = func.__name__ newfunc.__doc__ = func.__doc__ newfunc.__abstract__ = True _add_epytext_field(newfunc, "note", "This method is abstract.") # Return the function. return newfunc ########################################################################## # Wrapper for ElementTree Elements ########################################################################## class ElementWrapper(object): """ A wrapper around ElementTree Element objects whose main purpose is to provide nicer __repr__ and __str__ methods. In addition, any of the wrapped Element's methods that return other Element objects are overridden to wrap those values before returning them. This makes Elements more convenient to work with in interactive sessions and doctests, at the expense of some efficiency. """ # Prevent double-wrapping: def __new__(cls, etree): """ Create and return a wrapper around a given Element object. If C{etree} is an C{ElementWrapper}, then C{etree} is returned as-is. """ if isinstance(etree, ElementWrapper): return etree else: return object.__new__(ElementWrapper, etree) def __init__(self, etree): """ Initialize a new Element wrapper for C{etree}. If C{etree} is a string, then it will be converted to an Element object using C{ElementTree.fromstring()} first. """ if isinstance(etree, basestring): etree = ElementTree.fromstring(etree) self.__dict__['_etree'] = etree def unwrap(self): """ Return the Element object wrapped by this wrapper. """ return self._etree ##//////////////////////////////////////////////////////////// #{ String Representation ##//////////////////////////////////////////////////////////// def __repr__(self): s = ElementTree.tostring(self._etree) if len(s) > 60: e = s.rfind('<') if (len(s)-e) > 30: e = -20 s = '%s...%s' % (s[:30], s[e:]) return '' % s def __str__(self): """ @return: the result of applying C{ElementTree.tostring()} to the wrapped Element object. """ return ElementTree.tostring(self._etree).rstrip() ##//////////////////////////////////////////////////////////// #{ Element interface Delegation (pass-through) ##//////////////////////////////////////////////////////////// def __getattr__(self, attrib): return getattr(self._etree, attrib) def __setattr__(self, attr, value): return setattr(self._etree, attr, value) def __delattr__(self, attr): return delattr(self._etree, attr) def __setitem__(self, index, element): self._etree[index] = element def __delitem__(self, index): del self._etree[index] def __setslice__(self, start, stop, elements): self._etree[start:stop] = elements def __delslice__(self, start, stop): del self._etree[start:stop] def __len__(self): return len(self._etree) ##//////////////////////////////////////////////////////////// #{ Element interface Delegation (wrap result) ##//////////////////////////////////////////////////////////// def __getitem__(self, index): return ElementWrapper(self._etree[index]) def __getslice__(self, start, stop): return [ElementWrapper(elt) for elt in self._etree[start:stop]] def getchildren(self): return [ElementWrapper(elt) for elt in self._etree] def getiterator(self, tag=None): return (ElementWrapper(elt) for elt in self._etree.getiterator(tag)) def makeelement(self, tag, attrib): return ElementWrapper(self._etree.makeelement(tag, attrib)) def find(self, path): elt = self._etree.find(path) if elt is None: return elt else: return ElementWrapper(elt) def findall(self, path): return [ElementWrapper(elt) for elt in self._etree.findall(path)] ###################################################################### # Helper for Handling Slicing ###################################################################### def slice_bounds(sequence, slice_obj, allow_step=False): """ Given a slice, return the corresponding (start, stop) bounds, taking into account None indices and negative indices. The following guarantees are made for the returned start and stop values: - 0 <= start <= len(sequence) - 0 <= stop <= len(sequence) - start <= stop @raise ValueError: If C{slice_obj.step} is not C{None}. @param allow_step: If true, then the slice object may have a non-None step. If it does, then return a tuple (start, stop, step). """ start, stop = (slice_obj.start, slice_obj.stop) # If allow_step is true, then include the step in our return # value tuple. if allow_step: if slice_obj.step is None: slice_obj.step = 1 # Use a recursive call without allow_step to find the slice # bounds. If step is negative, then the roles of start and # stop (in terms of default values, etc), are swapped. if slice_obj.step < 0: start, stop = slice_bounds(sequence, slice(stop, start)) else: start, stop = slice_bounds(sequence, slice(start, stop)) return start, stop, slice_obj.step # Otherwise, make sure that no non-default step value is used. elif slice_obj.step not in (None, 1): raise ValueError('slices with steps are not supported by %s' % sequence.__class__.__name__) # Supply default offsets. if start is None: start = 0 if stop is None: stop = len(sequence) # Handle negative indices. if start < 0: start = max(0, len(sequence)+start) if stop < 0: stop = max(0, len(sequence)+stop) # Make sure stop doesn't go past the end of the list. Note that # we avoid calculating len(sequence) if possible, because for lazy # sequences, calculating the length of a sequence can be expensive. if stop > 0: try: sequence[stop-1] except IndexError: stop = len(sequence) # Make sure start isn't past stop. start = min(start, stop) # That's all folks! return start, stop ###################################################################### # Permission Checking ###################################################################### def is_writable(path): # Ensure that it exists. if not os.path.exists(path): return False # If we're on a posix system, check its permissions. if hasattr(os, 'getuid'): statdata = os.stat(path) perm = stat.S_IMODE(statdata.st_mode) # is it world-writable? if (perm & 0002): return True # do we own it? elif statdata.st_uid == os.getuid() and (perm & 0200): return True # are we in a group that can write to it? elif statdata.st_gid == os.getgid() and (perm & 0020): return True # otherwise, we can't write to it. else: return False # Otherwise, we'll assume it's writable. # [xx] should we do other checks on other platforms? return True nltk-2.0~b9/nltk/help.py0000644000175000017500000000311711327451603015034 0ustar bhavanibhavani# Natural Language Toolkit (NLTK) Help # # Copyright (C) 2001-2010 NLTK Project # Authors: Steven Bird # URL: # For license information, see LICENSE.TXT """ Provide structured access to documentation. """ import re from textwrap import wrap from nltk.data import load def brown_tagset(tagpattern=None): _format_tagset("brown_tagset", tagpattern) def claws5_tagset(tagpattern=None): _format_tagset("claws5_tagset", tagpattern) def upenn_tagset(tagpattern=None): _format_tagset("upenn_tagset", tagpattern) ##################################################################### # UTILITIES ##################################################################### def _print_entries(tags, tagdict): for tag in tags: entry = tagdict[tag] defn = [tag + ": " + entry[0]] examples = wrap(entry[1], width=75, initial_indent=' ', subsequent_indent=' ') print "\n".join(defn + examples) def _format_tagset(tagset, tagpattern=None): tagdict = load("help/tagsets/" + tagset + ".pickle") if not tagpattern: _print_entries(sorted(tagdict), tagdict) elif tagpattern in tagdict: _print_entries([tagpattern], tagdict) else: tagpattern = re.compile(tagpattern) tags = [tag for tag in sorted(tagdict) if tagpattern.match(tag)] if tags: _print_entries(tags, tagdict) else: print "No matching tags found." if __name__ == '__main__': brown_tagset(r'NN.*') upenn_tagset(r'.*\$') claws5_tagset('UNDEFINED') brown_tagset(r'NN') nltk-2.0~b9/nltk/grammar.py0000644000175000017500000015235311354233125015536 0ustar bhavanibhavani# -*- coding: utf-8 -*- # Natural Language Toolkit: Context Free Grammars # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # Jason Narad # Peter Ljunglöf # URL: # For license information, see LICENSE.TXT # """ Basic data classes for representing context free grammars. A X{grammar} specifies which trees can represent the structure of a given text. Each of these trees is called a X{parse tree} for the text (or simply a X{parse}). In a X{context free} grammar, the set of parse trees for any piece of a text can depend only on that piece, and not on the rest of the text (i.e., the piece's context). Context free grammars are often used to find possible syntactic structures for sentences. In this context, the leaves of a parse tree are word tokens; and the node values are phrasal categories, such as C{NP} and C{VP}. The L{ContextFreeGrammar} class is used to encode context free grammars. Each C{ContextFreeGrammar} consists of a start symbol and a set of productions. The X{start symbol} specifies the root node value for parse trees. For example, the start symbol for syntactic parsing is usually C{S}. Start symbols are encoded using the C{Nonterminal} class, which is discussed below. A Grammar's X{productions} specify what parent-child relationships a parse tree can contain. Each production specifies that a particular node can be the parent of a particular set of children. For example, the production C{ -> } specifies that an C{S} node can be the parent of an C{NP} node and a C{VP} node. Grammar productions are implemented by the C{Production} class. Each C{Production} consists of a left hand side and a right hand side. The X{left hand side} is a C{Nonterminal} that specifies the node type for a potential parent; and the X{right hand side} is a list that specifies allowable children for that parent. This lists consists of C{Nonterminals} and text types: each C{Nonterminal} indicates that the corresponding child may be a C{TreeToken} with the specified node type; and each text type indicates that the corresponding child may be a C{Token} with the with that type. The C{Nonterminal} class is used to distinguish node values from leaf values. This prevents the grammar from accidentally using a leaf value (such as the English word "A") as the node of a subtree. Within a C{ContextFreeGrammar}, all node values are wrapped in the C{Nonterminal} class. Note, however, that the trees that are specified by the grammar do B{not} include these C{Nonterminal} wrappers. Grammars can also be given a more procedural interpretation. According to this interpretation, a Grammar specifies any tree structure M{tree} that can be produced by the following procedure: - Set M{tree} to the start symbol - Repeat until M{tree} contains no more nonterminal leaves: - Choose a production M{prod} with whose left hand side M{lhs} is a nonterminal leaf of M{tree}. - Replace the nonterminal leaf with a subtree, whose node value is the value wrapped by the nonterminal M{lhs}, and whose children are the right hand side of M{prod}. The operation of replacing the left hand side (M{lhs}) of a production with the right hand side (M{rhs}) in a tree (M{tree}) is known as X{expanding} M{lhs} to M{rhs} in M{tree}. """ import re from nltk.internals import deprecated from nltk.compat import all from nltk.util import transitive_closure, invert_graph from probability import ImmutableProbabilisticMixIn from featstruct import FeatStruct, FeatDict, FeatStructParser, SLASH, TYPE ################################################################# # Nonterminal ################################################################# class Nonterminal(object): """ A non-terminal symbol for a context free grammar. C{Nonterminal} is a wrapper class for node values; it is used by C{Production}s to distinguish node values from leaf values. The node value that is wrapped by a C{Nonterminal} is known as its X{symbol}. Symbols are typically strings representing phrasal categories (such as C{"NP"} or C{"VP"}). However, more complex symbol types are sometimes used (e.g., for lexicalized grammars). Since symbols are node values, they must be immutable and hashable. Two C{Nonterminal}s are considered equal if their symbols are equal. @see: L{ContextFreeGrammar} @see: L{Production} @type _symbol: (any) @ivar _symbol: The node value corresponding to this C{Nonterminal}. This value must be immutable and hashable. """ def __init__(self, symbol): """ Construct a new non-terminal from the given symbol. @type symbol: (any) @param symbol: The node value corresponding to this C{Nonterminal}. This value must be immutable and hashable. """ self._symbol = symbol self._hash = hash(symbol) def symbol(self): """ @return: The node value corresponding to this C{Nonterminal}. @rtype: (any) """ return self._symbol def __eq__(self, other): """ @return: True if this non-terminal is equal to C{other}. In particular, return true iff C{other} is a C{Nonterminal} and this non-terminal's symbol is equal to C{other}'s symbol. @rtype: C{boolean} """ try: return ((self._symbol == other._symbol) \ and isinstance(other, self.__class__)) except AttributeError: return False def __ne__(self, other): """ @return: True if this non-terminal is not equal to C{other}. In particular, return true iff C{other} is not a C{Nonterminal} or this non-terminal's symbol is not equal to C{other}'s symbol. @rtype: C{boolean} """ return not (self==other) def __cmp__(self, other): try: return cmp(self._symbol, other._symbol) except: return -1 def __hash__(self): return self._hash def __repr__(self): """ @return: A string representation for this C{Nonterminal}. @rtype: C{string} """ if isinstance(self._symbol, basestring): return '%s' % (self._symbol,) else: return '%r' % (self._symbol,) def __str__(self): """ @return: A string representation for this C{Nonterminal}. @rtype: C{string} """ if isinstance(self._symbol, basestring): return '%s' % (self._symbol,) else: return '%r' % (self._symbol,) def __div__(self, rhs): """ @return: A new nonterminal whose symbol is C{M{A}/M{B}}, where C{M{A}} is the symbol for this nonterminal, and C{M{B}} is the symbol for rhs. @rtype: L{Nonterminal} @param rhs: The nonterminal used to form the right hand side of the new nonterminal. @type rhs: L{Nonterminal} """ return Nonterminal('%s/%s' % (self._symbol, rhs._symbol)) def nonterminals(symbols): """ Given a string containing a list of symbol names, return a list of C{Nonterminals} constructed from those symbols. @param symbols: The symbol name string. This string can be delimited by either spaces or commas. @type symbols: C{string} @return: A list of C{Nonterminals} constructed from the symbol names given in C{symbols}. The C{Nonterminals} are sorted in the same order as the symbols names. @rtype: C{list} of L{Nonterminal} """ if ',' in symbols: symbol_list = symbols.split(',') else: symbol_list = symbols.split() return [Nonterminal(s.strip()) for s in symbol_list] class FeatStructNonterminal(FeatDict, Nonterminal): """A feature structure that's also a nonterminal. It acts as its own symbol, and automatically freezes itself when hashed.""" def __hash__(self): self.freeze() return FeatStruct.__hash__(self) def symbol(self): return self def is_nonterminal(item): """ @return: True if the item is a C{Nonterminal}. @rtype: C{bool} """ return isinstance(item, Nonterminal) ################################################################# # Terminals ################################################################# def is_terminal(item): """ @return: True if the item is a terminal, which currently is if it is hashable and not a C{Nonterminal}. @rtype: C{bool} """ return hasattr(item, '__hash__') and not isinstance(item, Nonterminal) ################################################################# # Productions ################################################################# class Production(object): """ A grammar production. Each production maps a single symbol on the X{left-hand side} to a sequence of symbols on the X{right-hand side}. (In the case of context-free productions, the left-hand side must be a C{Nonterminal}, and the right-hand side is a sequence of terminals and C{Nonterminals}.) X{terminals} can be any immutable hashable object that is not a C{Nonterminal}. Typically, terminals are strings representing words, such as C{"dog"} or C{"under"}. @see: L{ContextFreeGrammar} @see: L{DependencyGrammar} @see: L{Nonterminal} @type _lhs: L{Nonterminal} @ivar _lhs: The left-hand side of the production. @type _rhs: C{tuple} of (C{Nonterminal} and (terminal)) @ivar _rhs: The right-hand side of the production. """ def __init__(self, lhs, rhs): """ Construct a new C{Production}. @param lhs: The left-hand side of the new C{Production}. @type lhs: L{Nonterminal} @param rhs: The right-hand side of the new C{Production}. @type rhs: sequence of (C{Nonterminal} and (terminal)) """ if isinstance(rhs, (str, unicode)): raise TypeError('production right hand side should be a list, ' 'not a string') self._lhs = lhs self._rhs = tuple(rhs) self._hash = hash((self._lhs, self._rhs)) def lhs(self): """ @return: the left-hand side of this C{Production}. @rtype: L{Nonterminal} """ return self._lhs def rhs(self): """ @return: the right-hand side of this C{Production}. @rtype: sequence of (C{Nonterminal} and (terminal)) """ return self._rhs def __len__(self): """ @return: the length of the right-hand side. @rtype: C{integer} """ return len(self._rhs) def is_nonlexical(self): """ @return: True if the right-hand side only contains C{Nonterminal}s @rtype: C{bool} """ return all([is_nonterminal(n) for n in self._rhs]) def is_lexical(self): """ @return: True if the right-hand contain at least one terminal token @rtype: C{bool} """ return not self.is_nonlexical() def __str__(self): """ @return: A verbose string representation of the C{Production}. @rtype: C{string} """ str = '%r ->' % (self._lhs,) for elt in self._rhs: str += ' %r' % (elt,) return str def __repr__(self): """ @return: A concise string representation of the C{Production}. @rtype: C{string} """ return '%s' % self def __eq__(self, other): """ @return: true if this C{Production} is equal to C{other}. @rtype: C{boolean} """ return (isinstance(other, self.__class__) and self._lhs == other._lhs and self._rhs == other._rhs) def __ne__(self, other): return not (self == other) def __cmp__(self, other): if not isinstance(other, self.__class__): return -1 return cmp((self._lhs, self._rhs), (other._lhs, other._rhs)) def __hash__(self): """ @return: A hash value for the C{Production}. @rtype: C{int} """ return self._hash class DependencyProduction(Production): """ A dependency grammar production. Each production maps a single head word to an unordered list of one or more modifier words. """ def __str__(self): """ @return: A verbose string representation of the C{DependencyProduction}. @rtype: C{string} """ str = '\'%s\' ->' % (self._lhs,) for elt in self._rhs: str += ' \'%s\'' % (elt,) return str class WeightedProduction(Production, ImmutableProbabilisticMixIn): """ A probabilistic context free grammar production. PCFG C{WeightedProduction}s are essentially just C{Production}s that have probabilities associated with them. These probabilities are used to record how likely it is that a given production will be used. In particular, the probability of a C{WeightedProduction} records the likelihood that its right-hand side is the correct instantiation for any given occurance of its left-hand side. @see: L{Production} """ def __init__(self, lhs, rhs, **prob): """ Construct a new C{WeightedProduction}. @param lhs: The left-hand side of the new C{WeightedProduction}. @type lhs: L{Nonterminal} @param rhs: The right-hand side of the new C{WeightedProduction}. @type rhs: sequence of (C{Nonterminal} and (terminal)) @param prob: Probability parameters of the new C{WeightedProduction}. """ ImmutableProbabilisticMixIn.__init__(self, **prob) Production.__init__(self, lhs, rhs) def __str__(self): return Production.__str__(self) + ' [%s]' % self.prob() def __eq__(self, other): return (isinstance(other, self.__class__) and self._lhs == other._lhs and self._rhs == other._rhs and self.prob() == other.prob()) def __ne__(self, other): return not (self == other) def __hash__(self): return hash((self._lhs, self._rhs, self.prob())) ################################################################# # Grammars ################################################################# class ContextFreeGrammar(object): """ A context-free grammar. A grammar consists of a start state and a set of productions. The set of terminals and nonterminals is implicitly specified by the productions. If you need efficient key-based access to productions, you can use a subclass to implement it. """ def __init__(self, start, productions, calculate_leftcorners=True): """ Create a new context-free grammar, from the given start state and set of C{Production}s. @param start: The start symbol @type start: L{Nonterminal} @param productions: The list of productions that defines the grammar @type productions: C{list} of L{Production} @param calculate_leftcorners: False if we don't want to calculate the leftcorner relation. In that case, some optimized chart parsers won't work. @type calculate_leftcorners: C{bool} """ self._start = start self._productions = productions self._categories = set(prod.lhs() for prod in productions) self._calculate_indexes() self._calculate_grammar_forms() if calculate_leftcorners: self._calculate_leftcorners() def _calculate_indexes(self): self._lhs_index = {} self._rhs_index = {} self._empty_index = {} self._lexical_index = {} for prod in self._productions: # Left hand side. lhs = prod._lhs if lhs not in self._lhs_index: self._lhs_index[lhs] = [] self._lhs_index[lhs].append(prod) if prod._rhs: # First item in right hand side. rhs0 = prod._rhs[0] if rhs0 not in self._rhs_index: self._rhs_index[rhs0] = [] self._rhs_index[rhs0].append(prod) else: # The right hand side is empty. self._empty_index[prod.lhs()] = prod # Lexical tokens in the right hand side. for token in prod._rhs: if is_terminal(token): self._lexical_index.setdefault(token, set()).add(prod) def _calculate_leftcorners(self): # Calculate leftcorner relations, for use in optimized parsing. self._immediate_leftcorner_categories = dict((cat, set([cat])) for cat in self._categories) self._immediate_leftcorner_words = dict((cat, set()) for cat in self._categories) for prod in self.productions(): if len(prod) > 0: cat, left = prod.lhs(), prod.rhs()[0] if is_nonterminal(left): self._immediate_leftcorner_categories[cat].add(left) else: self._immediate_leftcorner_words[cat].add(left) lc = transitive_closure(self._immediate_leftcorner_categories, reflexive=True) self._leftcorners = lc self._leftcorner_parents = invert_graph(lc) nr_leftcorner_categories = sum(map(len, self._immediate_leftcorner_categories.values())) nr_leftcorner_words = sum(map(len, self._immediate_leftcorner_words.values())) if nr_leftcorner_words > nr_leftcorner_categories > 10000: # If the grammar is big, the leftcorner-word dictionary will be too large. # In that case it is better to calculate the relation on demand. self._leftcorner_words = None return self._leftcorner_words = {} for cat, lefts in self._leftcorners.iteritems(): lc = self._leftcorner_words[cat] = set() for left in lefts: lc.update(self._immediate_leftcorner_words.get(left, set())) def start(self): """ @return: The start symbol of the grammar @rtype: L{Nonterminal} """ return self._start # tricky to balance readability and efficiency here! # can't use set operations as they don't preserve ordering def productions(self, lhs=None, rhs=None, empty=False): """ Return the grammar productions, filtered by the left-hand side or the first item in the right-hand side. @param lhs: Only return productions with the given left-hand side. @param rhs: Only return productions with the given first item in the right-hand side. @param empty: Only return productions with an empty right-hand side. @return: A list of productions matching the given constraints. @rtype: C{list} of C{Production} """ if rhs and empty: raise ValueError("You cannot select empty and non-empty " "productions at the same time.") # no constraints so return everything if not lhs and not rhs: if not empty: return self._productions else: return self._empty_index.values() # only lhs specified so look up its index elif lhs and not rhs: if not empty: return self._lhs_index.get(lhs, []) elif lhs in self._empty_index: return [self._empty_index[lhs]] else: return [] # only rhs specified so look up its index elif rhs and not lhs: return self._rhs_index.get(rhs, []) # intersect else: return [prod for prod in self._lhs_index.get(lhs, []) if prod in self._rhs_index.get(rhs, [])] def leftcorners(self, cat): """ Return the set of all nonterminals that the given nonterminal can start with, including itself. This is the reflexive, transitive closure of the immediate leftcorner relation: (A > B) iff (A -> B beta) @param cat: the parent of the leftcorners @type cat: C{Nonterminal} @return: the set of all leftcorners @rtype: C{set} of C{Nonterminal} """ return self._leftcorners.get(cat, set([cat])) def is_leftcorner(self, cat, left): """ True if left is a leftcorner of cat, where left can be a terminal or a nonterminal. @param cat: the parent of the leftcorner @type cat: C{Nonterminal} @param left: the suggested leftcorner @type left: C{Terminal} or C{Nonterminal} @rtype: C{bool} """ if is_nonterminal(left): return left in self.leftcorners(cat) elif self._leftcorner_words: return left in self._leftcorner_words.get(cat, set()) else: return any([left in _immediate_leftcorner_words.get(parent, set()) for parent in self.leftcorners(cat)]) def leftcorner_parents(self, cat): """ Return the set of all nonterminals for which the given category is a left corner. This is the inverse of the leftcorner relation. @param cat: the suggested leftcorner @type cat: C{Nonterminal} @return: the set of all parents to the leftcorner @rtype: C{set} of C{Nonterminal} """ return self._leftcorner_parents.get(cat, set([cat])) def check_coverage(self, tokens): """ Check whether the grammar rules cover the given list of tokens. If not, then raise an exception. @type tokens: C{list} of C{str} """ missing = [tok for tok in tokens if not self._lexical_index.get(tok)] if missing: missing = ', '.join('%r' % (w,) for w in missing) raise ValueError("Grammar does not cover some of the " "input words: %r." % missing) # [xx] does this still get used anywhere, or does check_coverage # replace it? @deprecated("Use ContextFreeGrammar.check_coverage instead.") def covers(self, tokens): """ Check whether the grammar rules cover the given list of tokens. @param tokens: the given list of tokens. @type tokens: a C{list} of C{string} objects. @return: True/False """ for token in tokens: if self._lexical_index.get(token): return False return True def _calculate_grammar_forms(self): """ Pre-calculate of which form(s) the grammar is. """ prods = self._productions self._is_lexical = all([p.is_lexical() for p in prods]) self._is_nonlexical = all([p.is_nonlexical() for p in prods if len(p) != 1]) self._min_len = min(len(p) for p in prods) self._max_len = max(len(p) for p in prods) self._all_unary_are_lexical = all([p.is_lexical() for p in prods if len(p) == 1]) def is_lexical(self): """ True if all productions are lexicalised. """ return self._is_lexical def is_nonlexical(self): """ True if all lexical rules are "preterminals", that is, unary rules which can be separated in a preprocessing step. This means that all productions are of the forms A -> B1 ... Bn (n>=0), or A -> "s". Note: is_lexical() and is_nonlexical() are not opposites. There are grammars which are neither, and grammars which are both. """ return self._is_nonlexical def min_len(self): """ The right-hand side length of the shortest grammar production. """ return self._min_len def max_len(self): """ The right-hand side length of the longest grammar production. """ return self._max_len def is_nonempty(self): """ True if there are no empty productions. """ return self._min_len > 0 def is_binarised(self): """ True if all productions are at most binary. Note that there can still be empty and unary productions. """ return self._max_len <= 2 def is_flexible_chomsky_normal_form(self): """ True if all productions are of the forms A -> B C, A -> B, or A -> "s". """ return self.is_nonempty() and self.is_nonlexical() and self.is_binarised() def is_chomsky_normal_form(self): """ A grammar is of Chomsky normal form if all productions are of the forms A -> B C, or A -> "s". """ return (self.is_flexible_chomsky_normal_form() and self._all_unary_are_lexical) def __repr__(self): return '' % len(self._productions) def __str__(self): str = 'Grammar with %d productions' % len(self._productions) str += ' (start state = %r)' % self._start for production in self._productions: str += '\n %s' % production return str from nltk.internals import Deprecated class Grammar(ContextFreeGrammar, Deprecated): """Use nltk.ContextFreeGrammar instead.""" class FeatureGrammar(ContextFreeGrammar): """ A feature-based grammar. This is equivalent to a L{ContextFreeGrammar} whose nonterminals are L{FeatStructNonterminal}s. A grammar consists of a start state and a set of productions. The set of terminals and nonterminals is implicitly specified by the productions. """ def __init__(self, start, productions): """ Create a new feature-based grammar, from the given start state and set of C{Production}s. @param start: The start symbol @type start: L{FeatStructNonterminal} @param productions: The list of productions that defines the grammar @type productions: C{list} of L{Production} """ ContextFreeGrammar.__init__(self, start, productions) # The difference with CFG is that the productions are # indexed on the TYPE feature of the nonterminals. # This is calculated by the method _get_type_if_possible(). def _calculate_indexes(self): self._lhs_index = {} self._rhs_index = {} self._empty_index = {} self._lexical_index = {} for prod in self._productions: # Left hand side. lhs = self._get_type_if_possible(prod._lhs) if lhs not in self._lhs_index: self._lhs_index[lhs] = [] self._lhs_index[lhs].append(prod) if prod._rhs: # First item in right hand side. rhs0 = self._get_type_if_possible(prod._rhs[0]) if rhs0 not in self._rhs_index: self._rhs_index[rhs0] = [] self._rhs_index[rhs0].append(prod) else: # The right hand side is empty. self._empty_index[self._get_type_if_possible(prod._lhs)] = prod # Lexical tokens in the right hand side. for token in prod._rhs: if is_terminal(token): self._lexical_index.setdefault(token, set()).add(prod) def productions(self, lhs=None, rhs=None, empty=False): """ Return the grammar productions, filtered by the left-hand side or the first item in the right-hand side. @param lhs: Only return productions with the given left-hand side. @param rhs: Only return productions with the given first item in the right-hand side. @param empty: Only return productions with an empty right-hand side. @return: A list of productions matching the given constraints. @rtype: C{list} of C{Production} """ if rhs and empty: raise ValueError("You cannot select empty and non-empty " "productions at the same time.") # no constraints so return everything if not lhs and not rhs: if not empty: return self._productions else: return self._empty_index.values() # only lhs specified so look up its index elif lhs and not rhs: if not empty: return self._lhs_index.get(self._get_type_if_possible(lhs), []) elif lhs in self._empty_index: return [self._empty_index[self._get_type_if_possible(lhs)]] else: return [] # only rhs specified so look up its index elif rhs and not lhs: return self._rhs_index.get(self._get_type_if_possible(rhs), []) # intersect else: return [prod for prod in self._lhs_index.get(self._get_type_if_possible(lhs), []) if prod in self._rhs_index.get(self._get_type_if_possible(rhs), [])] def leftcorners(self, cat): """ Return the set of all words that the given category can start with. Also called the I{first set} in compiler construction. """ raise NotImplementedError("Not implemented yet") def leftcorner_parents(self, cat): """ Return the set of all categories for which the given category is a left corner. """ raise NotImplementedError("Not implemented yet") def _get_type_if_possible(self, item): """ Helper function which returns the C{TYPE} feature of the C{item}, if it exists, otherwise it returns the C{item} itself """ if isinstance(item, dict) and TYPE in item: return FeatureValueType(item[TYPE]) else: return item class FeatureValueType(object): """ A helper class for L{FeatureGrammar}s, designed to be different from ordinary strings. This is to stop the C{FeatStruct} C{FOO[]} from being compare equal to the terminal "FOO". """ def __init__(self, value): self._value = value self._hash = hash(value) def __repr__(self): return '<%s>' % self.value def __cmp__(self, other): return cmp(FeatureValueType, type(other)) or cmp(self._value, other._value) def __hash__(self): return self._hash class DependencyGrammar(object): """ A dependency grammar. A DependencyGrammar consists of a set of productions. Each production specifies a head/modifier relationship between a pair of words. """ def __init__(self, productions): """ Create a new dependency grammar, from the set of C{Production}s. @param productions: The list of productions that defines the grammar @type productions: C{list} of L{Production} """ self._productions = productions def contains(self, head, mod): """ @param head: A head word. @type head: C{string}. @param mod: A mod word, to test as a modifier of 'head'. @type mod: C{string}. @return: true if this C{DependencyGrammar} contains a C{DependencyProduction} mapping 'head' to 'mod'. @rtype: C{boolean}. """ for production in self._productions: for possibleMod in production._rhs: if(production._lhs == head and possibleMod == mod): return True return False def __contains__(self, head, mod): """ @param head: A head word. @type head: C{string}. @param mod: A mod word, to test as a modifier of 'head'. @type mod: C{string}. @return: true if this C{DependencyGrammar} contains a C{DependencyProduction} mapping 'head' to 'mod'. @rtype: C{boolean}. """ for production in self._productions: for possibleMod in production._rhs: if(production._lhs == head and possibleMod == mod): return True return False # # should be rewritten, the set comp won't work in all comparisons # def contains_exactly(self, head, modlist): # for production in self._productions: # if(len(production._rhs) == len(modlist)): # if(production._lhs == head): # set1 = Set(production._rhs) # set2 = Set(modlist) # if(set1 == set2): # return True # return False def __str__(self): """ @return: A verbose string representation of the C{DependencyGrammar} @rtype: C{string} """ str = 'Dependency grammar with %d productions' % len(self._productions) for production in self._productions: str += '\n %s' % production return str def __repr__(self): """ @return: A concise string representation of the C{DependencyGrammar} """ return 'Dependency grammar with %d productions' % len(self._productions) class StatisticalDependencyGrammar(object): """ """ def __init__(self, productions, events, tags): self._productions = productions self._events = events self._tags = tags def contains(self, head, mod): """ @param head: A head word. @type head: C{string}. @param mod: A mod word, to test as a modifier of 'head'. @type mod: C{string}. @return: true if this C{DependencyGrammar} contains a C{DependencyProduction} mapping 'head' to 'mod'. @rtype: C{boolean}. """ for production in self._productions: for possibleMod in production._rhs: if(production._lhs == head and possibleMod == mod): return True return False def __str__(self): """ @return: A verbose string representation of the C{StatisticalDependencyGrammar} @rtype: C{string} """ str = 'Statistical dependency grammar with %d productions' % len(self._productions) for production in self._productions: str += '\n %s' % production str += '\nEvents:' for event in self._events: str += '\n %d:%s' % (self._events[event], event) str += '\nTags:' for tag_word in self._tags: str += '\n %s:\t(%s)' % (tag_word, self._tags[tag_word]) return str def __repr__(self): """ @return: A concise string representation of the C{StatisticalDependencyGrammar} """ return 'Statistical Dependency grammar with %d productions' % len(self._productions) class WeightedGrammar(ContextFreeGrammar): """ A probabilistic context-free grammar. A Weighted Grammar consists of a start state and a set of weighted productions. The set of terminals and nonterminals is implicitly specified by the productions. PCFG productions should be C{WeightedProduction}s. C{WeightedGrammar}s impose the constraint that the set of productions with any given left-hand-side must have probabilities that sum to 1. If you need efficient key-based access to productions, you can use a subclass to implement it. @type EPSILON: C{float} @cvar EPSILON: The acceptable margin of error for checking that productions with a given left-hand side have probabilities that sum to 1. """ EPSILON = 0.01 def __init__(self, start, productions, calculate_leftcorners=True): """ Create a new context-free grammar, from the given start state and set of C{WeightedProduction}s. @param start: The start symbol @type start: L{Nonterminal} @param productions: The list of productions that defines the grammar @type productions: C{list} of C{Production} @raise ValueError: if the set of productions with any left-hand-side do not have probabilities that sum to a value within EPSILON of 1. @param calculate_leftcorners: False if we don't want to calculate the leftcorner relation. In that case, some optimized chart parsers won't work. @type calculate_leftcorners: C{bool} """ ContextFreeGrammar.__init__(self, start, productions, calculate_leftcorners) # Make sure that the probabilities sum to one. probs = {} for production in productions: probs[production.lhs()] = (probs.get(production.lhs(), 0) + production.prob()) for (lhs, p) in probs.items(): if not ((1-WeightedGrammar.EPSILON) < p < (1+WeightedGrammar.EPSILON)): raise ValueError("Productions for %r do not sum to 1" % lhs) ################################################################# # Inducing Grammars ################################################################# # Contributed by Nathan Bodenstab def induce_pcfg(start, productions): """ Induce a PCFG grammar from a list of productions. The probability of a production A -> B C in a PCFG is: | count(A -> B C) | P(B, C | A) = --------------- where * is any right hand side | count(A -> *) @param start: The start symbol @type start: L{Nonterminal} @param productions: The list of productions that defines the grammar @type productions: C{list} of L{Production} """ # Production count: the number of times a given production occurs pcount = {} # LHS-count: counts the number of times a given lhs occurs lcount = {} for prod in productions: lcount[prod.lhs()] = lcount.get(prod.lhs(), 0) + 1 pcount[prod] = pcount.get(prod, 0) + 1 prods = [WeightedProduction(p.lhs(), p.rhs(), prob=float(pcount[p]) / lcount[p.lhs()]) for p in pcount] return WeightedGrammar(start, prods) ################################################################# # Parsing Grammars ################################################################# # Parsing CFGs def parse_cfg_production(input): """ @return: a C{list} of context-free L{Production}s. """ return parse_production(input, standard_nonterm_parser) def parse_cfg(input): """ @return: a L{ContextFreeGrammar}. @param input: a grammar, either in the form of a string or else as a list of strings. """ start, productions = parse_grammar(input, standard_nonterm_parser) return ContextFreeGrammar(start, productions) # Parsing Probabilistic CFGs def parse_pcfg_production(input): """ @return: a C{list} of PCFG L{WeightedProduction}s. """ return parse_production(input, standard_nonterm_parser, probabilistic=True) def parse_pcfg(input): """ @return: a probabilistic L{WeightedGrammar}. @param input: a grammar, either in the form of a string or else as a list of strings. """ start, productions = parse_grammar(input, standard_nonterm_parser, probabilistic=True) return WeightedGrammar(start, productions) # Parsing Feature-based CFGs def parse_fcfg_production(input, fstruct_parser): """ @return: a C{list} of feature-based L{Production}s. """ return parse_production(input, fstruct_parser) def parse_fcfg(input, features=None, logic_parser=None, fstruct_parser=None): """ @return: a feature structure based L{FeatureGrammar}. @param input: a grammar, either in the form of a string or else as a list of strings. @param features: a tuple of features (default: SLASH, TYPE) @param logic_parser: a parser for lambda-expressions (default: LogicParser()) @param fstruct_parser: a feature structure parser (only if features and logic_parser is None) """ if features is None: features = (SLASH, TYPE) if fstruct_parser is None: fstruct_parser = FeatStructParser(features, FeatStructNonterminal, logic_parser=logic_parser) elif logic_parser is not None: raise Exception('\'logic_parser\' and \'fstruct_parser\' must ' 'not both be set') start, productions = parse_grammar(input, fstruct_parser.partial_parse) return FeatureGrammar(start, productions) @deprecated("Use nltk.parse_fcfg() instead.") def parse_featcfg(input): return parse_fcfg(input) # Parsing generic grammars _ARROW_RE = re.compile(r'\s* -> \s*', re.VERBOSE) _PROBABILITY_RE = re.compile(r'( \[ [\d\.]+ \] ) \s*', re.VERBOSE) _TERMINAL_RE = re.compile(r'( "[^"]+" | \'[^\']+\' ) \s*', re.VERBOSE) _DISJUNCTION_RE = re.compile(r'\| \s*', re.VERBOSE) def parse_production(line, nonterm_parser, probabilistic=False): """ Parse a grammar rule, given as a string, and return a list of productions. """ pos = 0 # Parse the left-hand side. lhs, pos = nonterm_parser(line, pos) # Skip over the arrow. m = _ARROW_RE.match(line, pos) if not m: raise ValueError('Expected an arrow') pos = m.end() # Parse the right hand side. probabilities = [0.0] rhsides = [[]] while pos < len(line): # Probability. m = _PROBABILITY_RE.match(line, pos) if probabilistic and m: pos = m.end() probabilities[-1] = float(m.group(1)[1:-1]) if probabilities[-1] > 1.0: raise ValueError('Production probability %f, ' 'should not be greater than 1.0' % (probabilities[-1],)) # String -- add terminal. elif line[pos] in "\'\"": m = _TERMINAL_RE.match(line, pos) if not m: raise ValueError('Unterminated string') rhsides[-1].append(m.group(1)[1:-1]) pos = m.end() # Vertical bar -- start new rhside. elif line[pos] == '|': m = _DISJUNCTION_RE.match(line, pos) probabilities.append(0.0) rhsides.append([]) pos = m.end() # Anything else -- nonterminal. else: nonterm, pos = nonterm_parser(line, pos) rhsides[-1].append(nonterm) if probabilistic: return [WeightedProduction(lhs, rhs, prob=probability) for (rhs, probability) in zip(rhsides, probabilities)] else: return [Production(lhs, rhs) for rhs in rhsides] def parse_grammar(input, nonterm_parser, probabilistic=False): """ @return: a pair of - a starting category - a list of C{Production}s @param input: a grammar, either in the form of a string or else as a list of strings. @param nonterm_parser: a function for parsing nonterminals. It should take a C{(string,position)} as argument and return a C{(nonterminal,position)} as result. @param probabilistic: are the grammar rules probabilistic? """ if isinstance(input, str): lines = input.split('\n') else: lines = input start = None productions = [] continue_line = '' for linenum, line in enumerate(lines): line = continue_line + line.strip() if line.startswith('#') or line=='': continue if line.endswith('\\'): continue_line = line[:-1].rstrip()+' ' continue continue_line = '' try: if line[0] == '%': directive, args = line[1:].split(None, 1) if directive == 'start': start, pos = nonterm_parser(args, 0) if pos != len(args): raise ValueError('Bad argument to start directive') else: raise ValueError('Bad directive') else: # expand out the disjunctions on the RHS productions += parse_production(line, nonterm_parser, probabilistic) except ValueError, e: raise ValueError('Unable to parse line %s: %s\n%s' % (linenum+1, line, e)) if not productions: raise ValueError, 'No productions found!' if not start: start = productions[0].lhs() return (start, productions) _STANDARD_NONTERM_RE = re.compile('( [\w/][\w/^<>-]* ) \s*', re.VERBOSE) def standard_nonterm_parser(string, pos): m = _STANDARD_NONTERM_RE.match(string, pos) if not m: raise ValueError('Expected a nonterminal, found: ' + string[pos:]) return (Nonterminal(m.group(1)), m.end()) ################################################################# # Parsing Dependency Grammars ################################################################# _PARSE_DG_RE = re.compile(r'''^\s* # leading whitespace ('[^']+')\s* # single-quoted lhs (?:[-=]+>)\s* # arrow (?:( # rhs: "[^"]+" # doubled-quoted terminal | '[^']+' # single-quoted terminal | \| # disjunction ) \s*) # trailing space *$''', # zero or more copies re.VERBOSE) _SPLIT_DG_RE = re.compile(r'''('[^']'|[-=]+>|"[^"]+"|'[^']+'|\|)''') def parse_dependency_grammar(s): productions = [] for linenum, line in enumerate(s.split('\n')): line = line.strip() if line.startswith('#') or line=='': continue try: productions += parse_dependency_production(line) except ValueError: raise ValueError, 'Unable to parse line %s: %s' % (linenum, line) if len(productions) == 0: raise ValueError, 'No productions found!' return DependencyGrammar(productions) def parse_dependency_production(s): if not _PARSE_DG_RE.match(s): raise ValueError, 'Bad production string' pieces = _SPLIT_DG_RE.split(s) pieces = [p for i,p in enumerate(pieces) if i%2==1] lhside = pieces[0].strip('\'\"') rhsides = [[]] for piece in pieces[2:]: if piece == '|': rhsides.append([]) else: rhsides[-1].append(piece.strip('\'\"')) return [DependencyProduction(lhside, rhside) for rhside in rhsides] ################################################################# # Demonstration ################################################################# def cfg_demo(): """ A demonstration showing how C{ContextFreeGrammar}s can be created and used. """ from nltk import nonterminals, Production, parse_cfg # Create some nonterminals S, NP, VP, PP = nonterminals('S, NP, VP, PP') N, V, P, Det = nonterminals('N, V, P, Det') VP_slash_NP = VP/NP print 'Some nonterminals:', [S, NP, VP, PP, N, V, P, Det, VP/NP] print ' S.symbol() =>', `S.symbol()` print print Production(S, [NP]) # Create some Grammar Productions grammar = parse_cfg(""" S -> NP VP PP -> P NP NP -> Det N | NP PP VP -> V NP | VP PP Det -> 'a' | 'the' N -> 'dog' | 'cat' V -> 'chased' | 'sat' P -> 'on' | 'in' """) print 'A Grammar:', `grammar` print ' grammar.start() =>', `grammar.start()` print ' grammar.productions() =>', # Use string.replace(...) is to line-wrap the output. print `grammar.productions()`.replace(',', ',\n'+' '*25) print print 'Coverage of input words by a grammar:' print grammar.covers(['a','dog']) print grammar.covers(['a','toy']) toy_pcfg1 = parse_pcfg(""" S -> NP VP [1.0] NP -> Det N [0.5] | NP PP [0.25] | 'John' [0.1] | 'I' [0.15] Det -> 'the' [0.8] | 'my' [0.2] N -> 'man' [0.5] | 'telescope' [0.5] VP -> VP PP [0.1] | V NP [0.7] | V [0.2] V -> 'ate' [0.35] | 'saw' [0.65] PP -> P NP [1.0] P -> 'with' [0.61] | 'under' [0.39] """) toy_pcfg2 = parse_pcfg(""" S -> NP VP [1.0] VP -> V NP [.59] VP -> V [.40] VP -> VP PP [.01] NP -> Det N [.41] NP -> Name [.28] NP -> NP PP [.31] PP -> P NP [1.0] V -> 'saw' [.21] V -> 'ate' [.51] V -> 'ran' [.28] N -> 'boy' [.11] N -> 'cookie' [.12] N -> 'table' [.13] N -> 'telescope' [.14] N -> 'hill' [.5] Name -> 'Jack' [.52] Name -> 'Bob' [.48] P -> 'with' [.61] P -> 'under' [.39] Det -> 'the' [.41] Det -> 'a' [.31] Det -> 'my' [.28] """) def pcfg_demo(): """ A demonstration showing how C{WeightedGrammar}s can be created and used. """ from nltk.corpus import treebank from nltk import treetransforms from nltk import induce_pcfg from nltk.parse import pchart pcfg_prods = toy_pcfg1.productions() pcfg_prod = pcfg_prods[2] print 'A PCFG production:', `pcfg_prod` print ' pcfg_prod.lhs() =>', `pcfg_prod.lhs()` print ' pcfg_prod.rhs() =>', `pcfg_prod.rhs()` print ' pcfg_prod.prob() =>', `pcfg_prod.prob()` print grammar = toy_pcfg2 print 'A PCFG grammar:', `grammar` print ' grammar.start() =>', `grammar.start()` print ' grammar.productions() =>', # Use string.replace(...) is to line-wrap the output. print `grammar.productions()`.replace(',', ',\n'+' '*26) print print 'Coverage of input words by a grammar:' print grammar.covers(['a','boy']) print grammar.covers(['a','girl']) # extract productions from three trees and induce the PCFG print "Induce PCFG grammar from treebank data:" productions = [] for item in treebank.items[:2]: for tree in treebank.parsed_sents(item): # perform optional tree transformations, e.g.: tree.collapse_unary(collapsePOS = False) tree.chomsky_normal_form(horzMarkov = 2) productions += tree.productions() S = Nonterminal('S') grammar = induce_pcfg(S, productions) print grammar print print "Parse sentence using induced grammar:" parser = pchart.InsideChartParser(grammar) parser.trace(3) # doesn't work as tokens are different: #sent = treebank.tokenized('wsj_0001.mrg')[0] sent = treebank.parsed_sents('wsj_0001.mrg')[0].leaves() print sent for parse in parser.nbest_parse(sent): print parse def fcfg_demo(): import nltk.data g = nltk.data.load('grammars/book_grammars/feat0.fcfg') print g print def dg_demo(): """ A demonstration showing the creation and inspection of a C{DependencyGrammar}. """ grammar = parse_dependency_grammar(""" 'scratch' -> 'cats' | 'walls' 'walls' -> 'the' 'cats' -> 'the' """) print grammar def sdg_demo(): """ A demonstration of how to read a string representation of a CoNLL format dependency tree. """ dg = DependencyGraph(""" 1 Ze ze Pron Pron per|3|evofmv|nom 2 su _ _ 2 had heb V V trans|ovt|1of2of3|ev 0 ROOT _ _ 3 met met Prep Prep voor 8 mod _ _ 4 haar haar Pron Pron bez|3|ev|neut|attr 5 det _ _ 5 moeder moeder N N soort|ev|neut 3 obj1 _ _ 6 kunnen kan V V hulp|ott|1of2of3|mv 2 vc _ _ 7 gaan ga V V hulp|inf 6 vc _ _ 8 winkelen winkel V V intrans|inf 11 cnj _ _ 9 , , Punc Punc komma 8 punct _ _ 10 zwemmen zwem V V intrans|inf 11 cnj _ _ 11 of of Conj Conj neven 7 vc _ _ 12 terrassen terras N N soort|mv|neut 11 cnj _ _ 13 . . Punc Punc punt 12 punct _ _ """) tree = dg.tree() print tree.pprint() def demo(): cfg_demo() pcfg_demo() fcfg_demo() dg_demo() sdg_demo() if __name__ == '__main__': demo() __all__ = ['Nonterminal', 'nonterminals', 'Production', 'DependencyProduction', 'WeightedProduction', 'ContextFreeGrammar', 'WeightedGrammar', 'DependencyGrammar', 'StatisticalDependencyGrammar', 'induce_pcfg', 'parse_cfg', 'parse_cfg_production', 'parse_pcfg', 'parse_pcfg_production', 'parse_fcfg', 'parse_fcfg_production', 'parse_grammar', 'parse_production', 'parse_dependency_grammar', 'parse_dependency_production', 'demo', 'cfg_demo', 'pcfg_demo', 'dg_demo', 'sdg_demo', 'toy_pcfg1', 'toy_pcfg2'] nltk-2.0~b9/nltk/featstruct.py0000644000175000017500000030742611327451603016302 0ustar bhavanibhavani# Natural Language Toolkit: Feature Structures # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper , # Rob Speer, # Steven Bird # URL: # For license information, see LICENSE.TXT # # $Id$ """ Basic data classes for representing feature structures, and for performing basic operations on those feature structures. A X{feature structure} is a mapping from feature identifiers to feature values, where each feature value is either a basic value (such as a string or an integer), or a nested feature structure. There are two types of feature structure, implemented by two subclasses of L{FeatStruct}: - I{feature dictionaries}, implemented by L{FeatDict}, act like Python dictionaries. Feature identifiers may be strings or instances of the L{Feature} class. - I{feature lists}, implemented by L{FeatList}, act like Python lists. Feature identifiers are integers. Feature structures are typically used to represent partial information about objects. A feature identifier that is not mapped to a value stands for a feature whose value is unknown (I{not} a feature without a value). Two feature structures that represent (potentially overlapping) information about the same object can be combined by X{unification}. When two inconsistent feature structures are unified, the unification fails and returns C{None}. Features can be specified using X{feature paths}, or tuples of feature identifiers that specify path through the nested feature structures to a value. Feature structures may contain reentrant feature values. A X{reentrant feature value} is a single feature value that can be accessed via multiple feature paths. Unification preserves the reentrance relations imposed by both of the unified feature structures. In the feature structure resulting from unification, any modifications to a reentrant feature value will be visible using any of its feature paths. Feature structure variables are encoded using the L{nltk.sem.Variable} class. The variables' values are tracked using a X{bindings} dictionary, which maps variables to their values. When two feature structures are unified, a fresh bindings dictionary is created to track their values; and before unification completes, all bound variables are replaced by their values. Thus, the bindings dictionaries are usually strictly internal to the unification process. However, it is possible to track the bindings of variables if you choose to, by supplying your own initial bindings dictionary to the L{unify()} function. When unbound variables are unified with one another, they become X{aliased}. This is encoded by binding one variable to the other. Lightweight Feature Structures ============================== Many of the functions defined by L{nltk.featstruct} can be applied directly to simple Python dictionaries and lists, rather than to full-fledged L{FeatDict} and L{FeatList} objects. In other words, Python C{dicts} and C{lists} can be used as "light-weight" feature structures. >>> from nltk.featstruct import unify >>> unify(dict(x=1, y=dict()), dict(a='a', y=dict(b='b'))) {'y': {'b': 'b'}, 'x': 1, 'a': 'a'} However, you should keep in mind the following caveats: - Python dictionaries & lists ignore reentrance when checking for equality between values. But two FeatStructs with different reentrances are considered nonequal, even if all their base values are equal. - FeatStructs can be easily frozen, allowing them to be used as keys in hash tables. Python dictionaries and lists can not. - FeatStructs display reentrance in their string representations; Python dictionaries and lists do not. - FeatStructs may *not* be mixed with Python dictionaries and lists (e.g., when performing unification). - FeatStructs provide a number of useful methods, such as L{walk() } and L{cyclic() }, which are not available for Python dicts & lists. In general, if your feature structures will contain any reentrances, or if you plan to use them as dictionary keys, it is strongly recommended that you use full-fledged L{FeatStruct} objects. """ import re, copy from nltk.sem.logic import Variable, Expression, SubstituteBindingsI from nltk.sem.logic import LogicParser, ParseException import nltk.internals ###################################################################### # Feature Structure ###################################################################### class FeatStruct(SubstituteBindingsI): """ A mapping from feature identifiers to feature values, where each feature value is either a basic value (such as a string or an integer), or a nested feature structure. There are two types of feature structure: - I{feature dictionaries}, implemented by L{FeatDict}, act like Python dictionaries. Feature identifiers may be strings or instances of the L{Feature} class. - I{feature lists}, implemented by L{FeatList}, act like Python lists. Feature identifiers are integers. Feature structures may be indexed using either simple feature identifiers or 'feature paths.' A X{feature path} is a sequence of feature identifiers that stand for a corresponding sequence of indexing operations. In particular, C{fstruct[(f1,f2,...,fn)]} is equivalent to C{fstruct[f1][f2]...[fn]}. Feature structures may contain reentrant feature structures. A X{reentrant feature structure} is a single feature structure object that can be accessed via multiple feature paths. Feature structures may also be cyclic. A feature structure is X{cyclic} if there is any feature path from the feature structure to itself. Two feature structures are considered equal if they assign the same values to all features, and have the same reentrancies. By default, feature structures are mutable. They may be made immutable with the L{freeze()} function. Once they have been frozen, they may be hashed, and thus used as dictionary keys. """ _frozen = False """@ivar: A flag indicating whether this feature structure is frozen or not. Once this flag is set, it should never be un-set; and no further modification should be made to this feature structue.""" ##//////////////////////////////////////////////////////////// #{ Constructor ##//////////////////////////////////////////////////////////// def __new__(cls, features=None, **morefeatures): """ Construct and return a new feature structure. If this constructor is called directly, then the returned feature structure will be an instance of either the L{FeatDict} class or the L{FeatList} class. @param features: The initial feature values for this feature structure: - FeatStruct(string) -> FeatStructParser().parse(string) - FeatStruct(mapping) -> FeatDict(mapping) - FeatStruct(sequence) -> FeatList(sequence) - FeatStruct() -> FeatDict() @param morefeatures: If C{features} is a mapping or C{None}, then C{morefeatures} provides additional features for the C{FeatDict} constructor. """ # If the FeatStruct constructor is called directly, then decide # whether to create a FeatDict or a FeatList, based on the # contents of the `features` argument. if cls is FeatStruct: if features is None: return FeatDict.__new__(FeatDict, **morefeatures) elif _is_mapping(features): return FeatDict.__new__(FeatDict, features, **morefeatures) elif morefeatures: raise TypeError('Keyword arguments may only be specified ' 'if features is None or is a mapping.') if isinstance(features, basestring): if FeatStructParser._START_FDICT_RE.match(features): return FeatDict.__new__(FeatDict, features, **morefeatures) else: return FeatList.__new__(FeatList, features, **morefeatures) elif _is_sequence(features): return FeatList.__new__(FeatList, features) else: raise TypeError('Expected string or mapping or sequence') # Otherwise, construct the object as normal. else: return super(FeatStruct, cls).__new__(cls, features, **morefeatures) ##//////////////////////////////////////////////////////////// #{ Uniform Accessor Methods ##//////////////////////////////////////////////////////////// # These helper functions allow the methods defined by FeatStruct # to treat all feature structures as mappings, even if they're # really lists. (Lists are treated as mappings from ints to vals) def _keys(self): """Return an iterable of the feature identifiers used by this FeatStruct.""" raise NotImplementedError() # Implemented by subclasses. def _values(self): """Return an iterable of the feature values directly defined by this FeatStruct.""" raise NotImplementedError() # Implemented by subclasses. def _items(self): """Return an iterable of (fid,fval) pairs, where fid is a feature identifier and fval is the corresponding feature value, for all features defined by this FeatStruct.""" raise NotImplementedError() # Implemented by subclasses. ##//////////////////////////////////////////////////////////// #{ Equality & Hashing ##//////////////////////////////////////////////////////////// def equal_values(self, other, check_reentrance=False): """ @return: True if C{self} and C{other} assign the same value to to every feature. In particular, return true if C{self[M{p}]==other[M{p}]} for every feature path M{p} such that C{self[M{p}]} or C{other[M{p}]} is a base value (i.e., not a nested feature structure). @param check_reentrance: If true, then also return false if there is any difference between the reentrances of C{self} and C{other}. @note: the L{== operator <__eq__>} is equivalent to C{equal_values()} with C{check_reentrance=True}. """ return self._equal(other, check_reentrance, set(), set(), set()) def __eq__(self, other): """ Return true if C{self} and C{other} are both feature structures, assign the same values to all features, and contain the same reentrances. I.e., return C{self.equal_values(other, check_reentrance=True)}. @see: L{equal_values()} """ return self._equal(other, True, set(), set(), set()) def __ne__(self, other): """ Return true unless C{self} and C{other} are both feature structures, assign the same values to all features, and contain the same reentrances. I.e., return C{not self.equal_values(other, check_reentrance=True)}. """ return not self.__eq__(other) def __hash__(self): """ If this feature structure is frozen, return its hash value; otherwise, raise C{TypeError}. """ if not self._frozen: raise TypeError('FeatStructs must be frozen before they ' 'can be hashed.') try: return self.__hash except AttributeError: self.__hash = self._hash(set()) return self.__hash def _equal(self, other, check_reentrance, visited_self, visited_other, visited_pairs): """ @return: True iff self and other have equal values. @param visited_self: A set containing the ids of all C{self} feature structures we've already visited. @param visited_other: A set containing the ids of all C{other} feature structures we've already visited. @param visited_pairs: A set containing C{(selfid, otherid)} pairs for all pairs of feature structures we've already visited. """ # If we're the same object, then we're equal. if self is other: return True # If we have different classes, we're definitely not equal. if self.__class__ != other.__class__: return False # If we define different features, we're definitely not equal. # (Perform len test first because it's faster -- we should # do profiling to see if this actually helps) if len(self) != len(other): return False if set(self._keys()) != set(other._keys()): return False # If we're checking reentrance, then any time we revisit a # structure, make sure that it was paired with the same # feature structure that it is now. Note: if check_reentrance, # then visited_pairs will never contain two pairs whose first # values are equal, or two pairs whose second values are equal. if check_reentrance: if id(self) in visited_self or id(other) in visited_other: return (id(self), id(other)) in visited_pairs # If we're not checking reentrance, then we still need to deal # with cycles. If we encounter the same (self, other) pair a # second time, then we won't learn anything more by examining # their children a second time, so just return true. else: if (id(self), id(other)) in visited_pairs: return True # Keep track of which nodes we've visited. visited_self.add(id(self)) visited_other.add(id(other)) visited_pairs.add( (id(self), id(other)) ) # Now we have to check all values. If any of them don't match, # then return false. for (fname, self_fval) in self._items(): other_fval = other[fname] if isinstance(self_fval, FeatStruct): if not self_fval._equal(other_fval, check_reentrance, visited_self, visited_other, visited_pairs): return False else: if self_fval != other_fval: return False # Everything matched up; return true. return True def _hash(self, visited): """ @return: A hash value for this feature structure. @require: C{self} must be frozen. @param visited: A set containing the ids of all feature structures we've already visited while hashing. """ if id(self) in visited: return 1 visited.add(id(self)) hashval = 5831 for (fname, fval) in sorted(self._items()): hashval *= 37 hashval += hash(fname) hashval *= 37 if isinstance(fval, FeatStruct): hashval += fval._hash(visited) else: hashval += hash(fval) # Convert to a 32 bit int. hashval = int(hashval & 0x7fffffff) return hashval ##//////////////////////////////////////////////////////////// #{ Freezing ##//////////////////////////////////////////////////////////// #: Error message used by mutating methods when called on a frozen #: feature structure. _FROZEN_ERROR = "Frozen FeatStructs may not be modified." def freeze(self): """ Make this feature structure, and any feature structures it contains, immutable. Note: this method does not attempt to 'freeze' any feature values that are not C{FeatStruct}s; it is recommended that you use only immutable feature values. """ if self._frozen: return self._freeze(set()) def frozen(self): """ @return: True if this feature structure is immutable. Feature structures can be made immutable with the L{freeze()} method. Immutable feature structures may not be made mutable again, but new mutale copies can be produced with the L{copy()} method. """ return self._frozen def _freeze(self, visited): """ Make this feature structure, and any feature structure it contains, immutable. @param visited: A set containing the ids of all feature structures we've already visited while freezing. """ if id(self) in visited: return visited.add(id(self)) self._frozen = True for (fname, fval) in sorted(self._items()): if isinstance(fval, FeatStruct): fval._freeze(visited) ##//////////////////////////////////////////////////////////// #{ Copying ##//////////////////////////////////////////////////////////// def copy(self, deep=True): """ Return a new copy of C{self}. The new copy will not be frozen. @param deep: If true, create a deep copy; if false, create a shallow copy. """ if deep: return copy.deepcopy(self) else: return self.__class__(self) # Subclasses should define __deepcopy__ to ensure that the new # copy will not be frozen. def __deepcopy__(self, memo): raise NotImplementedError() # Implemented by subclasses. ##//////////////////////////////////////////////////////////// #{ Structural Information ##//////////////////////////////////////////////////////////// def cyclic(self): """ @return: True if this feature structure contains itself. """ return self._find_reentrances({})[id(self)] def reentrances(self): """ @return: A list of all feature structures that can be reached from C{self} by multiple feature paths. @rtype: C{list} of L{FeatStruct} """ reentrance_dict = self._find_reentrances({}) return [struct for (struct, reentrant) in reentrance_dict.items() if reentrant] def walk(self): """ Return an iterator that generates this feature structure, and each feature structure it contains. Each feature structure will be generated exactly once. """ return self._walk(set()) def _walk(self, visited): """ Return an iterator that generates this feature structure, and each feature structure it contains. @param visited: A set containing the ids of all feature structures we've already visited while freezing. """ raise NotImplementedError() # Implemented by subclasses. def _walk(self, visited): if id(self) in visited: return visited.add(id(self)) yield self for fval in self._values(): if isinstance(fval, FeatStruct): for elt in fval._walk(visited): yield elt # Walk through the feature tree. The first time we see a feature # value, map it to False (not reentrant). If we see a feature # value more than once, then map it to True (reentrant). def _find_reentrances(self, reentrances): """ Return a dictionary that maps from the C{id} of each feature structure contained in C{self} (including C{self}) to a boolean value, indicating whether it is reentrant or not. """ if reentrances.has_key(id(self)): # We've seen it more than once. reentrances[id(self)] = True else: # This is the first time we've seen it. reentrances[id(self)] = False # Recurse to contained feature structures. for fval in self._values(): if isinstance(fval, FeatStruct): fval._find_reentrances(reentrances) return reentrances ##//////////////////////////////////////////////////////////// #{ Variables & Bindings ##//////////////////////////////////////////////////////////// def substitute_bindings(self, bindings): """@see: L{nltk.featstruct.substitute_bindings()}""" return substitute_bindings(self, bindings) def retract_bindings(self, bindings): """@see: L{nltk.featstruct.retract_bindings()}""" return retract_bindings(self, bindings) def variables(self): """@see: L{nltk.featstruct.find_variables()}""" return find_variables(self) def rename_variables(self, vars=None, used_vars=(), new_vars=None): """@see: L{nltk.featstruct.rename_variables()}""" return rename_variables(self, vars, used_vars, new_vars) def remove_variables(self): """ @rtype: L{FeatStruct} @return: The feature structure that is obtained by deleting all features whose values are L{Variable}s. """ return remove_variables(self) ##//////////////////////////////////////////////////////////// #{ Unification ##//////////////////////////////////////////////////////////// def unify(self, other, bindings=None, trace=False, fail=None, rename_vars=True): return unify(self, other, bindings, trace, fail, rename_vars) def subsumes(self, other): """ @return: True if C{self} subsumes C{other}. I.e., return true if unifying C{self} with C{other} would result in a feature structure equal to C{other}. """ return subsumes(self, other) ##//////////////////////////////////////////////////////////// #{ String Representations ##//////////////////////////////////////////////////////////// def __repr__(self): """ Display a single-line representation of this feature structure, suitable for embedding in other representations. """ return self._repr(self._find_reentrances({}), {}) def _repr(self, reentrances, reentrance_ids): """ @return: A string representation of this feature structure. @param reentrances: A dictionary that maps from the C{id} of each feature value in self, indicating whether that value is reentrant or not. @param reentrance_ids: A dictionary mapping from the C{id}s of feature values to unique identifiers. This is modified by C{repr}: the first time a reentrant feature value is displayed, an identifier is added to reentrance_ids for it. """ raise NotImplementedError() # Mutation: disable if frozen. _FROZEN_ERROR = "Frozen FeatStructs may not be modified." _FROZEN_NOTICE = "\n%sIf self is frozen, raise ValueError." def _check_frozen(method, indent=''): """ Given a method function, return a new method function that first checks if C{self._frozen} is true; and if so, raises C{ValueError} with an appropriate message. Otherwise, call the method and return its result. """ def wrapped(self, *args, **kwargs): if self._frozen: raise ValueError(_FROZEN_ERROR) else: return method(self, *args, **kwargs) wrapped.__name__ = method.__name__ wrapped.__doc__ = (method.__doc__ or '') + (_FROZEN_NOTICE % indent) return wrapped ###################################################################### # Feature Dictionary ###################################################################### class FeatDict(FeatStruct, dict): """ A feature structure that acts like a Python dictionary. I.e., a mapping from feature identifiers to feature values, where feature identifiers can be strings or L{Feature}s; and feature values can be either basic values (such as a string or an integer), or nested feature structures. Feature identifiers for C{FeatDict}s are sometimes called X{feature names}. Two feature dicts are considered equal if they assign the same values to all features, and have the same reentrances. @see: L{FeatStruct} for information about feature paths, reentrance, cyclic feature structures, mutability, freezing, and hashing. """ def __init__(self, features=None, **morefeatures): """ Create a new feature dictionary, with the specified features. @param features: The initial value for this feature dictionary. If C{features} is a C{FeatStruct}, then its features are copied (shallow copy). If C{features} is a C{dict}, then a feature is created for each item, mapping its key to its value. If C{features} is a string, then it is parsed using L{FeatStructParser}. If C{features} is a list of tuples C{name,val}, then a feature is created for each tuple. @param morefeatures: Additional features for the new feature dictionary. If a feature is listed under both C{features} and C{morefeatures}, then the value from C{morefeatures} will be used. """ if isinstance(features, basestring): FeatStructParser().parse(features, self) self.update(**morefeatures) else: # update() checks the types of features. self.update(features, **morefeatures) #//////////////////////////////////////////////////////////// #{ Dict methods #//////////////////////////////////////////////////////////// _INDEX_ERROR = "Expected feature name or path. Got %r." def __getitem__(self, name_or_path): """If the feature with the given name or path exists, return its value; otherwise, raise C{KeyError}.""" if isinstance(name_or_path, (basestring, Feature)): return dict.__getitem__(self, name_or_path) elif isinstance(name_or_path, tuple): try: val = self for fid in name_or_path: if not isinstance(val, FeatStruct): raise KeyError # path contains base value val = val[fid] return val except (KeyError, IndexError): raise KeyError(name_or_path) else: raise TypeError(self._INDEX_ERROR % name_or_path) def get(self, name_or_path, default=None): """If the feature with the given name or path exists, return its value; otherwise, return C{default}.""" try: return self[name_or_path] except KeyError: return default def __contains__(self, name_or_path): """Return true if a feature with the given name or path exists.""" try: self[name_or_path]; return True except KeyError: return False def has_key(self, name_or_path): """Return true if a feature with the given name or path exists.""" return name_or_path in self def __delitem__(self, name_or_path): """If the feature with the given name or path exists, delete its value; otherwise, raise C{KeyError}.""" if self._frozen: raise ValueError(_FROZEN_ERROR) if isinstance(name_or_path, (basestring, Feature)): return dict.__delitem__(self, name_or_path) elif isinstance(name_or_path, tuple): if len(name_or_path) == 0: raise ValueError("The path () can not be set") else: parent = self[name_or_path[:-1]] if not isinstance(parent, FeatStruct): raise KeyError(name_or_path) # path contains base value del parent[name_or_path[-1]] else: raise TypeError(self._INDEX_ERROR % name_or_path) def __setitem__(self, name_or_path, value): """Set the value for the feature with the given name or path to C{value}. If C{name_or_path} is an invalid path, raise C{KeyError}.""" if self._frozen: raise ValueError(_FROZEN_ERROR) if isinstance(name_or_path, (basestring, Feature)): return dict.__setitem__(self, name_or_path, value) elif isinstance(name_or_path, tuple): if len(name_or_path) == 0: raise ValueError("The path () can not be set") else: parent = self[name_or_path[:-1]] if not isinstance(parent, FeatStruct): raise KeyError(name_or_path) # path contains base value parent[name_or_path[-1]] = value else: raise TypeError(self._INDEX_ERROR % name_or_path) clear = _check_frozen(dict.clear) pop = _check_frozen(dict.pop) popitem = _check_frozen(dict.popitem) setdefault = _check_frozen(dict.setdefault) def update(self, features=None, **morefeatures): if self._frozen: raise ValueError(_FROZEN_ERROR) if features is None: items = () elif hasattr(features, 'has_key'): items = features.items() elif hasattr(features, '__iter__'): items = features else: raise ValueError('Expected mapping or list of tuples') for key, val in items: if not isinstance(key, (basestring, Feature)): raise TypeError('Feature names must be strings') self[key] = val for key, val in morefeatures.items(): if not isinstance(key, (basestring, Feature)): raise TypeError('Feature names must be strings') self[key] = val ##//////////////////////////////////////////////////////////// #{ Copying ##//////////////////////////////////////////////////////////// def __deepcopy__(self, memo): memo[id(self)] = selfcopy = self.__class__() for (key, val) in self._items(): selfcopy[copy.deepcopy(key,memo)] = copy.deepcopy(val,memo) return selfcopy ##//////////////////////////////////////////////////////////// #{ Uniform Accessor Methods ##//////////////////////////////////////////////////////////// def _keys(self): return self.keys() def _values(self): return self.values() def _items(self): return self.items() ##//////////////////////////////////////////////////////////// #{ String Representations ##//////////////////////////////////////////////////////////// def __str__(self): """ Display a multi-line representation of this feature dictionary as an FVM (feature value matrix). """ return '\n'.join(self._str(self._find_reentrances({}), {})) def _repr(self, reentrances, reentrance_ids): segments = [] prefix = '' suffix = '' # If this is the first time we've seen a reentrant structure, # then assign it a unique identifier. if reentrances[id(self)]: assert not reentrance_ids.has_key(id(self)) reentrance_ids[id(self)] = `len(reentrance_ids)+1` # sorting note: keys are unique strings, so we'll never fall # through to comparing values. for (fname, fval) in sorted(self.items()): display = getattr(fname, 'display', None) if reentrance_ids.has_key(id(fval)): segments.append('%s->(%s)' % (fname, reentrance_ids[id(fval)])) elif (display == 'prefix' and not prefix and isinstance(fval, (Variable, basestring))): prefix = '%s' % fval elif display == 'slash' and not suffix: if isinstance(fval, Variable): suffix = '/%s' % fval.name else: suffix = '/%r' % fval elif isinstance(fval, Variable): segments.append('%s=%s' % (fname, fval.name)) elif fval is True: segments.append('+%s' % fname) elif fval is False: segments.append('-%s' % fname) elif isinstance(fval, Expression): segments.append('%s=<%s>' % (fname, fval)) elif not isinstance(fval, FeatStruct): segments.append('%s=%r' % (fname, fval)) else: fval_repr = fval._repr(reentrances, reentrance_ids) segments.append('%s=%s' % (fname, fval_repr)) # If it's reentrant, then add on an identifier tag. if reentrances[id(self)]: prefix = '(%s)%s' % (reentrance_ids[id(self)], prefix) return '%s[%s]%s' % (prefix, ', '.join(segments), suffix) def _str(self, reentrances, reentrance_ids): """ @return: A list of lines composing a string representation of this feature dictionary. @param reentrances: A dictionary that maps from the C{id} of each feature value in self, indicating whether that value is reentrant or not. @param reentrance_ids: A dictionary mapping from the C{id}s of feature values to unique identifiers. This is modified by C{repr}: the first time a reentrant feature value is displayed, an identifier is added to reentrance_ids for it. """ # If this is the first time we've seen a reentrant structure, # then tack on an id string. if reentrances[id(self)]: assert not reentrance_ids.has_key(id(self)) reentrance_ids[id(self)] = `len(reentrance_ids)+1` # Special case: empty feature dict. if len(self) == 0: if reentrances[id(self)]: return ['(%s) []' % reentrance_ids[id(self)]] else: return ['[]'] # What's the longest feature name? Use this to align names. maxfnamelen = max(len(str(k)) for k in self.keys()) lines = [] # sorting note: keys are unique strings, so we'll never fall # through to comparing values. for (fname, fval) in sorted(self.items()): fname = str(fname).ljust(maxfnamelen) if isinstance(fval, Variable): lines.append('%s = %s' % (fname,fval.name)) elif isinstance(fval, Expression): lines.append('%s = <%s>' % (fname, fval)) elif isinstance(fval, FeatList): fval_repr = fval._repr(reentrances, reentrance_ids) lines.append('%s = %r' % (fname, fval_repr)) elif not isinstance(fval, FeatDict): # It's not a nested feature structure -- just print it. lines.append('%s = %r' % (fname, fval)) elif reentrance_ids.has_key(id(fval)): # It's a feature structure we've seen before -- print # the reentrance id. lines.append('%s -> (%s)' % (fname, reentrance_ids[id(fval)])) else: # It's a new feature structure. Separate it from # other values by a blank line. if lines and lines[-1] != '': lines.append('') # Recursively print the feature's value (fval). fval_lines = fval._str(reentrances, reentrance_ids) # Indent each line to make room for fname. fval_lines = [(' '*(maxfnamelen+3))+l for l in fval_lines] # Pick which line we'll display fname on, & splice it in. nameline = (len(fval_lines)-1)/2 fval_lines[nameline] = ( fname+' ='+fval_lines[nameline][maxfnamelen+2:]) # Add the feature structure to the output. lines += fval_lines # Separate FeatStructs by a blank line. lines.append('') # Get rid of any excess blank lines. if lines[-1] == '': lines.pop() # Add brackets around everything. maxlen = max(len(line) for line in lines) lines = ['[ %s%s ]' % (line, ' '*(maxlen-len(line))) for line in lines] # If it's reentrant, then add on an identifier tag. if reentrances[id(self)]: idstr = '(%s) ' % reentrance_ids[id(self)] lines = [(' '*len(idstr))+l for l in lines] idline = (len(lines)-1)/2 lines[idline] = idstr + lines[idline][len(idstr):] return lines ###################################################################### # Feature List ###################################################################### class FeatList(FeatStruct, list): """ A list of feature values, where each feature value is either a basic value (such as a string or an integer), or a nested feature structure. Feature lists may contain reentrant feature values. A X{reentrant feature value} is a single feature value that can be accessed via multiple feature paths. Feature lists may also be cyclic. Two feature lists are considered equal if they assign the same values to all features, and have the same reentrances. @see: L{FeatStruct} for information about feature paths, reentrance, cyclic feature structures, mutability, freezing, and hashing. """ def __init__(self, features=()): """ Create a new feature list, with the specified features. @param features: The initial list of features for this feature list. If C{features} is a string, then it is paresd using L{FeatStructParser}. Otherwise, it should be a sequence of basic values and nested feature structures. """ if isinstance(features, basestring): FeatStructParser().parse(features, self) else: list.__init__(self, features) #//////////////////////////////////////////////////////////// #{ List methods #//////////////////////////////////////////////////////////// _INDEX_ERROR = "Expected int or feature path. Got %r." def __getitem__(self, name_or_path): if isinstance(name_or_path, (int, long)): return list.__getitem__(self, name_or_path) elif isinstance(name_or_path, tuple): try: val = self for fid in name_or_path: if not isinstance(val, FeatStruct): raise KeyError # path contains base value val = val[fid] return val except (KeyError, IndexError): raise KeyError(name_or_path) else: raise TypeError(self._INDEX_ERROR % name_or_path) def __delitem__(self, name_or_path): """If the feature with the given name or path exists, delete its value; otherwise, raise C{KeyError}.""" if self._frozen: raise ValueError(_FROZEN_ERROR) if isinstance(name_or_path, (int, long)): return list.__delitem__(self, name_or_path) elif isinstance(name_or_path, tuple): if len(name_or_path) == 0: raise ValueError("The path () can not be set") else: parent = self[name_or_path[:-1]] if not isinstance(parent, FeatStruct): raise KeyError(name_or_path) # path contains base value del parent[name_or_path[-1]] else: raise TypeError(self._INDEX_ERROR % name_or_path) def __setitem__(self, name_or_path, value): """Set the value for the feature with the given name or path to C{value}. If C{name_or_path} is an invalid path, raise C{KeyError}.""" if self._frozen: raise ValueError(_FROZEN_ERROR) if isinstance(name_or_path, (int, long)): return list.__setitem__(self, name_or_path, value) elif isinstance(name_or_path, tuple): if len(name_or_path) == 0: raise ValueError("The path () can not be set") else: parent = self[name_or_path[:-1]] if not isinstance(parent, FeatStruct): raise KeyError(name_or_path) # path contains base value parent[name_or_path[-1]] = value else: raise TypeError(self._INDEX_ERROR % name_or_path) __delslice__ = _check_frozen(list.__delslice__, ' ') __setslice__ = _check_frozen(list.__setslice__, ' ') __iadd__ = _check_frozen(list.__iadd__) __imul__ = _check_frozen(list.__imul__) append = _check_frozen(list.append) extend = _check_frozen(list.extend) insert = _check_frozen(list.insert) pop = _check_frozen(list.pop) remove = _check_frozen(list.remove) reverse = _check_frozen(list.reverse) sort = _check_frozen(list.sort) ##//////////////////////////////////////////////////////////// #{ Copying ##//////////////////////////////////////////////////////////// def __deepcopy__(self, memo): memo[id(self)] = selfcopy = self.__class__() selfcopy.extend([copy.deepcopy(fval,memo) for fval in self]) return selfcopy ##//////////////////////////////////////////////////////////// #{ Uniform Accessor Methods ##//////////////////////////////////////////////////////////// def _keys(self): return range(len(self)) def _values(self): return self def _items(self): return enumerate(self) ##//////////////////////////////////////////////////////////// #{ String Representations ##//////////////////////////////////////////////////////////// # Special handling for: reentrances, variables, expressions. def _repr(self, reentrances, reentrance_ids): # If this is the first time we've seen a reentrant structure, # then assign it a unique identifier. if reentrances[id(self)]: assert not reentrance_ids.has_key(id(self)) reentrance_ids[id(self)] = `len(reentrance_ids)+1` prefix = '(%s)' % reentrance_ids[id(self)] else: prefix = '' segments = [] for fval in self: if id(fval) in reentrance_ids: segments.append('->(%s)' % reentrance_ids[id(fval)]) elif isinstance(fval, Variable): segments.append(fval.name) elif isinstance(fval, Expression): segments.append('%s' % fval) elif isinstance(fval, FeatStruct): segments.append(fval._repr(reentrances, reentrance_ids)) else: segments.append('%r' % fval) return '%s[%s]' % (prefix, ', '.join(segments)) ###################################################################### # Variables & Bindings ###################################################################### def substitute_bindings(fstruct, bindings, fs_class='default'): """ @return: The feature structure that is obtained by replacing each variable bound by C{bindings} with its binding. If a variable is aliased to a bound variable, then it will be replaced by that variable's value. If a variable is aliased to an unbound variable, then it will be replaced by that variable. @type bindings: C{dict} with L{Variable} keys @param bindings: A dictionary mapping from variables to values. """ if fs_class == 'default': fs_class = _default_fs_class(fstruct) fstruct = copy.deepcopy(fstruct) _substitute_bindings(fstruct, bindings, fs_class, set()) return fstruct def _substitute_bindings(fstruct, bindings, fs_class, visited): # Visit each node only once: if id(fstruct) in visited: return visited.add(id(fstruct)) if _is_mapping(fstruct): items = fstruct.items() elif _is_sequence(fstruct): items = enumerate(fstruct) else: raise ValueError('Expected mapping or sequence') for (fname, fval) in items: while (isinstance(fval, Variable) and fval in bindings): fval = fstruct[fname] = bindings[fval] if isinstance(fval, fs_class): _substitute_bindings(fval, bindings, fs_class, visited) elif isinstance(fval, SubstituteBindingsI): fstruct[fname] = fval.substitute_bindings(bindings) def retract_bindings(fstruct, bindings, fs_class='default'): """ @return: The feature structure that is obtained by replacing each feature structure value that is bound by C{bindings} with the variable that binds it. A feature structure value must be identical to a bound value (i.e., have equal id) to be replaced. C{bindings} is modified to point to this new feature structure, rather than the original feature structure. Feature structure values in C{bindings} may be modified if they are contained in C{fstruct}. """ if fs_class == 'default': fs_class = _default_fs_class(fstruct) (fstruct, new_bindings) = copy.deepcopy((fstruct, bindings)) bindings.update(new_bindings) inv_bindings = dict((id(val),var) for (var,val) in bindings.items()) _retract_bindings(fstruct, inv_bindings, fs_class, set()) return fstruct def _retract_bindings(fstruct, inv_bindings, fs_class, visited): # Visit each node only once: if id(fstruct) in visited: return visited.add(id(fstruct)) if _is_mapping(fstruct): items = fstruct.items() elif _is_sequence(fstruct): items = enumerate(fstruct) else: raise ValueError('Expected mapping or sequence') for (fname, fval) in items: if isinstance(fval, fs_class): if id(fval) in inv_bindings: fstruct[fname] = inv_bindings[id(fval)] _retract_bindings(fval, inv_bindings, fs_class, visited) def find_variables(fstruct, fs_class='default'): """ @return: The set of variables used by this feature structure. @rtype: C{set} of L{Variable} """ if fs_class == 'default': fs_class = _default_fs_class(fstruct) return _variables(fstruct, set(), fs_class, set()) def _variables(fstruct, vars, fs_class, visited): # Visit each node only once: if id(fstruct) in visited: return visited.add(id(fstruct)) if _is_mapping(fstruct): items = fstruct.items() elif _is_sequence(fstruct): items = enumerate(fstruct) else: raise ValueError('Expected mapping or sequence') for (fname, fval) in items: if isinstance(fval, Variable): vars.add(fval) elif isinstance(fval, fs_class): _variables(fval, vars, fs_class, visited) elif isinstance(fval, SubstituteBindingsI): vars.update(fval.variables()) return vars def rename_variables(fstruct, vars=None, used_vars=(), new_vars=None, fs_class='default'): """ @return: The feature structure that is obtained by replacing any of this feature structure's variables that are in C{vars} with new variables. The names for these new variables will be names that are not used by any variable in C{vars}, or in C{used_vars}, or in this feature structure. @type vars: C{set} @param vars: The set of variables that should be renamed. If not specified, C{find_variables(fstruct)} is used; i.e., all variables will be given new names. @type used_vars: C{set} @param used_vars: A set of variables whose names should not be used by the new variables. @type new_vars: C{dict} from L{Variable} to L{Variable} @param new_vars: A dictionary that is used to hold the mapping from old variables to new variables. For each variable M{v} in this feature structure: - If C{new_vars} maps M{v} to M{v'}, then M{v} will be replaced by M{v'}. - If C{new_vars} does not contain M{v}, but C{vars} does contain M{v}, then a new entry will be added to C{new_vars}, mapping M{v} to the new variable that is used to replace it. To consistantly rename the variables in a set of feature structures, simply apply rename_variables to each one, using the same dictionary: >>> fstruct1 = FeatStruct('[subj=[agr=[gender=?y]], obj=[agr=[gender=?y]]]') >>> fstruct2 = FeatStruct('[subj=[agr=[number=?z,gender=?y]], obj=[agr=[number=?z,gender=?y]]]') >>> new_vars = {} # Maps old vars to alpha-renamed vars >>> fstruct1.rename_variables(new_vars=new_vars) [obj=[agr=[gender=?y2]], subj=[agr=[gender=?y2]]] >>> fstruct2.rename_variables(new_vars=new_vars) [obj=[agr=[gender=?y2, number=?z2]], subj=[agr=[gender=?y2, number=?z2]]] If new_vars is not specified, then an empty dictionary is used. """ if fs_class == 'default': fs_class = _default_fs_class(fstruct) # Default values: if new_vars is None: new_vars = {} if vars is None: vars = find_variables(fstruct, fs_class) else: vars = set(vars) # Add our own variables to used_vars. used_vars = find_variables(fstruct, fs_class).union(used_vars) # Copy ourselves, and rename variables in the copy. return _rename_variables(copy.deepcopy(fstruct), vars, used_vars, new_vars, fs_class, set()) def _rename_variables(fstruct, vars, used_vars, new_vars, fs_class, visited): if id(fstruct) in visited: return visited.add(id(fstruct)) if _is_mapping(fstruct): items = fstruct.items() elif _is_sequence(fstruct): items = enumerate(fstruct) else: raise ValueError('Expected mapping or sequence') for (fname, fval) in items: if isinstance(fval, Variable): # If it's in new_vars, then rebind it. if fval in new_vars: fstruct[fname] = new_vars[fval] # If it's in vars, pick a new name for it. elif fval in vars: new_vars[fval] = _rename_variable(fval, used_vars) fstruct[fname] = new_vars[fval] used_vars.add(new_vars[fval]) elif isinstance(fval, fs_class): _rename_variables(fval, vars, used_vars, new_vars, fs_class, visited) elif isinstance(fval, SubstituteBindingsI): # Pick new names for any variables in `vars` for var in fval.variables(): if var in vars and var not in new_vars: new_vars[var] = _rename_variable(var, used_vars) used_vars.add(new_vars[var]) # Replace all variables in `new_vars`. fstruct[fname] = fval.substitute_bindings(new_vars) return fstruct def _rename_variable(var, used_vars): name, n = re.sub('\d+$', '', var.name), 2 if not name: name = '?' while Variable('%s%s' % (name, n)) in used_vars: n += 1 return Variable('%s%s' % (name, n)) def remove_variables(fstruct, fs_class='default'): """ @rtype: L{FeatStruct} @return: The feature structure that is obtained by deleting all features whose values are L{Variable}s. """ if fs_class == 'default': fs_class = _default_fs_class(fstruct) return _remove_variables(copy.deepcopy(fstruct), fs_class, set()) def _remove_variables(fstruct, fs_class, visited): if id(fstruct) in visited: return visited.add(id(fstruct)) if _is_mapping(fstruct): items = fstruct.items() elif _is_sequence(fstruct): items = enumerate(fstruct) else: raise ValueError('Expected mapping or sequence') for (fname, fval) in items: if isinstance(fval, Variable): del fstruct[fname] elif isinstance(fval, fs_class): _remove_variables(fval, fs_class, visited) return fstruct ###################################################################### # Unification ###################################################################### class _UnificationFailure(object): def __repr__(self): return 'nltk.featstruct.UnificationFailure' UnificationFailure = _UnificationFailure() """A unique value used to indicate unification failure. It can be returned by L{Feature.unify_base_values()} or by custom C{fail()} functions to indicate that unificaiton should fail.""" # The basic unification algorithm: # 1. Make copies of self and other (preserving reentrance) # 2. Destructively unify self and other # 3. Apply forward pointers, to preserve reentrance. # 4. Replace bound variables with their values. def unify(fstruct1, fstruct2, bindings=None, trace=False, fail=None, rename_vars=True, fs_class='default'): """ Unify C{fstruct1} with C{fstruct2}, and return the resulting feature structure. This unified feature structure is the minimal feature structure that: - contains all feature value assignments from both C{fstruct1} and C{fstruct2}. - preserves all reentrance properties of C{fstruct1} and C{fstruct2}. If no such feature structure exists (because C{fstruct1} and C{fstruct2} specify incompatible values for some feature), then unification fails, and C{unify} returns C{None}. @type bindings: C{dict} with L{Variable} keys @param bindings: A set of variable bindings to be used and updated during unification. Bound variables are replaced by their values. Aliased variables are replaced by their representative variable (if unbound) or the value of their representative variable (if bound). I.e., if variable C{I{v}} is in C{bindings}, then C{I{v}} is replaced by C{bindings[I{v}]}. This will be repeated until the variable is replaced by an unbound variable or a non-variable value. Unbound variables are bound when they are unified with values; and aliased when they are unified with variables. I.e., if variable C{I{v}} is not in C{bindings}, and is unified with a variable or value C{I{x}}, then C{bindings[I{v}]} is set to C{I{x}}. If C{bindings} is unspecified, then all variables are assumed to be unbound. I.e., C{bindings} defaults to an empty C{dict}. @type trace: C{bool} @param trace: If true, generate trace output. @type rename_vars: C{bool} @param rename_vars: If true, then rename any variables in C{fstruct2} that are also used in C{fstruct1}. This prevents aliasing in cases where C{fstruct1} and C{fstruct2} use the same variable name. E.g.: >>> FeatStruct('[a=?x]').unify(FeatStruct('[b=?x]')) [a=?x, b=?x2] If you intend for a variables in C{fstruct1} and C{fstruct2} with the same name to be treated as a single variable, use C{rename_vars=False}. """ # Decide which class(es) will be treated as feature structures, # for the purposes of unification. if fs_class == 'default': fs_class = _default_fs_class(fstruct1) if _default_fs_class(fstruct2) != fs_class: raise ValueError("Mixing FeatStruct objects with Python " "dicts and lists is not supported.") assert isinstance(fstruct1, fs_class) assert isinstance(fstruct2, fs_class) # If bindings are unspecified, use an empty set of bindings. user_bindings = (bindings is not None) if bindings is None: bindings = {} # Make copies of fstruct1 and fstruct2 (since the unification # algorithm is destructive). Do it all at once, to preserve # reentrance links between fstruct1 and fstruct2. Copy bindings # as well, in case there are any bound vars that contain parts # of fstruct1 or fstruct2. (fstruct1copy, fstruct2copy, bindings_copy) = ( copy.deepcopy((fstruct1, fstruct2, bindings))) # Copy the bindings back to the original bindings dict. bindings.update(bindings_copy) if rename_vars: vars1 = find_variables(fstruct1copy, fs_class) vars2 = find_variables(fstruct2copy, fs_class) _rename_variables(fstruct2copy, vars1, vars2, {}, fs_class, set()) # Do the actual unification. If it fails, return None. forward = {} if trace: _trace_unify_start((), fstruct1copy, fstruct2copy) try: result = _destructively_unify(fstruct1copy, fstruct2copy, bindings, forward, trace, fail, fs_class, ()) except _UnificationFailureError: return None # _destructively_unify might return UnificationFailure, e.g. if we # tried to unify a mapping with a sequence. if result is UnificationFailure: if fail is None: return None else: return fail(fstruct1copy, fstruct2copy, ()) # Replace any feature structure that has a forward pointer # with the target of its forward pointer. result = _apply_forwards(result, forward, fs_class, set()) if user_bindings: _apply_forwards_to_bindings(forward, bindings) # Replace bound vars with values. _resolve_aliases(bindings) _substitute_bindings(result, bindings, fs_class, set()) # Return the result. if trace: _trace_unify_succeed((), result) if trace: _trace_bindings((), bindings) return result class _UnificationFailureError(Exception): """An exception that is used by C{_destructively_unify} to abort unification when a failure is encountered.""" def _destructively_unify(fstruct1, fstruct2, bindings, forward, trace, fail, fs_class, path): """ Attempt to unify C{fstruct1} and C{fstruct2} by modifying them in-place. If the unification succeeds, then C{fstruct1} will contain the unified value, the value of C{fstruct2} is undefined, and forward[id(fstruct2)] is set to fstruct1. If the unification fails, then a _UnificationFailureError is raised, and the values of C{fstruct1} and C{fstruct2} are undefined. @param bindings: A dictionary mapping variables to values. @param forward: A dictionary mapping feature structures ids to replacement structures. When two feature structures are merged, a mapping from one to the other will be added to the forward dictionary; and changes will be made only to the target of the forward dictionary. C{_destructively_unify} will always 'follow' any links in the forward dictionary for fstruct1 and fstruct2 before actually unifying them. @param trace: If true, generate trace output @param path: The feature path that led us to this unification step. Used for trace output. """ # If fstruct1 is already identical to fstruct2, we're done. # Note: this, together with the forward pointers, ensures # that unification will terminate even for cyclic structures. if fstruct1 is fstruct2: if trace: _trace_unify_identity(path, fstruct1) return fstruct1 # Set fstruct2's forward pointer to point to fstruct1; this makes # fstruct1 the canonical copy for fstruct2. Note that we need to # do this before we recurse into any child structures, in case # they're cyclic. forward[id(fstruct2)] = fstruct1 # Unifying two mappings: if _is_mapping(fstruct1) and _is_mapping(fstruct2): for fname in fstruct1: if getattr(fname, 'default', None) is not None: fstruct2.setdefault(fname, fname.default) for fname in fstruct2: if getattr(fname, 'default', None) is not None: fstruct1.setdefault(fname, fname.default) # Unify any values that are defined in both fstruct1 and # fstruct2. Copy any values that are defined in fstruct2 but # not in fstruct1 to fstruct1. Note: sorting fstruct2's # features isn't actually necessary; but we do it to give # deterministic behavior, e.g. for tracing. for fname, fval2 in sorted(fstruct2.items()): if fname in fstruct1: fstruct1[fname] = _unify_feature_values( fname, fstruct1[fname], fval2, bindings, forward, trace, fail, fs_class, path+(fname,)) else: fstruct1[fname] = fval2 return fstruct1 # Contains the unified value. # Unifying two sequences: elif _is_sequence(fstruct1) and _is_sequence(fstruct2): # If the lengths don't match, fail. if len(fstruct1) != len(fstruct2): return UnificationFailure # Unify corresponding values in fstruct1 and fstruct2. for findex in range(len(fstruct1)): fstruct1[findex] = _unify_feature_values( findex, fstruct1[findex], fstruct2[findex], bindings, forward, trace, fail, fs_class, path+(findex,)) return fstruct1 # Contains the unified value. # Unifying sequence & mapping: fail. The failure function # doesn't get a chance to recover in this case. elif ((_is_sequence(fstruct1) or _is_mapping(fstruct1)) and (_is_sequence(fstruct2) or _is_mapping(fstruct2))): return UnificationFailure # Unifying anything else: not allowed! raise TypeError('Expected mappings or sequences') def _unify_feature_values(fname, fval1, fval2, bindings, forward, trace, fail, fs_class, fpath): """ Attempt to unify C{fval1} and and C{fval2}, and return the resulting unified value. The method of unification will depend on the types of C{fval1} and C{fval2}: 1. If they're both feature structures, then destructively unify them (see L{_destructively_unify()}. 2. If they're both unbound variables, then alias one variable to the other (by setting bindings[v2]=v1). 3. If one is an unbound variable, and the other is a value, then bind the unbound variable to the value. 4. If one is a feature structure, and the other is a base value, then fail. 5. If they're both base values, then unify them. By default, this will succeed if they are equal, and fail otherwise. """ if trace: _trace_unify_start(fpath, fval1, fval2) # Look up the "canonical" copy of fval1 and fval2 while id(fval1) in forward: fval1 = forward[id(fval1)] while id(fval2) in forward: fval2 = forward[id(fval2)] # If fval1 or fval2 is a bound variable, then # replace it by the variable's bound value. This # includes aliased variables, which are encoded as # variables bound to other variables. fvar1 = fvar2 = None while isinstance(fval1, Variable) and fval1 in bindings: fvar1 = fval1 fval1 = bindings[fval1] while isinstance(fval2, Variable) and fval2 in bindings: fvar2 = fval2 fval2 = bindings[fval2] # Case 1: Two feature structures (recursive case) if isinstance(fval1, fs_class) and isinstance(fval2, fs_class): result = _destructively_unify(fval1, fval2, bindings, forward, trace, fail, fs_class, fpath) # Case 2: Two unbound variables (create alias) elif (isinstance(fval1, Variable) and isinstance(fval2, Variable)): if fval1 != fval2: bindings[fval2] = fval1 result = fval1 # Case 3: An unbound variable and a value (bind) elif isinstance(fval1, Variable): bindings[fval1] = fval2 result = fval1 elif isinstance(fval2, Variable): bindings[fval2] = fval1 result = fval2 # Case 4: A feature structure & a base value (fail) elif isinstance(fval1, fs_class) or isinstance(fval2, fs_class): result = UnificationFailure # Case 5: Two base values else: # Case 5a: Feature defines a custom unification method for base values if isinstance(fname, Feature): result = fname.unify_base_values(fval1, fval2, bindings) # Case 5b: Feature value defines custom unification method elif isinstance(fval1, CustomFeatureValue): result = fval1.unify(fval2) # Sanity check: unify value should be symmetric if (isinstance(fval2, CustomFeatureValue) and result != fval2.unify(fval1)): raise AssertionError( 'CustomFeatureValue objects %r and %r disagree ' 'about unification value: %r vs. %r' % (fval1, fval2, result, fval2.unify(fval1))) elif isinstance(fval2, CustomFeatureValue): result = fval2.unify(fval1) # Case 5c: Simple values -- check if they're equal. else: if fval1 == fval2: result = fval1 else: result = UnificationFailure # If either value was a bound variable, then update the # bindings. (This is really only necessary if fname is a # Feature or if either value is a CustomFeatureValue.) if result is not UnificationFailure: if fvar1 is not None: bindings[fvar1] = result result = fvar1 if fvar2 is not None and fvar2 != fvar1: bindings[fvar2] = result result = fvar2 # If we unification failed, call the failure function; it # might decide to continue anyway. if result is UnificationFailure: if fail is not None: result = fail(fval1, fval2, fpath) if trace: _trace_unify_fail(fpath[:-1], result) if result is UnificationFailure: raise _UnificationFailureError # Normalize the result. if isinstance(result, fs_class): result = _apply_forwards(result, forward, fs_class, set()) if trace: _trace_unify_succeed(fpath, result) if trace and isinstance(result, fs_class): _trace_bindings(fpath, bindings) return result def _apply_forwards_to_bindings(forward, bindings): """ Replace any feature structure that has a forward pointer with the target of its forward pointer (to preserve reentrancy). """ for (var, value) in bindings.items(): while id(value) in forward: value = forward[id(value)] bindings[var] = value def _apply_forwards(fstruct, forward, fs_class, visited): """ Replace any feature structure that has a forward pointer with the target of its forward pointer (to preserve reentrancy). """ # Follow our own forwards pointers (if any) while id(fstruct) in forward: fstruct = forward[id(fstruct)] # Visit each node only once: if id(fstruct) in visited: return visited.add(id(fstruct)) if _is_mapping(fstruct): items = fstruct.items() elif _is_sequence(fstruct): items = enumerate(fstruct) else: raise ValueError('Expected mapping or sequence') for fname, fval in items: if isinstance(fval, fs_class): # Replace w/ forwarded value. while id(fval) in forward: fval = forward[id(fval)] fstruct[fname] = fval # Recurse to child. _apply_forwards(fval, forward, fs_class, visited) return fstruct def _resolve_aliases(bindings): """ Replace any bound aliased vars with their binding; and replace any unbound aliased vars with their representative var. """ for (var, value) in bindings.items(): while isinstance(value, Variable) and value in bindings: value = bindings[var] = bindings[value] def _trace_unify_start(path, fval1, fval2): if path == (): print '\nUnification trace:' else: fullname = '.'.join(str(n) for n in path) print ' '+'| '*(len(path)-1)+'|' print ' '+'| '*(len(path)-1)+'| Unify feature: %s' % fullname print ' '+'| '*len(path)+' / '+_trace_valrepr(fval1) print ' '+'| '*len(path)+'|\\ '+_trace_valrepr(fval2) def _trace_unify_identity(path, fval1): print ' '+'| '*len(path)+'|' print ' '+'| '*len(path)+'| (identical objects)' print ' '+'| '*len(path)+'|' print ' '+'| '*len(path)+'+-->'+`fval1` def _trace_unify_fail(path, result): if result is UnificationFailure: resume = '' else: resume = ' (nonfatal)' print ' '+'| '*len(path)+'| |' print ' '+'X '*len(path)+'X X <-- FAIL'+resume def _trace_unify_succeed(path, fval1): # Print the result. print ' '+'| '*len(path)+'|' print ' '+'| '*len(path)+'+-->'+`fval1` def _trace_bindings(path, bindings): # Print the bindings (if any). if len(bindings) > 0: binditems = sorted(bindings.items(), key=lambda v:v[0].name) bindstr = '{%s}' % ', '.join( '%s: %s' % (var, _trace_valrepr(val)) for (var, val) in binditems) print ' '+'| '*len(path)+' Bindings: '+bindstr def _trace_valrepr(val): if isinstance(val, Variable): return '%s' % val else: return '%r' % val def subsumes(fstruct1, fstruct2): """ @return: True if C{fstruct1} subsumes C{fstruct2}. I.e., return true if unifying C{fstruct1} with C{fstruct2} would result in a feature structure equal to C{fstruct2.} """ return fstruct2 == unify(fstruct1, fstruct2) def conflicts(fstruct1, fstruct2, trace=0): """ @return: A list of the feature paths of all features which are assigned incompatible values by C{fstruct1} and C{fstruct2}. @rtype: C{list} of C{tuple} """ conflict_list = [] def add_conflict(fval1, fval2, path): conflict_list.append(path) return fval1 unify(fstruct1, fstruct2, fail=add_conflict, trace=trace) return conflict_list ###################################################################### # Helper Functions ###################################################################### def _is_mapping(v): return hasattr(v, 'has_key') and hasattr(v, 'items') def _is_sequence(v): return (hasattr(v, '__iter__') and hasattr(v, '__len__') and not isinstance(v, basestring)) def _default_fs_class(obj): if isinstance(obj, FeatStruct): return FeatStruct if isinstance(obj, (dict, list)): return (dict, list) else: raise ValueError('To unify objects of type %s, you must specify ' 'fs_class explicitly.' % obj.__class__.__name__) ###################################################################### # FeatureValueSet & FeatureValueTuple ###################################################################### class SubstituteBindingsSequence(SubstituteBindingsI): """ A mixin class for sequence clases that distributes variables() and substitute_bindings() over the object's elements. """ def variables(self): return ([elt for elt in self if isinstance(elt, Variable)] + sum([list(elt.variables()) for elt in self if isinstance(elt, SubstituteBindingsI)], [])) def substitute_bindings(self, bindings): return self.__class__([self.subst(v, bindings) for v in self]) def subst(self, v, bindings): if isinstance(v, SubstituteBindingsI): return v.substitute_bindings(bindings) else: return bindings.get(v, v) class FeatureValueTuple(SubstituteBindingsSequence, tuple): """ A base feature value that is a tuple of other base feature values. FeatureValueTuple implements L{SubstituteBindingsI}, so it any variable substitutions will be propagated to the elements contained by the set. C{FeatureValueTuple}s are immutable. """ def __repr__(self): # [xx] really use %s here? if len(self) == 0: return '()' return '(%s)' % ', '.join('%s' % (b,) for b in self) class FeatureValueSet(SubstituteBindingsSequence, frozenset): """ A base feature value that is a set of other base feature values. FeatureValueSet implements L{SubstituteBindingsI}, so it any variable substitutions will be propagated to the elements contained by the set. C{FeatureValueSet}s are immutable. """ def __repr__(self): # [xx] really use %s here? if len(self) == 0: return '{/}' # distinguish from dict. # n.b., we sort the string reprs of our elements, to ensure # that our own repr is deterministic. return '{%s}' % ', '.join(sorted('%s' % (b,) for b in self)) __str__ = __repr__ class FeatureValueUnion(SubstituteBindingsSequence, frozenset): """ A base feature value that represents the union of two or more L{FeatureValueSet}s or L{Variable}s. """ def __new__(cls, values): # If values contains FeatureValueUnions, then collapse them. values = _flatten(values, FeatureValueUnion) # If the resulting list contains no variables, then # use a simple FeatureValueSet instead. if sum(isinstance(v, Variable) for v in values) == 0: values = _flatten(values, FeatureValueSet) return FeatureValueSet(values) # If we contain a single variable, return that variable. if len(values) == 1: return list(values)[0] # Otherwise, build the FeatureValueUnion. return frozenset.__new__(cls, values) def __repr__(self): # n.b., we sort the string reprs of our elements, to ensure # that our own repr is deterministic. also, note that len(self) # is guaranteed to be 2 or more. return '{%s}' % '+'.join(sorted('%s' % (b,) for b in self)) class FeatureValueConcat(SubstituteBindingsSequence, tuple): """ A base feature value that represents the concatenation of two or more L{FeatureValueTuple}s or L{Variable}s. """ def __new__(cls, values): # If values contains FeatureValueConcats, then collapse them. values = _flatten(values, FeatureValueConcat) # If the resulting list contains no variables, then # use a simple FeatureValueTuple instead. if sum(isinstance(v, Variable) for v in values) == 0: values = _flatten(values, FeatureValueTuple) return FeatureValueTuple(values) # If we contain a single variable, return that variable. if len(values) == 1: return list(values)[0] # Otherwise, build the FeatureValueConcat. return tuple.__new__(cls, values) def __repr__(self): # n.b.: len(self) is guaranteed to be 2 or more. return '(%s)' % '+'.join('%s' % (b,) for b in self) def _flatten(lst, cls): """ Helper function -- return a copy of list, with all elements of type C{cls} spliced in rather than appended in. """ result = [] for elt in lst: if isinstance(elt, cls): result.extend(elt) else: result.append(elt) return result ###################################################################### # Specialized Features ###################################################################### class Feature(object): """ A feature identifier that's specialized to put additional constraints, default values, etc. """ def __init__(self, name, default=None, display=None): assert display in (None, 'prefix', 'slash') self._name = name # [xx] rename to .identifier? """The name of this feature.""" self._default = default # [xx] not implemented yet. """Default value for this feature. Use None for unbound.""" self._display = display """Custom display location: can be prefix, or slash.""" if self._display == 'prefix': self._sortkey = (-1, self._name) elif self._display == 'slash': self._sortkey = (1, self._name) else: self._sortkey = (0, self._name) name = property(lambda self: self._name) default = property(lambda self: self._default) display = property(lambda self: self._display) def __repr__(self): return '*%s*' % self.name def __cmp__(self, other): if not isinstance(other, Feature): return -1 if self._name == other._name: return 0 return cmp(self._sortkey, other._sortkey) def __hash__(self): return hash(self._name) #//////////////////////////////////////////////////////////// # These can be overridden by subclasses: #//////////////////////////////////////////////////////////// def parse_value(self, s, position, reentrances, parser): return parser.parse_value(s, position, reentrances) def unify_base_values(self, fval1, fval2, bindings): """ If possible, return a single value.. If not, return the value L{UnificationFailure}. """ if fval1 == fval2: return fval1 else: return UnificationFailure class SlashFeature(Feature): def parse_value(self, s, position, reentrances, parser): return parser.partial_parse(s, position, reentrances) class RangeFeature(Feature): RANGE_RE = re.compile('(-?\d+):(-?\d+)') def parse_value(self, s, position, reentrances, parser): m = self.RANGE_RE.match(s, position) if not m: raise ValueError('range', position) return (int(m.group(1)), int(m.group(2))), m.end() def unify_base_values(self, fval1, fval2, bindings): if fval1 is None: return fval2 if fval2 is None: return fval1 rng = max(fval1[0], fval2[0]), min(fval1[1], fval2[1]) if rng[1] < rng[0]: return UnificationFailure return rng SLASH = SlashFeature('slash', default=False, display='slash') TYPE = Feature('type', display='prefix') ###################################################################### # Specialized Feature Values ###################################################################### class CustomFeatureValue(object): """ An abstract base class for base values that define a custom unification method. A C{CustomFeatureValue}'s custom unification method will be used during feature structure unification if: - The C{CustomFeatureValue} is unified with another base value. - The C{CustomFeatureValue} is not the value of a customized L{Feature} (which defines its own unification method). If two C{CustomFeatureValue} objects are unified with one another during feature structure unification, then the unified base values they return I{must} be equal; otherwise, an C{AssertionError} will be raised. Subclasses must define L{unify()} and L{__cmp__()}. Subclasses may also wish to define L{__hash__()}. """ def unify(self, other): """ If this base value unifies with C{other}, then return the unified value. Otherwise, return L{UnificationFailure}. """ raise NotImplementedError('abstract base class') def __cmp__(self, other): raise NotImplementedError('abstract base class') def __hash__(self): raise TypeError('%s objects or unhashable' % self.__class__.__name__) ###################################################################### # Feature Structure Parser ###################################################################### class FeatStructParser(object): def __init__(self, features=(SLASH, TYPE), fdict_class=FeatStruct, flist_class=FeatList, logic_parser=None): self._features = dict((f.name,f) for f in features) self._fdict_class = fdict_class self._flist_class = flist_class self._prefix_feature = None self._slash_feature = None for feature in features: if feature.display == 'slash': if self._slash_feature: raise ValueError('Multiple features w/ display=slash') self._slash_feature = feature if feature.display == 'prefix': if self._prefix_feature: raise ValueError('Multiple features w/ display=prefix') self._prefix_feature = feature self._features_with_defaults = [feature for feature in features if feature.default is not None] if logic_parser is None: logic_parser = LogicParser() self._logic_parser = logic_parser def parse(self, s, fstruct=None): """ Convert a string representation of a feature structure (as displayed by repr) into a C{FeatStruct}. This parse imposes the following restrictions on the string representation: - Feature names cannot contain any of the following: whitespace, parenthases, quote marks, equals signs, dashes, commas, and square brackets. Feature names may not begin with plus signs or minus signs. - Only the following basic feature value are supported: strings, integers, variables, C{None}, and unquoted alphanumeric strings. - For reentrant values, the first mention must specify a reentrance identifier and a value; and any subsequent mentions must use arrows (C{'->'}) to reference the reentrance identifier. """ s = s.strip() value, position = self.partial_parse(s, 0, {}, fstruct) if position != len(s): self._error(s, 'end of string', position) return value _START_FSTRUCT_RE = re.compile(r'\s*(?:\((\d+)\)\s*)?(\??[\w-]+)?(\[)') _END_FSTRUCT_RE = re.compile(r'\s*]\s*') _SLASH_RE = re.compile(r'/') _FEATURE_NAME_RE = re.compile(r'\s*([+-]?)([^\s\(\)<>"\'\-=\[\],]+)\s*') _REENTRANCE_RE = re.compile(r'\s*->\s*') _TARGET_RE = re.compile(r'\s*\((\d+)\)\s*') _ASSIGN_RE = re.compile(r'\s*=\s*') _COMMA_RE = re.compile(r'\s*,\s*') _BARE_PREFIX_RE = re.compile(r'\s*(?:\((\d+)\)\s*)?(\??[\w-]+\s*)()') # This one is used to distinguish fdicts from flists: _START_FDICT_RE = re.compile(r'(%s)|(%s\s*(%s\s*(=|->)|[+-]%s|\]))' % ( _BARE_PREFIX_RE.pattern, _START_FSTRUCT_RE.pattern, _FEATURE_NAME_RE.pattern, _FEATURE_NAME_RE.pattern)) def partial_parse(self, s, position=0, reentrances=None, fstruct=None): """ Helper function that parses a feature structure. @param s: The string to parse. @param position: The position in the string to start parsing. @param reentrances: A dictionary from reentrance ids to values. Defaults to an empty dictionary. @return: A tuple (val, pos) of the feature structure created by parsing and the position where the parsed feature structure ends. """ if reentrances is None: reentrances = {} try: return self._partial_parse(s, position, reentrances, fstruct) except ValueError, e: if len(e.args) != 2: raise self._error(s, *e.args) def _partial_parse(self, s, position, reentrances, fstruct=None): # Create the new feature structure if fstruct is None: if self._START_FDICT_RE.match(s, position): fstruct = self._fdict_class() else: fstruct = self._flist_class() # Read up to the open bracket. match = self._START_FSTRUCT_RE.match(s, position) if not match: match = self._BARE_PREFIX_RE.match(s, position) if not match: raise ValueError('open bracket or identifier', position) position = match.end() # If there as an identifier, record it. if match.group(1): identifier = match.group(1) if identifier in reentrances: raise ValueError('new identifier', match.start(1)) reentrances[identifier] = fstruct if isinstance(fstruct, FeatDict): fstruct.clear() return self._partial_parse_featdict(s, position, match, reentrances, fstruct) else: del fstruct[:] return self._partial_parse_featlist(s, position, match, reentrances, fstruct) def _partial_parse_featlist(self, s, position, match, reentrances, fstruct): # Prefix features are not allowed: if match.group(2): raise ValueError('open bracket') # Bare prefixes are not allowed: if not match.group(3): raise ValueError('open bracket') # Build a list of the features defined by the structure. while position < len(s): # Check for the close bracket. match = self._END_FSTRUCT_RE.match(s, position) if match is not None: return fstruct, match.end() # Reentances have the form "-> (target)" match = self._REENTRANCE_RE.match(s, position) if match: position = match.end() match = _TARGET_RE.match(s, position) if not match: raise ValueError('identifier', position) target = match.group(1) if target not in reentrances: raise ValueError('bound identifier', position) position = match.end() fstruct.append(reentrances[target]) # Anything else is a value. else: value, position = ( self._parse_value(0, s, position, reentrances)) fstruct.append(value) # If there's a close bracket, handle it at the top of the loop. if self._END_FSTRUCT_RE.match(s, position): continue # Otherwise, there should be a comma match = self._COMMA_RE.match(s, position) if match is None: raise ValueError('comma', position) position = match.end() # We never saw a close bracket. raise ValueError('close bracket', position) def _partial_parse_featdict(self, s, position, match, reentrances, fstruct): # If there was a prefix feature, record it. if match.group(2): if self._prefix_feature is None: raise ValueError('open bracket or identifier', match.start(2)) prefixval = match.group(2).strip() if prefixval.startswith('?'): prefixval = Variable(prefixval) fstruct[self._prefix_feature] = prefixval # If group 3 is empty, then we just have a bare prefix, so # we're done. if not match.group(3): return self._finalize(s, match.end(), reentrances, fstruct) # Build a list of the features defined by the structure. # Each feature has one of the three following forms: # name = value # name -> (target) # +name # -name while position < len(s): # Use these variables to hold info about each feature: name = value = None # Check for the close bracket. match = self._END_FSTRUCT_RE.match(s, position) if match is not None: return self._finalize(s, match.end(), reentrances, fstruct) # Get the feature name's name match = self._FEATURE_NAME_RE.match(s, position) if match is None: raise ValueError('feature name', position) name = match.group(2) position = match.end() # Check if it's a special feature. if name[0] == '*' and name[-1] == '*': name = self._features.get(name[1:-1]) if name is None: raise ValueError('known special feature', match.start(2)) # Check if this feature has a value already. if name in fstruct: raise ValueError('new name', match.start(2)) # Boolean value ("+name" or "-name") if match.group(1) == '+': value = True if match.group(1) == '-': value = False # Reentrance link ("-> (target)") if value is None: match = self._REENTRANCE_RE.match(s, position) if match is not None: position = match.end() match = self._TARGET_RE.match(s, position) if not match: raise ValueError('identifier', position) target = match.group(1) if target not in reentrances: raise ValueError('bound identifier', position) position = match.end() value = reentrances[target] # Assignment ("= value"). if value is None: match = self._ASSIGN_RE.match(s, position) if match: position = match.end() value, position = ( self._parse_value(name, s, position, reentrances)) # None of the above: error. else: raise ValueError('equals sign', position) # Store the value. fstruct[name] = value # If there's a close bracket, handle it at the top of the loop. if self._END_FSTRUCT_RE.match(s, position): continue # Otherwise, there should be a comma match = self._COMMA_RE.match(s, position) if match is None: raise ValueError('comma', position) position = match.end() # We never saw a close bracket. raise ValueError('close bracket', position) def _finalize(self, s, pos, reentrances, fstruct): """ Called when we see the close brace -- checks for a slash feature, and adds in default values. """ # Add the slash feature (if any) match = self._SLASH_RE.match(s, pos) if match: name = self._slash_feature v, pos = self._parse_value(name, s, match.end(), reentrances) fstruct[name] = v ## Add any default features. -- handle in unficiation instead? #for feature in self._features_with_defaults: # fstruct.setdefault(feature, feature.default) # Return the value. return fstruct, pos def _parse_value(self, name, s, position, reentrances): if isinstance(name, Feature): return name.parse_value(s, position, reentrances, self) else: return self.parse_value(s, position, reentrances) def parse_value(self, s, position, reentrances): for (handler, regexp) in self.VALUE_HANDLERS: match = regexp.match(s, position) if match: handler_func = getattr(self, handler) return handler_func(s, position, reentrances, match) raise ValueError('value', position) def _error(self, s, expected, position): lines = s.split('\n') while position > len(lines[0]): position -= len(lines.pop(0))+1 # +1 for the newline. estr = ('Error parsing feature structure\n ' + lines[0] + '\n ' + ' '*position + '^ ' + 'Expected %s' % expected) raise ValueError, estr #//////////////////////////////////////////////////////////// #{ Value Parsers #//////////////////////////////////////////////////////////// #: A table indicating how feature values should be parsed. Each #: entry in the table is a pair (handler, regexp). The first entry #: with a matching regexp will have its handler called. Handlers #: should have the following signature:: #: #: def handler(s, position, reentrances, match): ... #: #: and should return a tuple (value, position), where position is #: the string position where the value ended. (n.b.: order is #: important here!) VALUE_HANDLERS = [ ('parse_fstruct_value', _START_FSTRUCT_RE), ('parse_var_value', re.compile(r'\?[a-zA-Z_][a-zA-Z0-9_]*')), ('parse_str_value', re.compile("[uU]?[rR]?(['\"])")), ('parse_int_value', re.compile(r'-?\d+')), ('parse_sym_value', re.compile(r'[a-zA-Z_][a-zA-Z0-9_]*')), ('parse_app_value', re.compile(r'<(app)\((\?[a-z][a-z]*)\s*,' r'\s*(\?[a-z][a-z]*)\)>')), # ('parse_logic_value', re.compile(r'<([^>]*)>')), #lazily match any character after '<' until we hit a '>' not preceded by '-' ('parse_logic_value', re.compile(r'<(.*?)(?')), ('parse_set_value', re.compile(r'{')), ('parse_tuple_value', re.compile(r'\(')), ] def parse_fstruct_value(self, s, position, reentrances, match): return self.partial_parse(s, position, reentrances) def parse_str_value(self, s, position, reentrances, match): return nltk.internals.parse_str(s, position) def parse_int_value(self, s, position, reentrances, match): return int(match.group()), match.end() # Note: the '?' is included in the variable name. def parse_var_value(self, s, position, reentrances, match): return Variable(match.group()), match.end() _SYM_CONSTS = {'None':None, 'True':True, 'False':False} def parse_sym_value(self, s, position, reentrances, match): val, end = match.group(), match.end() return self._SYM_CONSTS.get(val, val), end def parse_app_value(self, s, position, reentrances, match): """Mainly included for backwards compat.""" return self._logic_parser.parse('%s(%s)' % match.group(2,3)), match.end() def parse_logic_value(self, s, position, reentrances, match): try: try: expr = self._logic_parser.parse(match.group(1)) except ParseException: raise ValueError() return expr, match.end() except ValueError: raise ValueError('logic expression', match.start(1)) def parse_tuple_value(self, s, position, reentrances, match): return self._parse_seq_value(s, position, reentrances, match, ')', FeatureValueTuple, FeatureValueConcat) def parse_set_value(self, s, position, reentrances, match): return self._parse_seq_value(s, position, reentrances, match, '}', FeatureValueSet, FeatureValueUnion) def _parse_seq_value(self, s, position, reentrances, match, close_paren, seq_class, plus_class): """ Helper function used by parse_tuple_value and parse_set_value. """ cp = re.escape(close_paren) position = match.end() # Special syntax fo empty tuples: m = re.compile(r'\s*/?\s*%s' % cp).match(s, position) if m: return seq_class(), m.end() # Read values: values = [] seen_plus = False while True: # Close paren: return value. m = re.compile(r'\s*%s' % cp).match(s, position) if m: if seen_plus: return plus_class(values), m.end() else: return seq_class(values), m.end() # Read the next value. val, position = self.parse_value(s, position, reentrances) values.append(val) # Comma or looking at close paren m = re.compile(r'\s*(,|\+|(?=%s))\s*' % cp).match(s, position) if m.group(1) == '+': seen_plus = True if not m: raise ValueError("',' or '+' or '%s'" % cp, position) position = m.end() ###################################################################### #{ Demo ###################################################################### def display_unification(fs1, fs2, indent=' '): # Print the two input feature structures, side by side. fs1_lines = str(fs1).split('\n') fs2_lines = str(fs2).split('\n') if len(fs1_lines) > len(fs2_lines): blankline = '['+' '*(len(fs2_lines[0])-2)+']' fs2_lines += [blankline]*len(fs1_lines) else: blankline = '['+' '*(len(fs1_lines[0])-2)+']' fs1_lines += [blankline]*len(fs2_lines) for (fs1_line, fs2_line) in zip(fs1_lines, fs2_lines): print indent + fs1_line + ' ' + fs2_line print indent+'-'*len(fs1_lines[0])+' '+'-'*len(fs2_lines[0]) linelen = len(fs1_lines[0])*2+3 print indent+'| |'.center(linelen) print indent+'+-----UNIFY-----+'.center(linelen) print indent+'|'.center(linelen) print indent+'V'.center(linelen) bindings = {} result = fs1.unify(fs2, bindings) if result is None: print indent+'(FAILED)'.center(linelen) else: print '\n'.join(indent+l.center(linelen) for l in str(result).split('\n')) if bindings and len(bindings.bound_variables()) > 0: print repr(bindings).center(linelen) return result def interactive_demo(trace=False): import random, sys HELP = ''' 1-%d: Select the corresponding feature structure q: Quit t: Turn tracing on or off l: List all feature structures ?: Help ''' print ''' This demo will repeatedly present you with a list of feature structures, and ask you to choose two for unification. Whenever a new feature structure is generated, it is added to the list of choices that you can pick from. However, since this can be a large number of feature structures, the demo will only print out a random subset for you to choose between at a given time. If you want to see the complete lists, type "l". For a list of valid commands, type "?". ''' print 'Press "Enter" to continue...' sys.stdin.readline() fstruct_strings = [ '[agr=[number=sing, gender=masc]]', '[agr=[gender=masc, person=3]]', '[agr=[gender=fem, person=3]]', '[subj=[agr=(1)[]], agr->(1)]', '[obj=?x]', '[subj=?x]', '[/=None]', '[/=NP]', '[cat=NP]', '[cat=VP]', '[cat=PP]', '[subj=[agr=[gender=?y]], obj=[agr=[gender=?y]]]', '[gender=masc, agr=?C]', '[gender=?S, agr=[gender=?S,person=3]]' ] all_fstructs = [(i, FeatStruct(fstruct_strings[i])) for i in range(len(fstruct_strings))] def list_fstructs(fstructs): for i, fstruct in fstructs: print lines = str(fstruct).split('\n') print '%3d: %s' % (i+1, lines[0]) for line in lines[1:]: print ' '+line print while 1: # Pick 5 feature structures at random from the master list. MAX_CHOICES = 5 if len(all_fstructs) > MAX_CHOICES: fstructs = random.sample(all_fstructs, MAX_CHOICES) fstructs.sort() else: fstructs = all_fstructs print '_'*75 print 'Choose two feature structures to unify:' list_fstructs(fstructs) selected = [None,None] for (nth,i) in (('First',0), ('Second',1)): while selected[i] is None: print ('%s feature structure (1-%d,q,t,l,?): ' % (nth, len(all_fstructs))), try: input = sys.stdin.readline().strip() if input in ('q', 'Q', 'x', 'X'): return if input in ('t', 'T'): trace = not trace print ' Trace = %s' % trace continue if input in ('h', 'H', '?'): print HELP % len(fstructs); continue if input in ('l', 'L'): list_fstructs(all_fstructs); continue num = int(input)-1 selected[i] = all_fstructs[num][1] print except: print 'Bad sentence number' continue if trace: result = selected[0].unify(selected[1], trace=1) else: result = display_unification(selected[0], selected[1]) if result is not None: for i, fstruct in all_fstructs: if `result` == `fstruct`: break else: all_fstructs.append((len(all_fstructs), result)) print '\nType "Enter" to continue unifying; or "q" to quit.' input = sys.stdin.readline().strip() if input in ('q', 'Q', 'x', 'X'): return def demo(trace=False): """ Just for testing """ #import random # parser breaks with values like '3rd' fstruct_strings = [ '[agr=[number=sing, gender=masc]]', '[agr=[gender=masc, person=3]]', '[agr=[gender=fem, person=3]]', '[subj=[agr=(1)[]], agr->(1)]', '[obj=?x]', '[subj=?x]', '[/=None]', '[/=NP]', '[cat=NP]', '[cat=VP]', '[cat=PP]', '[subj=[agr=[gender=?y]], obj=[agr=[gender=?y]]]', '[gender=masc, agr=?C]', '[gender=?S, agr=[gender=?S,person=3]]' ] all_fstructs = [FeatStruct(fss) for fss in fstruct_strings] #MAX_CHOICES = 5 #if len(all_fstructs) > MAX_CHOICES: #fstructs = random.sample(all_fstructs, MAX_CHOICES) #fstructs.sort() #else: #fstructs = all_fstructs for fs1 in all_fstructs: for fs2 in all_fstructs: print "\n*******************\nfs1 is:\n%s\n\nfs2 is:\n%s\n\nresult is:\n%s" % (fs1, fs2, unify(fs1, fs2)) if __name__ == '__main__': demo() __all__ = ['FeatStruct', 'FeatDict', 'FeatList', 'unify', 'subsumes', 'conflicts', 'Feature', 'SlashFeature', 'RangeFeature', 'SLASH', 'TYPE', 'FeatStructParser'] nltk-2.0~b9/nltk/evaluate.py0000644000175000017500000000541411327451603015714 0ustar bhavanibhavani# Natural Language Toolkit: Evaluation # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT """ Utility functions for evaluating processing modules. """ import sys import math import random try: from scipy.stats.stats import betai except ImportError: betai = None import nltk from util import LazyConcatenation, LazyMap, LazyZip from probability import FreqDist from internals import deprecated @deprecated('Use nltk.metrics.accuracy() instead.') def accuracy(reference, test): return nltk.metrics.accuracy(reference, test) @deprecated('Use nltk.metrics.precision()instead.') def precision(reference, test): return nltk.metrics.precision(reference, test) @deprecated('Use nltk.metrics.recall() instead.') def recall(reference, test): return nltk.metrics.recall(reference, test) @deprecated('Use nltk.metrics.f_measure() instead.') def f_measure(reference, test, alpha=0.5): return nltk.metrics.f_measure(reference, test, alpha) @deprecated('Use nltk.metrics.log_likelihood() instead.') def log_likelihood(reference, test): return nltk.metrics.log_likelihood(reference, test) @deprecated('Use nltk.metrics.approxrand() instead.') def approxrand(a, b, **kwargs): return nltk.nltk.metrics.approxrand(a, b, **kwargs) class ConfusionMatrix(nltk.metrics.ConfusionMatrix): @deprecated('Use nltk.metrics.ConfusionMatrix instead.') def __init__(self, reference, test, sort_by_count=False): nltk.metrics.ConfusionMatrix.__init__(self, reference, test, sort_by_count) @deprecated('Use nltk.metrics.windowdiff instead.') def windowdiff(seg1, seg2, k, boundary="1"): return nltk.metrics.windowdiff(seg1, seg2, k, boundary) @deprecated('Use nltk.metrics.edit_dist instead.') def edit_dist(s1, s2): return nltk.metrics.edit_dist(s1, s2) def demo(): print '-'*75 reference = 'DET NN VB DET JJ NN NN IN DET NN'.split() test = 'DET VB VB DET NN NN NN IN DET NN'.split() print 'Reference =', reference print 'Test =', test print 'Confusion matrix:' print ConfusionMatrix(reference, test) print 'Accuracy:', accuracy(reference, test) print '-'*75 reference_set = set(reference) test_set = set(test) print 'Reference =', reference_set print 'Test = ', test_set print 'Precision:', precision(reference_set, test_set) print ' Recall:', recall(reference_set, test_set) print 'F-Measure:', f_measure(reference_set, test_set) print '-'*75 if __name__ == '__main__': demo() __all__ = ['ConfusionMatrix', 'accuracy', 'f_measure', 'log_likelihood', 'precision', 'recall', 'approxrand', 'edit_dist', 'windowdiff'] nltk-2.0~b9/nltk/downloader.py0000644000175000017500000025371611327451603016256 0ustar bhavanibhavani# Natural Language Toolkit: Corpus & Model Downloader # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT """ The NLTK corpus and module downloader. This module defines several interfaces which can be used to download corpora, models, and other data packages that can be used with NLTK. Downloading Packages ==================== If called with no arguments, L{download() } function will display an interactive interface which can be used to download and install new packages. If Tkinter is available, then a graphical interface will be shown; otherwise, a simple text interface will be provided. Individual packages can be downloaded by calling the C{download()} function with a single argument, giving the package identifier for the package that should be downloaded: >>> download('treebank') # doctest: +SKIP [nltk_data] Downloading package 'treebank'... [nltk_data] Unzipping corpora/treebank.zip. NLTK also provides a number of \"package collections\", consisting of a group of related packages. To download all packages in a colleciton, simply call C{download()} with the collection's identifier: >>> download('all-corpora') # doctest: +SKIP [nltk_data] Downloading package 'abc'... [nltk_data] Unzipping corpora/abc.zip. [nltk_data] Downloading package 'alpino'... [nltk_data] Unzipping corpora/alpino.zip. ... [nltk_data] Downloading package 'words'... [nltk_data] Unzipping corpora/words.zip. Download Directory ================== By default, packages are installed in either a system-wide directory (if Python has sufficient access to write to it); or in the current user's home directory. However, the C{download_dir} argument may be used to specify a different installation target, if desired. See L{Downloader.default_download_dir()} for more a detailed description of how the default download directory is chosen. NLTK Download Server ==================== Before downloading any packages, the corpus and module downloader contacts the NLTK download server, to retrieve an index file describing the available packages. By default, this index file is loaded from C{}. If necessary, it is possible to create a new L{Downloader} object, specifying a different URL for the package index file. Usage:: python nltk/downloader.py [-d DATADIR] [-q] [-f] [-k] PACKAGE_IDS or with py2.5+: python -m nltk.downloader [-d DATADIR] [-q] [-f] [-k] PACKAGE_IDS """ #---------------------------------------------------------------------- """ 0 1 2 3 [label][----][label][----] [column ][column ] Notes ===== Handling data files.. Some questions: * Should the data files be kept zipped or unzipped? I say zipped. * Should the data files be kept in svn at all? Advantages: history; automatic version numbers; 'svn up' could be used rather than the downloader to update the corpora. Disadvantages: they're big, which makes working from svn a bit of a pain. And we're planning to potentially make them much bigger. I don't think we want people to have to download 400mb corpora just to use nltk from svn. * Compromise: keep the data files in trunk/data rather than in trunk/nltk. That way you can check them out in svn if you want to; but you don't need to, and you can use the downloader instead. * Also: keep models in mind. When we change the code, we'd potentially like the models to get updated. This could require a little thought. * So.. let's assume we have a trunk/data directory, containing a bunch of packages. The packages should be kept as zip files, because we really shouldn't be editing them much (well -- we may edit models more, but they tend to be binary-ish files anyway, where diffs aren't that helpful). So we'll have trunk/data, with a bunch of files like abc.zip and treebank.zip and propbank.zip. For each package we could also have eg treebank.xml and propbank.xml, describing the contents of the package (name, copyright, license, etc). Collections would also have .xml files. Finally, we would pull all these together to form a single index.xml file. Some directory structure wouldn't hurt. So how about:: /trunk/data/ ....................... root of data svn index.xml ........................ main index file src/ ............................. python scripts packages/ ........................ dir for packages corpora/ ....................... zip & xml files for corpora grammars/ ...................... zip & xml files for grammars taggers/ ....................... zip & xml files for taggers tokenizers/ .................... zip & xml files for tokenizers etc. collections/ ..................... xml files for collections Where the root (/trunk/data) would contain a makefile; and src/ would contain a script to update the info.xml file. It could also contain scripts to rebuild some of the various model files. The script that builds index.xml should probably check that each zip file expands entirely into a single subdir, whose name matches the package's uid. Changes I need to make: - in index: change "size" to "filesize" or "compressed-size" - in index: add "unzipped-size" - when checking status: check both compressed & uncompressed size. uncompressed size is important to make sure we detect a problem if something got partially unzipped. define new status values to differentiate stale vs corrupt vs corruptly-uncompressed?? (we shouldn't need to re-download the file if the zip file is ok but it didn't get uncompressed fully.) - add other fields to the index: author, license, copyright, contact, etc. the current grammars/ package would become a single new package (eg toy-grammars or book-grammars). xml file should have: - authorship info - license info - copyright info - contact info - info about what type of data/annotation it contains? - recommended corpus reader? collections can contain other collections. they can also contain multiple package types (corpora & models). Have a single 'basics' package that includes everything we talk about in the book? n.b.: there will have to be a fallback to the punkt tokenizer, in case they didn't download that model. default: unzip or not? """ import time, re, os, zipfile, sys, textwrap, threading, itertools from cStringIO import StringIO try: from hashlib import md5 except: from md5 import md5 try: TKINTER = True from Tkinter import * from tkMessageBox import * from nltk.draw.table import Table from nltk.draw import ShowText except: TKINTER = False TclError = ValueError from nltk.etree import ElementTree import nltk urllib2 = nltk.internals.import_from_stdlib('urllib2') ###################################################################### # Directory entry objects (from the data server's index file) ###################################################################### class Package(object): """ A directory entry for a downloadable package. These entries are extracted from the XML index file that is downloaded by L{Downloader}. Each package consists of a single file; but if that file is a zip file, then it can be automatically decompressed when the package is installed. """ def __init__(self, id, url, name=None, subdir='', size=None, unzipped_size=None, checksum=None, svn_revision=None, copyright='Unknown', contact='Unknown', license='Unknown', author='Unknown', unzip=True, **kw): self.id = id """A unique identifier for this package.""" self.name = name or id """A string name for this package.""" self.subdir = subdir """The subdirectory where this package should be installed. E.g., C{'corpora'} or C{'taggers'}.""" self.url = url """A URL that can be used to download this package's file.""" self.size = int(size) """The filesize (in bytes) of the package file.""" self.unzipped_size = int(unzipped_size) """The total filesize of the files contained in the package's zipfile.""" self.checksum = checksum """The MD-5 checksum of the package file.""" self.svn_revision = svn_revision """A subversion revision number for this package.""" self.copyright = copyright """Copyright holder for this package.""" self.contact = contact """Name & email of the person who should be contacted with questions about this package.""" self.license = license """License information for this package.""" self.author = author """Author of this package.""" ext = os.path.splitext(url.split('/')[-1])[1] self.filename = os.path.join(subdir, id+ext) """The filename that should be used for this package's file. It is formed by joining C{self.subdir} with C{self.id}, and using the same extension as C{url}.""" self.unzip = bool(int(unzip)) # '0' or '1' """A flag indicating whether this corpus should be unzipped by default.""" # Include any other attributes provided by the XML file. self.__dict__.update(kw) @staticmethod def fromxml(xml): if isinstance(xml, basestring): xml = ElementTree.parse(xml) return Package(**xml.attrib) def __repr__(self): return '' % self.id class Collection(object): """ A directory entry for a collection of downloadable packages. These entries are extracted from the XML index file that is downloaded by L{Downloader}. """ def __init__(self, id, children, name=None, **kw): self.id = id """A unique identifier for this collection.""" self.name = name or id """A string name for this collection.""" self.children = children """A list of the L{Collections} or L{Packages} directly contained by this collection.""" self.packages = None """A list of L{Packages} contained by this collection or any collections it recursively contains.""" # Include any other attributes provided by the XML file. self.__dict__.update(kw) @staticmethod def fromxml(xml): if isinstance(xml, basestring): xml = ElementTree.parse(xml) children = [child.get('ref') for child in xml.findall('item')] return Collection(children=children, **xml.attrib) def __repr__(self): return '' % self.id ###################################################################### # Message Passing Objects ###################################################################### class DownloaderMessage(object): """A status message object, used by L{incr_download} to communicate its progress.""" class StartCollectionMessage(DownloaderMessage): """Data server has started working on a collection of packages.""" def __init__(self, collection): self.collection = collection class FinishCollectionMessage(DownloaderMessage): """Data server has finished working on a collection of packages.""" def __init__(self, collection): self.collection = collection class StartPackageMessage(DownloaderMessage): """Data server has started working on a package.""" def __init__(self, package): self.package = package class FinishPackageMessage(DownloaderMessage): """Data server has finished working on a package.""" def __init__(self, package): self.package = package class StartDownloadMessage(DownloaderMessage): """Data server has started downloading a package.""" def __init__(self, package): self.package = package class FinishDownloadMessage(DownloaderMessage): """Data server has finished downloading a package.""" def __init__(self, package): self.package = package class StartUnzipMessage(DownloaderMessage): """Data server has started unzipping a package.""" def __init__(self, package): self.package = package class FinishUnzipMessage(DownloaderMessage): """Data server has finished unzipping a package.""" def __init__(self, package): self.package = package class UpToDateMessage(DownloaderMessage): """The package download file is already up-to-date""" def __init__(self, package): self.package = package class StaleMessage(DownloaderMessage): """The package download file is out-of-date or corrupt""" def __init__(self, package): self.package = package class ErrorMessage(DownloaderMessage): """Data server encountered an error""" def __init__(self, package, message): self.package = package if isinstance(message, Exception): self.message = str(message) else: self.message = message class ProgressMessage(DownloaderMessage): """Indicates how much progress the data server has made""" def __init__(self, progress): self.progress = progress class SelectDownloadDirMessage(DownloaderMessage): """Indicates what download directory the data server is using""" def __init__(self, download_dir): self.download_dir = download_dir ###################################################################### # NLTK Data Server ###################################################################### class Downloader(object): """ A class used to access the NLTK data server, which can be used to download corpora and other data packages. """ #///////////////////////////////////////////////////////////////// # Configuration #///////////////////////////////////////////////////////////////// INDEX_TIMEOUT = 60*60 # 1 hour """The amount of time after which the cached copy of the data server index will be considered 'stale,' and will be re-downloaded.""" DEFAULT_URL = 'http://nltk.googlecode.com/svn/trunk/nltk_data/index.xml' """The default URL for the NLTK data server's index. An alternative URL can be specified when creating a new C{Downloader} object.""" #///////////////////////////////////////////////////////////////// # Status Constants #///////////////////////////////////////////////////////////////// INSTALLED = 'installed' """A status string indicating that a package or collection is installed and up-to-date.""" NOT_INSTALLED = 'not installed' """A status string indicating that a package or collection is not installed.""" STALE = 'out of date' """A status string indicating that a package or collection is corrupt or out-of-date.""" PARTIAL = 'partial' """A status string indicating that a collection is partially installed (i.e., only some of its packages are installed.)""" #///////////////////////////////////////////////////////////////// # Cosntructor #///////////////////////////////////////////////////////////////// def __init__(self, server_index_url=None, download_dir=None): self._url = server_index_url or self.DEFAULT_URL """The URL for the data server's index file.""" self._collections = {} """Dictionary from collection identifier to L{Collection}""" self._packages = {} """Dictionary from package identifier to L{Package}""" self._download_dir = download_dir """The default directory to which packages will be downloaded.""" self._index = None """The XML index file downloaded from the data server""" self._index_timestamp = None """Time at which L{self._index} was downloaded. If it is more than L{INDEX_TIMEOUT} seconds old, it will be re-downloaded.""" self._status_cache = {} """Dictionary from package/collection identifier to status string (L{INSTALLED}, L{NOT_INSTALLED}, L{STALE}, or L{PARTIAL}). Cache is used for packages only, not collections.""" self._errors = None """Flag for telling if all packages got successfully downloaded or not.""" # decide where we're going to save things to. if self._download_dir is None: self._download_dir = self.default_download_dir() #///////////////////////////////////////////////////////////////// # Information #///////////////////////////////////////////////////////////////// def list(self, download_dir=None, show_packages=True, show_collections=True, header=True, more_prompt=False, skip_installed=False): lines = 0 # for more_prompt if download_dir is None: download_dir = self._download_dir print 'Using default data directory (%s)' % download_dir if header: print '='*(26+len(self._url)) print ' Data server index for <%s>' % self._url print '='*(26+len(self._url)) lines += 3 # for more_prompt stale = partial = False categories = [] if show_packages: categories.append('packages') if show_collections: categories.append('collections') for category in categories: print '%s:' % category.capitalize() lines += 1 # for more_prompt for info in sorted(getattr(self, category)()): status = self.status(info, download_dir) if status == self.INSTALLED and skip_installed: continue if status == self.STALE: stale = True if status == self.PARTIAL: partial = True prefix = {self.INSTALLED:'*', self.STALE:'-', self.PARTIAL:'P', self.NOT_INSTALLED: ' '}[status] name = textwrap.fill('-'*27 + (info.name or info.id), 75, subsequent_indent=27*' ')[27:] print ' [%s] %s %s' % (prefix, info.id.ljust(20, '.'), name) lines += len(name.split('\n')) # for more_prompt if more_prompt and lines > 20: user_input = raw_input("Hit Enter to continue: ") if (user_input.lower() in ('x', 'q')): return lines = 0 print msg = '([*] marks installed packages' if stale: msg += '; [-] marks out-of-date or corrupt packages' if partial: msg += '; [P] marks partially installed collections' print textwrap.fill(msg+')', subsequent_indent=' ', width=76) def packages(self): self._update_index() return self._packages.values() def corpora(self): self._update_index() return [pkg for (id,pkg) in self._packages.items() if pkg.subdir == 'corpora'] def models(self): self._update_index() return [pkg for (id,pkg) in self._packages.items() if pkg.subdir != 'corpora'] def collections(self): self._update_index() return self._collections.values() #///////////////////////////////////////////////////////////////// # Downloading #///////////////////////////////////////////////////////////////// def _info_or_id(self, info_or_id): if isinstance(info_or_id, basestring): return self.info(info_or_id) else: return info_or_id # [xx] When during downloading is it 'safe' to abort? Only unsafe # time is *during* an unzip -- we don't want to leave a # partially-unzipped corpus in place because we wouldn't notice # it. But if we had the exact total size of the unzipped corpus, # then that would be fine. Then we could abort anytime we want! # So this is really what we should do. That way the threaded # downloader in the gui can just kill the download thread anytime # it wants. def incr_download(self, info_or_id, download_dir=None, force=False): # If they didn't specify a download_dir, then use the default one. if download_dir is None: download_dir = self._download_dir yield SelectDownloadDirMessage(download_dir) # If they gave us a list of ids, then download each one. if isinstance(info_or_id, (list,tuple)): for msg in self._download_list(info_or_id, download_dir, force): yield msg return # Look up the requested collection or package. try: info = self._info_or_id(info_or_id) except (IOError, ValueError), e: yield ErrorMessage(None, 'Error loading %s: %s' % (info_or_id, e)) return # Handle collections. if isinstance(info, Collection): yield StartCollectionMessage(info) for msg in self.incr_download(info.children, download_dir, force): yield msg yield FinishCollectionMessage(info) # Handle Packages (delegate to a helper function). else: for msg in self._download_package(info, download_dir, force): yield msg def _num_packages(self, item): if isinstance(item, Package): return 1 else: return len(item.packages) def _download_list(self, items, download_dir, force): # Look up the requested items. for i in range(len(items)): try: items[i] = self._info_or_id(items[i]) except (IOError, ValueError), e: yield ErrorMessage(items[i], e) return # Download each item, re-scaling their progress. num_packages = sum(self._num_packages(item) for item in items) progress = 0 for i, item in enumerate(items): if isinstance(item, Package): delta = 1./num_packages else: delta = float(len(item.packages))/num_packages for msg in self.incr_download(item, download_dir, force): if isinstance(msg, ProgressMessage): yield ProgressMessage(progress + msg.progress*delta) else: yield msg progress += 100*delta def _download_package(self, info, download_dir, force): yield StartPackageMessage(info) yield ProgressMessage(0) # Do we already have the current version? status = self.status(info, download_dir) if not force and status == self.INSTALLED: yield UpToDateMessage(info) yield ProgressMessage(100) yield FinishPackageMessage(info) return # Remove the package from our status cache self._status_cache.pop(info.id, None) # Check for (and remove) any old/stale version. filepath = os.path.join(download_dir, info.filename) if os.path.exists(filepath): if status == self.STALE: yield StaleMessage(info) os.remove(filepath) # Ensure the download_dir exists if not os.path.exists(download_dir): os.mkdir(download_dir) if not os.path.exists(os.path.join(download_dir, info.subdir)): os.mkdir(os.path.join(download_dir, info.subdir)) # Download the file. This will raise an IOError if the url # is not found. yield StartDownloadMessage(info) yield ProgressMessage(5) try: infile = urllib2.urlopen(info.url) outfile = open(filepath, 'wb') #print info.size num_blocks = max(1, float(info.size)/(1024*16)) for block in itertools.count(): s = infile.read(1024*16) # 16k blocks. outfile.write(s) if not s: break if block % 2 == 0: # how often? yield ProgressMessage(min(80, 5+75*(block/num_blocks))) infile.close() outfile.close() except IOError, e: yield ErrorMessage(info, 'Error downloading %r from <%s>:' '\n %s' % (info.id, info.url, e)) return yield FinishDownloadMessage(info) yield ProgressMessage(80) # If it's a zipfile, uncompress it. if info.filename.endswith('.zip'): zipdir = os.path.join(download_dir, info.subdir) # Unzip if we're unzipping by default; *or* if it's already # been unzipped (presumably a previous version). if info.unzip or os.path.exists(os.path.join(zipdir, info.id)): yield StartUnzipMessage(info) for msg in _unzip_iter(filepath, zipdir, verbose=False): # Somewhat of a hack, but we need a proper package reference msg.package = info yield msg yield FinishUnzipMessage(info) yield FinishPackageMessage(info) def download(self, info_or_id=None, download_dir=None, quiet=False, force=False, prefix='[nltk_data] ', halt_on_error=True, raise_on_error=False): # If no info or id is given, then use the interactive shell. if info_or_id is None: # [xx] hmm -- changing self._download_dir here seems like # the wrong thing to do. Maybe the _interactive_download # function should make a new copy of self to use? if download_dir is not None: self._download_dir = download_dir self._interactive_download() return True else: # Define a helper function for displaying output: def show(s, prefix2=''): print textwrap.fill(s, initial_indent=prefix+prefix2, subsequent_indent=prefix+prefix2+' '*4) for msg in self.incr_download(info_or_id, download_dir, force): # Error messages if isinstance(msg, ErrorMessage): show(msg.message) if raise_on_error: raise ValueError(msg.message) if halt_on_error: return False self._errors = True if not quiet: print "Error installing package. Retry? [n/y/e]" choice = raw_input().strip() if choice in ['y', 'Y']: if not self.download(msg.package.id, download_dir, quiet, force, prefix, halt_on_error, raise_on_error): return False elif choice in ['e', 'E']: return False # All other messages if not quiet: # Collection downloading messages: if isinstance(msg, StartCollectionMessage): show('Downloading collection %r' % msg.collection.id) prefix += ' | ' print prefix elif isinstance(msg, FinishCollectionMessage): print prefix prefix = prefix[:-4] if self._errors: show('Downloaded collection %r with errors' % msg.collection.id) else: show('Done downloading collection %r' % msg.collection.id) # Package downloading messages: elif isinstance(msg, StartPackageMessage): show('Downloading package %r to %s...' % (msg.package.id, download_dir)) elif isinstance(msg, UpToDateMessage): show('Package %s is already up-to-date!' % msg.package.id, ' ') #elif isinstance(msg, StaleMessage): # show('Package %s is out-of-date or corrupt' % # msg.package.id, ' ') elif isinstance(msg, StartUnzipMessage): show('Unzipping %s.' % msg.package.filename, ' ') # Data directory message: elif isinstance(msg, SelectDownloadDirMessage): download_dir = msg.download_dir return True def is_stale(self, info_or_id, download_dir=None): return self.status(info_or_id, download_dir) == self.STALE def is_installed(self, info_or_id, download_dir=None): return self.status(info_or_id, download_dir) == self.INSTALLED def clear_status_cache(self, id=None): if id is None: self._status_cache.clear() else: self._status_cache.pop(id, None) def status(self, info_or_id, download_dir=None): """ Return a constant describing the status of the given package or collection. Status can be one of L{INSTALLED}, L{NOT_INSTALLED}, L{STALE}, or L{PARTIAL}. """ if download_dir is None: download_dir = self._download_dir info = self._info_or_id(info_or_id) # Handle collections: if isinstance(info, Collection): pkg_status = [self.status(pkg.id) for pkg in info.packages] if self.STALE in pkg_status: return self.STALE elif self.PARTIAL in pkg_status: return self.PARTIAL elif (self.INSTALLED in pkg_status and self.NOT_INSTALLED in pkg_status): return self.PARTIAL elif self.NOT_INSTALLED in pkg_status: return self.NOT_INSTALLED else: return self.INSTALLED # Handle packages: else: filepath = os.path.join(download_dir, info.filename) if download_dir != self._download_dir: status = self._pkg_status(info, filepath) else: if info.id not in self._status_cache: self._status_cache[info.id] = self._pkg_status(info, filepath) return self._status_cache[info.id] def _pkg_status(self, info, filepath): if not os.path.exists(filepath): return self.NOT_INSTALLED # Check if the file has the correct size. try: filestat = os.stat(filepath) except OSError: return self.NOT_INSTALLED if filestat.st_size != int(info.size): return self.STALE # Check if the file's checksum matches if md5_hexdigest(filepath) != info.checksum: return self.STALE # If it's a zipfile, and it's been at least partially # unzipped, then check if it's been fully unzipped. if filepath.endswith('.zip'): unzipdir = filepath[:-4] if not os.path.exists(unzipdir): return self.INSTALLED # but not unzipped -- ok! if not os.path.isdir(unzipdir): return self.STALE unzipped_size = sum(os.stat(os.path.join(d, f)).st_size for d, _, files in os.walk(unzipdir) for f in files) if unzipped_size != info.unzipped_size: return self.STALE # Otherwise, everything looks good. return self.INSTALLED def update(self, quiet=False, prefix='[nltk_data] '): """ Re-download any packages whose status is STALE. """ self.clear_status_cache() for pkg in self.packages(): if self.status(pkg) == self.STALE: self.download(pkg, quiet=quiet, prefix=prefix) #///////////////////////////////////////////////////////////////// # Index #///////////////////////////////////////////////////////////////// def _update_index(self, url=None): """A helper function that ensures that self._index is up-to-date. If the index is older than self.INDEX_TIMEOUT, then download it again.""" # Check if the index is aleady up-to-date. If so, do nothing. if not (self._index is None or url is not None or time.time()-self._index_timestamp > self.INDEX_TIMEOUT): return # If a URL was specified, then update our URL. self._url = url or self._url # Download the index file. self._index = nltk.internals.ElementWrapper( ElementTree.parse(urllib2.urlopen(self._url)).getroot()) self._index_timestamp = time.time() # Build a dictionary of packages. packages = [Package.fromxml(p) for p in self._index.findall('packages/package')] self._packages = dict((p.id, p) for p in packages) # Build a dictionary of collections. collections = [Collection.fromxml(c) for c in self._index.findall('collections/collection')] self._collections = dict((c.id, c) for c in collections) # Replace identifiers with actual children in collection.children. for collection in self._collections.values(): for i, child_id in enumerate(collection.children): if child_id in self._packages: collection.children[i] = self._packages[child_id] if child_id in self._collections: collection.children[i] = self._collections[child_id] # Fill in collection.packages for each collection. for collection in self._collections.values(): packages = {} queue = [collection] for child in queue: if isinstance(child, Collection): queue.extend(child.children) else: packages[child.id] = child collection.packages = packages.values() # Flush the status cache self._status_cache.clear() def index(self): """ Return the XML index describing the packages available from the data server. If necessary, this index will be downloaded from the data server. """ self._update_index() return self._index def info(self, id): """Return the L{Package} or L{Collection} record for the given item.""" self._update_index() if id in self._packages: return self._packages[id] if id in self._collections: return self._collections[id] raise ValueError('Package %r not found in index' % id) def xmlinfo(self, id): """Return the XML info record for the given item""" self._update_index() for package in self._index.findall('packages/package'): if package.get('id') == id: return package for collection in self._index.findall('collections/collection'): if collection.get('id') == id: return collection raise ValueError('Package %r not found in index' % id) #///////////////////////////////////////////////////////////////// # URL & Data Directory #///////////////////////////////////////////////////////////////// def _set_url(self, url): # If we're unable to contact the given url, then keep the # original url. original_url = self._url try: self._update_index(url) except: self._url = original_url raise url = property(lambda self: self._url, _set_url, doc=""" The URL for the data server's index file.""") def default_download_dir(self): """ Return the directory to which packages will be downloaded by default. This value can be overridden using the constructor, or on a case-by-case basis using the C{download_dir} argument when calling L{download()}. On Windows, the default download directory is C{I{PYTHONHOME}/lib/nltk}, where C{I{PYTHONHOME}} is the directory containing Python (e.g. C{C:\\Python25}). On all other platforms, the default directory is determined as follows: - If C{/usr/share} exists and is writable, then return C{/usr/share/nltk} - If C{/usr/local/share} exists and is writable, then return C{/usr/local/share/nltk} - If C{/usr/lib} exists and is writable, then return C{/usr/lib/nltk} - If C{/usr/local/lib} exists and is writable, then return C{/usr/local/lib/nltk} - Otherwise, return C{~/nltk_data}, where C{~} is the current user's home directory. """ # Check if we have sufficient permissions to install in a # variety of system-wide locations. for nltkdir in nltk.data.path: if (os.path.exists(nltkdir) and nltk.internals.is_writable(nltkdir)): return nltkdir # On Windows, use %APPDATA% if sys.platform == 'win32' and 'APPDATA' in os.environ: homedir = os.environ['APPDATA'] # Otherwise, install in the user's home directory. else: homedir = os.path.expanduser('~/') if homedir == '~/': raise ValueError("Could not find a default download directory") # append "nltk_data" to the home directory return os.path.join(homedir, 'nltk_data') def _set_download_dir(self, download_dir): self._download_dir = download_dir # Clear the status cache. self._status_cache.clear() download_dir = property(lambda self: self._download_dir, _set_download_dir, doc=""" The default directory to which packages will be downloaded. This defaults to the value returned by L{default_download_dir()}. To override this default on a case-by-case basis, use the C{download_dir} argument when calling L{download()}.""") #///////////////////////////////////////////////////////////////// # Interactive Shell #///////////////////////////////////////////////////////////////// def _interactive_download(self): # Try the GUI first; if that doesn't work, try the simple # interactive shell. if TKINTER: try: DownloaderGUI(self).mainloop() except TclError: DownloaderShell(self).run() else: DownloaderShell(self).run() class DownloaderShell(object): def __init__(self, dataserver): self._ds = dataserver def _simple_interactive_menu(self, *options): print '-'*75 spc = (68 - sum(len(o) for o in options))/(len(options)-1)*' ' print ' ' + spc.join(options) #w = 76/len(options) #fmt = ' ' + ('%-'+str(w)+'s')*(len(options)-1) + '%s' #print fmt % options print '-'*75 def run(self): print 'NLTK Downloader' while True: self._simple_interactive_menu( 'd) Download', 'l) List', 'c) Config', 'h) Help', 'q) Quit') user_input = raw_input('Downloader> ').strip() if not user_input: print; continue command = user_input.lower().split()[0] args = user_input.split()[1:] if command == 'l': print self._ds.list(self._ds.download_dir, header=False, more_prompt=True) elif command == 'h': self._simple_interactive_help() elif command == 'c': self._simple_interactive_config() elif command in ('q', 'x'): return elif command == 'd': self._simple_interactive_download(args) else: print 'Command %r unrecogmized' % user_input # try checking if user_input is a package name, & # downloading it? print def _simple_interactive_download(self, args): if args: for arg in args: try: self._ds.download(arg, prefix=' ') except (IOError, ValueError), e: print e else: while True: print print 'Download which package (l=list; x=cancel)?' user_input = raw_input(' Identifier> ') if user_input.lower()=='l': self._ds.list(self._ds.download_dir, header=False, more_prompt=True, skip_installed=True) continue elif user_input.lower() in ('x', 'q', ''): return elif user_input: for id in user_input.split(): try: self._ds.download(id, prefix=' ') except (IOError, ValueError), e: print e break def _simple_interactive_help(self): print print 'Commands:' print ' d) Download a package or collection h) Help' print ' l) List packages & collections q) Quit' print ' c) View & Modify Configuration' def _show_config(self): print print 'Data Server:' print ' - URL: <%s>' % self._ds.url print (' - %d Package Collections Available' % len(self._ds.collections())) print (' - %d Individual Packages Available' % len(self._ds.packages())) print print 'Local Machine:' print ' - Data directory: %s' % self._ds.download_dir def _simple_interactive_config(self): self._show_config() while True: print self._simple_interactive_menu( 's) Show Config', 'u) Set Server URL', 'd) Set Data Dir', 'm) Main Menu') user_input = raw_input('Config> ').strip().lower() if user_input == 's': self._show_config() elif user_input == 'd': new_dl_dir = raw_input(' New Directory> ').strip().lower() if new_dl_dir in ('', 'x', 'q'): print ' Cancelled!' elif os.path.isdir(new_dl_dir): self._ds.download_dir = new_dl_dir else: print ('Directory %r not found! Create it first.' % new_dl_dir) elif user_input == 'u': new_url = raw_input(' New URL> ').strip().lower() if new_url in ('', 'x', 'q'): print ' Cancelled!' else: if not new_url.startswith('http://'): new_url = 'http://'+new_url try: self._ds.url = new_url except Exception, e: print 'Error reading <%r>:\n %s' % (new_url, e) elif user_input == 'm': break class DownloaderGUI(object): """ Graphical interface for downloading packages from the NLTK data server. """ #///////////////////////////////////////////////////////////////// # Column Configuration #///////////////////////////////////////////////////////////////// COLUMNS = ['', 'Identifier', 'Name', 'Size', 'Status', 'Unzipped Size', 'Copyright', 'Contact', 'License', 'Author', 'SVN Revision', 'Subdir', 'Checksum'] """A list of the names of columns. This controls the order in which the columns will appear. If this is edited, then L{_package_to_columns()} may need to be edited to match.""" COLUMN_WEIGHTS = {'': 0, 'Name': 5, 'Size': 0, 'Status': 0} """A dictionary specifying how columns should be resized when the table is resized. Columns with weight 0 will not be resized at all; and columns with high weight will be resized more. Default weight (for columns not explicitly listed) is 1.""" COLUMN_WIDTHS = {'':1, 'Identifier':20, 'Name':45, 'Size': 10, 'Unzipped Size': 10, 'Status': 12} """A dictionary specifying how wide each column should be, in characters. The default width (for columns not explicitly listed) is specified by L{DEFAULT_COLUMN_WIDTH}.""" DEFAULT_COLUMN_WIDTH = 30 """The default width for columns that are not explicitly listed in C{COLUMN_WIDTHS}.""" INITIAL_COLUMNS = ['', 'Identifier', 'Name', 'Size', 'Status'] """The set of columns that should be displayed by default.""" # Perform a few import-time sanity checks to make sure that the # column configuration variables are defined consistently: for c in COLUMN_WEIGHTS: assert c in COLUMNS for c in COLUMN_WIDTHS: assert c in COLUMNS for c in INITIAL_COLUMNS: assert c in COLUMNS #///////////////////////////////////////////////////////////////// # Color Configuration #///////////////////////////////////////////////////////////////// _BACKDROP_COLOR = ('#000', '#ccc') _ROW_COLOR = {Downloader.INSTALLED: ('#afa', '#080'), Downloader.PARTIAL: ('#ffa', '#880'), Downloader.STALE: ('#faa', '#800'), Downloader.NOT_INSTALLED: ('#fff', '#888')} _MARK_COLOR = ('#000', '#ccc') #_FRONT_TAB_COLOR = ('#ccf', '#008') #_BACK_TAB_COLOR = ('#88a', '#448') _FRONT_TAB_COLOR = ('#fff', '#45c') _BACK_TAB_COLOR = ('#aaa', '#67a') _PROGRESS_COLOR = ('#f00', '#aaa') _TAB_FONT = 'helvetica -16 bold' #///////////////////////////////////////////////////////////////// # Constructor #///////////////////////////////////////////////////////////////// def __init__(self, dataserver, use_threads=True): self._ds = dataserver self._use_threads = use_threads # For the threaded downloader: self._download_lock = threading.Lock() self._download_msg_queue = [] self._download_abort_queue = [] self._downloading = False # For tkinter after callbacks: self._afterid = {} # A message log. self._log_messages = [] self._log_indent = 0 self._log('NLTK Downloader Started!') # Create the main window. top = self.top = Tk() top.geometry('+50+50') top.title('NLTK Downloader') top.configure(background=self._BACKDROP_COLOR[1]) # Set up some bindings now, in case anything goes wrong. top.bind('', self.destroy) top.bind('', self.destroy) self._destroyed = False self._column_vars = {} # Initialize the GUI. self._init_widgets() self._init_menu() self._fill_table() self._show_info() self._select_columns() self._table.select(0) # Make sure we get notified when we're destroyed, so we can # cancel any download in progress. self._table.bind('', self._destroy) def _log(self, msg): self._log_messages.append('%s %s%s' % (time.ctime(), ' | '*self._log_indent, msg)) #///////////////////////////////////////////////////////////////// # Internals #///////////////////////////////////////////////////////////////// def _init_widgets(self): # Create the top-level frame structures f1 = Frame(self.top, relief='raised', border=2, padx=8, pady=0) f1.pack(sid='top', expand=True, fill='both') f1.grid_rowconfigure(2, weight=1) f1.grid_columnconfigure(0, weight=1) Frame(f1, height=8).grid(column=0, row=0) # spacer tabframe = Frame(f1) tabframe.grid(column=0, row=1, sticky='news') tableframe = Frame(f1) tableframe.grid(column=0, row=2, sticky='news') buttonframe = Frame(f1) buttonframe.grid(column=0, row=3, sticky='news') Frame(f1, height=8).grid(column=0, row=4) # spacer infoframe = Frame(f1) infoframe.grid(column=0, row=5, sticky='news') Frame(f1, height=8).grid(column=0, row=6) # spacer progressframe = Frame(self.top, padx=3, pady=3, background=self._BACKDROP_COLOR[1]) progressframe.pack(side='bottom', fill='x') self.top['border'] = 0 self.top['highlightthickness'] = 0 # Create the tabs self._tab_names = ['Collections', 'Corpora', 'Models', 'All Packages',] self._tabs = {} for i, tab in enumerate(self._tab_names): label = Label(tabframe, text=tab, font=self._TAB_FONT) label.pack(side='left', padx=((i+1)%2)*10) label.bind('', self._select_tab) self._tabs[tab.lower()] = label # Create the table. column_weights = [self.COLUMN_WEIGHTS.get(column, 1) for column in self.COLUMNS] self._table = Table(tableframe, self.COLUMNS, column_weights=column_weights, highlightthickness=0, listbox_height=16, reprfunc=self._table_reprfunc) self._table.columnconfig(0, foreground=self._MARK_COLOR[0]) # marked for i, column in enumerate(self.COLUMNS): width = self.COLUMN_WIDTHS.get(column, self.DEFAULT_COLUMN_WIDTH) self._table.columnconfig(i, width=width) self._table.pack(expand=True, fill='both') self._table.focus() self._table.bind_to_listboxes('', self._download) self._table.bind('', self._table_mark) self._table.bind('', self._download) self._table.bind('', self._prev_tab) self._table.bind('', self._next_tab) self._table.bind('', self._mark_all) # Create entry boxes for URL & download_dir infoframe.grid_columnconfigure(1, weight=1) info = [('url', 'Server Index:', self._set_url), ('download_dir','Download Directory:',self._set_download_dir)] self._info = {} for (i, (key, label, callback)) in enumerate(info): Label(infoframe, text=label).grid(column=0, row=i, sticky='e') entry = Entry(infoframe, font='courier', relief='groove', disabledforeground='black') self._info[key] = (entry, callback) entry.bind('', self._info_save) entry.bind('', lambda e,key=key: self._info_edit(key)) entry.grid(column=1, row=i, sticky='ew') # If the user edits url or download_dir, and then clicks outside # the entry box, then save their results. self.top.bind('', self._info_save) # Create Download & Refresh buttons. self._download_button = Button( buttonframe, text='Download', command=self._download, width=8) self._download_button.pack(side='left') self._refresh_button = Button( buttonframe, text='Refresh', command=self._refresh, width=8) self._refresh_button.pack(side='right') # Create Progress bar self._progresslabel = Label(progressframe, text='', foreground=self._BACKDROP_COLOR[0], background=self._BACKDROP_COLOR[1]) self._progressbar = Canvas(progressframe, width=200, height=16, background=self._PROGRESS_COLOR[1], relief='sunken', border=1) self._init_progressbar() self._progressbar.pack(side='right') self._progresslabel.pack(side='left') def _init_menu(self): menubar = Menu(self.top) filemenu = Menu(menubar, tearoff=0) filemenu.add_command(label='Download', underline=0, command=self._download, accelerator='Return') filemenu.add_separator() filemenu.add_command(label='Change Server Index', underline=7, command=lambda: self._info_edit('url')) filemenu.add_command(label='Change Download Directory', underline=0, command=lambda: self._info_edit('download_dir')) filemenu.add_separator() filemenu.add_command(label='Show Log', underline=5, command=self._show_log) filemenu.add_separator() filemenu.add_command(label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-x') menubar.add_cascade(label='File', underline=0, menu=filemenu) # Create a menu to control which columns of the table are # shown. n.b.: we never hide the first two columns (mark and # identifier). viewmenu = Menu(menubar, tearoff=0) for column in self._table.column_names[2:]: var = IntVar(self.top) assert column not in self._column_vars self._column_vars[column] = var if column in self.INITIAL_COLUMNS: var.set(1) viewmenu.add_checkbutton(label=column, underline=0, variable=var, command=self._select_columns) menubar.add_cascade(label='View', underline=0, menu=viewmenu) # Create a sort menu # [xx] this should be selectbuttons; and it should include # reversed sorts as options. sortmenu = Menu(menubar, tearoff=0) for column in self._table.column_names[1:]: sortmenu.add_command(label='Sort by %s' % column, command=(lambda c=column: self._table.sort_by(c, 'ascending'))) sortmenu.add_separator() #sortmenu.add_command(label='Descending Sort:') for column in self._table.column_names[1:]: sortmenu.add_command(label='Reverse sort by %s' % column, command=(lambda c=column: self._table.sort_by(c, 'descending'))) menubar.add_cascade(label='Sort', underline=0, menu=sortmenu) helpmenu = Menu(menubar, tearoff=0) helpmenu.add_command(label='About', underline=0, command=self.about) helpmenu.add_command(label='Instructions', underline=0, command=self.help, accelerator='F1') menubar.add_cascade(label='Help', underline=0, menu=helpmenu) self.top.bind('', self.help) self.top.config(menu=menubar) def _select_columns(self): for (column, var) in self._column_vars.items(): if var.get(): self._table.show_column(column) else: self._table.hide_column(column) def _refresh(self): self._ds.clear_status_cache() self._fill_table() self._table.select(0) def _info_edit(self, info_key): self._info_save() # just in case. (entry, callback) = self._info[info_key] entry['state'] = 'normal' entry['relief'] = 'sunken' entry.focus() def _info_save(self, e=None): focus = self._table for entry, callback in self._info.values(): if entry['state'] == 'disabled': continue if e is not None and e.widget is entry and e.keysym != 'Return': focus = entry else: entry['state'] = 'disabled' entry['relief'] = 'groove' callback(entry.get()) focus.focus() def _table_reprfunc(self, row, col, val): if self._table.column_names[col].endswith('Size'): if isinstance(val, basestring): return ' %s' % val elif val < 1024**2: return ' %.1f KB' % (val/1024.**1) elif val < 1024**3: return ' %.1f MB' % (val/1024.**2) else: return ' %.1f GB' % (val/1024.**3) if col in (0, ''): return str(val) else: return ' %s' % val def _set_url(self, url): if url == self._ds.url: return try: self._ds.url = url self._fill_table() except IOError, e: showerror('Error Setting Server Index', str(e)) self._show_info() def _set_download_dir(self, download_dir): if self._ds.download_dir == download_dir: return # check if the dir exists, and if not, ask if we should create it? # Clear our status cache, & re-check what's installed self._ds.download_dir = download_dir self._fill_table() self._show_info() def _show_info(self): print 'showing info', self._ds.url for entry,cb in self._info.values(): entry['state'] = 'normal' entry.delete(0, 'end') self._info['url'][0].insert(0, self._ds.url) self._info['download_dir'][0].insert(0, self._ds.download_dir) for entry,cb in self._info.values(): entry['state'] = 'disabled' def _prev_tab(self, *e): for i, tab in enumerate(self._tab_names): if tab.lower() == self._tab and i > 0: self._tab = self._tab_names[i-1].lower() return self._fill_table() def _next_tab(self, *e): for i, tab in enumerate(self._tab_names): if tab.lower() == self._tab and i < (len(self._tabs)-1): self._tab = self._tab_names[i+1].lower() return self._fill_table() def _select_tab(self, event): self._tab = event.widget['text'].lower() self._fill_table() _tab = 'collections' #_tab = 'corpora' _rows = None def _fill_table(self): selected_row = self._table.selected_row() self._table.clear() if self._tab == 'all packages': items = self._ds.packages() elif self._tab == 'corpora': items = self._ds.corpora() elif self._tab == 'models': items = self._ds.models() elif self._tab == 'collections': items = self._ds.collections() else: assert 0, 'bad tab value %r' % self._tab rows = [self._package_to_columns(item) for item in items] self._table.extend(rows) # Highlight the active tab. for tab, label in self._tabs.items(): if tab == self._tab: label.configure(foreground=self._FRONT_TAB_COLOR[0], background=self._FRONT_TAB_COLOR[1]) else: label.configure(foreground=self._BACK_TAB_COLOR[0], background=self._BACK_TAB_COLOR[1]) self._table.sort_by('Identifier', order='ascending') self._color_table() self._table.select(selected_row) # This is a hack, because the scrollbar isn't updating its # position right -- I'm not sure what the underlying cause is # though. (This is on OS X w/ python 2.5) The length of # delay that's necessary seems to depend on how fast the # comptuer is. :-/ self.top.after(150, self._table._scrollbar.set, *self._table._mlb.yview()) self.top.after(300, self._table._scrollbar.set, *self._table._mlb.yview()) def _update_table_status(self): for row_num in range(len(self._table)): status = self._ds.status(self._table[row_num, 'Identifier']) self._table[row_num, 'Status'] = status self._color_table() def _download(self, *e): # If we're using threads, then delegate to the threaded # downloader instead. if self._use_threads: return self._download_threaded(*e) marked = [self._table[row, 'Identifier'] for row in range(len(self._table)) if self._table[row, 0] != ''] selection = self._table.selected_row() if not marked and selection is not None: marked = [self._table[selection, 'Identifier']] download_iter = self._ds.incr_download(marked, self._ds.download_dir) self._log_indent = 0 self._download_cb(download_iter, marked) _DL_DELAY=10 def _download_cb(self, download_iter, ids): try: msg = download_iter.next() except StopIteration: #self._fill_table(sort=False) self._update_table_status() afterid = self.top.after(10, self._show_progress, 0) self._afterid['_download_cb'] = afterid return def show(s): self._progresslabel['text'] = s self._log(s) if isinstance(msg, ProgressMessage): self._show_progress(msg.progress) elif isinstance(msg, ErrorMessage): show(msg.message) if msg.package is not None: self._select(msg.package.id) self._show_progress(None) return # halt progress. elif isinstance(msg, StartCollectionMessage): show('Downloading collection %r' % msg.collection.id) self._log_indent += 1 elif isinstance(msg, StartPackageMessage): show('Downloading package %r' % msg.package.id) elif isinstance(msg, UpToDateMessage): show('Package %s is up-to-date!' % msg.package.id) #elif isinstance(msg, StaleMessage): # show('Package %s is out-of-date or corrupt' % msg.package.id) elif isinstance(msg, FinishDownloadMessage): show('Finished downloading %r.' % msg.package.id) elif isinstance(msg, StartUnzipMessage): show('Unzipping %s' % msg.package.filename) elif isinstance(msg, FinishCollectionMessage): self._log_indent -= 1 show('Finished downloading collection %r.' % msg.collection.id) self._clear_mark(msg.collection.id) elif isinstance(msg, FinishPackageMessage): self._clear_mark(msg.package.id) afterid = self.top.after(self._DL_DELAY, self._download_cb, download_iter, ids) self._afterid['_download_cb'] = afterid def _select(self, id): for row in range(len(self._table)): if self._table[row, 'Identifier'] == id: self._table.select(row) return def _color_table(self): # Color rows according to status. for row in range(len(self._table)): bg, sbg = self._ROW_COLOR[self._table[row, 'Status']] fg, sfg = ('black', 'white') self._table.rowconfig(row, foreground=fg, selectforeground=sfg, background=bg, selectbackground=sbg) # Color the marked column self._table.itemconfigure(row, 0, foreground=self._MARK_COLOR[0], background=self._MARK_COLOR[1]) def _clear_mark(self, id): for row in range(len(self._table)): if self._table[row, 'Identifier'] == id: self._table[row, 0] = '' def _mark_all(self, *e): for row in range(len(self._table)): self._table[row,0] = 'X' def _table_mark(self, *e): selection = self._table.selected_row() if selection >= 0: if self._table[selection][0] != '': self._table[selection,0] = '' else: self._table[selection,0] = 'X' self._table.select(delta=1) def _show_log(self): text = '\n'.join(self._log_messages) ShowText(self.top, 'NLTK Downloader Log', text) def _package_to_columns(self, pkg): """ Given a package, return a list of values describing that package, one for each column in L{self.COLUMNS}. """ row = [] for column_index, column_name in enumerate(self.COLUMNS): if column_index == 0: # Mark: row.append('') elif column_name == 'Identifier': row.append(pkg.id) elif column_name == 'Status': row.append(self._ds.status(pkg)) else: attr = column_name.lower().replace(' ', '_') row.append(getattr(pkg, attr, 'n/a')) return row #///////////////////////////////////////////////////////////////// # External Interface #///////////////////////////////////////////////////////////////// def destroy(self, *e): if self._destroyed: return self.top.destroy() self._destroyed = True def _destroy(self, *e): if self.top is not None: for afterid in self._afterid.values(): self.top.after_cancel(afterid) # Abort any download in progress. if self._downloading and self._use_threads: self._abort_download() # Make sure the garbage collector destroys these now; # otherwise, they may get destroyed when we're not in the main # thread, which would make Tkinter unhappy. self._column_vars.clear() def mainloop(self, *args, **kwargs): self.top.mainloop(*args, **kwargs) #///////////////////////////////////////////////////////////////// # HELP #///////////////////////////////////////////////////////////////// HELP = textwrap.dedent("""\ This tool can be used to download a variety of corpora and models that can be used with NLTK. Each corpus or model is distributed in a single zip file, known as a \"package file.\" You can download packages individually, or you can download pre-defined collections of packages. When you download a package, it will be saved to the \"download directory.\" A default download directory is chosen when you run the downloader; but you may also select a different download directory. On Windows, the default download directory is \"package.\" The NLTK downloader can be used to download a variety of corpora, models, and other data packages. Keyboard shortcuts:: [return]\t Download [up]\t Select previous package [down]\t Select next package [left]\t Select previous tab [right]\t Select next tab """) def help(self, *e): # The default font's not very legible; try using 'fixed' instead. try: ShowText(self.top, 'Help: NLTK Dowloader', self.HELP.strip(), width=75, font='fixed') except: ShowText(self.top, 'Help: NLTK Downloader', self.HELP.strip(), width=75) def about(self, *e): ABOUT = ("NLTK Downloader\n"+ "Written by Edward Loper") TITLE = 'About: NLTK Downloader' try: from tkMessageBox import Message Message(message=ABOUT, title=TITLE).show() except ImportError: ShowText(self._top, TITLE, ABOUT) #///////////////////////////////////////////////////////////////// # Progress Bar #///////////////////////////////////////////////////////////////// _gradient_width = 5 def _init_progressbar(self): c = self._progressbar width, height = int(c['width']), int(c['height']) for i in range(0, (int(c['width'])*2)/self._gradient_width): c.create_line(i*self._gradient_width+20, -20, i*self._gradient_width-height-20, height+20, width=self._gradient_width, fill='#%02x0000' % (80 + abs(i%6-3)*12)) c.addtag_all('gradient') c.itemconfig('gradient', state='hidden') # This is used to display progress c.addtag_withtag('redbox', c.create_rectangle( 0, 0, 0, 0, fill=self._PROGRESS_COLOR[0])) def _show_progress(self, percent): c = self._progressbar if percent is None: c.coords('redbox', 0, 0, 0, 0) c.itemconfig('gradient', state='hidden') else: width, height = int(c['width']), int(c['height']) x = percent * int(width) / 100 + 1 c.coords('redbox', 0, 0, x, height+1) def _progress_alive(self): c = self._progressbar if not self._downloading: c.itemconfig('gradient', state='hidden') else: c.itemconfig('gradient', state='normal') x1, y1, x2, y2 = c.bbox('gradient') if x1 <= -100: c.move('gradient', (self._gradient_width*6)-4, 0) else: c.move('gradient', -4, 0) afterid = self.top.after(200, self._progress_alive) self._afterid['_progress_alive'] = afterid #///////////////////////////////////////////////////////////////// # Threaded downloader #///////////////////////////////////////////////////////////////// def _download_threaded(self, *e): # If the user tries to start a new download while we're already # downloading something, then abort the current download instead. if self._downloading: self._abort_download() return # Change the 'download' button to an 'abort' button. self._download_button['text'] = 'Cancel' marked = [self._table[row, 'Identifier'] for row in range(len(self._table)) if self._table[row, 0] != ''] selection = self._table.selected_row() if not marked and selection is not None: marked = [self._table[selection, 'Identifier']] # Create a new data server object for the download operation, # just in case the user modifies our data server during the # download (e.g., clicking 'refresh' or editing the index url). ds = Downloader(self._ds.url, self._ds.download_dir) # Start downloading in a seperate thread. assert self._download_msg_queue == [] assert self._download_abort_queue == [] self._DownloadThread(ds, marked, self._download_lock, self._download_msg_queue, self._download_abort_queue).start() # Monitor the download message queue & display its progress. self._log_indent = 0 self._downloading = True self._monitor_message_queue() # Display an indication that we're still alive and well by # cycling the progress bar. self._progress_alive() def _abort_download(self): if self._downloading: self._download_lock.acquire() self._download_abort_queue.append('abort') self._download_lock.release() class _DownloadThread(threading.Thread): def __init__(self, data_server, items, lock, message_queue, abort): self.data_server = data_server self.items = items self.lock = lock self.message_queue = message_queue self.abort = abort threading.Thread.__init__(self) def run (self): for msg in self.data_server.incr_download(self.items): self.lock.acquire() self.message_queue.append(msg) # Check if we've been told to kill ourselves: if self.abort: self.message_queue.append('aborted') self.lock.release() return self.lock.release() self.lock.acquire() self.message_queue.append('finished') self.lock.release() _MONITOR_QUEUE_DELAY=100 def _monitor_message_queue(self): def show(s): self._progresslabel['text'] = s self._log(s) # Try to acquire the lock; if it's busy, then just try again later. if not self._download_lock.acquire(): return for msg in self._download_msg_queue: # Done downloading? if msg == 'finished' or msg == 'aborted': #self._fill_table(sort=False) self._update_table_status() self._downloading = False self._download_button['text'] = 'Download' del self._download_msg_queue[:] del self._download_abort_queue[:] self._download_lock.release() if msg == 'aborted': show('Download aborted!') self._show_progress(None) else: afterid = self.top.after(100, self._show_progress, None) self._afterid['_monitor_message_queue'] = afterid return # All other messages elif isinstance(msg, ProgressMessage): self._show_progress(msg.progress) elif isinstance(msg, ErrorMessage): show(msg.message) if msg.package is not None: self._select(msg.package.id) self._show_progress(None) self._downloading = False return # halt progress. elif isinstance(msg, StartCollectionMessage): show('Downloading collection %r' % msg.collection.id) self._log_indent += 1 elif isinstance(msg, StartPackageMessage): self._ds.clear_status_cache(msg.package.id) show('Downloading package %r' % msg.package.id) elif isinstance(msg, UpToDateMessage): show('Package %s is up-to-date!' % msg.package.id) #elif isinstance(msg, StaleMessage): # show('Package %s is out-of-date or corrupt; updating it' % # msg.package.id) elif isinstance(msg, FinishDownloadMessage): show('Finished downloading %r.' % msg.package.id) elif isinstance(msg, StartUnzipMessage): show('Unzipping %s' % msg.package.filename) elif isinstance(msg, FinishUnzipMessage): show('Finished installing %s' % msg.package.id) elif isinstance(msg, FinishCollectionMessage): self._log_indent -= 1 show('Finished downloading collection %r.' % msg.collection.id) self._clear_mark(msg.collection.id) elif isinstance(msg, FinishPackageMessage): self._update_table_status() self._clear_mark(msg.package.id) # Let the user know when we're aborting a download (but # waiting for a good point to abort it, so we don't end up # with a partially unzipped package or anything like that). if self._download_abort_queue: self._progresslabel['text'] = 'Aborting download...' # Clear the message queue and then release the lock del self._download_msg_queue[:] self._download_lock.release() # Check the queue again after MONITOR_QUEUE_DELAY msec. afterid = self.top.after(self._MONITOR_QUEUE_DELAY, self._monitor_message_queue) self._afterid['_monitor_message_queue'] = afterid ###################################################################### # Helper Functions ###################################################################### # [xx] It may make sense to move these to nltk.internals. def md5_hexdigest(file): """ Calculate and return the MD5 checksum for a given file. C{file} may either be a filename or an open stream. """ if isinstance(file, basestring): file = open(file, 'rb') md5_digest = md5() while True: block = file.read(1024*16) # 16k blocks if not block: break md5_digest.update(block) return md5_digest.hexdigest() # change this to periodically yield progress messages? # [xx] get rid of topdir parameter -- we should be checking # this when we build the index, anyway. def unzip(filename, root, verbose=True): """ Extract the contents of the zip file C{filename} into the directory C{root}. """ for message in _unzip_iter(filename, root, verbose): if isinstance(message, ErrorMessage): raise Exception, message def _unzip_iter(filename, root, verbose=True): if verbose: sys.stdout.write('Unzipping %s' % os.path.split(filename)[1]) sys.stdout.flush() try: zf = zipfile.ZipFile(filename) except zipfile.error, e: yield ErrorMessage(filename, 'Error with downloaded zip file') return except Exception, e: yield ErrorMessage(filename, e) return # Get lists of directories & files namelist = zf.namelist() dirlist = [x for x in namelist if x.endswith('/')] filelist = [x for x in namelist if not x.endswith('/')] # Create the target directory if it doesn't exist if not os.path.exists(root): os.mkdir(root) # Create the directory structure for dirname in sorted(dirlist): pieces = dirname[:-1].split('/') for i in range(len(pieces)): dirpath = os.path.join(root, *pieces[:i+1]) if not os.path.exists(dirpath): os.mkdir(dirpath) # Extract files. for i, filename in enumerate(filelist): filepath = os.path.join(root, *filename.split('/')) out = open(filepath, 'wb') try: contents = zf.read(filename) except Exception, e: yield ErrorMessage(filename, e) return out.write(contents) out.close() if verbose and (i*10/len(filelist) > (i-1)*10/len(filelist)): sys.stdout.write('.') sys.stdout.flush() if verbose: print ###################################################################### # Index Builder ###################################################################### # This may move to a different file sometime. import subprocess, zipfile def build_index(root, base_url): """ Create a new data.xml index file, by combining the xml description files for various packages and collections. C{root} should be the path to a directory containing the package xml and zip files; and the collection xml files. The C{root} directory is expected to have the following subdirectories:: root/ packages/ .................. subdirectory for packages corpora/ ................. zip & xml files for corpora grammars/ ................ zip & xml files for grammars taggers/ ................. zip & xml files for taggers tokenizers/ .............. zip & xml files for tokenizers etc. collections/ ............... xml files for collections For each package, there should be two files: C{I{package}.zip} contains the package itself, as a compressed zip file; and C{I{package}.xml} is an xml description of the package. The zipfile C{I{package}.zip} should expand to a single subdirectory named C{I{package/}}. The base filename C{I{package}} must match the identifier given in the package's xml file. For each collection, there should be a single file C{I{collection}.zip}, describing the collection. All identifiers (for both packages and collections) must be unique. """ # Find all packages. packages = [] for pkg_xml, zf, subdir in _find_packages(os.path.join(root, 'packages')): zipstat = os.stat(zf.filename) url = '%s/%s/%s' % (base_url, subdir, os.path.split(zf.filename)[1]) unzipped_size = sum(zf_info.file_size for zf_info in zf.infolist()) # Fill in several fields of the package xml with calculated values. pkg_xml.set('unzipped_size', '%s' % unzipped_size) pkg_xml.set('size', '%s' % zipstat.st_size) pkg_xml.set('checksum', '%s' % md5_hexdigest(zf.filename)) pkg_xml.set('subdir', subdir) #pkg_xml.set('svn_revision', _svn_revision(zf.filename)) pkg_xml.set('url', url) # Record the package. packages.append(pkg_xml) # Find all collections collections = list(_find_collections(os.path.join(root, 'collections'))) # Check that all UIDs are unique uids = set() for item in packages+collections: if item.get('id') in uids: raise ValueError('Duplicate UID: %s' % item.get('id')) uids.add(item.get('id')) # Put it all together top_elt = ElementTree.Element('nltk_data') top_elt.append(ElementTree.Element('packages')) for package in packages: top_elt[0].append(package) top_elt.append(ElementTree.Element('collections')) for collection in collections: top_elt[1].append(collection) _indent_xml(top_elt) return top_elt def _indent_xml(xml, prefix=''): """ Helper for L{build_index()}: Given an XML ElementTree, modify it (and its descendents) C{text} and C{tail} attributes to generate an indented tree, where each nested element is indented by 2 spaces with respect to its parent. """ if len(xml) > 0: xml.text = (xml.text or '').strip() + '\n' + prefix + ' ' for child in xml: _indent_xml(child, prefix+' ') for child in xml[:-1]: child.tail = (child.tail or '').strip() + '\n' + prefix + ' ' xml[-1].tail = (xml[-1].tail or '').strip() + '\n' + prefix def _check_package(pkg_xml, zipfilename, zf): """ Helper for L{build_index()}: Perform some checks to make sure that the given package is consistent. """ # The filename must patch the id given in the XML file. uid = os.path.splitext(os.path.split(zipfilename)[1])[0] if pkg_xml.get('id') != uid: raise ValueError('package identifier mismatch (%s vs %s)' % (pkg_xml.get('id'), uid)) # Zip file must expand to a subdir whose name matches uid. if sum( (name!=uid and not name.startswith(uid+'/')) for name in zf.namelist() ): raise ValueError('Zipfile %s.zip does not expand to a single ' 'subdirectory %s/' % (uid, uid)) def _svn_revision(filename): """ Helper for L{build_index()}: Calculate the subversion revision number for a given file (by using C{subprocess} to run C{svn}). """ p = subprocess.Popen(['svn', 'status', '-v', filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE) (stdout, stderr) = p.communicate() if p.returncode != 0 or stderr or not stdout: raise ValueError('Error determining svn_revision for %s: %s' % (os.path.split(filename)[1], textwrap.fill(stderr))) return stdout.split()[2] def _find_collections(root): """ Helper for L{build_index()}: Yield a list of ElementTree.Element objects, each holding the xml for a single package collection. """ packages = [] for dirname, subdirs, files in os.walk(root): for filename in files: if filename.endswith('.xml'): xmlfile = os.path.join(dirname, filename) yield ElementTree.parse(xmlfile).getroot() def _find_packages(root): """ Helper for L{build_index()}: Yield a list of tuples C{(pkg_xml, zf, subdir)}, where: - C{pkg_xml} is an ElementTree.Element holding the xml for a package - C{zf} is a zipfile.ZipFile for the package's contents. - C{subdir} is the subdirectory (relative to C{root}) where the package was found (e.g. 'corpora' or 'grammars'). """ from nltk.corpus.reader.util import _path_from # Find all packages. packages = [] for dirname, subdirs, files in os.walk(root): relpath = '/'.join(_path_from(root, dirname)) for filename in files: if filename.endswith('.xml'): xmlfilename = os.path.join(dirname, filename) zipfilename = xmlfilename[:-4]+'.zip' try: zf = zipfile.ZipFile(zipfilename) except Exception, e: raise ValueError('Error reading file %r!\n%s' % (zipfilename, e)) try: pkg_xml = ElementTree.parse(xmlfilename).getroot() except Exception, e: raise ValueError('Error reading file %r!\n%s' % (xmlfilename, e)) # Check that the UID matches the filename uid = os.path.split(xmlfilename[:-4])[1] if pkg_xml.get('id') != uid: raise ValueError('package identifier mismatch (%s ' 'vs %s)' % (pkg_xml.get('id'), uid)) # Check that the zipfile expands to a subdir whose # name matches the uid. if sum( (name!=uid and not name.startswith(uid+'/')) for name in zf.namelist() ): raise ValueError('Zipfile %s.zip does not expand to a ' 'single subdirectory %s/' % (uid, uid)) yield pkg_xml, zf, relpath # Don't recurse into svn subdirectories: try: subdirs.remove('.svn') except ValueError: pass ###################################################################### # Main: ###################################################################### # There should be a command-line interface # Aliases _downloader = Downloader() download = _downloader.download def download_shell(): DownloaderShell(_downloader).run() def download_gui(): DownloaderGUI(_downloader).mainloop() def update(): _downloader.update() if __name__ == '__main__': from optparse import OptionParser parser = OptionParser() parser.add_option("-d", "--dir", dest="dir", help="download package to directory DIR", metavar="DIR") parser.add_option("-q", "--quiet", dest="quiet", action="store_true", default=False, help="work quietly") parser.add_option("-f", "--force", dest="force", action="store_true", default=False, help="download even if already installed") parser.add_option("-e", "--exit-on-error", dest="halt_on_error", action="store_true", default=False, help="exit if an error occurs") (options, args) = parser.parse_args() if args: for pkg_id in args: rv = download(info_or_id=pkg_id, download_dir=options.dir, quiet=options.quiet, force=options.force, halt_on_error=options.halt_on_error) if rv==False and options.halt_on_error: break else: download(download_dir=options.dir, quiet=options.quiet, force=options.force, halt_on_error=options.halt_on_error) nltk-2.0~b9/nltk/decorators.py0000644000175000017500000001622411140171432016244 0ustar bhavanibhavani""" Decorator module by Michele Simionato Copyright Michele Simionato, distributed under the terms of the BSD License (see below). http://www.phyast.pitt.edu/~micheles/python/documentation.html Included in NLTK for its support of a nice memoization decorator. """ __docformat__ = 'restructuredtext en' ## The basic trick is to generate the source code for the decorated function ## with the right signature and to evaluate it. ## Uncomment the statement 'print >> sys.stderr, func_src' in _decorator ## to understand what is going on. __all__ = ["decorator", "new_wrapper", "getinfo"] import inspect, sys try: set except NameError: from sets import Set as set def getinfo(func): """ Returns an info dictionary containing: - name (the name of the function : str) - argnames (the names of the arguments : list) - defaults (the values of the default arguments : tuple) - signature (the signature : str) - doc (the docstring : str) - module (the module name : str) - dict (the function __dict__ : str) >>> def f(self, x=1, y=2, *args, **kw): pass >>> info = getinfo(f) >>> info["name"] 'f' >>> info["argnames"] ['self', 'x', 'y', 'args', 'kw'] >>> info["defaults"] (1, 2) >>> info["signature"] 'self, x, y, *args, **kw' """ assert inspect.ismethod(func) or inspect.isfunction(func) regargs, varargs, varkwargs, defaults = inspect.getargspec(func) argnames = list(regargs) if varargs: argnames.append(varargs) if varkwargs: argnames.append(varkwargs) signature = inspect.formatargspec(regargs, varargs, varkwargs, defaults, formatvalue=lambda value: "")[1:-1] return dict(name=func.__name__, argnames=argnames, signature=signature, defaults = func.func_defaults, doc=func.__doc__, module=func.__module__, dict=func.__dict__, globals=func.func_globals, closure=func.func_closure) # akin to functools.update_wrapper def update_wrapper(wrapper, model, infodict=None): infodict = infodict or getinfo(model) try: wrapper.__name__ = infodict['name'] except: # Python version < 2.4 pass wrapper.__doc__ = infodict['doc'] wrapper.__module__ = infodict['module'] wrapper.__dict__.update(infodict['dict']) wrapper.func_defaults = infodict['defaults'] wrapper.undecorated = model return wrapper def new_wrapper(wrapper, model): """ An improvement over functools.update_wrapper. The wrapper is a generic callable object. It works by generating a copy of the wrapper with the right signature and by updating the copy, not the original. Moreovoer, 'model' can be a dictionary with keys 'name', 'doc', 'module', 'dict', 'defaults'. """ if isinstance(model, dict): infodict = model else: # assume model is a function infodict = getinfo(model) assert not '_wrapper_' in infodict["argnames"], ( '"_wrapper_" is a reserved argument name!') src = "lambda %(signature)s: _wrapper_(%(signature)s)" % infodict funcopy = eval(src, dict(_wrapper_=wrapper)) return update_wrapper(funcopy, model, infodict) # helper used in decorator_factory def __call__(self, func): return new_wrapper(lambda *a, **k : self.call(func, *a, **k), func) def decorator_factory(cls): """ Take a class with a ``.caller`` method and return a callable decorator object. It works by adding a suitable __call__ method to the class; it raises a TypeError if the class already has a nontrivial __call__ method. """ attrs = set(dir(cls)) if '__call__' in attrs: raise TypeError('You cannot decorate a class with a nontrivial ' '__call__ method') if 'call' not in attrs: raise TypeError('You cannot decorate a class without a ' '.call method') cls.__call__ = __call__ return cls def decorator(caller): """ General purpose decorator factory: takes a caller function as input and returns a decorator with the same attributes. A caller function is any function like this:: def caller(func, *args, **kw): # do something return func(*args, **kw) Here is an example of usage: >>> @decorator ... def chatty(f, *args, **kw): ... print "Calling %r" % f.__name__ ... return f(*args, **kw) >>> chatty.__name__ 'chatty' >>> @chatty ... def f(): pass ... >>> f() Calling 'f' decorator can also take in input a class with a .caller method; in this case it converts the class into a factory of callable decorator objects. See the documentation for an example. """ if inspect.isclass(caller): return decorator_factory(caller) def _decorator(func): # the real meat is here infodict = getinfo(func) argnames = infodict['argnames'] assert not ('_call_' in argnames or '_func_' in argnames), ( 'You cannot use _call_ or _func_ as argument names!') src = "lambda %(signature)s: _call_(_func_, %(signature)s)" % infodict # import sys; print >> sys.stderr, src # for debugging purposes dec_func = eval(src, dict(_func_=func, _call_=caller)) return update_wrapper(dec_func, func, infodict) return update_wrapper(_decorator, caller) def getattr_(obj, name, default_thunk): "Similar to .setdefault in dictionaries." try: return getattr(obj, name) except AttributeError: default = default_thunk() setattr(obj, name, default) return default @decorator def memoize(func, *args): dic = getattr_(func, "memoize_dic", dict) # memoize_dic is created at the first call if args in dic: return dic[args] else: result = func(*args) dic[args] = result return result if __name__ == "__main__": import doctest; doctest.testmod() ########################## LEGALESE ############################### ## Redistributions of source code must retain the above copyright ## notice, this list of conditions and the following disclaimer. ## Redistributions in bytecode form must reproduce the above copyright ## notice, this list of conditions and the following disclaimer in ## the documentation and/or other materials provided with the ## distribution. ## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ## HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, ## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, ## BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS ## OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR ## TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE ## USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH ## DAMAGE. nltk-2.0~b9/nltk/data.py0000644000175000017500000013000611372151101015001 0ustar bhavanibhavani# Natural Language Toolkit: Utility functions # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT """ Functions to find and load NLTK X{resource files}, such as corpora, grammars, and saved processing objects. Resource files are identified using URLs, such as"C{nltk:corpora/abc/rural.txt}" or "C{http://nltk.org/sample/toy.cfg}". The following URL protocols are supported: - "C{file:I{path}}": Specifies the file whose path is C{I{path}}. Both relative and absolute paths may be used. - "C{http://I{host}/{path}}": Specifies the file stored on the web server C{I{host}} at path C{I{path}}. - "C{nltk:I{path}}": Specifies the file stored in the NLTK data package at C{I{path}}. NLTK will search for these files in the directories specified by L{nltk.data.path}. If no protocol is specified, then the default protocol "C{nltk:}" will be used. This module provides to functions that can be used to access a resource file, given its URL: L{load()} loads a given resource, and adds it to a resource cache; and L{retrieve()} copies a given resource to a local file. """ import sys import os, os.path import textwrap import weakref import yaml import re import urllib2 import zipfile import codecs from gzip import GzipFile, READ as GZ_READ, WRITE as GZ_WRITE try: from zlib import Z_SYNC_FLUSH as FLUSH except: from zlib import Z_FINISH as FLUSH try: import cPickle as pickle except: import pickle try: from cStringIO import StringIO except: from StringIO import StringIO import nltk ###################################################################### # Search Path ###################################################################### path = [] """A list of directories where the NLTK data package might reside. These directories will be checked in order when looking for a resource in the data package. Note that this allows users to substitute in their own versions of resources, if they have them (e.g., in their home directory under ~/nltk_data).""" # User-specified locations: path += [d for d in os.environ.get('NLTK_DATA', '').split(os.pathsep) if d] if os.path.expanduser('~/') != '~/': path += [ os.path.expanduser('~/nltk_data')] # Common locations on Windows: if sys.platform.startswith('win'): path += [ r'C:\nltk_data', r'D:\nltk_data', r'E:\nltk_data', os.path.join(sys.prefix, 'nltk_data'), os.path.join(sys.prefix, 'lib', 'nltk_data'), os.path.join(os.environ.get('APPDATA', 'C:\\'), 'nltk_data')] # Common locations on UNIX & OS X: else: path += [ '/usr/share/nltk_data', '/usr/local/share/nltk_data', '/usr/lib/nltk_data', '/usr/local/lib/nltk_data'] ###################################################################### # Path Pointers ###################################################################### class PathPointer(object): """ An abstract base class for 'path pointers,' used by NLTK's data package to identify specific paths. Two subclasses exist: L{FileSystemPathPointer} identifies a file that can be accessed directly via a given absolute path. L{ZipFilePathPointer} identifies a file contained within a zipfile, that can be accessed by reading that zipfile. """ def open(self, encoding=None): """ Return a seekable read-only stream that can be used to read the contents of the file identified by this path pointer. @raise IOError: If the path specified by this pointer does not contain a readable file. """ raise NotImplementedError('abstract base class') def file_size(self): """ Return the size of the file pointed to by this path pointer, in bytes. @raise IOError: If the path specified by this pointer does not contain a readable file. """ raise NotImplementedError('abstract base class') def join(self, fileid): """ Return a new path pointer formed by starting at the path identified by this pointer, and then following the relative path given by C{fileid}. The path components of C{fileid} should be seperated by forward slashes (C{/}), regardless of the underlying file system's path seperator character. """ raise NotImplementedError('abstract base class') class FileSystemPathPointer(PathPointer, str): """ A path pointer that identifies a file which can be accessed directly via a given absolute path. C{FileSystemPathPointer} is a subclass of C{str} for backwards compatibility purposes -- this allows old code that expected C{nltk.data.find()} to expect a string to usually work (assuming the resource is not found in a zipfile). It also permits open() to work on a FileSystemPathPointer. """ def __init__(self, path): """ Create a new path pointer for the given absolute path. @raise IOError: If the given path does not exist. """ path = os.path.abspath(path) if not os.path.exists(path): raise IOError('No such file or directory: %r' % path) self._path = path # There's no need to call str.__init__(), since it's a no-op; # str does all of its setup work in __new__. path = property(lambda self: self._path, doc=""" The absolute path identified by this path pointer.""") def open(self, encoding=None): stream = open(self._path, 'rb') if encoding is not None: stream = SeekableUnicodeStreamReader(stream, encoding) return stream def file_size(self): return os.stat(self._path).st_size def join(self, fileid): path = os.path.join(self._path, *fileid.split('/')) return FileSystemPathPointer(path) def __repr__(self): return 'FileSystemPathPointer(%r)' % self._path def __str__(self): return self._path class BufferedGzipFile(GzipFile): """ A C{GzipFile} subclass that buffers calls to L{read()} and L{write()}. This allows faster reads and writes of data to and from gzip-compressed files at the cost of using more memory. The default buffer size is 2mb. C{BufferedGzipFile} is useful for loading large gzipped pickle objects as well as writing large encoded feature files for classifier training. """ SIZE = 2 * 2**20 def __init__(self, filename=None, mode=None, compresslevel=9, fileobj=None, **kwargs): """ @return: a buffered gzip file object @rtype: C{BufferedGzipFile} @param filename: a filesystem path @type filename: C{str} @param mode: a file mode which can be any of 'r', 'rb', 'a', 'ab', 'w', or 'wb' @type mode: C{str} @param compresslevel: The compresslevel argument is an integer from 1 to 9 controlling the level of compression; 1 is fastest and produces the least compression, and 9 is slowest and produces the most compression. The default is 9. @type compresslevel: C{int} @param fileobj: a StringIO stream to read from instead of a file. @type fileobj: C{StringIO} @kwparam size: number of bytes to buffer during calls to L{read()} and L{write()} @type size: C{int} """ GzipFile.__init__(self, filename, mode, compresslevel, fileobj) self._size = kwargs.get('size', self.SIZE) self._buffer = StringIO() # cStringIO does not support len. self._len = 0 def _reset_buffer(self): # For some reason calling StringIO.truncate() here will lead to # inconsistent writes so just set _buffer to a new StringIO object. self._buffer = StringIO() self._len = 0 def _write_buffer(self, data): # Simply write to the buffer and increment the buffer size. if data is not None: self._buffer.write(data) self._len += len(data) def _write_gzip(self, data): # Write the current buffer to the GzipFile. GzipFile.write(self, self._buffer.getvalue()) # Then reset the buffer and write the new data to the buffer. self._reset_buffer() self._write_buffer(data) def close(self): # GzipFile.close() doesn't actuallly close anything. if self.mode == GZ_WRITE: self._write_gzip(None) self._reset_buffer() return GzipFile.close(self) def flush(self, lib_mode=FLUSH): self._buffer.flush() GzipFile.flush(self, lib_mode) def read(self, size=None): if not size: size = self._size contents = StringIO() while True: blocks = GzipFile.read(self, size) if not blocks: contents.flush() break contents.write(blocks) return contents.getvalue() else: return GzipFile.read(self, size) def write(self, data, size=-1): """ @param data: C{str} to write to file or buffer @type data: C{str} @param size: buffer at least size bytes before writing to file @type size: C{int} """ if not size: size = self._size if self._len + len(data) <= size: self._write_buffer(data) else: self._write_gzip(data) class GzipFileSystemPathPointer(FileSystemPathPointer): """ A subclass of C{FileSystemPathPointer} that identifies a gzip-compressed file located at a given absolute path. C{GzipFileSystemPathPointer} is appropriate for loading large gzip-compressed pickle objects efficiently. """ def open(self, encoding=None): stream = BufferedGzipFile(self._path, 'rb') if encoding: stream = SeekableUnicodeStreamReader(stream, encoding) return stream class ZipFilePathPointer(PathPointer): """ A path pointer that identifies a file contained within a zipfile, which can be accessed by reading that zipfile. """ def __init__(self, zipfile, entry=''): """ Create a new path pointer pointing at the specified entry in the given zipfile. @raise IOError: If the given zipfile does not exist, or if it does not contain the specified entry. """ if isinstance(zipfile, basestring): zipfile = OpenOnDemandZipFile(os.path.abspath(zipfile)) # Normalize the entry string: entry = re.sub('(^|/)/+', r'\1', entry) # Check that the entry exists: if entry: try: zipfile.getinfo(entry) except: # Sometimes directories aren't explicitly listed in # the zip file. So if `entry` is a directory name, # then check if the zipfile contains any files that # are under the given directory. if (entry.endswith('/') and [n for n in zipfile.namelist() if n.startswith(entry)]): pass # zipfile contains a file in that directory. else: # Otherwise, complain. raise IOError('Zipfile %r does not contain %r' % (zipfile.filename, entry)) self._zipfile = zipfile self._entry = entry zipfile = property(lambda self: self._zipfile, doc=""" The C{zipfile.ZipFile} object used to access the zip file containing the entry identified by this path pointer.""") entry = property(lambda self: self._entry, doc=""" The name of the file within C{zipfile} that this path pointer points to.""") def open(self, encoding=None): data = self._zipfile.read(self._entry) stream = StringIO(data) if self._entry.endswith('.gz'): stream = BufferedGzipFile(self._entry, fileobj=stream) elif encoding is not None: stream = SeekableUnicodeStreamReader(stream, encoding) return stream def file_size(self): return self._zipfile.getinfo(self._entry).file_size def join(self, fileid): entry = '%s/%s' % (self._entry, fileid) return ZipFilePathPointer(self._zipfile, entry) def __repr__(self): return 'ZipFilePathPointer(%r, %r)' % ( self._zipfile.filename, self._entry) ###################################################################### # Access Functions ###################################################################### # Don't use a weak dictionary, because in the common case this # causes a lot more reloading that necessary. _resource_cache = {} """A dictionary used to cache resources so that they won't need to be loaded more than once.""" def find(resource_name): """ Find the given resource by searching through the directories and zip files in L{nltk.data.path}, and return a corresponding path name. If the given resource is not found, raise a C{LookupError}, whose message gives a pointer to the installation instructions for the NLTK downloader. Zip File Handling: - If C{resource_name} contains a component with a C{.zip} extension, then it is assumed to be a zipfile; and the remaining path components are used to look inside the zipfile. - If any element of C{nltk.data.path} has a C{.zip} extension, then it is assumed to be a zipfile. - If a given resource name that does not contain any zipfile component is not found initially, then C{find()} will make a second attempt to find that resource, by replacing each component I{p} in the path with I{p.zip/p}. For example, this allows C{find()} to map the resource name C{corpora/chat80/cities.pl} to a zip file path pointer to C{corpora/chat80.zip/chat80/cities.pl}. - When using C{find()} to locate a directory contained in a zipfile, the resource name I{must} end with the C{'/'} character. Otherwise, C{find()} will not locate the directory. @type resource_name: C{str} @param resource_name: The name of the resource to search for. Resource names are posix-style relative path names, such as C{'corpora/brown'}. In particular, directory names should always be separated by the C{'/'} character, which will be automatically converted to a platform-appropriate path separator. @rtype: C{str} """ # Check if the resource name includes a zipfile name m = re.match('(.*\.zip)/?(.*)$|', resource_name) zipfile, zipentry = m.groups() # Check each item in our path for path_item in path: # Is the path item a zipfile? if os.path.isfile(path_item) and path_item.endswith('.zip'): try: return ZipFilePathPointer(path_item, resource_name) except IOError: continue # resource not in zipfile # Is the path item a directory? elif os.path.isdir(path_item): if zipfile is None: p = os.path.join(path_item, *resource_name.split('/')) if os.path.exists(p): if p.endswith('.gz'): return GzipFileSystemPathPointer(p) else: return FileSystemPathPointer(p) else: p = os.path.join(path_item, *zipfile.split('/')) if os.path.exists(p): try: return ZipFilePathPointer(p, zipentry) except IOError: continue # resource not in zipfile # Fallback: if the path doesn't include a zip file, then try # again, assuming that one of the path components is inside a # zipfile of the same name. if zipfile is None: pieces = resource_name.split('/') for i in range(len(pieces)): modified_name = '/'.join(pieces[:i]+[pieces[i]+'.zip']+pieces[i:]) try: return find(modified_name) except LookupError: pass # Display a friendly error message if the resource wasn't found: msg = textwrap.fill( 'Resource %r not found. Please use the NLTK Downloader to ' 'obtain the resource: >>> nltk.download().' % (resource_name,), initial_indent=' ', subsequent_indent=' ', width=66) msg += '\n Searched in:' + ''.join('\n - %r' % d for d in path) sep = '*'*70 resource_not_found = '\n%s\n%s\n%s' % (sep, msg, sep) raise LookupError(resource_not_found) def retrieve(resource_url, filename=None, verbose=True): """ Copy the given resource to a local file. If no filename is specified, then use the URL's filename. If there is already a file named C{filename}, then raise a C{ValueError}. @type resource_url: C{str} @param resource_url: A URL specifying where the resource should be loaded from. The default protocol is C{"nltk:"}, which searches for the file in the the NLTK data package. """ if filename is None: if resource_url.startswith('file:'): filename = os.path.split(filename)[-1] else: filename = re.sub(r'(^\w+:)?.*/', '', resource_url) if os.path.exists(filename): filename = os.path.abspath(filename) raise ValueError, "File %r already exists!" % filename if verbose: print 'Retrieving %r, saving to %r' % (resource_url, filename) # Open the input & output streams. infile = _open(resource_url) outfile = open(filename, 'wb') # Copy infile -> outfile, using 64k blocks. while True: s = infile.read(1024*64) # 64k blocks. outfile.write(s) if not s: break # Close both files. infile.close() outfile.close() #: A dictionary describing the formats that are supported by NLTK's #: L{load()} method. Keys are format names, and values are format #: descriptions. FORMATS = { 'pickle': "A serialized python object, stored using the pickle module.", 'yaml': "A serialized python object, stored using the yaml module.", 'cfg': "A context free grammar, parsed by nltk.parse_cfg().", 'pcfg': "A probabilistic CFG, parsed by nltk.parse_pcfg().", 'fcfg': "A feature CFG, parsed by nltk.parse_fcfg().", 'fol': "A list of first order logic expressions, parsed by " "nltk.sem.parse_fol() using nltk.sem.logic.LogicParser.", 'logic': "A list of first order logic expressions, parsed by " "nltk.sem.parse_logic(). Requires an additional logic_parser " "parameter", 'val': "A semantic valuation, parsed by nltk.sem.parse_valuation().", 'raw': "The raw (byte string) contents of a file.", } #: A dictionary mapping from file extensions to format names, used #: by L{load()} when C{format="auto"} to decide the format for a #: given resource url. AUTO_FORMATS = { 'pickle': 'pickle', 'yaml': 'yaml', 'cfg': 'cfg', 'pcfg': 'pcfg', 'fcfg': 'fcfg', 'fol': 'fol', 'logic': 'logic', 'val': 'val'} def load(resource_url, format='auto', cache=True, verbose=False, logic_parser=None, fstruct_parser=None): """ Load a given resource from the NLTK data package. The following resource formats are currently supported: - C{'pickle'} - C{'yaml'} - C{'cfg'} (context free grammars) - C{'pcfg'} (probabilistic CFGs) - C{'fcfg'} (feature-based CFGs) - C{'fol'} (formulas of First Order Logic) - C{'logic'} (Logical formulas to be parsed by the given logic_parser) - C{'val'} (valuation of First Order Logic model) - C{'raw'} If no format is specified, C{load()} will attempt to determine a format based on the resource name's file extension. If that fails, C{load()} will raise a C{ValueError} exception. @type resource_url: C{str} @param resource_url: A URL specifying where the resource should be loaded from. The default protocol is C{"nltk:"}, which searches for the file in the the NLTK data package. @type cache: C{bool} @param cache: If true, add this resource to a cache. If C{load} finds a resource in its cache, then it will return it from the cache rather than loading it. The cache uses weak references, so a resource wil automatically be expunged from the cache when no more objects are using it. @type verbose: C{bool} @param verbose: If true, print a message when loading a resource. Messages are not displayed when a resource is retrieved from the cache. @type logic_parser: C{LogicParser} @param logic_parser: The parser that will be used to parse logical expressions. @type fstruct_parser: C{FeatStructParser} @param fstruct_parser: The parser that will be used to parse the feature structure of an fcfg. """ # If we've cached the resource, then just return it. if cache: resource_val = _resource_cache.get(resource_url) if resource_val is not None: if verbose: print '<>' % (resource_url,) return resource_val # Let the user know what's going on. if verbose: print '<>' % (resource_url,) # Determine the format of the resource. if format == 'auto': resource_url_parts = resource_url.split('.') ext = resource_url_parts[-1] if ext == 'gz': ext = resource_url_parts[-2] format = AUTO_FORMATS.get(ext) if format is None: raise ValueError('Could not determine format for %s based ' 'on its file\nextension; use the "format" ' 'argument to specify the format explicitly.' % resource_url) # Load the resource. if format == 'pickle': resource_val = pickle.load(_open(resource_url)) elif format == 'yaml': resource_val = yaml.load(_open(resource_url)) elif format == 'cfg': resource_val = nltk.grammar.parse_cfg(_open(resource_url).read()) elif format == 'pcfg': resource_val = nltk.grammar.parse_pcfg(_open(resource_url).read()) elif format == 'fcfg': resource_val = nltk.grammar.parse_fcfg(_open(resource_url).read(), logic_parser=logic_parser, fstruct_parser=fstruct_parser) elif format == 'fol': resource_val = nltk.sem.parse_logic(_open(resource_url).read(), logic_parser=nltk.sem.logic.LogicParser()) elif format == 'logic': resource_val = nltk.sem.parse_logic(_open(resource_url).read(), logic_parser=logic_parser) elif format == 'val': resource_val = nltk.sem.parse_valuation(_open(resource_url).read()) elif format == 'raw': resource_val = _open(resource_url).read() else: assert format not in FORMATS raise ValueError('Unknown format type!') # If requested, add it to the cache. if cache: try: _resource_cache[resource_url] = resource_val except TypeError: # We can't create weak references to some object types, like # strings and tuples. For now, just don't cache them. pass return resource_val def show_cfg(resource_url, escape='##'): """ Write out a grammar file, ignoring escaped and empty lines @type resource_url: C{str} @param resource_url: A URL specifying where the resource should be loaded from. The default protocol is C{"nltk:"}, which searches for the file in the the NLTK data package. @type escape: C{str} @param escape: Prepended string that signals lines to be ignored """ resource_val = load(resource_url, format='raw', cache=False) lines = resource_val.splitlines() for l in lines: if l.startswith(escape): continue if re.match('^$', l): continue print l def clear_cache(): """ Remove all objects from the resource cache. @see: L{load()} """ _resource_cache.clear() def _open(resource_url): """ Helper function that returns an open file object for a resource, given its resource URL. If the given resource URL uses the 'ntlk' protocol, or uses no protocol, then use L{nltk.data.find} to find its path, and open it with the given mode; if the resource URL uses the 'file' protocol, then open the file with the given mode; otherwise, delegate to C{urllib2.urlopen}. @type resource_url: C{str} @param resource_url: A URL specifying where the resource should be loaded from. The default protocol is C{"nltk:"}, which searches for the file in the the NLTK data package. """ # Divide the resource name into ":". protocol, path = re.match('(?:(\w+):)?(.*)', resource_url).groups() if protocol is None or protocol.lower() == 'nltk': return find(path).open() elif protocol.lower() == 'file': # urllib might not use mode='rb', so handle this one ourselves: return open(path, 'rb') else: return urllib2.urlopen(resource_url) ###################################################################### # Lazy Resource Loader ###################################################################### class LazyLoader(object): def __init__(self, path): self.__path = path def __load(self): resource = load(self.__path) # This is where the magic happens! Transform ourselves into # the object by modifying our own __dict__ and __class__ to # match that of `resource`. self.__dict__ = resource.__dict__ self.__class__ = resource.__class__ def __getattr__(self, attr): self.__load() # This looks circular, but its not, since __load() changes our # __class__ to something new: return getattr(self, attr) def __repr__(self): self.__load() # This looks circular, but its not, since __load() changes our # __class__ to something new: return '%r' % self ###################################################################### # Open-On-Demand ZipFile ###################################################################### class OpenOnDemandZipFile(zipfile.ZipFile): """ A subclass of C{zipfile.ZipFile} that closes its file pointer whenever it is not using it; and re-opens it when it needs to read data from the zipfile. This is useful for reducing the number of open file handles when many zip files are being accessed at once. C{OpenOnDemandZipFile} must be constructed from a filename, not a file-like object (to allow re-opening). C{OpenOnDemandZipFile} is read-only (i.e., C{write} and C{writestr} are disabled. """ def __init__(self, filename): if not isinstance(filename, basestring): raise TypeError('ReopenableZipFile filename must be a string') zipfile.ZipFile.__init__(self, filename) assert self.filename == filename self.close() def read(self, name): assert self.fp is None self.fp = open(self.filename, 'rb') value = zipfile.ZipFile.read(self, name) self.close() return value def write(self, *args, **kwargs): """@raise NotImplementedError: OpenOnDemandZipfile is read-only""" raise NotImplementedError('OpenOnDemandZipfile is read-only') def writestr(self, *args, **kwargs): """@raise NotImplementedError: OpenOnDemandZipfile is read-only""" raise NotImplementedError('OpenOnDemandZipfile is read-only') def __repr__(self): return 'OpenOnDemandZipFile(%r)' % self.filename ###################################################################### #{ Seekable Unicode Stream Reader ###################################################################### class SeekableUnicodeStreamReader(object): """ A stream reader that automatically encodes the source byte stream into unicode (like C{codecs.StreamReader}); but still supports the C{seek()} and C{tell()} operations correctly. This is in contrast to C{codecs.StreamReader}, which provide *broken* C{seek()} and C{tell()} methods. This class was motivated by L{StreamBackedCorpusView}, which makes extensive use of C{seek()} and C{tell()}, and needs to be able to handle unicode-encoded files. Note: this class requires stateless decoders. To my knowledge, this shouldn't cause a problem with any of python's builtin unicode encodings. """ DEBUG = True #: If true, then perform extra sanity checks. def __init__(self, stream, encoding, errors='strict'): # Rewind the stream to its beginning. stream.seek(0) self.stream = stream """The underlying stream.""" self.encoding = encoding """The name of the encoding that should be used to encode the underlying stream.""" self.errors = errors """The error mode that should be used when decoding data from the underlying stream. Can be 'strict', 'ignore', or 'replace'.""" self.decode = codecs.getdecoder(encoding) """The function that is used to decode byte strings into unicode strings.""" self.bytebuffer = '' """A buffer to use bytes that have been read but have not yet been decoded. This is only used when the final bytes from a read do not form a complete encoding for a character.""" self.linebuffer = None """A buffer used by L{readline()} to hold characters that have been read, but have not yet been returned by L{read()} or L{readline()}. This buffer consists of a list of unicode strings, where each string corresponds to a single line. The final element of the list may or may not be a complete line. Note that the existence of a linebuffer makes the L{tell()} operation more complex, because it must backtrack to the beginning of the buffer to determine the correct file position in the underlying byte stream.""" self._rewind_checkpoint = 0 """The file position at which the most recent read on the underlying stream began. This is used, together with L{_rewind_numchars}, to backtrack to the beginning of L{linebuffer} (which is required by L{tell()}).""" self._rewind_numchars = None """The number of characters that have been returned since the read that started at L{_rewind_checkpoint}. This is used, together with L{_rewind_checkpoint}, to backtrack to the beginning of L{linebuffer} (which is required by L{tell()}).""" self._bom = self._check_bom() """The length of the byte order marker at the beginning of the stream (or C{None} for no byte order marker).""" #///////////////////////////////////////////////////////////////// # Read methods #///////////////////////////////////////////////////////////////// def read(self, size=None): """ Read up to C{size} bytes, decode them using this reader's encoding, and return the resulting unicode string. @param size: The maximum number of bytes to read. If not specified, then read as many bytes as possible. @rtype: C{unicode} """ chars = self._read(size) # If linebuffer is not empty, then include it in the result if self.linebuffer: chars = ''.join(self.linebuffer) + chars self.linebuffer = None self._rewind_numchars = None return chars def readline(self, size=None): """ Read a line of text, decode it using this reader's encoding, and return the resulting unicode string. @param size: The maximum number of bytes to read. If no newline is encountered before C{size} bytes have been read, then the returned value may not be a complete line of text. """ # If we have a non-empty linebuffer, then return the first # line from it. (Note that the last element of linebuffer may # not be a complete line; so let _read() deal with it.) if self.linebuffer and len(self.linebuffer) > 1: line = self.linebuffer.pop(0) self._rewind_numchars += len(line) return line readsize = size or 72 chars = '' # If there's a remaining incomplete line in the buffer, add it. if self.linebuffer: chars += self.linebuffer.pop() self.linebuffer = None while True: startpos = self.stream.tell() - len(self.bytebuffer) new_chars = self._read(readsize) # If we're at a '\r', then read one extra character, since # it might be a '\n', to get the proper line ending. if new_chars and new_chars.endswith('\r'): new_chars += self._read(1) chars += new_chars lines = chars.splitlines(True) if len(lines) > 1: line = lines[0] self.linebuffer = lines[1:] self._rewind_numchars = len(new_chars)-(len(chars)-len(line)) self._rewind_checkpoint = startpos break elif len(lines) == 1: line0withend = lines[0] line0withoutend = lines[0].splitlines(False)[0] if line0withend != line0withoutend: # complete line line = line0withend break if not new_chars or size is not None: line = chars break # Read successively larger blocks of text. if readsize < 8000: readsize *= 2 return line def readlines(self, sizehint=None, keepends=True): """ Read this file's contents, decode them using this reader's encoding, and return it as a list of unicode lines. @rtype: C{list} of C{unicode} @param sizehint: Ignored. @param keepends: If false, then strip newlines. """ return self.read().splitlines(keepends) def next(self): """Return the next decoded line from the underlying stream.""" line = self.readline() if line: return line else: raise StopIteration def __iter__(self): """Return self""" return self def xreadlines(self): """Return self""" return self #///////////////////////////////////////////////////////////////// # Pass-through methods & properties #///////////////////////////////////////////////////////////////// closed = property(lambda self: self.stream.closed, doc=""" True if the underlying stream is closed.""") name = property(lambda self: self.stream.name, doc=""" The name of the underlying stream.""") mode = property(lambda self: self.stream.mode, doc=""" The mode of the underlying stream.""") def close(self): """ Close the underlying stream. """ self.stream.close() #///////////////////////////////////////////////////////////////// # Seek and tell #///////////////////////////////////////////////////////////////// def seek(self, offset, whence=0): """ Move the stream to a new file position. If the reader is maintaining any buffers, tehn they will be cleared. @param offset: A byte count offset. @param whence: If C{whence} is 0, then the offset is from the start of the file (offset should be positive). If C{whence} is 1, then the offset is from the current position (offset may be positive or negative); and if 2, then the offset is from the end of the file (offset should typically be negative). """ if whence == 1: raise ValueError('Relative seek is not supported for ' 'SeekableUnicodeStreamReader -- consider ' 'using char_seek_forward() instead.') self.stream.seek(offset, whence) self.linebuffer = None self.bytebuffer = '' self._rewind_numchars = None self._rewind_checkpoint = self.stream.tell() def char_seek_forward(self, offset): """ Move the read pointer forward by C{offset} characters. """ if offset < 0: raise ValueError('Negative offsets are not supported') # Clear all buffers. self.seek(self.tell()) # Perform the seek operation. self._char_seek_forward(offset) def _char_seek_forward(self, offset, est_bytes=None): """ Move the file position forward by C{offset} characters, ignoring all buffers. @param est_bytes: A hint, giving an estimate of the number of bytes that will be neded to move foward by C{offset} chars. Defaults to C{offset}. """ if est_bytes is None: est_bytes = offset bytes = '' while True: # Read in a block of bytes. newbytes = self.stream.read(est_bytes-len(bytes)) bytes += newbytes # Decode the bytes to characters. chars, bytes_decoded = self._incr_decode(bytes) # If we got the right number of characters, then seek # backwards over any truncated characters, and return. if len(chars) == offset: self.stream.seek(-len(bytes)+bytes_decoded, 1) return # If we went too far, then we can back-up until we get it # right, using the bytes we've already read. if len(chars) > offset: while len(chars) > offset: # Assume at least one byte/char. est_bytes += offset-len(chars) chars, bytes_decoded = self._incr_decode(bytes[:est_bytes]) self.stream.seek(-len(bytes)+bytes_decoded, 1) return # Otherwise, we haven't read enough bytes yet; loop again. est_bytes += offset - len(chars) def tell(self): """ Return the current file position on the underlying byte stream. If this reader is maintaining any buffers, then the returned file position will be the position of the beginning of those buffers. """ # If nothing's buffered, then just return our current filepos: if self.linebuffer is None: return self.stream.tell() - len(self.bytebuffer) # Otherwise, we'll need to backtrack the filepos until we # reach the beginning of the buffer. # Store our original file position, so we can return here. orig_filepos = self.stream.tell() # Calculate an estimate of where we think the newline is. bytes_read = ( (orig_filepos-len(self.bytebuffer)) - self._rewind_checkpoint ) buf_size = sum([len(line) for line in self.linebuffer]) est_bytes = (bytes_read * self._rewind_numchars / (self._rewind_numchars + buf_size)) self.stream.seek(self._rewind_checkpoint) self._char_seek_forward(self._rewind_numchars, est_bytes) filepos = self.stream.tell() # Sanity check if self.DEBUG: self.stream.seek(filepos) check1 = self._incr_decode(self.stream.read(50))[0] check2 = ''.join(self.linebuffer) assert check1.startswith(check2) or check2.startswith(check1) # Return to our original filepos (so we don't have to throw # out our buffer.) self.stream.seek(orig_filepos) # Return the calculated filepos return filepos #///////////////////////////////////////////////////////////////// # Helper methods #///////////////////////////////////////////////////////////////// def _read(self, size=None): """ Read up to C{size} bytes from the underlying stream, decode them using this reader's encoding, and return the resulting unicode string. C{linebuffer} is *not* included in the result. """ if size == 0: return u'' # Skip past the byte order marker, if present. if self._bom and self.stream.tell() == 0: self.stream.read(self._bom) # Read the requested number of bytes. if size is None: new_bytes = self.stream.read() else: new_bytes = self.stream.read(size) bytes = self.bytebuffer + new_bytes # Decode the bytes into unicode characters chars, bytes_decoded = self._incr_decode(bytes) # If we got bytes but couldn't decode any, then read further. if (size is not None) and (not chars) and (len(new_bytes) > 0): while not chars: new_bytes = self.stream.read(1) if not new_bytes: break # end of file. bytes += new_bytes chars, bytes_decoded = self._incr_decode(bytes) # Record any bytes we didn't consume. self.bytebuffer = bytes[bytes_decoded:] # Return the result return chars def _incr_decode(self, bytes): """ Decode the given byte string into a unicode string, using this reader's encoding. If an exception is encountered that appears to be caused by a truncation error, then just decode the byte string without the bytes that cause the trunctaion error. @return: A tuple C{(chars, num_consumed)}, where C{chars} is the decoded unicode string, and C{num_consumed} is the number of bytes that were consumed. """ while True: try: return self.decode(bytes, 'strict') except UnicodeDecodeError, exc: # If the exception occurs at the end of the string, # then assume that it's a truncation error. if exc.end == len(bytes): return self.decode(bytes[:exc.start], self.errors) # Otherwise, if we're being strict, then raise it. elif self.errors == 'strict': raise # If we're not strcit, then re-process it with our # errors setting. This *may* raise an exception. else: return self.decode(bytes, self.errors) _BOM_TABLE = { 'utf8': [(codecs.BOM_UTF8, None)], 'utf16': [(codecs.BOM_UTF16_LE, 'utf16-le'), (codecs.BOM_UTF16_BE, 'utf16-be')], 'utf16le': [(codecs.BOM_UTF16_LE, None)], 'utf16be': [(codecs.BOM_UTF16_BE, None)], 'utf32': [(codecs.BOM_UTF32_LE, 'utf32-le'), (codecs.BOM_UTF32_BE, 'utf32-be')], 'utf32le': [(codecs.BOM_UTF32_LE, None)], 'utf32be': [(codecs.BOM_UTF32_BE, None)], } def _check_bom(self): # Normalize our encoding name enc = re.sub('[ -]', '', self.encoding.lower()) # Look up our encoding in the BOM table. bom_info = self._BOM_TABLE.get(enc) if bom_info: # Read a prefix, to check against the BOM(s) bytes = self.stream.read(16) self.stream.seek(0) # Check for each possible BOM. for (bom, new_encoding) in bom_info: if bytes.startswith(bom): if new_encoding: self.encoding = new_encoding return len(bom) return None __all__ = ['path', 'PathPointer', 'FileSystemPathPointer', 'BufferedGzipFile', 'GzipFileSystemPathPointer', 'GzipFileSystemPathPointer', 'find', 'retrieve', 'FORMATS', 'AUTO_FORMATS', 'load', 'show_cfg', 'clear_cache', 'LazyLoader', 'OpenOnDemandZipFile', 'GzipFileSystemPathPointer', 'SeekableUnicodeStreamReader'] nltk-2.0~b9/nltk/containers.py0000644000175000017500000001442111327451603016251 0ustar bhavanibhavani# Natural Language Toolkit: Miscellaneous container classes # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT class SortedDict(dict): """ A very rudamentary sorted dictionary, whose main purpose is to allow dictionaries to be displayed in a consistent order in regression tests. keys(), items(), values(), iter*(), and __repr__ all sort their return values before returning them. (note that the sort order for values() does *not* correspond to the sort order for keys(). I.e., zip(d.keys(), d.values()) is not necessarily equal to d.items(). """ def keys(self): return sorted(dict.keys(self)) def items(self): return sorted(dict.items(self)) def values(self): return sorted(dict.values(self)) def iterkeys(self): return iter(sorted(dict.keys(self))) def iteritems(self): return iter(sorted(dict.items(self))) def itervalues(self): return iter(sorted(dict.values(self))) def __iter__(self): return iter(sorted(dict.keys(self))) def repr(self): items = ['%s=%s' % t for t in sorted(self.items())] return '{%s}' % ', '.join(items) # OrderedDict: Written Doug Winter # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/438823 class OrderedDict(dict): """ This implementation of a dictionary keeps track of the order in which keys were inserted. """ def __init__(self, d={}): self._keys = d.keys() dict.__init__(self, d) def __delitem__(self, key): dict.__delitem__(self, key) self._keys.remove(key) def __setitem__(self, key, item): dict.__setitem__(self, key, item) # a peculiar sharp edge from copy.deepcopy # we'll have our set item called without __init__ if not hasattr(self, '_keys'): self._keys = [key,] if key not in self._keys: self._keys.append(key) def clear(self): dict.clear(self) self._keys = [] def items(self): for i in self._keys: yield i, self[i] def keys(self): return self._keys def popitem(self): if len(self._keys) == 0: raise KeyError('dictionary is empty') else: key = self._keys[-1] val = self[key] del self[key] return key, val def setdefault(self, key, failobj = None): dict.setdefault(self, key, failobj) if key not in self._keys: self._keys.append(key) def update(self, d): for key in d.keys(): if key not in self: self._keys.append(key) dict.update(self, d) def values(self): for i in self._keys: yield self[i] def move(self, key, index): """ Move the specified to key to *before* the specified index. """ try: cur = self._keys.index(key) except ValueError: raise KeyError(key) self._keys.insert(index, key) # this may have shifted the position of cur, if it is after index if cur >= index: cur = cur + 1 del self._keys[cur] def index(self, key): if key not in self: raise KeyError(key) return self._keys.index(key) ########################################################################## # TRIES ########################################################################## # Trie structure, by James Tauber and Leonardo Maffi (V. 1.2, July 18 2006) # Extended by Steven Bird class Trie: """A Trie is like a dictionary in that it maps keys to values. However, because of the way keys are stored, it allows look up based on the longest prefix that matches. Keys must be strings. """ def __init__(self, trie=None): if trie is None: self._root = [None, {}, 0] else: self._root = trie def clear(self): self._root = [None, {}, 0] def isleaf(self, key): """Return True if the key is present and it's a leaf of the Trie, False otherwise.""" curr_node = self._root for char in key: curr_node_1 = curr_node[1] if char in curr_node_1: curr_node = curr_node_1[char] else: return False return curr_node[0] is not None def find_prefix(self, key): """Find as much of the key as one can, by using the longest prefix that has a value. Return (value, remainder) where remainder is the rest of the given string.""" curr_node = self._root remainder = key for char in key: if char in curr_node[1]: curr_node = curr_node[1][char] else: return curr_node[0], remainder remainder = remainder[1:] return curr_node[0], remainder def subtrie(self, key): curr_node = self._root for char in key: curr_node = curr_node[1][char] return Trie(trie=curr_node) def __len__(self): return self._root[2] def __eq__(self, other): return self._root == other._root def __ne__(self, other): return not (self == other) def __setitem__(self, key, value): curr_node = self._root for char in key: curr_node[2] += 1 curr_node = curr_node[1].setdefault(char, [None, {}, 0]) curr_node[0] = value curr_node[2] += 1 def __getitem__(self, key): """Return the value for the given key if it is present, raises a KeyError if key not found, and return None if it is present a key2 that starts with key.""" curr_node = self._root for char in key: curr_node = curr_node[1][char] return curr_node[0] def __contains__(self, key): """Return True if the key is present or if it is present a key2 string that starts with key.""" curr_node = self._root for char in key: curr_node_1 = curr_node[1] if char in curr_node_1: curr_node = curr_node_1[char] else: return False return True def __str__(self): return str(self._root) def __repr__(self): return "Trie(%r)" % self._root nltk-2.0~b9/nltk/compat.py0000644000175000017500000000610311327451603015365 0ustar bhavanibhavani# Natural Language Toolkit: Compatibility Functions # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT """ Backwards compatibility with previous versions of Python. This module provides backwards compatibility by defining functions and classes that were not available in earlier versions of Python. Intented usage: >>> from nltk.compat import * Currently, NLTK requires Python 2.4 or later. """ ###################################################################### # New in Python 2.5 ###################################################################### # ElementTree try: from xml.etree import ElementTree except ImportError: from nltk.etree import ElementTree # collections.defaultdict # originally contributed by Yoav Goldberg # new version by Jason Kirtland from Python cookbook. # try: from collections import defaultdict except ImportError: class defaultdict(dict): def __init__(self, default_factory=None, *a, **kw): if (default_factory is not None and not hasattr(default_factory, '__call__')): raise TypeError('first argument must be callable') dict.__init__(self, *a, **kw) self.default_factory = default_factory def __getitem__(self, key): try: return dict.__getitem__(self, key) except KeyError: return self.__missing__(key) def __missing__(self, key): if self.default_factory is None: raise KeyError(key) self[key] = value = self.default_factory() return value def __reduce__(self): if self.default_factory is None: args = tuple() else: args = self.default_factory, return type(self), args, None, None, self.iteritems() def copy(self): return self.__copy__() def __copy__(self): return type(self)(self.default_factory, self) def __deepcopy__(self, memo): import copy return type(self)(self.default_factory, copy.deepcopy(self.items())) def __repr__(self): return 'defaultdict(%s, %s)' % (self.default_factory, dict.__repr__(self)) # [XX] to make pickle happy in python 2.4: import collections collections.defaultdict = defaultdict # all, any try: all([True]) all = all except NameError: def all(iterable): for i in iterable: if not i: return False else: return True try: any([True]) any = any except NameError: def any(iterable): for i in iterable: if i: return True else: return False __all__ = ['ElementTree', 'defaultdict', 'all', 'any'] nltk-2.0~b9/nltk/collocations.py0000644000175000017500000002353311327451603016601 0ustar bhavanibhavani# Natural Language Toolkit: Collocations and Association Measures # # Copyright (C) 2001-2010 NLTK Project # Author: Joel Nothman # URL: # For license information, see LICENSE.TXT # """ Tools to identify X{collocation}s --- words that often appear consecutively --- within corpora. They may also be used to find other X{association}s between word occurrences. See Manning and Schutze ch. 5 at http://nlp.stanford.edu/fsnlp/promo/colloc.pdf and the Text::NSP Perl package at http://ngram.sourceforge.net Finding collocations requires first calculating the frequencies of words and their appearance in the context of other words. Often the collection of words will then requiring filtering to only retain useful content terms. Each ngram of words may then be scored according to some X{association measure}, in order to determine the relative likelihood of each ngram being a collocation. The L{BigramCollocationFinder} and L{TrigramCollocationFinder} classes provide these functionalities, dependent on being provided a function which scores a ngram given appropriate frequency counts. A number of standard association measures are provided in L{bigram_measures} and L{trigram_measures}. """ # Possible TODOs: # - consider the distinction between f(x,_) and f(x) and whether our # approximation is good enough for fragmented data, and mention it # - add a n-gram collocation finder with measures which only utilise n-gram # and unigram counts (raw_freq, pmi, student_t) import itertools as _itertools from operator import itemgetter as _itemgetter from nltk.compat import any from nltk.probability import FreqDist from nltk.util import ingrams from nltk.metrics import ContingencyMeasures, BigramAssocMeasures, TrigramAssocMeasures from nltk.metrics.spearman import * class AbstractCollocationFinder(object): """ An abstract base class for X{collocation finder}s whose purpose is to collect collocation candidate frequencies, filter and rank them. """ def __init__(self, word_fd, ngram_fd): """As a minimum, collocation finders require the frequencies of each word in a corpus, and the joint frequency of word tuples. This data should be provided through L{nltk.probability.FreqDist} objects or an identical interface. """ self.word_fd = word_fd self.ngram_fd = ngram_fd @classmethod def from_documents(cls, documents): """Constructs a collocation finder given a collection of documents, each of which is a list (or iterable) of tokens. """ return cls.from_words(_itertools.chain(*documents)) @staticmethod def _ngram_freqdist(words, n): return FreqDist(tuple(words[i:i+n]) for i in range(len(words)-1)) def _apply_filter(self, fn=lambda ngram, freq: False): """Generic filter removes ngrams from the frequency distribution if the function returns True when passed an ngram tuple. """ for ngram, freq in self.ngram_fd.items(): if fn(ngram, freq): try: del self.ngram_fd[ngram] except KeyError: pass def apply_freq_filter(self, min_freq): """Removes candidate ngrams which have frequency less than min_freq.""" self._apply_filter(lambda ng, freq: freq < min_freq) def apply_ngram_filter(self, fn): """Removes candidate ngrams (w1, w2, ...) where fn(w1, w2, ...) evaluates to True. """ self._apply_filter(lambda ng, f: fn(*ng)) def apply_word_filter(self, fn): """Removes candidate ngrams (w1, w2, ...) where any of (fn(w1), fn(w2), ...) evaluates to True. """ self._apply_filter(lambda ng, f: any(fn(w) for w in ng)) def _score_ngrams(self, score_fn): """Generates of (ngram, score) pairs as determined by the scoring function provided. """ for tup in self.ngram_fd: score = self.score_ngram(score_fn, *tup) if score is not None: yield tup, score def score_ngrams(self, score_fn): """Returns a sequence of (ngram, score) pairs ordered from highest to lowest score, as determined by the scoring function provided. """ return sorted(self._score_ngrams(score_fn), key=_itemgetter(1), reverse=True) def nbest(self, score_fn, n): """Returns the top n ngrams when scored by the given function.""" return [p for p,s in self.score_ngrams(score_fn)[:n]] def above_score(self, score_fn, min_score): """Returns a sequence of ngrams, ordered by decreasing score, whose scores each exceed the given minimum score. """ for ngram, score in self.score_ngrams(score_fn): if score > min_score: yield ngram else: break class BigramCollocationFinder(AbstractCollocationFinder): """A tool for the finding and ranking of bigram collocations or other association measures. It is often useful to use from_words() rather than constructing an instance directly. """ @classmethod def from_words(cls, words, window_size=2): """Construct a BigramCollocationFinder for all bigrams in the given sequence. By default, bigrams must be contiguous. """ wfd = FreqDist() bfd = FreqDist() if window_size < 2: raise ValueError, "Specify window_size at least 2" for window in ingrams(words, window_size, pad_right=True): w1 = window[0] wfd.inc(w1) for w2 in window[1:]: if w2 is not None: bfd.inc((w1, w2)) return cls(wfd, bfd) def score_ngram(self, score_fn, w1, w2): """Returns the score for a given bigram using the given scoring function. """ n_all = self.word_fd.N() n_ii = self.ngram_fd[(w1, w2)] if not n_ii: return n_ix = self.word_fd[w1] n_xi = self.word_fd[w2] return score_fn(n_ii, (n_ix, n_xi), n_all) class TrigramCollocationFinder(AbstractCollocationFinder): """A tool for the finding and ranking of bigram collocations or other association measures. It is often useful to use from_words() rather than constructing an instance directly. """ def __init__(self, word_fd, bigram_fd, wildcard_fd, trigram_fd): """Construct a TrigramCollocationFinder, given FreqDists for appearances of words, bigrams, two words with any word between them, and trigrams. """ AbstractCollocationFinder.__init__(self, word_fd, trigram_fd) self.wildcard_fd = wildcard_fd self.bigram_fd = bigram_fd @classmethod def from_words(cls, words): """Construct a TrigramCollocationFinder for all trigrams in the given sequence. """ wfd = FreqDist() wildfd = FreqDist() bfd = FreqDist() tfd = FreqDist() for w1, w2, w3 in ingrams(words, 3, pad_right=True): wfd.inc(w1) if w2 is None: continue bfd.inc((w1, w2)) if w3 is None: continue wildfd.inc((w1, w3)) tfd.inc((w1, w2, w3)) return cls(wfd, bfd, wildfd, tfd) def bigram_finder(self): """Constructs a bigram collocation finder with the bigram and unigram data from this finder. Note that this does not include any filtering applied to this finder. """ return BigramCollocationFinder(self.word_fd, self.bigram_fd) def score_ngram(self, score_fn, w1, w2, w3): """Returns the score for a given trigram using the given scoring function. """ n_all = self.word_fd.N() n_iii = self.ngram_fd[(w1, w2, w3)] if not n_iii: return n_iix = self.bigram_fd[(w1, w2)] n_ixi = self.wildcard_fd[(w1, w3)] n_xii = self.bigram_fd[(w2, w3)] n_ixx = self.word_fd[w1] n_xix = self.word_fd[w2] n_xxi = self.word_fd[w3] return score_fn(n_iii, (n_iix, n_ixi, n_xii), (n_ixx, n_xix, n_xxi), n_all) def demo(scorer=None, compare_scorer=None): """Finds trigram collocations in the files of the WebText corpus.""" from nltk.metrics import BigramAssocMeasures, spearman_correlation, ranks_from_scores if scorer is None: scorer = BigramAssocMeasures.likelihood_ratio if compare_scorer is None: compare_scorer = BigramAssocMeasures.raw_freq from nltk import corpus ignored_words = corpus.stopwords.words('english') word_filter = lambda w: len(w) < 3 or w.lower() in ignored_words for file in corpus.webtext.files(): words = [word.lower() for word in corpus.webtext.words(file)] cf = BigramCollocationFinder.from_words(words) cf.apply_freq_filter(3) cf.apply_word_filter(word_filter) print file print '\t', [' '.join(tup) for tup in cf.nbest(scorer, 15)] print '\t Correlation to %s: %0.4f' % (compare_scorer.__name__, spearman_correlation( ranks_from_scores(cf.score_ngrams(scorer)), ranks_from_scores(cf.score_ngrams(compare_scorer)))) # Slows down loading too much # bigram_measures = BigramAssocMeasures() # trigram_measures = TrigramAssocMeasures() if __name__ == '__main__': import sys from nltk.metrics import BigramAssocMeasures try: scorer = eval('BigramAssocMeasures.' + sys.argv[1]) except IndexError: scorer = None try: compare_scorer = eval('BigramAssocMeasures.' + sys.argv[2]) except IndexError: compare_scorer = None demo(scorer, compare_scorer) __all__ = ['BigramCollocationFinder', 'TrigramCollocationFinder'] nltk-2.0~b9/nltk/book.py0000644000175000017500000000630711332125367015043 0ustar bhavanibhavani# Natural Language Toolkit: Some texts for exploration in chapter 1 of the book # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # # URL: # For license information, see LICENSE.TXT from nltk.corpus import gutenberg, genesis, inaugural,\ nps_chat, webtext, treebank, wordnet from nltk.text import Text from nltk.probability import FreqDist from nltk.util import bigrams from nltk.misc import babelize_shell print "*** Introductory Examples for the NLTK Book ***" print "Loading text1, ..., text9 and sent1, ..., sent9" print "Type the name of the text or sentence to view it." print "Type: 'texts()' or 'sents()' to list the materials." text1 = Text(gutenberg.words('melville-moby_dick.txt')) print "text1:", text1.name text2 = Text(gutenberg.words('austen-sense.txt')) print "text2:", text2.name text3 = Text(genesis.words('english-kjv.txt'), name="The Book of Genesis") print "text3:", text3.name text4 = Text(inaugural.words(), name="Inaugural Address Corpus") print "text4:", text4.name text5 = Text(nps_chat.words(), name="Chat Corpus") print "text5:", text5.name text6 = Text(webtext.words('grail.txt'), name="Monty Python and the Holy Grail") print "text6:", text6.name text7 = Text(treebank.words(), name="Wall Street Journal") print "text7:", text7.name text8 = Text(webtext.words('singles.txt'), name="Personals Corpus") print "text8:", text8.name text9 = Text(gutenberg.words('chesterton-thursday.txt')) print "text9:", text9.name def texts(): print "text1:", text1.name print "text2:", text2.name print "text3:", text3.name print "text4:", text4.name print "text5:", text5.name print "text6:", text6.name print "text7:", text7.name print "text8:", text8.name print "text9:", text9.name sent1 = ["Call", "me", "Ishmael", "."] sent2 = ["The", "family", "of", "Dashwood", "had", "long", "been", "settled", "in", "Sussex", "."] sent3 = ["In", "the", "beginning", "God", "created", "the", "heaven", "and", "the", "earth", "."] sent4 = ["Fellow", "-", "Citizens", "of", "the", "Senate", "and", "of", "the", "House", "of", "Representatives", ":"] sent5 = ["I", "have", "a", "problem", "with", "people", "PMing", "me", "to", "lol", "JOIN"] sent6 = ['SCENE', '1', ':', '[', 'wind', ']', '[', 'clop', 'clop', 'clop', ']', 'KING', 'ARTHUR', ':', 'Whoa', 'there', '!'] sent7 = ["Pierre", "Vinken", ",", "61", "years", "old", ",", "will", "join", "the", "board", "as", "a", "nonexecutive", "director", "Nov.", "29", "."] sent8 = ['25', 'SEXY', 'MALE', ',', 'seeks', 'attrac', 'older', 'single', 'lady', ',', 'for', 'discreet', 'encounters', '.'] sent9 = ["THE", "suburb", "of", "Saffron", "Park", "lay", "on", "the", "sunset", "side", "of", "London", ",", "as", "red", "and", "ragged", "as", "a", "cloud", "of", "sunset", "."] def sents(): print "sent1:", " ".join(sent1) print "sent2:", " ".join(sent2) print "sent3:", " ".join(sent3) print "sent4:", " ".join(sent4) print "sent5:", " ".join(sent5) print "sent6:", " ".join(sent6) print "sent7:", " ".join(sent7) print "sent8:", " ".join(sent8) print "sent9:", " ".join(sent9) nltk-2.0~b9/nltk/__init__.py0000644000175000017500000001135611423114517015644 0ustar bhavanibhavani# Natural Language Toolkit (NLTK) # # Copyright (C) 2001-2010 NLTK Project # Authors: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT """ NLTK -- the Natural Language Toolkit -- is a suite of open source Python modules, data sets and tutorials supporting research and development in natural language processing. @version: 2.0b9 """ ##////////////////////////////////////////////////////// ## Metadata ##////////////////////////////////////////////////////// # Version. For each new release, the version number should be updated # here and in the Epydoc comment (above). __version__ = "2.0b9" # Copyright notice __copyright__ = """\ Copyright (C) 2001-2010 NLTK Project. Distributed and Licensed under the Apache License, Version 2.0, which is included by reference. """ __license__ = "Apache License, Version 2.0" # Description of the toolkit, keywords, and the project's primary URL. __longdescr__ = """\ The Natural Language Toolkit (NLTK) is a Python package for processing natural language text. NLTK requires Python 2.4 or higher.""" __keywords__ = ['NLP', 'CL', 'natural language processing', 'computational linguistics', 'parsing', 'tagging', 'tokenizing', 'syntax', 'linguistics', 'language', 'natural language'] __url__ = "http://www.nltk.org/" # Maintainer, contributors, etc. __maintainer__ = "Steven Bird, Edward Loper, Ewan Klein" __maintainer_email__ = "sb@csse.unimelb.edu.au" __author__ = __maintainer__ __author_email__ = __maintainer_email__ # "Trove" classifiers for Python Package Index. __classifiers__ = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Intended Audience :: Education', 'Intended Audience :: Information Technology', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: Apache Software License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.4', 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Topic :: Scientific/Engineering', 'Topic :: Scientific/Engineering :: Artificial Intelligence', 'Topic :: Scientific/Engineering :: Human Machine Interfaces', 'Topic :: Scientific/Engineering :: Information Analysis', 'Topic :: Text Processing', 'Topic :: Text Processing :: Filters', 'Topic :: Text Processing :: General', 'Topic :: Text Processing :: Indexing', 'Topic :: Text Processing :: Linguistic', ] from internals import config_java ########################################################### # TOP-LEVEL MODULES ########################################################### # Import top-level functionality into top-level namespace from compat import * from containers import * from collocations import * from decorators import decorator, memoize from featstruct import * from grammar import * from olac import * from probability import * from text import * from tree import * from util import * from yamltags import * # don't import contents into top-level namespace: import ccg import data import help ########################################################### # PACKAGES ########################################################### # Processing packages -- these define __all__ carefully. import chunk; from chunk import * import classify; from classify import * import inference; from inference import * import metrics; from metrics import * import model; from model import * import parse; from parse import * import tag; from tag import * import tokenize; from tokenize import * import sem; from sem import * import stem; from stem import * # Packages which can be lazily imported # (a) we don't import * # (b) they're slow to import or have run-time dependencies # that can safely fail at run time import lazyimport app = lazyimport.LazyModule('app', locals(), globals()) chat = lazyimport.LazyModule('chat', locals(), globals()) corpus = lazyimport.LazyModule('corpus', locals(), globals()) draw = lazyimport.LazyModule('draw', locals(), globals()) toolbox = lazyimport.LazyModule('toolbox', locals(), globals()) try: import numpy except ImportError: pass else: import cluster; from cluster import * from downloader import download, download_shell try: import Tkinter except ImportError: pass else: try: from downloader import download_gui except RuntimeError, e: import warnings warnings.warn("Corpus downloader GUI not loaded " "(RuntimeError during import: %s" % str(e)) # override any accidentally imported demo def demo(): print "To run the demo code for a module, type nltk.module.demo()" nltk-2.0~b9/javasrc/README.txt0000644000175000017500000000124211327452001015677 0ustar bhavanibhavaniNLTK-Java Interface Code Copyright (C) 2001-2010 NLTK Project For license information, see LICENSE.TXT The Java code in this directory is used by NLTK to communicate with external Java packages, such as Mallet. In particular, this directory defines several command-line interfaces that are used by NLTK to communicate with external Java packages, by spawning them as subprocesss. In cases where an external Java package already provides a command-line interface, teh replacement interface provided here is either more functional or more stable (or both). These command-line interfaces may be called directly by users, but they are primarily intended for use by NLTK. nltk-2.0~b9/javasrc/Makefile0000644000175000017500000000237311363770700015660 0ustar bhavanibhavani# Natural Language Toolkit: java interface code Makefile # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # Dependencies. MALLET_HOME = /usr/local/mallet-0.4 # Locate the NLTK java source code JAVA_SRC = $(shell find org/nltk -name '*.java') JAVA_CLS = $(JAVA_SRC:.java=.class) # Set up java. JAVAC=javac CLASSPATH = .:$(MALLET_HOME)/class/:$(MALLET_HOME)/lib/mallet-deps.jar:$(MALLET_HOME)/lib/mallet.jar ######################################################################## # Targets ######################################################################## .PHONY: find-mallet javac clean jar jar2 jar: find-mallet nltk.jar find-mallet: @if [ -d $(MALLET_HOME) ]; then \ echo "Found Mallet: $(MALLET_HOME)"; \ else \ echo; \ echo "Unable to locate required Mallet dependencies. Use:"; \ echo " make MALLET_HOME=/path/to/mallet [target...]"; \ echo "to specify the location of Mallet. Mallet can be "; \ echo "downloaded from http://mallet.cs.umass.edu/"; \ echo; false; fi nltk.jar: $(JAVA_SRC) $(JAVAC) -cp "$(CLASSPATH)" $(JAVA_SRC) jar -cf nltk.jar `find org/nltk -name '*.class'` clean: rm -f $(JAVA_CLS) nltk.jar nltk-2.0~b9/nltk-2.0b9.pkg/Contents/PkgInfo0000644000175000017500000000001111423120503020072 0ustar bhavanibhavanipkmkrpkg1nltk-2.0~b9/nltk-2.0b9.pkg/Contents/._PkgInfo0000644000175000017500000000024711423120503020322 0ustar bhavanibhavaniMac OS X  2u§ATTRè T§œ œ com.apple.TextEncodingMACINTOSH;0nltk-2.0~b9/nltk-2.0b9.pkg/Contents/Info.plist0000644000175000017500000000211711423120504020574 0ustar bhavanibhavani CFBundleIdentifier org.nltk.packagemaker CFBundleShortVersionString 2.0b9 IFMajorVersion 2 IFMinorVersion 0 IFPkgFlagAllowBackRev IFPkgFlagAuthorizationAction RootAuthorization IFPkgFlagDefaultLocation / IFPkgFlagFollowLinks IFPkgFlagInstallFat IFPkgFlagInstalledSize 4336 IFPkgFlagIsRequired IFPkgFlagOverwritePermissions IFPkgFlagRelocatable IFPkgFlagRestartAction None IFPkgFlagRootVolumeOnly IFPkgFlagUpdateInstalledLanguages IFPkgFormatVersion 0.10000000149011612 nltk-2.0~b9/nltk-2.0b9.pkg/Contents/Archive.pax.gz0000644000175000017500000340004411423120504021342 0ustar bhavanibhavani‹ì½cÛ6’0¼ÿž>×Ù®¨X–-ÙIºNÕ[ÇqZß:NžØéÞžë×GK´ÌšU’Š­öú~öw~ ‚å¸ÝÞ¾vK$0 ƒÁ`0³õb ÿ§v¶^<{VüìПmþÕïï ¶ûƒ­g[}~«UìýaˆÖßÚé¿x¡AŸ hÏuh›ùí¬ q€ø•!öûNˆ½ AÜœÆùÍF4Íò ŽÃ´ÜÀ6T|þ|§„²j`0ØyöS×wú ±çW›‡Ç'§{GG½ü>ÿÃiâ‰ÞñÑéߺ^:ŸzY˜Ïg½Ù»J“[/˜zÁø6šFYžy’zÁh”̧y× {“Þn«åÁ'›o¶È¯­º€Üj½ZWshC< òÊát>ÂïY×›Åa…P9ô®ó|¶»¹yww×CÔ{I:Ù'wÓ8 Æ­yvÜôßzæ¤ÿö2úoþ| ²tT‡gËÆaûùö UeG²Î`ûÅv]C›oƒ›ð*ŠÃ?<ñŽƒ|ž±wL'ó`z§IßDù®‡…by˜^£Ð%ãГ[OZO¼ýd¶H£ÉuîùûX¡¿1ÔhX½÷iòC8Ê¡ØÞÆ(ÝõÆwA:öŽ’Y˜z_…ã¿üu’ã(œæ½Q”õæ³p:í…ãù×Pï㇣]ï+124*ÙUoæø‡7ŽFáÆ/š^%é- p—óèpÿàøä wúŸ§-(û:¨ãp:ŠÂ¬×z |xpzñí»·ÞÐÛœgéfœŒ‚xóI”olõv°Ò<ËC/¿¹;DŒ,™§‚­ÿØûnïâäÃ>ù“Ÿ]‡ÀjWÑtì!ç ²ÞÆ4¸ ½öÓVmw¸ÂþÑ Uµwéí°7Šƒ,ë`Ã'aîÍgÔ^êìñû¨µ´wrò~ïô[ÐÛý“¯u¤³Iõ7í§qt)»5g´•VÁ·€À#} '§A: óìñ@¶zï¿}wü]"ôcM„y#˜ÊSøžâƒV {j#þ¡jOw[ÿö×èÊ;ó6ÆžIïü%ŽýÔû¾õoÿŽ®oí H 1Lªh_{‰Ã¸QVx©Uý8 .c`¦Ä‹™­ÒðÇy”†œ7ÖYÔó>fáîšåÝ-L=Okt¸9 òëÍ<ãçåDð^¯wnT†f³Y8Š®Ä΄ ÃäJ4- ¯êîuºXô–HõM@³,Ù_¬ÖXI)¨iŒ^•Zìo­´h=kºh©G”î/]Ä/vžI%ê…økìóç«4¼¹ÿáÍ!,$Qÿ°ù´å=m¶BA¹‡-QPÑX£ í^7^¦¼§›­Ö,Ýà Õwìe«ÝÎ’”eÝ}ïþ6îÍ‚4 Ó¬÷ô¥þ®7Ï£Øù, 'á}ï}Ãj>Uﱡ»íQoœÜjµð)¶‘A[¬(qB“É$šNzGð7LU ’ %„¼¸‹+±ÈiZå*Ë{0À;@ŒÙühêÑ\÷Ä ·~&p–FŸP¾«äP„{1ÿRüèM÷&ò›/ˆ ãóc+~§ó’NÑîIžB_½y ×þ}M¼Û|úÔ;áú)H£` :Œ5IÛI0Ï2x„¨Ñ£ ÷¢Ì›g NA0øÌc¨óSÈÈ=õîBäWX€#´VÇÉ— î;ÙÎÐë÷¶,$Úãð*˜ÇyÛ‹ƒË0îzw×ÑèZ5dÞ5hÖIº€°ÄM.$Á(Ÿ qÓâå£êÝõ}ìÖe>E:À*°bøtl¼µFGÔÎÐ[{gî6¸nç·Þt~{ cP#`Uš-3 "lÐnT¬|X”z »4ð‡WÄ!Žít 1…­A—Wøˆ!>9,³htãž$oæi1¬À* ´4Pˆ½9Q•†Þ›GGÿ¸Øw||°zðúâäôÃÇýÓš‚_5•5tÎ]éÛ½£7uúUuN¿ýpppñ>î}8=øpRQ{PUûÕáÑÞ«ƒ££ôvUéw^|¸86 ï8Fª  M©’·¤ª6pr´`üFsàï[˜U4Xjøv=šÀøßÎboæòЃøƒdÌG^êï/agj”ìì,%8:òÌñ^ad¥Ëh J_]!˜„Éðd56´Ë]ì'Ó)¬eT’¯‚ªÄó0¡Nä$ð’K\øJ¬‹¿R¥¾6g náY\y“4™ÏÜÿN%¾Á öÎ|`G!߃É>ÍÆ¼Q?]ÌBÇ+rÕï_ZâEè··°sÃ}Z’ÆèUTæ QD7 ]…qæ8̆de˜m*•@E­×¢.,eð¯“0cÕ$ƒ˜,ôg¶<ÿn—,ÅO°,]Áî’Ó±UN/È«jÁ*?«EÓìvõ¥ýN,R §ó(ˆ÷“,nU ¾²·Aó,Ë¡’I–U—¤¬¾ ”bÖÓŠýâè¼Å°«‘Àï“0‰9> þe4¹ù( ]aÙ†ôÓ0gÄåÇlŠ` É/´ ÈØ4¹Ë@f¡N ˜\E“9/Æ÷£pÆïÉÞ¿Õ*>RÙ<|§êhÀ+vZ†‹!:ÁF•dþëwoÓFó[ÐÛ½ËyƒPW_‹¯øù›`DÚÐøòʺßõ¦áÝ¡ÐqüÎË*H8XÞz§WL‰Z„*èÚáS|É ¾äEET‚:ˆCüC:©ÆKrPHjѸ?þ˜4l¿Pžÿ•G‡(½ÂðNgsÑ0=oQ'?O§ú£ŒYý8ýë ‚ dƒax$1¨!ðÑì§$‹>Š~xi’€®Z â1h(¤ƒMá ê X ° dÄ·™NIܳø¨2Ma©õ¦_Q-Úö†ÓI~ E½éúºÞ€l„@=…jÀ†ìÖŸj€ñ]¡N=ÕôB· ?bÙȃ‰K’þÚÒB‡¥{á°!Íü5{¼Öq5ƒÇVÚ: ïOPñ%};ˆ‡£é_<2èÚè[ÙêÖ­ ¯hù¡mÜêFí}°hõpúà†m-»ºm9²9ëâM;Œjë‰f'Ó$ ÷˜kB‡v+×§¥;W¥¢·õ _¼yu°÷ö3šgŸ‰Ä«ÏEâÕ# ñæ³±xó8hŒ¿}öüí¨‘jȸD{°ryæÓ<º Õ"믽Âu•[V  @F4žmlQªžc¨ØÓÖ,{è|ÖöÞKÚÑvAnMm¥kD–¶Û®^Óæ¬Mvu£îÝ84¿—ƒ»4èzk¸Ó[[c?^†½mýÅ{õ‡]Ûâ×6­ÙVm¸ô¤rÎÔÍ—Óð~ÆF+ÔÖÖá_«5³%Õ][á@äËÒöòašÜÎò±•Õ›_tÝ´.mzIÕ‹g$*(:%Msòú·—¦Á² ùòè?Jk£ÚÇBu+h ÀE¨ÀE_L-.ª×â´j¤ÊEÿdU.,êdƒÄ=`W_ŽÅŸŽ–þw²£úŠ0ÍþgÑO@Ro8ô¶ì¡(ñ¢²ËOa––uCØ’N™ÊíUKáÄëóÃí×UŽEœ„®GàÛ ¾Z­ýªó’5z†áÿ™ƒ<SRû³F8,?y6¯¢p(ŸâØb²Ü2ˬë c6šÃ¼Ô¹ÇlµnÞÍ3í„ÆQÇòíë¬ëáaÍe’_¯™ùÂ:]ÈC` y]¬×E 5]„ØVo Y ûûoÂ\AaÛï•W€ÞT)$÷Šö p½^¯<½ÔLç6 ‹&µBúLÛZ¦³îe# ”]¾º}UdIë¨2mñðé æäQP¨dà(ýÿ‡•Ãdƒ ®†!>ßÊ¡³ÁÒö-†øüÖi-±vàä?- ÚZ»BÏÙ쿈ÎPФç´è³”—6@KÛÕÉ'%¨F{žÇáÙ1MÊ4Ÿ½X ã¬.€ˆiUžÛZ;À6V«8aBÙ«Åi0Á™côFg[¾ìÅÂÏ2›š/ކ”)o N¶Õ*AGˆ’ºd9é€TH>†v\Ù ÈÙ¹I¶ ‡ÁèÚ‘^Ypþ€‚ó‡¯*`ë²ø,ó à©æ¡>¨,WЪžòg?œjj«[ž¼Í>kZ¶~ë¸:Öb¶ÿZýš¿‰èÖqœðŽ@A›øKÄݤºÓVãÒéñD{‚¬q7ée³8Êýµï¿ÏÖ«û¦1wNã)€Ô‹i°ÏÍêŠAnAn¾* ¯2OÔµxvsNØfð¥JLº÷å†9J²N!{îð2)íÔm×÷~]ƒ$wívÃÍ÷î6°•vð¥Ê¿‹}¼†UÕnÞ1jO¯ ¿êΞÿ¥?¶s‰MkÛœmƒ°ˆ+¸Ü*¥¤UÁÞÂØMWWtz¹Í •w ÀoB•±ÚúOr¿ ct÷~æ×É8{Ì–ôIóLRYtXž™‚E™Â­£ _Ô²ZèʽÖDVÿ£'›17„¢tÇk¾£?³mmk¯¯‘\[—-¶lZa¯5íì-ŠTU8›÷6OÞϲa¨¢8¸#Õ% ^jbÔKPªücrW~CŽý…¿EhÒêç¥ÇFŸ‚x É^›rCö[ú[ÛîÕÆººJ¨¯ƒ†lKsA2n²÷þÝÉáéáw‡ÇoOÿñÒMaQœ,°ýÓ˜”[¨«u¸ÁcGñ?VÔ°u:ïAë[™,.yéûÜ„ ‘Ž%7œkî…MD™D4Á_}Tœþb!_Y€?NûŠOM31V1ƅΠV'Užðõh ”ÙˆÒµ]-õ Ú%PÚmY#øvçÑ ¤'#šŠX’²z5+þažåÒ :ë’¤º‹Æùµ±½FÇ_n—¡O_‰BæþÜ[zkÞZI+ÊJ"ÿ‘S彎²Y,]å¥tÌÀý÷@"éõšÌ E~öfø:†‘À+Ugkëó4Z_;×7F©Â¯¯¸k‡ʼõ’Ã_5áœ'®½Ñg赆¾Ö6ì;ìZ“o¦ÉC µÄ[¿ªÙï§â*óþìi†W|þÿ:>: #a¨£p-ÙÅ݇\þIa´¥úSªlJ*…¨OWÍ×Ö™½×`<Ì“.š®7Ø{s€/Ž#¼Ÿè0´“Š*H«._qAPg ëkÛÐåt”pZ•ËaqqÄó6¾ö\Ž\E9ÅJ6Íè\pËeÜ0ˆH¼9lëà4Ë$j×#Íèfªá™«¸müuî¢sE/‡¸Üt”³Îk\@éXYU °Ñ¯qû3úît»«&rÝy‰¡t«d±,òôwÙ´ ¶Ìx<,ÎÙ¿òÔ_nò­"« —Õ¨V¬õ¥‰g¸¹¶^9r Këk›Æ™£ùëÙ‡Na?¾Uˆ¯‹‰;²´`ÞÑÔó•e?H'™ºÇ£4ª\ ¾¸ZBçYÂð'o›`ý³-}ÂÑ¡—\–O(N=\‘¥–ñK)&F°ZìõøùÖÎJ±@1¼eèc7ÜÞÓñFMC- (’\§M—ÿ»|gûrA'{Pë¾#Ze»øÝÛØ 'Ó ¼®ã½9<:ÀÞnlx?E3º¬«Ì4çËåÞ(½Ú 2Ý߯´N˜T~°4Áîe |øÖ¶è§p,P¤kâT Ó¨ÀòŸ›˜ *øÔy î¦îzÉ;{N=´N"ºµÈdáóÔž ¾ÍP 1Y€%°n&Ió!Ìèær†Þ]å9^¥F•aŒWú‹›QU]Íõ µá'Øexí|¥²8ø˜E³F©œ\ŠWȳ«&eí#sÐèzW)°ì]’Þ8ÃHÅÙñê̱ä¶2`I]°‘f¡I`’ãs3hK›G‰"WEÈͧOÀSÑ&†»µÉs%P‡a¦ŠÃay½[ Äu`Ûø3,lr9¢µp†B‹ª÷\á.ã`zCt(ŸõFÉí „Œ¿öÿ|ÿ}öôOj‹ó+›À¥/î¥HÝÇÓÿGÛ¾¶\ÝåÍ‚X0¸õÝ)P· JÅpÒJYª ^û'$"< h¸itA¥,ÖT0Q€ï½=€ïy:]ˆ‚ŠJÉ… ±¤ ¯k\Z…–iØêŽÎ¦€ºZ…Þœö6Zµ´î`…¦]"›ÔH”Õv‰Ê›GŠ®ÏtViÙÁ_µ¥WÎËíæÞèØ‹Ë–7…s(—Vùåñ§©[Áê=îD}5÷CàÕaH‹ÎÑU„,ù¾ËËÈÒÛŒª¾F%K¯ª=(ª¾‡Ñ›•CÒg†îÀ9žÎoÐ"¬®_n½4Þ^…ù]N±4–ë?7 Ân)ßKɱǜ°¯J@ÁËÄORñ-×·ýëC¥º ¶\÷C"»ØSÛ›ÉR›€ÓÇ ,ѵÛ+l«‡°>N‚xO4í>Ï‚f½š}`Ҷ2ûsçœRŒ)Õcë¼:ø•;©,ݶŽïÞ²"¾–Üøç!¬íÞ¾{}pd¢¬³— @+ItÁ¬äM•ú¯hFÓñ'ñ—w¥â©»ÏZsPî`Š_?É/C ‰Î,ð‘¿fl¼tlßQd##Ђ¶¤à‘)•òµ&¬ÇÔ¢nãàùÁâv:^ÖC†§ÛQ@mŽ“,´/%É FwE ¢Ò­ŽMÂ)mÙTx¡Ot3QsDiM+ QDÙàq¡Žð%ïkÇO•M±Áŵ0óüà&ð8ThVØÒf@Éœ#¶¾g!ÂÒ§AV#ïf|‹7­íC½Öe1п²èvÎM[t)U•täT ºNÆqÑ*)F«RŒ#ËkrߟY :Ò;³y§!°#XÙ.%M{²'ÝB»fÅ©c5EÔPÊ{n‡Ï­0T«fù¢—Ó,­ÝIá1&Z^¥Â J õ_<é¨&ˆD˜]ÛF@$U¡FÙsÜh+™çܲ§¼z °…2Ô“ö o„ëÔ®‡ýõ²ëd£b®Í6PJ2xy–.,~—Á…èÓdÝ›Œ&Ú8¤8ÖkëÑ:(­“$_[çʘŠÓ1f˜9]2ÉHcŒ‰haG§ußC[¶Ž'ÎKÈF8e2Ý…añèôò„%ß¶²¢—; #&$ŒÈŒ#L?jþØnÇ垊CàÒˆðF$ò"Œí ?xöb%Cà)êiš)ðwa TÆ;D®T[ª–¿‘ï_Ì6×$®luDÛ¦ño¥è·6þj&;ÙS=h¯Fß²ÍKN,aõz„ žCkÏ£n!›G°‡NëÈ2óˆç+±£,$jª71‘TÙHtÉ`Û|Áˆõ€~­fÂâ~)€zØ…Eg©À4öxk ïïÆ`pKóä$9Eá%eruÓØƒìH…mŒÑƒ^!†l.êéö¢†!‹õ, 5€dÊyl[ Nú³´<h‡G@POGÃt1í„Q¦«$a€ù˜yÙ^_X~°öq’S’…¬@âQy»¡•©ÚfÐŒrßqa¼‰ŽÁƒ“é§0ͬ ƇrµYS x¡rv$¼g¥›”õ'°c½Á†÷“úµk“§K±Ï{ðO˜ÌMÒ¦HÈBL:KáÕ™»(¿V›×.j'1ìh‘³–è0䋚ۋg×ÁeˆÇ ñ^UÄ­p0]ˆ;àx§HÃu"q••ýN+¡‹À8¼÷‡õt—­Õv”ñØ!¥›™WÚfºáë «·›ÕÓÇ/σڛ‘•ç?îÝ« ]ÅP·„²ÏÁŽ?³Ö7†ƒùlÐS/™^E/a­[T3¿þ{ï1R¾tâSÃ\ãÝ'êŸZQ¿TU;˜Ca“¡^Âv‚#\˜!$43àÊÝÖ:å+È¥ÙcÈ Áé3„é<*T{ÿˆ!Ø– °T™¯:VN ISŽO•‹ Âᇿk~|ò‘rÌ[rþÁ¥m×8 ¥ðqkFs«qÝ’ß”¯±1Z$”¡¢cehs¯“)ƒ]QQhö²Ã©¯Ot—ãórdªâÔÔã‚ñpŸòˆH —G±©Çµ<ç±iXŽqS‘  Ó |íõž£Eô›æG#îWK63N¿S°¹nL/owK¶5õŠÄp £™x+9:Nt gû8:‰)š«|o,×Cï Ïáì§=V‚ߤÉ-ùc8[òu{}¡ÅT­Òö}7qûAº\‡Åh£)1`Âd¬8\ºƒÑ…®5ñwE“vHB(¸G|ñ%óƒËOåÿú¿”?Mý_L+ä?ÑÆ<'ûßåSÆXç/yÏ à¤ÞÎùU#N¬úÛ»ÂH³»Ž¶eˆµ.‡¬à°(x5¿º‚).ÀŠ_z )~¤¯Éü$ éê6[óéÜÉV6…(²u=P·¼­ ¢³t¥ÊŽÑŒ9ò ù 7 +—Ñé^0ä½¥#´¿Ÿ¶+ôQ#Ò+È+j¬o]ºŠÂšv”;­p…QÞ)”îXÛRªNwp¡eA*Okâtˆ“| S¼>êPrq†ö]:ŒGì¡sõ;'ü³~JPΗÆÝ*æKû¾ñÐ=ƒ*®ÝMf 5éîÞ÷¡c(¥{J*¨ª=xƒ»Tž|QçLtðÿßSº:x—Š„:ÿó»2ÿëýe :ƒëÑßäP§÷ˆ(ϳk}–bïˆ=¹;¶)!øTƒmç¿Ç œæÎWòç3&q3L¹$J»‡ 8Pß…·4j½m@’Ò›"d¬%¯™•®Ï©”ËýÁΠÖkæÃÁÞë·½ü>ÿº}lüî•gÌ>вÕjä(ÒØ›£…çNÔΈƒ¢ñ‘í8JCN %ÓË >I»½O# ʼnGG-ÿ0Å\$|CÐö;]c^Z•üpŠ9@Åì¸]«©ùŸ H!º&îÒ3>ÜËnb-1¯ 1¨“Í‚;:¾¿$[ÙüRè¡c: 2hìîæ1æÂuÂò‚—€…U?E°ëjh£[ú5¬³~PlÂåIT{ÔV”µÂˆŽÎnñPM&¢„–ñø†r÷ÂoŒ£ÝÁ{88˜8µ‚fxAéÏCcô˜cšÇ $Ä3NIæy ]aA » Ò ˆéXÈ|ª—8=ÐŒs€ ul*@ôZÿ’Ëœ}F¸"‰à¹¢”ä$‚5O¬€$_,1i3Ùû @´Õú8q_•†?ÎaäˆçA3²Äqp‡¬LÒ½!¡i‰ÀÝ0“«üÆ´5Ž2M§DŠ&bQ(,ßÞ;ñOÚÞ«½“Ónë߾ûxêý}ïǽãÓÃïÝoÿÝñëÃÓÃwÇðë·wüïo‡Ç¯»žàaØ#¥ˆ;ª H9¼w†FãÒ±FêÛУédŽÓk‚ MÓY˜ÞFYF¦/`ðVÝF¹¸“Qꎃ;Ÿoíl½xñÌâÍ­gÏ%wî ¶ûÖÄ—Û­Œ“;ñçJ­¼X6°•þγ>5»£ß~ö¼²•Í‹ <—»¸èÍxâ£á „Á‘¤Ñi’Ä7Ì œ Ö“Ö“f΄O„/a¶ ú4ˆÜ©÷*JÇÞWÙå_Gú¢M£Û0¾DÂ^0ÿÊ«Ïj·Oj}Ÿ4w=l­­­µ¨èŸ Ã]I xÀ šNÀÝÛÙDÖz¿€nOQǙǸƒgæëãsXŒ"¼Â“Íg脇Ü,)®fÀzc ôXqo*PL;S^½V믟Xâì¢È¹ü u¡õäÉ­:O`Þ†y€(? Ð\ÈAX9úJÕ­4ŒÃå$RW`.“· nLË=£+€‹£rŠ8˜-ÆÉˆV!¤Ž\ÂîôZØÅ~!R 4N¡ <%Gò—][û¾‘δ~­I0Djµõ Û‚$ÐO%¦£x>fA›†¨É“ÿÝÅ…àUF¯àôïu˜Òˆ=üduæÑ®w.î’tœ‰}à5qv¥‰x3©'ÓÉA49­ã–<»KæW«àÐ2÷blB` "­Xp2 cÐÛAA~ CÞt@Ž‹ Ù Âì¬}|ô¾ÝõÚûGøoÍÔh—SÚ¸¶ÎY¨c%(5‡FšqfNüŠÞ| È3;úIÌÓ<¸Ço0‰ „tûz9Oc&½Kˆ'¿…W}ô×A¯-bÄU«0áÞª Iº]C¢Â¯;X}ÿ‡ÑtͬÂwˆ[Lcù€Ä:•2[-Þp\à±;k§ ”†kÚ¦0£uZpÂ{ÁMdaÇÞi嘈°íך¬ÄCøyæíîzϼ œ¶Âygó„ôY1íC©rîÍq9E j8½¶Ø€$þ©-uX,2Þi8ºž&q2YÔV9Ñ×Íb!…¥ EÞ‚xA]êÂo!N„&E„¬ùŽ.àäÛ ‚Š6' ˜, ”‚¥õö˪™¥ÅPìâÒ²³Jág«~. Ÿ&3PÍ$-rTÔ6¦0Ã0-fã²bD™”c01$vG$î ¾Ã.¦Üè÷9j‹œ­Bg‚=7‹,*WÇÞ{[hU¾ÇGo¢8ר´®è7t=0nR”æYC Ž”¬¥Ï[-:T§ýàUq½€=÷.Ðþ€JɃ? /Nß½ß8:øîàïš~<:8ù,xðQÌ“ÙFŒ“_ÛãM3èK¢½¤\ ˜Dt—’ —ý|* A—•^pjÚM˜¯ðf¹¹¨êI×» o“è§K¢WŸX hŠ©õ4‰ƒ‘õdÇepqÿŒ70Øz»=ëÞ±-‚Û˜|‡Õc íäc®ñ@Žgj•ÝU·QF£‰üJ ¨ø~ƳÏeŸ÷{ûÛûæó¹F› ÒÜ#6 YÈ—YBXò`s‹Ô¤ò¦·ë=¼žOo،ό¿ ÚÉR¼Ö-^ÊRâw© ìfXw|é‰ (~—JÞ† 0Œ²—²añ»\í©‚l¨µKQ4a­ý.•¾'ÌQÁ¤\†õ©PvUþ.ÌÂ[ü.—ÉU!.“ë…pôä±&ŽÞû—hßø‰LQT¶~ÐÁßâóK<Ëm4’ÅÉCäM*ïÝé|ºž{ž\faÈÄÞ– ‹Øh\…ÐâhCèámŸù@kð›‡d»¤Ÿco­Â!aÍ×›öÆsR‰¸Õ]ï‹lÍûÁýí ¦Ñ8¤;Áh¡n ´˜‘0—n4ßÓ¿#HEå×Nž?tçä6ásÜIÂÓõ(¹<_s¤'=³V²âÁÎßiÅë?s[ñž7±âmÂ,-YòýFÖìþ³TDölïôÝÖlÙRCknÈÀË:‰XËV7î­zQ¸0ò5´ >’i;ö§Ãñ®§‘Ç{±ó| ;ø— ìã_¼­þîÖóÝ­Á ä·¾÷'¶ Ò& „<‘B' è0 RÓœB Š·¿8‘ZÚ¢U^½úNðxjã€-׸?Ø'5@–Ö”ET÷§cKœ’ºå¦ ŠÇxT°‹nÀi¾‘\md³0Ä¥­x™¶¦Àð'¤V¡t`?;Þ1Ξ”‘“fuDÅn++ü=ìÂhžfH ´?áÞ[àž¥z±“ëè*ßøb`Ym9Ó0èü~‡¹÷ ýÃà-YÀ å\HË ¼»<–Ut/Ø–ÆS`¼²¬uÉAK³á,­[KÄßš?#§ÄJ!G—i.:½µŽ&Þ…¨ç›há× ÓßêUC9«¨·Z…\u´×z%ÅEÎ:êmQyÍQ…$79 ÊWEaÉSŽÂòUQX°—£¬xÓ*¯«R©]ÄÁ¥¾€–f…ÅS2N›½JÛ-œÊ JÖZDÈàBNqÑil£¢×øJ›Tw÷åùtw_?•îîk5Ï{ÂÊl†wÙóxQ«ÂÜÝ׌’|·t ¨]«YÜŠ]Îs>X…ÿ¿úðîoÇ,»X šE(–Ëïs~<ØÝ`»~ùµfó²UX_'ôå·\‰ÿ#$[Wzã}Ü¥ ñ¿šÐß¿¢µ¶—³ùïdí.ÓÖûrçÅ_<$ .áýmoëÙîÎÖîöŽs ßCSÉ ¶~AL«©‰°ÒÅ i¨Þç°gß×óYÇUÞGx&.Ÿñ*ç×·¼¹›£KGÀÖ‹ÂÏSxEÞ5±†ÛÆ­GãIÏóð MgöNC\¤ñ8Ù{ˆ&% Ç“pÍ.¢˜kØ&ØÃ3H<$b,@k†É¿JsŽGßÓ]Q¦tJ‡Õ3±;nã1Ÿúë°È£O€#8|$‘Ä,¹r°‰¡:쉣$m‰£);Ï{“^Ûã,Þ ‘u÷{žäÉíÆ|Ö鱑1<<~¨Ç‘RJ!¸ÈCÚS#$¨ÒÊÄ}ù.;VÞ$B&–Ö3Ô†ptGtjy˜g2 û.M‹mXxëz?*6ÌØÊ‡²jVí%ߎÆcBÅQLâ÷R×9œ[ääLÌçOlªvf˜%¬Ü‰ñÊèJŒi} MôpÀ¿–†¸ ;Ð “»Âù‰ãàÀÒëð?í¬±¥Ø¿zŒ€Ð÷§šš=¦Æ-¡€Üšó©1ì¿g²œzßõЕ†,)ÔAaÏANlÑíiäˆÉ‚JÝEÙ5Å]Ậ+Â÷ð ýìÓˆh¢¿ÙmtOï#r²¤{'0.¼«8ÇJcUÏûcëe˜‚ÈE×Öi¸1A÷Úp\ôü“80.:ñ <Ìð‹jᨕ;¬‹=…ßâÈø R+Kn’I„iñîTt´4$çE^i< û…4×ÕO¨î"h å…úR¦¨Y4º‰…:¿A¯Ù×Ày‰²äÙM|W†±éâ#t‡Z–:.-PZîP« DYo†/¹QZ3H>ô˜»L4½ÖmÕ§ð]{eÇÑâ.ˆ»,u.FW­¼n㎦Ñ8Ö¡¡ýªç0ƒ/š|·ÿ曃q„Ѭ2:ºøý"O.ðoN8­!öá„ü†ÇøoSä·ËùDh ßE0|Ä”×|³Š%· I«B8í ŽâDT±ñYqJºë½ùjÎ;蘱ŠãÞg™Ý-ü23^z|4˜-ŽQ…€)œÆ~‚+ùOá +ÑÞ‡ïþî ½“Åíe35{'ÿxûêÝÑÉY›´«Ë»ö9§ÐÒÄf‚BÄó ¦æU—6ÞJ~x$Ô I+¤‚¦˜ò¡ŠŸÁ5tŒSǹV€¦u*sa€1 ƒ ´žU_—©:ê¹ß)‹NKyÒéR-ø¬ZEc µ¿ Õ]©Lt 1NÎ*A^5¦mØ­“ ;ÙQ|¦aû: ‘ª‹%B[4DåÏnoŠñòö ¥€WQ<ð¹Í …Ç\ÿõ¹ÂÏ¿tΰR¶_4†·É§°¦!Ž›¹@âãbiD×Y«õ3ª_ȨòÌ#e£º&cjZgtå£P-=Ë@ÿ)Å@2P×Ô Eªë ÃûWL§NI/ÂUltyÁžV Ü0veIŸÛÖu!œ¦4I¥Ñ¯ëýðÅãðJ-Ít\+îΫC0_¬±¾dØŽºð¢9 Àº<îÐEÅ©7N<×t,¬aå×- t•³<§KB¢¨®–)½ÄÜAxÔMîÝàî¿#[¦¹Usj¥-†ÅP«­‘[Y±®1Þ˜E”°>;¯ê§A*š£8Ý4á+ AO‡D6»¼ÔŒ¡¶ˆ‡v=l9/qïcÉ,½úSÖQaqóÝ+ôy¥A»YM#+ŒÑJ¨`³6*ó© ‡mÌ5Ôƒuõ;™Æ‹Rß±i%Úb=h޼9½x»÷á›CÜ2½z§ýè?S¥Nß½/žG…rÂç¼êð2YßÛ0ÛµÅöØ¡ÍÄ!HETëýb‹ORgšû£³6)qíóΆ†jgó¸(¼Ð ³ÅK(o½Âz•Hj¢LD7E>Nrî”í¨éþk$"ñ^ Jåkxo @L</ÖÇ‹ÍÁzbZKÆžÿŽþïY®“tØÖmQåæþýúøàk Û-Z\ï¯Öæ´ªM$Œo´4^<õ£õ~Gï_]ÛñýÓãõåÆAv=l÷jñP‘ ªËõ…ãŠæ5>ÙóðJIÀGžäárÄä¡eçP(bÔš…ü ¨ÏÔu$YǺÚÂ(g`¿»Z[‘•§$g(.`ùl³YšÊ$ûÙÚ [drtã­¡ÿ°– ­qåˆ*?¨a6k ûIy›\25`Ø×R)iþdM[Ù?CT†?(K¨¦  œüŒhóNgHx úŸ7lô¿ÆO]õ[)dwõøþ6¸ïz ø×Ãü‡†°îz¶@VÕ*Æw£†zþ!¯Ó·…ú¶ŒÈqŸm•¶—K‡(146ŒšµÈ'w˜Á§Ê”@£fé»åM嬨èÑ6þ<½¹³6Kš%˜ÊÛ% Uô±ÎŒŸˆóèœô×; ”O¤—Ÿ G+ëðx¥`é«5”À¬÷ä#å: F£¿å(;,Î:™ ÕïÜqðk¾øÛ&·«üÁw娷+!4?kþ¼3ä'Ò‹t9E.…»© ³h®ã*ßèlôÞ¢„u(+@™ç²uBÆyd«cB¾b+5Ê©|÷âx…–éV#^ 7ڎë‡4}"y|eÔìp¡¡k(|Ä˧ÆöT—gÑ\Èz 3œ&ó žF–Žoµ£è¥§ÖbHi†Ä9Ö2™¯v}ήT™Ÿøä‰¼2q3žB+¬H¢|…%X`'6Œ0lˆJyjUÂF¯ÊÛU¬‹'-U»»£ª…⊿q Æn¹Ò³ÃB©ìÇ °› Iô²†É/1»ÕѾñ¡Z £'”Ú8Ê9¡±‚ns%?Îe:xÁÐ2¤±ð‡&Y3þk;jæOÞXkÆ bÝ¡¬%÷^$qo9qg]õ´ÒÖ5ÙâQÅ$Bc9ÐÊvaÍ”ü°0Q™ü S°£{´â°+Obð»K[ØXÚHH°Ðú³ísorˆ'\ôpw…±°L|¸ÚWÎ4em°›¤ºâÀ(©þ=H¥üw?€¿ƒŽ*!:X£c8nØjkr;i4~ù¥áEBžÒ±Yã >Ýuø4ƒì›¹?ÜcÄ\Á¼aì6Ì¥öá͹ټÌâùÍÛ0Ãܯ’ûºï…x§ÒÚ]°DWÛㄯI»x»Nu\> %c¡h!Èœ¾²¼î£:X{\͈%N >[@òKç<~p&ÂD„yÈsÚBÅžÜ"EÉlƒò*d§…}¹Žh«¸ò I;r ˼ÿ0Æ©¼˜ºÿóÛ9Å=™Þ†Óù/t«WE¼U´f+eêpÄd<˜ÀI.[&£…¸|3é°Ë0Hz;e*²­Ó/ô;)„åÊôQµQK7£M×®OŽe‚ÝÉ#­T£gLÓd&«ìñ¬Œy)2]x˜kcà­éd&2vöL¡pñî=_ÀÄ{íö®ÿ8¶7í`:†—î››dü—ãÛç®Ú ËVUžO©Ö/Åø9ìÛO‰’ö™aj [äÊE[\Ȃᢠ*t E <ÎOæ‡ €Hi1Ï^Í‹8¸ óä†_ƒRøÔûÒZGÂÛY¾ë4 ¿/ê»p%Îþ,¦­˜œp¹”Ù›t%çPŵ®m(&zU‰B¦(½¹ÀÙ mia5fEßDwèrÑü2„a`r–¬’\YÞ-µô‹M:‘ ˆm^85S\([pœ“Æ79’íc‡'z^fmµ²ÏnJºVù¼Wñ(KJ~ž«-TWº/WÒú&L¶ù‡ [Ä67ó(Ô—BÕ¬¹¾FÀ®QîëgZIŽð± 0³/-Æ1ÖÌrZw€5iî‹”$‚uc£#e%†1¥‘cCAA Y×/Œß³ÂŒ‘áã$™-UšÖBB(ëÑDé‡e°y,,Ö,¦ëç-œ+—®r‰—Ú„FÅ/1%ŠÄÂÿÀ|`Wñ (H“««¡fHuøÆŸ ù´àÛIJ$.`£hD$¨†íý<7’eGøE¸‚-ë~dÁ·0 1¶H3„(·gnsb”=#ŒhÒ£,±É9ê¡|pO·C‹†k}¼<÷¡¤«¯úa¨`qk.Á"(ÛG.1ÚßêRñ¡DÚ< á½ÉV”ka%¦â.rê:\óÊ ]÷«­fj½Ý§Ó‘!]²9OañZ„- ®a¢2Vµšã³áÆÇÅÙÕèžÜ3– K&‰oÕxÙkĽ‚ ̸•NÔhK¢³¯¶tˆ[(T]´Zºc«k%Ù,^>ü²µP>â–]/¢Ëµ—{»KÁ)á˜ÌDð6fcûy xÌ™Z…©]±úѰŪþ8§VÑDË\\Áº[èÚÅí'£-üŸF“ú«=Žò‚xîSÿ¸†.Fj»)@/ù¤;ñ*%oqþKq úE%ª&¦@Ì0CH5¦‘jß8¿ÏÄÝš®=¯ØcÛ¨m·LfêÃ3—‹V ,7œZ “–ÅVhµÓKåBFADV«kF!!´êN8=h@Ÿ)b,k½ùx‹6+®³³æÁCç¼ ¿„¼¦£Ã¡›5p´K{¥ÉT r‡Zú@ =; ymg`»ñø¹$ƒ{ÅrÌîŠD¥jVªA,uŽÍ eQ1xQ1x\QaZ0›ÈŠÁ겂©oyui¡Ûoâbð¿A\è½øÍä…ÑèÆRM%†¨NdhJzS##†šŒpOþ Z’H`5¥ìŸXžûMyÁmö³…Zü>WH(%$”1¿$•US’êbË\ ¡bžÿÅU¡rnk­þžçvчǘ٠ç¶ÖègöMçµƘÕͨkíDtËhiRånPëÜ¿íµ¦pï”DÛ%º™µ¡ÚÒPç]ßò›‚Þðð¶šXª¬MZåqSÊPRG¸‘í ÒnP羄Paä£keAlH,®…S§ör€=9ôó€Òìxâ:/ÉÔyIa,à»j$ªFEÕÙVªjb<ùüQ縄~‹Ñ‰åËÇ2žk»\AV:ñ(…AÓ1)µãµy€Ç¬u†…mï/}ïdzVé ÿcnZî.Û„™íQ¼oØæA u•½®çÆé¬£ë®óÖš¾Z Ýy VƒÞW+” º¯b×)Vq„:ôȽÀ–sFäw5rÐrƒ*ì­P^7ôØ6¯Ð’ÿHÀö¿ÝûpzñæðèàâôïNÐÛÀo¿§¸ïtš.Á=Žß®‰çå·÷â˜*P ì§íιæhŸ¨èg~Ú ªÏa9² åZ½ñüv&‚FÞŒNÕA,Ĉ"-2ÛÐÊzÒÐU›sêŸÐãFÇ5é8h!h5@o&ÄŒú¶ë}‘~?%ã& ´Þ„¢/j€KKN£Öó<Êð>ÚàVŸ¨ËbŸ9„xžªá?gÃ'ÆMõW»n#V Á÷XÅ·™<Õ™\Œ³ÈçÐìSj MZŠ•Ã.f˜‚Ã/†m×ÓV°³ÝçuµÇr¥çöb¦¯s·‘MS9Þ;3qvÚÈŠbš1áðÊ #Ju£ÛÏ Y”±ǜҪË<4*e‡é‡¤V#M•¡ì<æ¶Ûånvã+Y,^,uº²ÐºEš2øbMgÿnû4Ë­Ë µ¡øÉ¡ZT”Ñö½îðQ±BŸH?ýÇTz5›@e\#3ò©æÕ\Å‚fÔ$ÓìPucÜ-ÄS§!Ý‘¸’i>%“šáT¸í:jÜò|tf3°±µ:P™kïaG«!²Â†å¢6ÞI‡ÇìQwšV_Þè¡“ÃÑ ¦œôÛÒ†=Öt£µgO2“g¾• ¤Ùd“ gÜ}®œB°=ÊÂ4Ñ´1ü5zB‚¡ uQËtdÐö"¿}6}ì¢ø© nÙ*¨Æ»¾ß/¹  ¬’¤K Un‹:©WXèlbºâøu¨÷qª–‹Z 5Š&—XŽ! õÑ+rõX!4boµ»ÐÖ"bBŠ6ƒ|Acp•kXE%MñDµËÎ:†ý§\ɺuýÄÛÇ›{ÞŸµH¿œìÑé_4Së!ï.^adÒX­¶^åaFQ²QY˜­.fÙ<äÔz )ð"Íï¿åD.":¡ªÑD1<‡Ò~D”ôieëÚ ùµT¢Ô?~!/ޜɊö¶H+õ½N]äšbÓµfy³Ù–ͬ8åQ•¤{`ýBæ˜<£‰ äÿ‚Dî-J#n§WmÛ³/ÝTMMk*Ž1ìnØ'¹‚:äºg!WMjÎUÁÖ}tEdúbÒ¾õø×aµØiui¼p¡I¦¡ðÄ®J8œÉœ¤ðʹz¹ðöù*‡Û›Í~ñrΞNyK½,Ÿ)åWžx¤ÝqT¼xag2õ>i”Ì3;¡r³†£ëiôã<Ì(Û-ÇËΑĺ¨]qd^§›Ww!ݸDŸ8ï®È]&ûgLÛ`á]â¨1Ý~yéW9§†’ò)§y™Lñ[ÖÞÿ™¥¦y©R&Ð÷1ý æD£¥™1,bcL¤Ìì”ó’r¯ÔmIWôžãôÜ…mʘ+2ïêsâG9Í ãÎ(…Z (V)Ì•Ø3J°BÊ÷ÌóÞ2…ñÜ¥KÑ4ª¯’•ƒ"y.j®ˆš¬V€†9ßWÔ1Âîa.Ί[âtS–32ˆ(JXX@£”êAH÷` ù7˜.šuTÎã¢CHZŒ¡¥` è>§H¸g\*çË‡Ø ¦3/¤ÚþÏÔ åËH¯3øÛïRÜââÁ ëÅô¥c25ŽŒì´di¼!§úG8Þ7¡¼ˆ5^ª¦e—@Hˆ¬¿ÚÅGñ@?á¹$}œŒˆ×s˜Pæþ®Ò„éœT]W ¶£:Í‹êª4ŒòÀ/åb&‘fCQ³-•2MhÛ%ô&óZ6Ë‘û‘Af29ëè½I sc+®Kf¢LÄ·@0 þëHáH å @éáoÃÛ˰Ðìî@û” •„D¼ )¬øT$ O˜ ²9ùì¦ —W0P:câéX̘dªáŒÒÚˆ°γè'§ï£{LH·3¡Ÿvßy>g…fÎ ØG†û#›ÄÙ!óËÓ²BƒÊ W;Sº2ù¹„@&&uåì”<£ãŒƒÈ8“¸š§)/º˜Äe)­¤%LL!B%HxaJ _¡h+Üà œ$½(Œ”H­ŒY˜B¼ìa¦Þ¤.ŽöÞ\œ¼ßÛ?<þ¦ˆ\ªÅuN?@áïŽ.Nÿëß ø Ÿ¯v¶Š·ÙYa£¥mÈB¯JjzŸD‘äU2…ÎGÁO„2ð‹$}ÏYë‰÷>MF!èC7áâ.IÇžýe‘YiV¶7w= ÀÓVÏÚ]Oóä¢ÇJ8XÅås¨Ò·õc9þZùˆ³—|V‰0´ÀH ¾t{_¸ÚQ¦—š©Õ4“RjUÿ†  S¬œpB€©5?Å~AÉWü!¹.Où㥠Â;|ÀÁå»ð2±êÏö•÷ŒU9)n Ø,¸Yãh²†¥ÓôÑ"0‚¹TAö2´Ò%v¢WóußøH½-¼÷ŒdŸ6uÔâ©|²fU¾$?;×¾GïàyœG3•z=™j‹Â¿Û¼æ¦QnãÆqC‘Xc 1K¼S/7 =T$¢!²£øçƽmc…‹OE0ñÚŒ€›+Ьvy¼ï‡ßyÿuv©vù¸Ugà7©€oØ)íG˜§²9³Ì¥WJ½ Õ”?'Ì”îz²u „÷Ôe"×Õ|=Ç(ŠqDÉtDÈÆ€Þ”os‹Êní ?z泋ìÒ¸é!v-PÝ¥xTµ@¯YÄ„Û[v\m³¨×Ê÷ŸUtÄ©êá'º2Ž^Ðíj¯[%ôû•íââeLJ‡ßÈ^º”¬W™ÎV&SuFæ³ œµ‹ûlÅynáÿÚËë–:a¶÷¤¢AmP­üf)§— aG(цÛ)èåPãK³™¸ ãb¢zt/³Ãûš°.&„¶·F°z+Žj¥ r ´9PE­®ƒ0¯–Á4ˆYDÊåU4™§#ê:¹ó.#Þ„±¼/t|kö„ŸJ> "…«Y6 QïqÄÚKîÌ»;Zd¾ˆIM¦+”‰;`8Üsàsé¹T£¡]äÖÝñw$'~ÈܨòC¾Px”îd³Ìd­Ö™øø2‰Ç¤ñƒ¿Á Ë=üÇ¿ n£x14n5ßñ”mc•†©+±SCSÙt)¡ËÿüÖpà`§óÕužÏv77ïîîz®z½» Ã0ã°BöÂñ|3¹ §›ÞþíMßÀ%ì]ç·ñ×ÖÙ"s!ŽW­¬U·s¶†×´³Fò*OÈŽBt¯=ÅÚk]¶qDhLnJu©£½¨ðj¬Ýî­Ì·a< S2»ÊÑ.2¹ÚßH“šKðb'4¹¹îµ/]†JKþ£ºÌD§ð­â’…Ǹ:'–Ë£‰Ùå›è“LHýÍt=ëD¾W%iëÀ° SùØÈÔyY¾!oì,ÚvWßÊTi/(º¾Æ$Ó)Æâ#Bb§L~¥­Å%z&•°}>”¯hì°Ð9k/œ*ŽëÐ%ÍÍ‹#'¿÷ÚÒCÖeàt½M† ¬# †ã m‘­ËWØâÈ&5¤ÜÕ˜EÉ¥«à-&"а_‰¬íjbÇÎÓ¢7›Jî’Îײ‹ ²NsAÊÏZçí¹ƒ…4Â’j°\fô­”ŽRjAÍkˆEºy… ]T*˜ÆCKA5(0)Q ûÍ#$*ô Ø…†>Ñ2¬§NmW;µ]S½Õðc5×8EÕªZZñUóŽÎ¬tÕø0Û̓ә,ìšMâiˆ̤ŽN‰‹é,"]Û «³»$[yÚ…½¥Ï5Îj8 ÷jÙN¯£#ùípQ˜àU@OœƒÔ“ì¦Ú å‹j!ñB#BAû@œ+‹åLW~x ’a¯ôQ-%{Úq3ïFÁ< X"† „yZ“'êЦWÈ«CI%ìÏT4Ä{ÿç›Üƒt2¿¥DkdŒrF" ?ý˜U;îàÆH£ÅÇ ÒòÏÒ©ÂO9ö׬7¬âå÷U–뢄fp.¿,‹“â%Rlv+=§ãNÚ”;Á•vÆåm}Ù{²ÂÕp •æù"uºéŸïèõxìr'wàªéDX–]ävÙŒ?Åsë Ò>¥kSlA]‡‚r$8[žRA†Øá³¢:bÙ6îpvðùg €à¡{+¨£L9]Ü,êg †}4âh9­Ôk d†zæˆ yt9·MNI =TU®_hÝû ÄçÀûŠ€Ö|üúøh õ‡Ãl0†ðž•G]¬eEeñ@Ïx$fAÍCùâB·¡ GóäÉ®éMµaœ‘Û¯”Ð÷#-›“½*! •T¤„¨±z¨€Ha5ŤF ,Ջ޳#sm’ɪu¥¥ â~b– ƒò䊯3É#”2Íx™^C+vâüÐdÉ9Ÿª¦×¥TE¿Óé”j¹Ó9`Á"@À:¥n"A鈺÷CM}Q¯NÞ-#Ú¨Mã´1û2¤5É¡Å"HD=ÅeÁÞên‰£›l¹)Œ¬P¦^»¼’ÊŒ}{ËæY]-œ©/e .Jäƒ>8Æk•$¨ôFn±ÞÙàÜ{blâôc~£‹¥©ØA2ÉÃé$¿Æð\}× idXÅ|(¢pl °NËv1ï Úó.÷bû„}ëëÎÿ2÷ž-–º°>ÈÖáV…€Â(œ‡.F±)¬)B•¤ž±{ºd‡%Ö”†(½E÷’Ì‚ðÔm€àŸ‚(¦+³´ ¸Ú¹  l¥ÂTð:ñŽßR,!€HïŸHóéÕ ª#¨Ä‚ª2Á¢Ú}V¨îØÆJá dúÍë+|íÒóŠeS š3|dÝ­Q^†óíZ¾ª"´_h*MT3üØ—k0‚KÃÐò… S #\åÜ]cÐì¾}ÍáP¸õ1ÄqR*Ïð=´”O¹"붃ÃF¾® KU;½÷˜lmÐÄzï³°c%np@˵l¤S­K&ÖPPþzÈÓ˜¶š¦6[¨™.³@à›Ä ®C k?æA° äê0ée²×e ?—@î»»§©¼}Ÿ»º…}Y‚¦eM '* ‡öÈÅ‹9Âò>¡¡Há4˜BEçÀ—³Vê©Ñy™¸²Ý¨‚6úXrÑaUŽZ¢[®Ùò>9-@ù0ƒ˜OpÖAÕ}j(fô˜Ì4ÅNy¡”7}•Y/'ävÙ~1/^ž í·§›+y¨ûžâÍ«­ÊmiÜ¥Jaœ¹¿Ø/:›å†‹ŠšmºØ a¬bWU( «¨Äòµ¾@I¤©nÒ¼ï“ý†v5œ’·‘ѹ…û€³sÕê0ˆaˆìâ·uVö¥=Ç}}›˜‚öØâJ[•·šCk’k[ _÷ ëÞs½Í»Xÿ|Ý~´}ÞÙè­™| b[ÝÐUƒ…ø{¿.~¯lz Jô¢òÏ Š5ÈCü8ãÛ€’…âý:_ El¯6®5!ªì—¦ñœ[OCz”=K•vF¦ö©šÔïYŸ&agß YŠPÕ½gï@lpnfL§­¹þ›RŽR³^ú i§â²ß°Xå†K¦ä( ¿g-Œ©©Ïz€%EBôÕ˜öWìR²ÑGÏŸÑeMéAÃÒŠ JKb6Mˆ¸ ÛšÞpíË EB+bh€=½NúÈßÅïJwý•Ð)nyIŰMÿt1A;mÂ} ¼®€~Þ* U͹t§0Ž£ ž%ˆ€Z…æáC\#ÌaFú«ÈÃB©ºÜæ½&r[Um`T´ÒÏ-øþˆ¤Zõ‰†R1ùŽÇs}ÑÔP¯ÆÓØ6ueÅÕ_cº[F¥(oE*³u.`Tc¶ʮї5T—Õ”jÔÈ™9¾1ôí•ñÙYRÃbвžÑèT?«wi—½|F@²4ÊȰBæÂñ¢v¬€Ê°¿:½!t6É¡ýdëË6ýÝXvK ÏUcojÞÆÎ—#jc°ó¼Ô†K_¬Fv늑ÝúRwâ)â¦Cx:„²ã«*ï=¼x%¥š³@i\ȼ™Þ´j1Õϧ§.Üê¤íGª¡d­B ‰d¬”|oeèD S\ðíœ ¶,Fdmp¬ºG©ú±¥ ºá8Öéõc¼X+—ÜŒ¡µœž;޽*^P:åó´:²=.bzñ£[á©°ºeņÎ÷»Þ·š;7ùÁ«SN+¡¥iiÖLšTM[Âô!.EN! íhàÅ@­¢zV fØÿ‹زUVÚƒI€R†o›×½áÈ.nž“÷¤^å$ˆö¬ÿ9»Œßè8¨Ùiu¶C@º^ùÀ”ÈW:ÇYv‚c B­±“¸ŽŠ\ÆN¿6º]Q¶.—SQ5û‘3S¤:\4zTÌæ¼Ê3­µqÙ]Ø"k¯$–5Êiü|ê[sÝÐĺ´ 8‹]w 5‚n$_#}!x)y]£Ú±vŠÔ‹Â›:…­#&a°03ÏH²ŠG5‚õ#»5À7t:¼()̶Ÿ 9Á¡KU± } ÓEŽPǽ nuLˆ°]åcEØ¿¤t2çpE7 _qe.†í«ºØ,ðÀSïðgØ:«kZPÊö]Å—A÷N±H3;¢ÊIaâWê·{¬ƒ–z+-çÑ…“ý åRëƒÎÓ%ÆÚÑY[ç¯öùä>K»J›»M0ž9Nz)_§âÆ%/%Õó-a&^=’TyW9©Ò9‚Œ·Q7…È8-£#Ð>”'‹¼¢-¯ax“4Óky©¹9TZDüº›,¡‘6Uø‹¾zwzúî-äÙÖ–vÿmÔ/$^AÉâõ ìølÙ®?ÚnMeÔDs-^”ä3 ÍÖyÜV·rþ¬÷­%Wýèi錃2O Ñe²_Ö§óþpÔ7#ˆŸà_¦gÙ åÑÀcxp?ï—ö•££™Áp4p7ã\œ­ôV­»²ùöp´Ý¼PVƒµmÁÚÑ`±-ÐRãûïè¿»†f¶dÎYZ_";ÖµXÜ+{_x;¸ ³ÎŸHZ÷wq¢€å¸ g¢úG>ú¯Ò©%±Åe;{ON+<¼ÌiÓÎÙÑ¢ã22ÚãhPcùX2©ÞjAÓ@E¢a¤Þn wBƒ4Å.IµD¶û@ÝKâ1Qº÷Œ®[äbƒ#j¶LTÏ\±§‡âo(âãß®§{™Ü[à·xk„™¶%…ç­Û¡Æji´ÅV>Ù’í@lø w5g?–¿î°3)8¸Ë&Ú±²Lë«ËzÙ¢â²â,Ýbv½kM"œ±‚‰}×sØÒ|á`P–)¿¬iTRuŠá±ðª˜ _ ½¾k.(Vå$³…ùAd­m1¦™NA¡ëú0ªÝ€6PƒÌàóËy‡AnʇKñHÙ;Ðv3ŽŠ4êbýòêô2qpœ /-cÕ»èŠÿ/,uÃ5 ’¦aÙRn¬j›e¦ä†7Øzꪱm”÷³QkÞäÝ]Ò.(y–w Åû««+ó½5"³$^L’)iƒë}ܹnO¸‹ýÉÏ¿tΰ .HC¼²WӆÊ iᚨZ?£ú…*]Öª(×suMÆÔTh)ßó®7 ²LÛâX— ôíØæ=lE*哌µÊñ´˜_^p§§ Ñ]YÒç¶3ú{æq˜="Pvh‘†I»Ít)6å¡Ë¨¢4¦û«¼²0vÅ'"F˜g…PF—¤§®J-1pÝ“Iœ°_Ø-w§ÓDÞû–Ž¿XYlðfùò0ŵÇà(* ÁÛC Ü 2mßYWÈá3èUZ”mñ´¢à~EØ”øï0Â(`c„?v„“;’@XÞîÙC£Ê-àÁ½¸€oØ |;뻲؆vzsüñíÅÁëoNh`‹'"I¤ûEèšY)&°¹‡FŒ$/ꇡȢz]™FiÐÍA,! ¿Æ¸x‹(Í9Ôˆ {AÓ¢*–ÆÄ€'¹ñ4™½Nî¦ïÓ³“(Þ”_ºV|¨R$™Ê+Šûqæ¨/_5p^åûI: Óå ö“ÛKX?* ¾™OÇ4 b¨º(¿µr%HžŽ3­y{À;Gý*)âTB7SÆ8fªâBö¶îŠŒzí*tuù'Þ!€Š‚Xº+Ψ¸5•©9~ã[­UÆã¹ŒóR\A-C…¾”JÍ °2?EQJ¤© +ËáÄó4‰7~TáÄE6ˆ*›,giÀ`2–…žm{ÕÙ:åâƒÕŠ÷W+¾íÈ`!7$SLᨦŽGŸqª© bo‹øÌ}‹¤oqÚ]Ñ Óè–˜Ø9øT„d3Ev`ü* âºIÇ-\pPYð’ŒÕÛ®,hŽÑ¯Ýä›ö„(Þ§a6çpc¥n>åí2 RXç‘1¿”kÎ0ú¢€Ž˜ÐL*³8Ëk¤)j8/qÈ[lNõâØÕ9ó³¤A”iYke!VýV›ÞU.Ú¼-·Y–7ªmÌŠ'ÉLoœ³YYhèÛ߃©4¦ÀH—o™ÐÉ»£f¦òDzþ@Êgv›PtXrc΀B?ÝÐrq¢]¥*g+t. Ú\ÍòÎKŽ<ȆeDŽ€sQÌ«‚`–NÚ!ƒ8‡êž¼iXáÁ‹öªéE4Æ“õ:ê+‚Ú¤dš?ÙüŒC($v<æ üÙ#€/ÖO}=s®¡¥Mªx &¾Ù¦\Î:´o<©ÉªíHiÆKçyþó2ä!eâÓ”ßÀ­ä~?ëEjm÷u4ŽúÅ!÷¬Çé…K;Žæ|WG3à~P– ÝE9hŒ†…qTIë°6­;v=«#Z{x —A×=Ô¾)•õƆ¸¯Á©Q&á”<•÷RéÎŒêkÝÃß~©•"ïñcï"Å#cÚ²fiÆ‘ÿ…)8 púwج¶1ötZäJÄ›þd¸f˜óE‘ B@ækPÃÃiþ¬˜®,Stç f†o ÔQž>ÊÑ×)™¤~³l.„bRŠsþëäsÑÛ³®Ðˆ,ÙñÄÛƒyyÇÓ%Ç¿£´KB@Lkd\*YQžÆ§oÌ$;fh€Ù¤Z¿¶·ÁqaoTc ªÛíÎ77ô°V™ÅDiœ Q– ½-=å¸IûBÕä Ìq¶ú¤Ÿó›’«,ae=b.`ݲ=,eµ}êØ"ÄŠx#PªäÁeéÄ=Âù=²xû(×,• Æàλm{V*˜úö刽gl†¼ÂMªG¬ªíAUÛâ¤fg™—þ‘é{&À–ŸÐuKž3/¿”ò@ôû¥ 8Õ=­+jNrº1Ë»ELΘÃbš7êgVD-Åj¯œçÇãÆØhUð^ÅX”3‹•wÖÎbu»W}»[ÞçÉÌÞL«ÒÖÌ-Éä“tCñû)«pÕüb$júËÖhkŒNô¨kɧìcV @f·¤q$%³S7Oj‘~y ´¶ž|6¶6Ý6שaäf¤>Mf>¿Ÿžä(Ÿ&‹ßŠÞÀ”Ý͵Ôp]gjûÀSïãì·ïχ‹ùìQ{æá ¶{ÿ¼!ò#B¢¶sµ½Ø|F{×笠ÏiÜ£«­¿< £¡]uxÊfùRfÜz“n°L—½Þ[ƒ£þsH¡ôqi°jßµ9ú½<6òþ)tÐ&ê?“-´¦ß–!®Š†+VS ¤©×iGÒÏ=>δü©Ÿz*¡Ë¬*ë „pÕ¹[Wï}%©QQd¬«t t/×YÖTÍqÎòJ÷«TzÓW¥ac?s0TuC™ªšèÈ‚[Á&&ª^œãæõ â³ØWµa,k2Õ•.e%‡æP]kT®åZkú™ÁÄÐògˆ³²>¬42d^^eZuÕÌÍšrƒnGö,‹ÈˆëVCߘqp{9¼° »Çn Ì/5Ø —×ÔÕ__^Ûºä‡;Ì¥ýÊt¸™¶ÕÜ%/0Ÿ\ž„Á«$ää9›%ãÄcmß÷ž8íGŶ.ŠC¬gW°Púiru5ÔÎË:lfaYíÓζ¿x‘€,=í.'Τë@Ël †Ä/DB× F£0f»÷°½Ÿ§ :ÚÍ09ñ¹˜BÆ…IÒŽÈó™¨Üra‘Va‘…x ú+ Ý7<ß+J× XIR,œµECRßÖª?¸ÌYWÚ@H¬”®¡Ñí'bB Ód6 Æ¡lÿM„Ú”ÁT|(‘vÌ[±«Î[YÇIx§ H˳Uh¡/Kê•©5i¯€ê)(GŸ‰§\ VF4o6¬ˆ§sXeçʦôU‡UÖqÑŠ= Þ’ë…ËóUÈE™^Ø£Y³Ø—ãáÒŽZ!ˆMþ.bK4ÄFƒñªÄ–u\½V6„¨óÐþ—tÎU˜TçÏ:|‹Ýîç#\ÖwWÁøreŒÝ6´ÏÇÞ¡w¯Ò‘QUG\kR³žÖïµë:ó94}x«º½bµiƒ¶ÃÕ6í«µØÌ°Sר4Ø5jW³¡ˆ&WÑ|tCH‘ˆ¯öœ`™(qÔö-bK³ª8Ôª1 Úy£±†û¡µUf¦ø<ôΜúnÁFÖ5'í2i0޳CÇI±}„މñÜ1ž‡uÄuàr'w¿&6 2¬”H¾QCr'9Ó[˜¿b'^Ú4:1\­oÐàWìÂöº°ÞHM]c¬Ã5sÄ‚Bë¹Cú)ñ´¢Àu*Ù>¦(³ìɉ[—SÀt‚᛽6…/Cèä/E>&FƒÏÄè-¬8óÛGEiç3Q: ÒIø¨}ù™};\„ËHT³ÿWB>åuXvB[‡Ñ’¾êœ’u\*ÇÞe2ÿ «S€Õ›µt8…Y×xxƒØ‚eJyÓo¶ßF?i'™%ÖõvpΔjˆVѧûDyÍ~ }ô&º0 >mîû‹2Hâ‹Ø¨¥Ò$-ïYQ8g¥Lµ:´fyQðÎøŒ/âxaÕÃÀÔÅOÃpN!ÏÕØèSO ×±€SèòŽÆÐöÆÇ–} §L(ΧS9t›¤ È[­n÷zÑE—Ù0RCùeR}*2êÚ®‰zå¥m°o¶áºmDdo÷OF6U…ƒ™qFf*ºÚÒ£•ªÂEP³GFsââÙž–a ßî¿HtÖ´I(k¤‡61kÉh\FjNÆš<øZ 麜j »n6©ñÎurw1­ÍÕÌpDZr¤”>º³žpæczÕ©@¹ž TÎE‹*&ZÂ’”ùhùz,–,áÿ[s°ÑyY®¢âõ®ùSöA.±´áº»9Ï!niIÃE[^Z+³ªáž®‘è”®…±<º·9£!Fñâp*ø/ázsº§Ó¾ŠîÃqÛè:a0îiɩ싺»´ãTàBìûnåµè*“’q1NFF;Ÿùé¨óâY—¢bÚ"긮ø¡¤6AuÐ5*{¯Þ}<ÅøikÇG§«lýûéš#ëÊÚßÓ(ÏÃ)˜;ßá•Σd¦kE÷ó›·a†,ö*¹ï¡@‹¦W‰ÏÚm]g±Gdv<«óP#,^ßÿvïÃéśãƒ‹Ó¼§g~û=hE˜OÛ½ýnׄbÄ1U@í»ý´-‚@óaïíÛ½åâ·÷¹¼ÏW46ºšÔµ„m­ŽŸCÅYÅI{Å5Ù5:2Ä"#®†Î 4Š›œøkŠ—°†^Ý#Må¿ä‹Y(.lÚ72²ñ” §eXô^Ä(vd»¥û¤%é"/ 2$:äös_‚‚¥m+øXóDUúëÖ•…Ü·¼¥¢5K^Ëä1Y¹ªÒ²`#We­X¦j›UzIe)•Þýád»Vºuºž­?)‰—¯€ûâ&8r®ƒ£ “´?N)žwžx8ŒÄ »Þ)†k–CªÉÕÂ㥊ûÉ9Cr?ÅôiÀû6È•û%þ|?žßÎÌÛaö$¸Ó'Á£4PYè%‡3üYq¤u÷•%’NÉåG‘uåuà¡ÒŽ–‡‡5†"…{átœaŠ:¿à¡rL·âæùJÒÑKW†êÂtƪ—‚:è»ä-Š9~%7¤¥ ÙLÐ4uŒâÏCdM=®"EÈW¿ ÎÒ¥Žë:¾°D áÇÍlXhè¨]F%MÆE6c1<=-^±_®CQP››Q¸®†ëb8@‰Y/¾ ’L˜"1Åy ,¿K¡ý±14‚Õ)†¢ˆôaïtýÐo‘}?EÅ"åžé•­•êbI â?ÚÄ낚—7-õjŠ’¯gÅܬÛÿ*5ÊŽµQ¼,´Aß›š& •ë•«4ÂæÚ Ñè£íèÐMï1 º¤k«¼ÿæl2Im»1«º¥¥F—ß¡ ^–Y5d‹P¢+W³&#SªlÒÁHýV²äˆ”oC™ôí‡$Ri1¬8-Ê ïÜÞŸžxòDï(VY(è0ÿh.ôŠ P.]¼Ž‚8™Xƒ ªé»¯¹†ºÆØ xä‚k:‡¸²Åƒ3ª• FJ9{²jA$ôÙã¼ $À×F„(ät¬ –¯í¢´É:ú(gLeè.ûŒz)Üg뺹B”C<"?á±ö£ó“;MöÒEg!N/²"Ô“3dL¹`—ÂÌ8B–Ø@ã~.Äyèàxžr‘áFp™qa»´ŒqѼ†Šù³¼~T/'¥^êL8œ*Á(ªî18å=‡ÅzL>ဴÒEW¤ôØ¥ó-õÖ†-’ž´1óšd)‘ó/Î9ÎST1gp'[”Ö/‡íÇaÀ‘õ(f±8qóþŒ™EnJ ßôó8êC©â ±Œ: ¡Á-á®Î8­SO{Såe5ÙÆy|ƒÒÆ{gVŽI ýŸDŸˆÞ¢¢Èî‘éÇz%†Þ¢ÿÌÀ¡À¶s^Þ€j`$‡´øÈŠ\©¸tŽ«”ö±à“‘GÕZ[ÜÀŠædXG§Ô^rh\.Zy>÷VÔM¹òÕR³|5uÊ­¬Ò‚5±BƒjXh¬_[&Ôq]NG& •ç¦R–QÊ.TEiljw©÷ÍšKÕêOw£ãðªÇ©õŸ¢•í…4ƾþKUÒÚsåo.ÊYügèV 8ŠÀ{IZ’…øÂ‘‘Þ£°£~¡¬õ(Aš¿M©PÜ,jshuKޤö¥–ú+´T- 4€ƒà4(¯ÇÚÀKï-%ÅO¯ÍUK­j1–†W‹ü¢ÝÃ9cÄ =¿žíæÒ¥ÅØäSZž‡ÃÊæ‹`cgm´aR²Òv±Å,YT1£5×(Ö3ËrI%gLeï|¡¶ÉûUQø1hÛ<ñ^©ä "áÍÅéë‹ÃãÃSêæÐ;Qû‹È©çªØû¯÷OõbZl~UòÕÇ‹$@G½èѾ(].ª…éW5Þ|<~½÷öàøtïkTFã÷åA7è"±T|"½ë'§öN¾ù+H±ntwÝhU!®ÕT]®( }´JË^[ä ‘[µrT‘Ÿ³"¿q«r‘qHˆžìe×+Ô[¢¸^µ2PA/;ñƒLP´¤®­­Ö„ r·”3ÂY»·r#blº5©%е b«5¤ {וrÂÕ)k±oÞ%É}«Òîá-j,ߌ¥Û·+ó_Ñ^’,»Y©þº(eœò'âCèV$³<ñ6¾öŽß{ß½W¾{Ïàß÷ï½ÿñ¾Ã·ðG½>¦×¯CXjáù1SïÞÓ»÷ðT´ÞW·ÍÚÿ‘\OÛª}X,‡ßÁމ^Ý.èOP”8¦÷ãdB/FIrqÑOQøapÃ_@o¿)j~G5aè]hqú¡„Ñ¡‹ô ¹P¶S,Ih‘Åe{à)¯VÌY”ˆD¹t¢ƒÕ½À+°hVË–mÀ…ý— ˆ$‚†žÏõp¯BJ ìUœÇƒg øµ ºM8Çz»ÛÖ›è¿b|D¥…qöa @u`ÍÊÎbGŽ_ëõ„­}IÁ"–¾6"Ìî‚:ÏÅê_í‹ ,uqÑæþÑLQ«)yÃ``ÿ S.ð„Ì£”¸½™ßÎ6a£‡ y ÜUÕ‡ ÁŒÇ ¿¤‹¥ùLc=à ¡±/°ðŸ2¥ÞqÙèv–Î0rxÈÏÄ^:ŸúmLj0ð;è!GmÊ—x§cÂÈò@ä¼~‚1mùwïÿõ++ÏØÁ´à˜¦—¤‹Œ«äÑ-yóæ·íNF]¼z¾µ¼öˆî¤1ríÖŰŽ)Œ "Юµõ‚þ§>ƒmØÄ<¾³eúâO{ðbçYÿÙ *òœ÷Ÿí ¶·{ÔßiœßlPŠÌ8Sú¹ mŽ®çÓ>ÅJo¶øÃï8€Ȳ£`:™°ã8M’ø&Êw½á$¼Ÿ{A-g¶#L…”ÌœÑÝßïxÀccˆzäÿú>M0cÛ›ç×Iºkøµz_…ã¿üu‚7×PŒ"Ü¿‡SŠÿ5ÔûøáÈ ”}é%édß¾ÁtŠÑ&ʃ+¼A˧ÚYzG‡ûÇ'½Óÿ<%Lÿt8Þõ²”»]÷ž=ßú büb£?ØØî{[Û»[ƒÝþa´âOáô2LÿÔjá²³‡ÂfvMB?ú°Íä~'©È f2zš†¥¤Ã$^˜øè×âv=ÿèg&+Q•‰úuŠŠõR|Ù+ùú—N¯ÕúkžŒ“]ooŒ~Qw™ Ù&|  ¨´ Ÿ=¦ÈÏ_Üã¬%Ä·4f˜ ö†˜]6Áô%f½Lî=”LãØûÄw.àƒ0u¼Y~Ïs ­¨Xä2™ß8ÈÜseºº$ráY}íÑ(µ„HÀɧ¾ÃòÆNþNÕ›šLnU%„‘Œ†ƒÒíŠW˜úF{5Ï£X¾ÉR´·xÃ(òT¦óÒFµ”ÎKê6ŸÏh´T}>³)Y}‚séçoŽÞÿòÿ±÷¦‰ëÊ¢èûܿ§û¬ é$L ™N§÷5`!@˜BHwŸ\c 8 &ØȾû¿¿*ɃäH:k÷lÖ¢!X*•¤R©Jª £¹Žˆ# oŒëå¿¥3Ú7l>hÉO®º? 2 I@a;2_ ™$G «Æ&B<¼S†Iü $ÝSi5l(K˜­!R)u­£¢úmÐÁ„(Éd õŸ‡[^Ä 50®*CÙpˆ´DÖfFÚYs ùφãžALS,4$t6ø«#5D²ÙÈùÔõyO›Ò³E€ùdi«°3ÁÓÛúíB¹È­nH@4óÐT_°n‘l΂%Pcaº˜t1P¹À¼"õ -%ö^Ôy—­ŸkÒ'9dmê­jׯc1«þ%ÅȘ‰&ÝS`Ò ÄEXó’ÞP«,LÖà€‘T‰c°|›ƒíCÕSa ƒåÃ-R¿¥¥`M€¢ÁF‰\_Û½Æ%LÀûº&DšUZÊÔùºv­ºžNÓR­+Z·ˆ¶OªâqZ( /®·´Ô5;wd8H"PÜ%Y@7 ZäsëLÈœg²¡ Ð Ã< ½"7(e‹`ÊzO )U©PÄ+@n¸‘̱ð'æ9…BŸãÁav:&ß2f… øT¼øÌÈFbÝ^¡hQ9ÀL74\ÝqcÑFãùµ[¥Ñ)“¡o¬'] ¢ ëUûïv¡1½ñ¿ ÌJ¨ ]õâ釮™[¦ &© Y8n• g&ÆÖsöZ • "C8;$™v.=ø_&§Dø[NÆñ†‡ 6U†–TÄ Ð@ʰÐ_I a(c ŽHh'Ó:žý‚ØÂ~Oíç¢ÖÝ'e<—ØãËQKœû‡K3േx˜m œmJNƇ iÑ™@=H¸Î1¤ ¢Â–³‡KÅÅlû'!2§‘åXTRP20»Lu]c¿Ë†›ìT¶ Åu> ç}ì|-uZ €\rê ÕH™A„®Ñ–žízŽÑ`ª C¬Ùàû@*ŗ怷ø™Á:Åû“¨Ÿb/Àî+|jƒáϾ;ßÔÞ·¸û3m„¢«Àþ ~t ƒ^c˜PT…=4G¶NDšïÎ7hÅý.µÍÕƒ-NÐߦ˻AÕú6iúüû-Nÿà¤z¬tލæ*–Sä ªCS,0ǰÚ9eñ¿È&n-ÚDý/‹ƒ‘¬:^ yÖ;žh€;󋦃²g¥À"v·‚Üëi–Âm·@y·”°exH|ôœŽRŸ„ñœ¬,à hbTšºÓ…«ŒÅ1ˆ2½Ç0tô"Ð ECždÐØ80˜™B‘i C} ²Ñtí]Î »¤¥Äcè`Xüº$RŸë¯hâa–Ø#:0ò9S3‡ÀP@¸P k¬Xê ø‘à é1í Ý+ ‰›¦˜ÖR…ç,zD¦Áxª:mLAVdzÝ(´þ!Á+²wà_ÞƳk$Gè‘Lw3züocdåigª1ÞïÑÙ–PPH&X¡Ï߆ÉïI†AÍàË߀á$¿ïsôú…•BÙ Î ÖÜ‘ˆJ¼!fáåÖVÌ~ÅïqˆËßX\þºÍÁe®NôÀÅÓ6…CÑRñÀ‹µulDÈÁ'‰ƒãUŠˆ¡è5ðÍB\¨9k‰Ð#ÁÑB0 Ç9fk °9ËZê;â‰ãÆâ,˰§ª–èíb xl¨Ê‚¤ÁG˜£r²8¢Á’O‡Tî¡”(œpÄø¥"" I®0Ô ÂÐXà:[_uýV6AþŸþ4¢C†J¬ÝkoYy:°0dÿi~ûö"Ï¿7¿ÅñãûOS&¤DoVAÈØÇ\ç£TóážOä5=磲ã±€Ö°ØŠ–þ 4:ÍÌÚUØG›ªí#ކú¼P­ "ID$ÝÆÅ*16ùê’xÿKþX[(s÷Ô&öÓœÐùp‹À‹J໘áŠ/eîý4›Èz Gª?b>§™·ö« ¡ Š$ÀùŸXKqRº-‹ÂÑmH!MoS?Y»»wNÏö9߈&ýÅ5é)_ŠèãD°úp!±•‰NºÃýeÏCòñ0šŸâÊ‘V2€»¯–Ì1%ìïß*•ï(­‘?~N#\!g- ßÈ0|ÿ¬èËx¥(Ñ¿·W bä¦*"w™Ø×· Bµ‹ø]¦ðt`»ƒ¹íØaXrÍ·>+Hhñ\ÔíùN¯g¾ÿzŸ{KCsyº±þìNãu¼žvƒŸ_¾Ë澓Xö¡’µj¬s“±ýÿ"$‰x6¤ÑPç¶TŸ÷k¡juƒªÕD×¶W„ÀDˆŠ°k"Â?ùFï¾ãïÐkr(1PËÈ S?4]?Ìú’,ãÔX‹V`a°=Nºf(Ô²Ã9²Ï“½^¯ç)ï(’vy7js¾P=À“»”¢Cb—ƒ £”¼D“pë&ä!;Cé«ÇV;ñT{!q.üãrvÖ÷ô“Rp`á®|ê)L7°ì‰|•µms?ðâ6KŒÐr„|XУt'Þ<椱ã8Ä|þü™! Æ='§Áhqr´mXù+’ÿ¢f‚¶QƒýrC  pбæhˆXz)¦jx£EÀÐÛ4_Q:[ÄÛ6Ѻ+$ÈÚîÛ¼¼JQ¡,y‚^e±ó î>šSÚV¶™¯#¬z«X#OG §™œ›Ý_StJ.„œk·Iïß,M¿Ý†q@úÎõ0È#Gãð(Ù¿2ŠòßbSo¡±i†„žµÿpJ9_´¾…»ë7oý}¨•“ßœô‹*ZïOÔž—Æ Î‡—ŸpŸ¿8C¼ÄÓÄ¥¿w´ê’Z:S³Ô˜S!fM; …ŽsY›ÆÌ•Ùûòã"™Hðáñˆ«· ñ’¡éÛµËÓIð4ÒbË7‰Ž*Üa~‰F»kMGSÌ‹ç½ÑýÃ@— fÀŸm2a‡¹7…¥!ØO» mÌXç;F „v‰;4<~™w1%v/½­œí­ÇEób*º»`#Æ–ñÓÍ_Ylþ E#3MP™ ¢`¡:&–£ »D÷\ÍÈÆe)“#k¬c—¾ø–z _¼­xç T)9œ7ô„[ì„{[ÚÀè…(ÏÉ÷ÂÛÁó"Œíu˜ô§O´©u+àmÂj¹¯ÍÑx~¸»Œ1^§Ó³ Ü£‰'Xƒìah8A#uMŽL‚fК h’dÍž*lXÌMfy‘Qqœi\’Øxp„/rh±iÐog.Š>[SÎÂuão[› œw¹X«íiÜø‡Z Á ¶¯îyŽ;žšì°ì8„LíŠlOĆ¡¿GAÓýºçíËÍ_Ý*·@Hßÿ&P'"{{%‰Q#$„lPbÙõð³Ï›øpDˆ½La1³ÒÉhSÛÄ=8Ðg¶ˆC¿6GŒ6qÆ,+•u4²ŸNÀÿþ11Ùµ/ð}ΈžÂ]h=ù†ÆÆ°2Ÿ¿ÛÁ ­ƒ{,êw Rj$6-® °;×0³1B‹@—±˜Ò.DÏœ[lÜŠHRœš9 èxãØ1å0<«Ù K´÷!æ¼ÔÀ•¼Ññ# <1‚š"O­Š¡Ïq´€„°ž[ô‚f±}]Y`\f¶' ³ÈܾNâœYQÃl]‡Í ¢ ­pû¾·1ØäÔ¹¨Óž«3îG¸[« z$¢Žy„B —r1¤5뜅³§¢=X0ëàǃÄá¡óMxÆ* "?üT‰ÅA¾ØDÍS‡šmJB·qÌ|UpƒÚXÅtª˜ú`0Viˆ!ä j`½’º¾ÅýÞ©F»»± Rx,Oº=Y°£ƒrÙ÷6Vתs¼$BxOo·ÀØ6/[ªo›#†@ë*z—º»ñ¥²˜k‹G##QAÚÃ4°§éb@¶¯ÙX7}ÔJYM©~šmºÿ %–£(D­Øö—JÉÀ¬8ž¾ðžÛø¦Ã0SÞ¥E°By”Ç¡øO4à°xÓ¹£œ&3ÍyZO0 îG5É´ë‹@ú5yÇ›ežÜɰ3êDÅ̈õ°$YS­ç.«C…ÄJ¯º5­ª]6(Ùiƒ3{RM½3ã)‰ó¼[«$ LÖ\Ü$»7ÌåUÍšs`HoJ{ϲªÝP&¹L>UýM¨²l1€B‡×ÂU¸¢ú‡sr[nÿ`YjÕŽH+OÖ»-ÿ:× %êŒÝón)tó4Ÿ—?…®4ÓT¥w]JvÙš¬ßÛúø¬ß'¿‰ÑÇgýNýî }xÖï­I¶·aôÑY¿~#ë7Fo\Kv'˜µDÅŸ]W“[:lÒ ÷”o·Ù3dŒÂ¶cu®´—¢ê-]ñÒ‡dâ;‘úu"õ‘Hÿƒ:‘Þ½–!ãmxˆÞ†oAÈjuLJY¯vFù]V«]6h¿'ùKß/:GÛFÁòZö3#»­‹ ÏVîÒÅK«…éܧDnL•A3¸hÉ€söPIâsœiÓw‡!1ºée±e=ÙÈŠ7Ò#ä¥AÃ{Õñê#·4iAµ¬ôé)Íc®.¶oŠ’:5/­”‚D)qÔF§¿ðþJÇŒé91cÁ›Rò€¨:ò›­Ú *IlûH±Zµ}ñ ?C Ï¢^E _lEž¾È!ºrOÅHµÑJBüi‘­Š»{ÿËv×#ôÆÄ„DgžK.ud{DŸýhұ뉧ñ Ì¥õA£D‡á]˜}áªAp@ùNgªºˆ_x xú{x‰1vB¡o ªï"¿ÿÖút_ÆÉ팖¯;ṕ÷w¯ŒcêÆ7!ˆšÛÝCvmlY\c¹ˆnh ®ÖÖ±Ûº¸Ú¸ßc XƉ=_-Ž@Ý‚Ë{—,#î·}R+é¯Å­<÷_y¿D¸‘m%vg†.‡ |HÐdãH>ƒ¹Ö£¸g|Ž˜ô0:¼Ã·¥"”P©ák4ªÅ“‰GŸ»í9!ÔÍ-šŸ Àp(Dáã/¸&™5º½ÕkÜ—î‡ôötü•ÂBx¶cÕèꀊ:+²#ЭÄÎ"¾fð¦ýôó}CT>\[""ùØÌOúµ«{¯{«‘nY-sõXÚc®hYé`cD<åF[GV“Žl9· GU=¢7Jÿèýñâýý×° dÄBñ >[á¶Õ÷ BE½#¿°Í`²Ü“ …·îìoÁc×qð¡! Å•-ñêõùù²öš0Žv D7š§ÇS§‹ "ãþvb€ÂW!õs1a;;z/ÿ#-†q±]ÛÝ™ jn:nÎÞn7N¥ÃŸÑÕaê` ïÕ>|î§Þ ’X -ÑGÿ[ ¿‰€|äôd…„ÀarE[s5î áXªÿÇ?MB‹ÎH|EƒŸýàÊTÓõÔ¤Ó·©Zž†ò±ë‘Z” I­S^ñ:a`[Â\àËš¹/sóq¦œy³Ëcvº<™¤XDéclˆ²1‘Þ;»J6*ÚþÅÈf¼~_úÇ:CèÖqé1>|œ/¦Sš£.莔ñ&³5f” ÿƒls¶•z ¿3ÐáÈØ/ÞÏŠŠ=‰´ebäQò6þ–Íý1Ķ} ç?xyÇðC€9KTe„—jäŒT˱ÙhYlSÿ'CðSúBÓرõ0›‹4½<1øl¤H#Q µæ°[ï¹ /­mÈÏ16tÙÞÃüNƒƒÖãμÕ&MR§½(Ѧ|§^Ûô€£®Ã€Ì š#§ô›šÙquTÚnξ²Ln´ñöØÙÆúͧ•´‘ Ól n'Y¿°Ò£B\ŸÂW+ÆÁÌÌõ5JÕ§“Ä–m“?̘R4ãñeT’!ÝÛž‡‡ŒQ8ñù#«ª« ‰]¾íJ<¸<ž[_¦Óo=P½äø›› µ˜g<ˆ_‰}™Å‰ò$,¤Í(R?¼H/eö…§3ÞZû &ñv“B†gïÅ`A½sJ%.`$kl¬Ê/ªáMcî]:1ò/©q@rÛ-µâ%›µí„ÄÃ{-à™9Þg/Khß¹~†ñÏ ýª>2cÁ}¥™ ]ÕÇ[ý«u§‰ð –#ÃG÷Þ€Šòï­Ý´ÎüaV… yݾ7&ÃÈ!`GxÓ|Óà×Afs}`y›¡ÖøÎ,ï§9ans aκ´êÉ3Œ¿0^?’-kZ›,bVJ…½ÜL4öN1ÐU6"@¯ïÍ÷2)Ï…AÏD —zá±pp—ÜH84¶MW5—(zƒ¡j牻ã2ç‹íNÉw¿cêú(Ø©N…ï¼&`üqõL¦÷ß…tâ"9¹Ûƒûóá&‹V¼SðÌ-©ô5„XÜÜÛÆTü­³'ÄÛô*@Ölï ëýycï`8ví]±òŽÞí“Þ¹ôιBYôÍù/ô1kjžyü%g±_->Ć•ÙóÔˆá•Ç£¢“©ܘ/iˆ–ãÝ*9RÁæúÒ-tJ'w(*ÍìÆŒ(¿£»î*ÈÉ𜖶q@ü;,ƒ6üºu?³GÒo*ØA†XÎ@›7øÚ¨uÜkY©_„ZÈ8‘lHJ‹ËHw,+£ÐÒLȘœy“ = s¥SVf0+øa˜š2Z_Fmïm ãP5éV­HíoˆÔ ^È$D7j=ói»å»Áç/­*èAb0gQÇ ³F;]_€‡mÒ-A6¬§–QÝšB³Qð·A\C˜Û²…$ßyÂ˜ê° 7ÄP‡týÈY÷öH0TÐxÖØQÐä¤ÜîJì¼Òœáîc–õÂx‚pÃîHqxè#R{!qñžöb3@7jhµ†‹mi¿‚:ѶöQqzûÌDÙ¹Sx @kËÍœÅq´ÚÌ^l”øÐp{­q3w¶¬|5Ij–Ká¯{'‘Y0I'w¥5®Râa¬Ä¾Ÿ‚ÊNº":õA×R8n=Šç¿" ØAsµçÄá®}Mù±÷\º|Á™~ûvK-hG#5iƒáËÁEÊr¤ëÃq^µJÉÀa/mÇT4¬E´pTƒ”j›L~À?¨vF¸btÒ=ƒi‡ö³,-/]RvÓ‘?O3ÞÞ·x°×SÒAÒ™ø‰_ðÿ¯˜;¶ž]6d™òàÑbÜ{xK>i]ŽÑøò€æ…5ÜãV6–p€€Ô|?¤áwį_-˜<ŠîTyz¹i¿´šãß[6K]ÏNÉ= Þèø–bœ¤ç;ËÇ[vY|×£ ‰øìx+ÚÆÜ<¤ª¹—¡¿YT´¹¹7àc#AÃ|´·œ-¹\ÊwŠîäMÈqÓ‡4KC­‘Óïe!iFŸ;M/(l9âwók»$¦>rç ôp¦Ožb–ºÃþﺔG¿Pí€k´Lêpª8Ÿt `{£|ž Î|yk›=íËòn,Šoÿää$b§Ž˜êÓË;É’œÜHG‰·CR1r³):( ¥ˆÓžCî5DèõDPü˜©Ê†Û ¶º•'À‡á±oÚBw\oõD¢¿cõnФ%ä][ç²Z&´:¶CYòQ |VÊ›C#X'Êœ%…O’Þ—U +Ü|Ãà­óÅ[©×.:UÃö'´¢<HLËðCÛ®Åw…f,ñ?z{ÄöŸãQ¶ïÔ,-é 5ÈØ˜ înîvÊŽÐiuªIb[m4‹ÁÕ'£q®æ„+…îäW¹Üë¹+0x¸ðeYK±d€žŽ¿L’“Ó'¹œÞ1~<4óÃ7^îæŒÉj“;khÓà#7¬^wð›«ÀA2gÓ Ó\a±']›;KFAŒùÃÀ7î$b:Ù?­Ðé†>7ÕžunGòÅð‘f ?R×—–)FÔà(ØÖQÖrˆË7²Çy.òI:D{¶Tábg²=}M%ü³H PPØþè‘Êð'Í{a…ýšÉ&:Þ˜D¢1ö¢ßþ0¾ïEc_ÿ_ãøh8ÆÍ „í“©EÇŒaKÇlÑc¸ÓŒÏƒÊöÔŽd—‹v°þ&1œ¢I@éß(½&÷ÂEÉ`xû’÷ã‘âðH½ßˆ#£Ý=Àw['ÿ:Ùá\Õa™œ…cøÞËÛEy7ª–2ª‚'Ä¡¿)¼}Sû»4år@¦¸ ùŒ›¶å¼ƒ6õ‰°ða#ŽwC[%[ÐçàýE­,Hˆ®ëŽ$w60ûíˆÜù/,h ­B6øZ¤7°\3¶Ö/E+Éà3 öz4%’ ž¢OAшäšœê£ c·ìAÊN±Ü|1…žlmß™S°íg4¡ÙI¸áúAψ-ZXõð$%<Íz½È§÷ m=5åÕ£cÏe4݈¦™@_QêBé¦k²®1Èå@’“=Ó: á1ØÁÝÕÁɯ©mB0ÜÕCÖ‘ÍF¿šýŠU³êP*FÕ&”uC6îyøØÙjƒ\agcÍŒÕÓ=Àñs\-OÈÈ·èƒ{AkŒº zè ÈÃï‹Ð¢Þ›îIÒñ…  ‰`ËazÑrMÚôÀ_F:@yòþèËž%ˆ;,Ån§2øÃçÂJê£9÷¨ÔßòÞ†‰Ý‹ vÛ`,†Â7(Î:.¡ŸÀ½æ.„îOöϾ7o¾a noÑiÉÊÁ°)úñ¦X$ŽZfÃñkl¶_©¯sÞ´eë"Pçö'*Iî½ONr*êSÆŽóÍ,&Ï•Dù³=´Ú%^ñÖ­±ø‘üEe5c©™ Õ,@ˆy3³áŸý'ùèCGÀkô8ó½¥u:¡on½òuÇö'ú‹jê‘- °þX—]_·_SøW lඉŠz¤+÷,ãU? +ƒkÔ! Biî’ÛfnüI3V–„æMíÈRÝsaØfX,ÁÑ–ýâ"‰"# Cïxƒ¼}—•Ε-=ü>hÙzršzz|ÁÇû”o¾á ØÂï÷6Úk¸ÀÉ)Ê`!'d»¢à¹þà ›™ë?VSèQñÏŸÀÛ F±[îÍ.šö‰;ö=ã[å¹½ùkâ"é>$ùt!r†_<M3¯;9¾¤)Þ}§³Û/Ö°•°;5oO~8¸YpYq,ü’¥½–‹[7s 2ok†xS‹‰­?.“¿ðxÊwÂÇ•-Z·Lh5j¸=¶£¶Çî•77¬EÏ!Å‘5óg—ŒHfÇKî7>BÝÛp‚¤G­¿ŸÚ­äž0G f>ëÀnݳX‡ÌxÑâs%Ã6`Q?‰ÞGh4â ‚ñßdvé()x 4ùÖ˜²¤@ Ã#¹'nȤàó?ë’º(•sÔK/tª/Ä Û£}Já´àg[L=7Š%¶Z—rîHoTšt£ïf=v5så"Ö,h·¶®môöš¶1Ðî5ÉÙ$g~CAØ·~à¨ËŒ™‚_Qö_§#‹Àªvd© LöajSO0¤mçf`‰þüû¿?þû ŽÑ/±¯{±xO"Öp?Ò0w±É0y í£fÑTÝ{ë»§¾~NÌé^ìáétPpÿuëø+©á•ÂŽ“CgË%-ã@ðŒCà ~äÇ·ïýÛ/{à‚W=7Þ0œv×ö0jW4˜­ñí#÷ö ¨ÁÌ4Ùó#ß¾G‚± a{úÇ— ÈŽ6´×e0@†OY•‰Fð§ñ¦ÍöV”㳈ç¬slLÒ[áã0õ7Æ=S{B”‰MBè…Ó» ´ ©ÒÊG‡µKr8È=?×'ä‡f°†çžMGs$þEª¢¥1-{Â4ebîàÄVíéÈ+‡Ü}Ï[ŽÈ}éfÉV¹=(iØÙ8ÏB3|S´Ê!€=¸Gаc]Ç£ A6ík§‰ê‹WëåÒý9çÅß©E˜t蛦/ÍàÁø/5 µ…¡qn54ZU‘p„˰„ö-ý±<ƒ€1¢¹—ÿâ¶ozCÙÒÐËaW:ˆØTºcy:¢ª`ÌË(·!ª¼pÁ{é²ë} ¾‚·Þ€{—íLtIË”i1= 9D‘NƒC8CŒŒÛ³}ã”so|©7ÔéoÜýæ¾oGh²ˆ9„-óÀ¿cÆæëmY½÷I! kÓ!¡ÁE·¦Ñy~jŸsú:nŽáÍûq ;&=Ÿ 8¶ŽÔ‚ë?ภ_agìÄ3>P[Ï$½uYW¦wVj™ÝÙSng±rURëZÙ`YwPèÚúo?-t[¡†›Ôª—+ò[v·_i7Ûàÿ7 t9ù»™éúZüóuCš¤½ôêQª¡ìªØaôíál.Ûä‚ U½»Z”ÝÖŠÈÈ÷½€=„”¹1@@³63CŸ¨KüÑ>Ûuòv¦%ÃÙÆ¹Xê è{¼Huàz: (޶-Òr¨ÝU ‚GWîż«$6ˆÐ|šA }{Bt²ÏÑ¿~«T¾ÿmﳟoT¡B}†¾à§LLЍ8@Ì'ª3@F›Inò §÷ZR2·ì¸ Á$à)-±‰Gb7|&‰¨ÐYW™ì‰}#ðë 䢀4ýkïc€A~&ÙP«°Å[»§Ò[6t·×†ãvï ("÷xt4ÿ³í§C$–_ÀÖ s|öV ¦sÌUƒ>Ekw dÜ£rO°ª¹ÛÕ{¥ìåf[»¹h0òˆeëCž5Ä;Ébúؔʷ7bSØFT™QXˆ´(ÜZ‰¥lÍmÀ¦Öþˆbó{ÿÐS”½¥„>\`-Òo=ø‡MœäfK (h'I‚×Vr¤€RNN$,eåä2È•–[îG7ÈU—CÃtÑsìß×@£”'A¢ò@û1Ï,úæzFÕh„-©ˆa:cÀ_6ìè@¯L.)BïÍÝ C#ƒøJ²:ÅɹtúàW™! H2Ä«ænXíÀÃŒ G!‡Ib,è‘GB“µáE˜ÜYþ…ú ¾ÀÞ·ûhù±H^übÍwò¥eq:I8LS6㥊$£ÁÚ6E×ß±Bf‹z±‘pÖªÉß—;è ¼ùÐgê4jO3è’!([Î5;Èh0úƒºfrm [¹$š¤BUÉË:‘ ÜöHZ îã¥ož/ù龤|u‹¸ÐÇöyñß-e¬œåÏX—{ÿÊìçñƒ™Å±Š·o™¾mÑ8rÝ‹¡|`9ë¦!òß?7ï‹?~?¿¾ú}sœ—“™/ö6@8âÞÑÄÆÞÛogyŸ!ßiß]~ïâÝ>–'.¥¹2ÿÅw0Òƒˆò]Üo‹le&gjÃÕ¶Ä-ƒúa³Œ1¤‚_è²êzù'c£=iÑvË_:ìᢉº&Öh‰Æ$¼¼¤;«ÃŒí?×MÄbÑ´µgÓ‚­°w¦ÙWâÒ9§ý€œ˜ÌøýœÚÃý -e¹BFgpÉ5{V=°w눚3®³Hìýb¾~CÚIñLv1Ÿ#UÙ3µEC*‚ìù Ƃ᷵„ »ÏÂú°8<›=3]l“¾M_îÂÏA¶íb¦ÚjâŠø\¹i–põKÜê‡áÆà ½°ù9ýÕõszj½±Š¦ua~7ÎpzòÓ'Âg3;ÿsÕ(â¯nU:ÅŸ ÁG"s<>‡ÇG,ñøhY[PŸ>=>ÂÀb PÇá§È¯OŸ§ä?ç•:†Ýûää8á}%­äQêô8LŸ’"')úóQ €ÄâædÇÒCbâ “8'Æ¡±¸¢íKtã~ˆÍÖÿß¡"ÃØÈcáFžèÔÔõñH~›eʳÌõÓ¡¼P†$ùLH¡U„Ùè‘ÅEY©M@ãh*ƒ–¶ÐÖ mL<6uä®9PÄ‹ghÒ€a‰I|Â+›Ùš†Èˆf÷èbò0 ýæv®?©Š ÅÄ…9ÔçBc1YŒ†Ba¨÷FªðÍ/ÿ[1 5¶˜‚(9îÆÔÞ"&/¾C­VýæBø64ÍÙE<¾\.cäJYŸâø4OöV¤gÄ·}Òiä…ªpSÌJ•†kÞ7?}ùôÉÚ9Ì!jZè³bÿ‚?‘1 °¦6¶·‹,™§°\ºrW£·´U(?WŸsÀ™RÄuÃzLŒ|`}àgNGBc öæò’kñë§OÙjý¶Õx¼©Š9)÷(ÝIÜ¿#ß¾eoè_ß¿G>IõzµNÊ+…G« SVºÉº…?=椼غ!Ϥé`¬à !3×—°+ëóÙ¢W‹‰>ß‹|"°ªu]ø3ù¬ D*OÖ¤Fã0+6­º6Š4X†m¯FJÅØ–(cä²Âj#­Aw±Ê[à Q’beïð¡êÃî—?"SuItIô‡ßdôIŒÌUtf7"¾KTêêXàáòïaíR8^ 8¬ÝUžOA¦ýÍfÇÌ®­6 ½w„¼Fv±ßmÝ àûÜ®XÔa!Bµßr f×V­5ö{mHx‹•ÛHÚ ½iMgÞCm]œm<¬màŽ)\ë°OÃfô¦†°jWžŽBÊÑÒ ä½†6…ìm° RçqK¹…©@OÄñL›êok@&uBà^¡ÑúÞjkòÔÙ¡·µ ‘ÚV ä¨ &Œ‘ËzO“·°c,0O2 X~¬Ï1*–ËáÈCy¼+½õ-!ÝcÛ*‹ÙÃ2lWU§µÌ\~Õvnk"+@ųaØ`r­É@™=ýýMaív3yJ›ìiRãö{šjÌ¡ÿíÓ'e,'›ÝÁÖ@a?fÄl©P¯¶*9Øsoª­úeäK>Ÿ_–CÍdÝÈi¾ÐGï56' Íáý ÛHóê?¤ ::=ÂR61X…ç“´1ºíȧ=LK› ŠvO LÔé³Ü>džAÀhÄêúq(OAîš^@änƒ~”é•%ÞXr/Óªé(a|–UT+ª>QÍù:I§«“tbŸüÏó`)rˆ‘–µo`,=e­TnYÐçúøðù{Ä àF"ð¥A™$ú0´~­oÉ‹¶Qœ‰;Ô^øl~´à½ÑòeÀð‘žû>iž‡Ï.P´:¿ô™ãS¬è<Â/ ÜG=HÖ°l-»új§ò3­^v*J=¶uìÆÝW¦A›Âz žíƒïÁô…fx3çÐL/áÂöŒ9¡‚‚]žæQp!¹ Ân¤|ÓNñ`1¿ !(kJͪ)J5¾Â¦´HÙ˜‡“œ'±Õç²}vCû`) ±‘º†õ³,2w褎xÕ.Ì2ð!®»ö€¡€<96¸yGW¯LÍ©>}´noìNp7 “:$1âOpOƒ\s¨)#y R(ÉÚ Ãí[Ø “ }”!fæŒI|K&Œ©xrpØäÌÓ êjÿ²Ñª”¤ÊnDc1–„C>I‡|¼FEVê_íŽKÏì(h¯´˜Ð£u^š¾Å‚E¹Üó1Ͱ”]Ī Ižëý>I>ᛴ®J¶b‹â¢Äãã2"­4“K¼œµÜ“;Žì*0*Š´:—M’ ì9âëmU6¹§Ú­â]'×*zƒâ—6ªLÇQÓÝÜq6¡L¹Òví-Å ’s¹§éV~ ÑÞ<{rä0SrÇõ ¬ÕË”7 ™3Þ‚Tú£J RÉÄGaEn)ߌ–6}ÑG*{¿`O}Ö 4Ø}S3]È )n h©§²±ðK½vtIRÌí¹ëa:mŸéûó™MÂ*¯`·œë ˆuKáa`^Aé-Ò³C¸iƒä-µññKp®Y-És5fz‡!5Rž›SV2¬Ì­Z0¤É ÌQ/hðŸh_žhãõeÆš@+%r[Iž„Ù„øòzp ¬äI˜ûÂ%Ë ®Âgß¼òUØÄ2vî¢È1®_šÑm ç)Ôb* %ƒ÷öŽ<ŸìÀå¡õ¼þY­¾fv_6èSÕZGË`1óä$notÅ#ìbÀÚ¸ wÇ`ÅcðŤ^ÄßÈ¥ÂR^“l/ºÖ³£’‚:Ë3üÝÍ@+€ª>Vö“dËk¼%…–å:ÿÿpoA-ÜI¨?í$gûåèà9Ð’a ÒyáÐژᕼÞ$‹î d1ÚÞŸ¬]a A€b}X™îXMÁÍ‹¾Ïæ7³~wj2ác‚_¸§™±ynã"†@7L²‚¯æ^V9¥AÒ­ÐŽ`ÞXo'H>Øìô'üÛЃíD³Ø`V¨} gÿð4H` À¨&B :~ ‚áœñ\ÔYê%­oà¡•Ú 9”ð›<ñ"°Ð3•‚f`C)kYy£#0*ëkN<²ÐÉ"D„}÷¼!À°b1íÏUõÚá )Ã[@Á+"N®ñØRóÌõ›zÌ÷ :Á½€Š|tWûb—øâ¬NæGÖ€?(q‚´òd½/xb fcezÑçààq„3ÌžãÖ¨…᩺|tߨ†Ÿårõ˜ÉµÙmàB Ÿ‹ð1sâÌò}=ôûø2`t™³šÝ¦~çY¢2gDœIÿˆÑØ™‚>®s~Û½ïÞÓ -šñ¢]´ Öª'üÇ¥[)ȳßÇox6h‘6p‰X,ñŸ§EÌÓé{Í9Ró.¹qž·åAðOqP‹ÌóíñGY¡Ûò•øœŒ%>R%®¼lô¯ÍUºQ‡ðù/ymNB´Lé#ôø5ñN(Ä|R¼u³æ—³½Šm[1ËPÈ¿iS ÁŽ6%C ÀÈÚ˜=’üÄœÞÙêð·­îìýK6ˆpHo"å[!qÉ´›ªÐ>d ,ãèØ‹sÒŠs ³ð(°ÖwX=!1=|5½äÏó Í 1£±”C/(Òõ†»5_M5g”yVMÉÄú뽫}®/¹Ë ‚FV†4ž€*Ë,ï" ó(#Rñ3B(?¿àïÏðß>ý;Iþþ9ýÌ3WÄ¿£ìÀXü×ôäýÂÕ 6ÝÀÛ×éA 劥˜`B!ä> Æ#å2–¯»Þg“€ó蟡cí$µ 2’ŽTÄ0õ‡¥Q6OªX&Æl9Î&¡„x3ùØŸwž±ÔîóŒ!Ôù#Ý”l!E“¬%@;·œ{Áuì@•;NyЧgâ}æ M€¿Äˆ7›•\¿ÊÝã ²»aWº¤ ¦kc·#åÂ#_‹…iXLºª'ú$ý ÈÆR®‡[ ñ‘àÙ'>²Þ¢V ‡!-ì _8Ã÷K·8/;ûåþÁ³i {pcD­Îâ_Ç>›¬¥ÑlM‡-i/J_?~×Eà¯û¾á KòêþKÿØn˜8Šôü÷¯r†´+i’ ³viŽî;Ì*vŒÓcMòiÜ· Q>s Û[Ù'Æo`!*‰uÞdùÀZ?6ÅalÅË H›^ç-|»¸@óŽN[¿¢þù%òèb†·ÙŽ5¾©N@b7=á¯Hãô´¯1l°•«qyAVHßÑFÅ’ÈiªG•© Ô\Ì@€Ep?´ m?õ+0A3I(x”ÒïH¾ËÄ¢~úE·ÒqgŽ.“KÌÑ×ïý°¾ÿ1[8âVÒ‹tçQmô=£†Àöqó;}·*€N¨oÂñNut׆ŠÉ ÈùƒÞE{ï-®ª_²úv¼ZwìAÉb“,<^ W7ðn>½Ô–H°¹Ú ,ºëeäêæ±M¿[w™L©1#ݸв ü = ñ=˜«ëà6à›Ì˜öäV£É"V`à<\D›L;ŠŠkëk_#G|¾.f›Åjå£@I¾o^&bGÌM׎ž¼ž ù·;¯3~opç=GwÞ³]ÜyÝC(:ÞâÏ{Žþ¼gÿïúó>/ÔùzgoÞYïßp'´Ž‚§æê±Û«Ó Ë[ ÿ³;*º¯»,¾Å;0îRóë㸵é¿o/Þé¹µè"ù§ôãïáE‰œfgzÇÂÝ>[Ü®¾¥xØ”œ$0ûÆ"é ²Ó(zæa«ï¿“wNÓô[ú|öÏÐ糿oŸa‰ütúÝ u[¯ý Õ^A 5£öA{\¢.Lϕ߶êä³qú1“!ÿÖdœþÎ\Ès±¹Ïçÿ }>ÿûö9™ügè4`ñçôúm«Nì›Ä)…þŸ Rgv¶´7"s(jªN2a¶v(ðîîÀ»„¥òлÐÿ'uò¬Â‡xúwˆ§ÅOüÑÓŸ·på)á”òà à¤)žPZàmGüïÐ856Æ ç{©ô¶h9ٴܪÜx”WX矾€?øëNA6µúÝpÇÄ:•-©kbÞsX'öÖÎÙ¬Õ2òl†ƒÙçо)¦¨}ÈâÛØ°ãè˜Njþ; 7(oľ> 8ûzW  ]_ï(ľþE‚ ù€`&YK’¥Ôéw€6Õ*Çf/ß׊s¸±öjñß¡’þ*ɆðïPIÿ•ôÏ*Éå(Á;·…ÆSùÝ8=Û#¬8x„F1ù6Å5qpØ5ešß@@ÈÂʹŸ{ÓŸmªø÷Ëwfó‡KïåøÇæAûGEÎ qFá!Yò8¬©‰6%9ü6CcZ’{½ §W\ç‚(@Ì‚½¹@òW@x A®ÛoÀ9ùúxß… Ë®àŸ7ºÉPZàfåtO^Í,¢Ç²„ð‘Ì™5KÀ[$CcU°q[Èï°Q¹‘Kl¸V*îˆÀ)ç=uÅ1–áp°.¼\ƒ”Â8~.7Œø –™¼Õ!‹{Ù¨Æ#Á+ljÔõ1˜aÒøŸUÙà“ÄU@{ààîBÎï+F'à#!¶bBK} |îbg…íሄ9>‘{ 5CT ÏÒ§=^ö¡O0°“Ÿír88q‚¼ðœRN¸/n­ÂÌ ¶vú@§0†Œ>>óv‘mŠôh«øí¿ÒÕÏ‚d.W²â‰&‚b‰²¢©±K˜fÿPwˆ»éŒØ.qb¹ûÚ]*|dÚÝâˆ÷'pÔÃ{RüÏPš Pš PƒþnJ‡²ŽŒ* QöÎ¥Þ&v Pú/á÷ïn7Ä[“:rîš¿±ôƒb‰zþ °¥t˜ZÌpÈ AUÉ)îoÖI7àè¿ZpË 1ʺóÝû¸£Ag'‚u*àá&.K‡&ÎÎûðFÒwìG×B–",tà'˜=?žá¡5wÅÁ9p÷ïป̾uûnµ»•szÃØ…\€xò'ü^×ÞÛò¿vPßíCx WÆ=½÷rY'V­Oþe™b¨üã«åÅÖ&ë7Õw4m=°O^ì+Z¶† }>áp„’4ð‹=L÷ A!Wù‘Ä¡td(»ÁÄ/ú`‡ð½6@fèXÖE oWI¥o(ÿSâ¹z¯¢v æÊ”÷î? ¤Ruu¹‰GöM²€£Ës®`;•Ð`è¿«·>{÷í[–ž ÞÝ?f˾º<Ç ¿wß Ú‹Ã䯰-ç­’"¾¨œÊ³†62U—¼ …/äh‹ááÖ…(9˜+Rƒ.èE7:üŒE@ð¡±q#äqÀa2Êf  ›À"?ÄÇ_ûÿi&aÀØ^?N¸Ý} È»/À®ÿ„ö»TcCóX÷@(ü–wD[RÔùî7bINÎf?E›boì^Pè#&vïω…}²5vâè$yœLXØé”Uíô8y´9öTènì<†Š›„¨<í uu6–øsO¨@©áV Ú3$c ‹sX‰“ÿ…O_+hµl̦1YAc!r”Œj·•xV×G]xÇo׿PŸÆëª¢ÍÔøñùÉéÙѧOŸ?þ´ ™OŸŠØàRuu°ËsìHÊÆ§,pR êÕ]{Ñ$€-kÅæHC#VŒºlŽüQ®5xfê:^¬,}ÙD“E˜¤Ï›è yhò áêý<ƾþeO%ÿNÈ¿n!¬L •&ÅŸ©ÒÏ£¢ó¬Ií€៟n@î13¡§Q¨ÐLèÊD¡”#ÅDâuV›i††èàb$RTjª¶0€éÀ¢ÀÎÁúT´žÖC.¿0ñþïjT“ÂÆ³ÇÁTä±ö¼cŸZ TNµ &~-T“'xXBLì s¾èP¡ÎÍ$1È…Åx,OBÆB€¶D@j3( º-1aÖ9ÒhÊŒ}Ê!Hyaª‚6_&´³Ä*¦wHM‰4õ¢3bö‚=/ Š6ÛCZýÅ@“MaŠ¡™©†QÞcŸ¨ ­.pa tE‘UÊ)‹™Ö“M¬½v”CdÑá”Åx&c¿½ß‡a–ñîIãÓ‰>F4d †Ã°Æu1‰YDge¢á¶?ç‹•Üç‹ÏõĸP͇eQÔÄ[E,÷ãø::…w:Ÿ&_ÎO«}ü±?÷á÷¾ºzéW_úªT{éÂ{ŽïR¿'ûQ†wQ¿œïÇ_NáÞOq5¹ˆ·W‹xa®î'ÇÕ}s ïau¿%và]UÏOËêù>çðùŸOøYTÏå²ÚWÓû}õhÑW_ª¢î«R{Ñ{­-zrQÅöz¥2¼á3[‚w±wª&ÍÓö«yZ]™§…—ÞÉ9¾ç½“ä¬w2~ªœ´ð­žŸHóô+¼ç-3=¯™é§r/Ý(öÒ¥b¯;MŸwÕ#³[x…÷KONË•äó‡S|·Í‡×¼«½‡×rïê=ÈðwþnÀ÷¼³%x*tQ9m' ÀÅ8-˜ÊÉùÊ89ÇϹrbΔ“ñ¸|¢àû© 8Ngé×¶‘ž7Œt£¤¤Kø.”å¤ $2Wäñ ¼§e¹5SN•3h_y˜·Œ‡y>á»\3ðPð€w¡ÜY•ΪpsTXÍSI³›Jλ©ñ¬›RàSyºIµ¦ðÃ{x“’”Óäi{žWáÝwkVoÔà]z¨g«ð.=ÔVø.v^qu ïWùxõÚ9^Í•ãÕSK_Éð· Ÿ†°õU¶ÜY¦ E€{\…÷\=®?uàÝÖëP¶Þ€ÏF ÞåNÊC;ZºÔ6î͹|dÊx7¦f©zofËð.ÝW೜†÷ªØ6­‰YjÀ»61³U ª2¼Kð.¶+Y²Uñ®¸*µŠµln4,ˆðÊ^í‹cñF$¯¥˜-ÂÇS\¥â•¤-õr!—Í”ŸïËð½U½*?¯Šª¤Õõ‡»Ù©2{H·çùãv©(öR‰³^A¬Iw]­y—¶žÊ­óýY§6¹df«ÁËÝršë•–µb¶r§ïkÒ¨9(õr½Ö5°ÁàN¼/%WÅ£¦$ž´ŠóÔâúLË<ä¤rßž ú]§þpªÝ¬Ÿ3 #¡i’ùP»+i¹Â*SL.Ž*õ“qkVÈ•gåÆ@ªegÃÖéC*õ2$zÅ›ûzvÖ¹Y4²Jçºy¼|¾™šœymŽÆW«l2uZ½Ê>ä®ó3©þ\7«tuº¬Í&“ì4œwƒãvñªš¬wö§ÝÚy»^r×ãÉô~y½žÏ îËÙâk§|§=e2ƒN9¿*–Ÿ¯3冒¸2OŸ‡ñýÂì©ù´ctÔ (4íbfP;YgÕeQ¿Ífgqe}_Z…îýÝ"QSÄÜC%“›ÏÏWå›Â}§ÖZ&RË“d­u´ìu+í\·vwW,Ëåe>ŸÌÉÚíMF\ŸJ‰ã‘ÜŸ?5´W¹tt“¯T”„(v굜!^]կשb®vz•’ûR)÷¤‰ÚSc8’²ÙìYm°?jK7åªiè5££ ^êõÚ2]oä…*PP=)¾ÜÖŠÏ'¥¬~.–ª×í\ïøL_ÖÆWõãܽTº=×Öõyi?W©dš‹ãú²x›WAìnN‡¢>‰gÇiœœˆ¹ñÓúü6S*®;ÇÚ ±ÍL½1=?ËWó¹Völ¶¬UoGƒ«n¶ ¾›P50ÊéÓÉÕ“¶:ßdŠóòý**Jû¾•]kõŒ’‘¤lOº>?+™â³™ÉvjJ¼ú’“*Óññ 3æÓÈŸž^×ãQ¡pw®:­»Š>¨Åjf±¥áS!q¶¬rWq ¦»Ù§¢žUÓ‹ekt–¨—šÍÆlv[‡,d‹ÅFµv.Þg'Jö¤]\/_²3½|%êJ=³Þw¯nò7‹§Îèî8µª¬2ÐȺ0W@X—³žQç¹\ç²ëšt“6¤ÄÙ‘4,këÌpUÈ_•Ë©ÄI®¥­ «‰”XF·ö”­_gå|)¿ÔšwâÝÝHP¼»-æ¯Kó«~§S_j'ÕlîõuQ¼.æ‡÷b.!)Ê­Ñ©kõs©‘›å•v£\Têz¦<ª­ÅT¦ð+–0ÙmxÖ¬×Çw«rº&fî:›Õ`©©/ñÛ›rvU«‰9ãò3u÷ýÜ×'ÂÔ,¦DE>l㧸“-øÕèâöwþÂ<¶¯‡µ2½n´ÆÅuù©Ø†°8գʫ‚S•§Nr¥ ÀhVjŒ®³µ§QºÚ€?êƒNA x²Ðûx1 »Da¢Ê¥çE'%n´ÅK9+¶2‹«¶r|V&´|q*u²ÝãQ£–]Ýf´òjTÖõš4¸Öù»LçªR($êÚCõê©;n‹šÖ9îrC©£gf‰“žØî¯ÅÚº¤³ÌÉWƒi&Q_:bcPK‡¹Eá:'¤”ëwùÆtVË^_‹µrZÔ®šÏÏO±š­n²¹z¹•¯E˜ò»Iz"KËåUU4nsÍY©,­Ü¸zœ5Œ¦ÒÝOåÄÙY§]WòzV2–ùÚQ-Wo6ï`A%ÉZw`d{0£ârPÌ,;yqÙ0_kg- $¶;e8Z'Öw™åigq?KæqQœ¿Ü_ÝɃç|¦>¸odÅu§-Þe¯Ê0ªPöºß¸IÇë]ñ¶t;¬u:OwÙNG|ÉÞ&2+)'çβCU¯*ËçáY¹\¸m,7êu{¸¼j<=:ÅýÕà¸\ÎJƒÚ“Yy8m‰àHÃrþ4»ªVªíëQ§¾?‡-âøz|7»[•rËŠYjŸO†ºùœm–ƒ¶TçHɧôs<~Ò<+ÝëÓ¼Ñδë‘(eµÎÝÓz¤ëÙ;1q]¸’Ú­“½j›yµ{ÔéZ6›, R©–-Äëñ0¿ЧÉaº±-[åôó³šm2M‘Ú£~Ek7¤º™‘{ÇוbF®Áÿò0ÙÔ×…‡Œ85аðŒäóôY‹wOE)5˜Š™d3½JhÊàõ¾yz®ÍåjUº:¹ÊtëR¦|U R'qVÔoÄÑk&Ùéä—O«e£UhTZ7µëi¥µ¬wÆYe-Ä›âJÊŠ·çJ¥\3Y}¸>z079¹~šÐ[°3…ìH\éËl!+Šý\§X³÷º4.ÔÞ};—é RóD±=2sBG̜߶¯n·å^þAÎ'Nµ˜¯äIJX$jbñJ¨S©ëBþvÐU[ÙêMröP[ŸVÚt:Ͷ%X`§6[¨K·7<[xb5›x|\íï·Jýý²tÛ§¬Þéxÿå4®*µU©öU¥ÔW[ExnÕ–ïì­Z« íSí´Ôl¦ÉÅ>¨+ûçÊþ~4SÞß7;ðn/öͼ‹ý1|Žkð.«û-|—à]íG‚·XÝ/g«}Дú/«EJX¼AÓ9ƒ¤ì«IP‡’ õ$ñ»¼¯l`«pUÕ|oe+ ÀVG´´4[ ªÂ÷ª ï¼§ÐDz§ІÎÇ•“ä°rbÊç'ã†Ù­¿«Î{Ý6²LÐ|Úð7ÔíVAûrò¸eÊR¶|j±S¢ÕTMå´€ÚÍ‹âh6ç Ù$A£+g "”†¬ØïL‘¡Œ Ïå$h1eñæ¨:­5’—.ÑLLÐJLÐFÌñMûP8 ã\…&áÓ|ºi+%9•ù³‰Ÿ 9 61ÆOÐ&Æê Ñà{»ß[ ¼;éu²6#R¿òïáõº¥¤×åNºa»¥¦‰”_ÆOÞíYC’Ó ©¡¯[êñº<,®¥Îqc\ê4”¶å޾AJGéý´s´(Ã["oåÈ| ý$ôyéÞ|*ÞWñªÚ*ßW¥n/ðnO+óÆd1έr{Q.Ì×öÄœwR¦\n›r¡PmUÛÕ²X¨¼¶&•§6¼“Š ßå¼«íŠ ’|#›VÄüì©5ž5Êw³FÞR~V*&æqo»n4à]¾».á»»µ˜/¦³ÒsK”ž%Qš½Ö`G+¶fYøÌ¤YV”ô´$é+Qº~ª¶®³ø% ]lW ;Â[.6ÇÙjs´*ûo ´1—…÷¼“ÒÕ¦e¥™”ϳY(Ó {è¬o1;ÂoCøìˆY¨“ÍÏŬ¿IBfø$Á[Ì †ðîF1¨Ið3"¾aO…çâ°ï’(м³bFÅ ðqoØo°\ض\×l”ê•(+I[‹ÅëêÑuyl$W|.å[ñ³òUq4Hƒu¿:Ö俍˜Ð®¤“¥”jfƒNñ¦&¯ûâúlQÈ–^ÏŒ\)× Gš¤Ü·nÆ×£ëÛåX;ªæŸ†ç­ˆ9ÒI%3Ç­û+ýNLÝë͇Aÿ¶”i«™Ül(¶«½~Yé—s7c]­ ‹å§nû.[Kç§'Rª•_ fÆ ÿÔ)J17hŒÏ’Ô~j>ÏÌôñkr±h?7‹Ê -ŽNo¥láµØ‚-Z7r“Õ´õëѲv=z¾[ÜŸ&_'eÝ׫b¯;½¯K·ùµÛöPÅb<•†‰y{4{=›Ë·W“ÓÞs©¤ÄK ºgÒ4YŽ¿jE*ª7-©V<‹‹QB;Ï¨ÇÆó“1¯œœ(çñê´«w‡³úÑÕ¨ØÒí»iFJ€T”/•ò¹Ä•˜oÕÔÓÓ«Ú VL뙜tUV`“:?ïf^µáÃ"{[¼¹Îp ÒðëR¬ ór t™«³”Ñ ÔÊU±‘ËŒÎÚ Ä(ÆsPiOªë×§×#£U¨TªÕtçø¬S•úHe8Í4”Ú]"1:KÝŸ>½‚r½È(O¯Z^Wôxü8›ÓÕõð¶|{3º~I™çëI½¶Ï©–;«(%q|}ÍåSŇZ&›Íu¤³Zùá5®iÙÆ*1U¯ ÅÓLq8V¦ûÉÁª>ÓG’Rg§†ÚnÆo—Íõê¹ײ’t÷Z›´ôIwš«¯êÒêõö|š©§n³¥›|î¨u½TÊ·r¼Q†Î´¯'•êÝt¿||¼•ë™ãRAiç*7wCà+RñZÃå®3Wåëâý䨍u†ÏëU¶^«nEcš|–•TëumÔë×ûøðC"Ÿ(ÂΔ눵ŒxÞŽƒ˜UUêÚ2±|]®šš>ÊŸ÷nÊÓÅK¼¬ï÷³ùÓ§Ì<±êæ€ØaKî”ÝäËëSnz¨TË7÷‰âÃJ,=§šÍ'S®>-«Æýº°oœ×ÍbÍhõŠ…+ýú¶¶<>;­ãÍ\ÃLO ËZþa¥'Nô³+-w âb|lÞ\Í€£-/¯éܬùô2Y ‘ôéHWÇ™ª,µ¬,ßfnnËew’HÍÔzfšyx¾._ß$¥ìküê¾ÓYÅ+=±X­Þn±¾ÑçÆr=ié‰åbÙµf®R¯^€|+qßËJû­e£YŸSc£!6ûÇËQáÅÜo>U^ooæµµ:[£EêJ{J+½³›áj]>«ÇËäÙÕ@éeO2ÍL¾‘›ôkφþbš/jN)œŽó7WÅÓ´"' ±ž]©mcºÊίÄÜméd½|ÍeëÏ=!]]µ‡åA3Y¼~}Õ*Š–¬œÞ–KÓF§_¾9?‰Ÿ'Díº¤®ã‰’Ù½¹OÖJ‰ù¼2­¥“«Å vÚ¸Ž@cVÌR]9.ÅafYRŠÕÎzr¬Ó¥æêlì¥þNle²]HÃ"jZÀö)‹c弆խ.öÙœXÉŒŠµ¤T® ÅÁ0?3;’8’OÒò>£Ÿ/Kµ0hg—­µqû´l$=+÷›ëF§8ì”Wƒ›Ì“Td[7/Ùe¢ÖzúݰU¼nå³C舸ꨵr²’¬–›â ˆã!­ŠÇMi˜ïæÚ|/{,eïÊ×OËq:³4jrb1‘†­»ñPjäÏcÐÙæâ4=½›eʹ•ú«×ºW™A;Ÿi´só&3Í?•@ôÎÏïžkÆ­‘è”$¥|ŸN´$yœÏµWÇ °¥ŠËÛ§ÙL,åFóf»Ôiz§}éú:/=Ìʉ„R}͆ǒtÖõöªTÑV5]ÏÔËë‡rf)vŠõ̰5¸ÉÇe=s­êb¥ ƒU3älýaOŸålëzÛaåê)§—¤\f|ݺzeÆLS«µ*…§N¹¼Ê ;GwíÑIý¸ eôrK¥Åg½“yAQÑ—J¡k•`[¿Þ=LÚ׃qW-ŹÖF½‡×qiTÔ@šç^Úi½ <Ö8>;»n.uÇ—½—»f¾Ù*.4Q¼,ºæY!ŸÖÔÙ³ÔkÙ|+Ù*^ÇÏâ ?l¢’m$Äêɸ•.ÏV•z£+U ¹á Ý¸zBÑPÌvKŦö:ÑgzF>)½T—Oç‹Å”_­ŸŽVêhƒïié¥]ÒÅ̱V»ÉV³ÙøôI9} V-¼¼Êûbæáa ¬®+Êq¾š(0èF¶5¿R_”3³W,7‹â%ÏÏ ©ÚrÙ;Ëfе–¸”Š1“É\ƒ‚u^O/¥ñà®~³lWs™ÌêX)dÎ Òk®Võ££láZ< ÷¯õ«¸2n j­Dç%7ìõ'×½¥”Ïž4žƒ³µt q;¦ò©”2N‰µîô¤Ök†ÍçµX[ƒX]¹‘ÉœêwÇ)9Y¦¯ªâDï´d«­ˆ¹ynýpßiªru(ƻŻE1sk–jÇÃÓ;­£ŽZ·=äÆîkó5[Ÿ¥³bn+ñi¿Z/Üf”l©Wú9ê‚s’È—ù«Æp\×«ñæE«ª-½÷ÿ³÷îïi#IÃèû3Ïù#´øÍÀàK2ñÄÙÅ·Ä;v’±d2?<dÐ#F¶É~û¿Ÿºt·º¥'³»ç;ËìÆ õ¥ºººººº.ü|¿ý©ürÒñÝÛùÖé»›ßßÍ;îæ·éÁ>+|þYêã¥™Ã•×ŽŽžïm¾(×Ëk?´ŽÚíòUÉ»Žöµ"£ýÍM,r´µ±½õ¹ö½ñེ·Ú=x+ïl –G!‹Ê;2îvcä•wªåÛ`âöƒr½½%"ᕳ1Ê;²–þH{¨BKO¥”P40ô2OCÕB‘µ£ç‡­ƒ-¥î¢g›í£v¹ ¢÷vÝh;=²;m \Ú394 ‡‡0Â(DéWñ«2µXÚÑ #ü‘÷Ï€lõ /“ud²,‰oš{+ ÍL ^ʈkZ0.°”“õÉM½qpŒ¿«œŒz·2ô¯+u¼OÝåÛÏKúÓœM§^X­]ÙÚ:ðooÉHy•³- ¹ŽÐv­ÊÈjˆö´2ª˜Šf…wñÇ+‚ë±—ÂA_裑zºM˜4©ÊËfêúÔÕŠMûK¥@ „„¬ B­Ôur1ÌÕ}º„GŠê†Í€*ëQ ™Y\úWz@7€Q=·˜f¥š(#G)?ÕZJ⢱â7ª5\©ÁÑÊÀâàú}bFmèsIìO“ZúãTôA,€¦‚b¹$îY™Â¥•ëø¬<×(&Öê)ŠJEòãj©j 3'ÚÞõÉÎ,, æÙ}SÛXâ*Âat&)9Ö.ÆBì¶UŒeÝ`œ@ËQ¥ê³oÀuèE9ÑHpÆöç}2¤Q6Í>>D£’¶se¿”Y/Šýx[ã¥h+[ò¦„¹¤C‰å˜À.oˆ¹TMñe4öAÞ\Å4ÿ6w0¸p‡ÂkàÖpÜB»–Ý4R8$\vu« T´ŽÓúôIÔ/?IL]õglóºz³úZ³´Î¯—E MC j-Žß6ô%þ!ÉÔÆÒ1‚õ¥ESË‚ ç¶dP Œ\å¬GTÉ #SS8v<°}&,Æ©\.×0›#ûìq^ ‚Ô(Cã­á>FÙ&‡°â—´&· §£CA¶­u(yúáäâøäøíႹÖÁ«þõýKÚ«'Q­ì¨÷fJ„Z‰Œ%öpôܱƒâpZñÿcæÆà±‰eÈ||„¾,þ Û ¤r¼«³üŸÄ§²œU3þÕÞɪ\qc»Uí‰3Dô#K±€M%êšipVÒ¢å—þ·sfà3¿ìl4«´§šêæ¶ò겜)x®³VTÖ5«æº* 3b¶Â>ôò/¿ýVûí·êe«ñâêi­¬a–´bd•2¦M°Ãg’:FêSógà?o·ZÏžm-0Àßx¾µÝÞ~ÎøÏÄËgÛ[­Åøá`ꆰú#ü·h£ ”-ÓÂ; #ßøñ ¼? #ÿÎs¼C0;ï©®Ó™NÇ~Ÿl¯Kk¥5g?˜Îé âT÷khšßnl4ð÷}`(Ö™Å#~ÃÁ½œ“ÎåÎKo0Æ/b®.è¤Ù÷#8³{“IÓÌ^A½g';ÎKaãߤDßA8\Ç·G Ÿ4ð4~øäN>‹‘ç9'Çû‡oÏ›¿\¤ÿ{<ØqR8p~ØzþÂA€x{Óimïlµv6·~uÎcôÞóÃAÛù_vè8ètÃ;xš`ïê‡)0zé=Tx¼qÍRébÁk ›Ãÿ#ÇÁÊó0¼†ßaŠ»m—Ðú½£­ºH»Îh/~RZn ú1r#§çyn}ÐtœãØ™E^TÚ?zÆæƒÅu„vq ¤®qªy†ß‘“Á1**ÁÒ¿ƒ¯î¡UÇØ¯Éñ¼®ª£Á=‹Ò„S|¦h~ÛÜUËçp°#ÈüÈÁøËF@e'wn„)­±Dc Y ½©8Ⱦë‘Åò È%C`Zàµ7óÇ€¾ÙTàÕIx-¢ÐÚ8 ?zwz%$I¢£hÇA¯¬†Sf• ‡Ð’‰Ev-§?‚¾B;“ŽÝ ˆ@·Mn›[F NÍН%.hj§7¸ñ&¢"ª;âþ) ‡¢È¨“%ÂèŠYr7³h_Æ¿“OQô GÝD£NžùÔÏ8`^Á|·Àûòu“î{a (Á9Ç»ía^¢gÄ ÆŸ€!ç^Ì„Êp`}€†‹ Bò%£,SãÀF‚2ê9h˜ž ?*)"è&mJÏ@‰Œâ0PÀ˜ªÐƒó7_qÇ÷îß㵚i ÀĘ ¦„Q±Ñ º!VŽ%{€ éöÖ ï&™Ñ,õG¶ÂÈUm—$rëNvÍ¤Ë /ãˆûˆ±4-%<¶®“´C FAËĺYŽ•ÃG2hì5‚Ê$© Áû“™ÇúKn"hDr –8óÇ8;!-!†‰¼Ï>Ü C¬£R='ÇKBˆƒáуÏqÑ‹jîÄþ­‡kаB¡²˜mgA'–q±  sRMÜŸÅÑŽ<]žOAV½ú $&æ 9¥Ú •‰ó<%´’,ÙŒ‹Mœ#İ+³¡ÁQªasü²w¢x}º§[£\š}ÉR=³ÔÈèŠÅÏÐ_›fYbHcæº>BÚ±mãD²üË¿ñÆRu¹‡ãÆ” ÂPòñøä癓¸&Ý+#J2©~¢Y"2'Y’hJúiâöŸ¼š¡ MŽU¦½„î½QäûôK½éÌËþõ0y'ÃJ·"ÄD!|ƒÐmF4Ãáá߯‘H™Uc-V2øœ²æ‘¹çÐä> {9?5LY`hÃCÎ))g†‹9–l`$,âsØVÐ¥–Æ™¤~B¬Ñ¬‡ ¨=T²o±ˆ˜õgÞò‰+%0¼ wFì™›_óÜŸE‚íóÊ“£ LMì™_—[¿Êñ”¬N”Á©þþ?Ò3ɧ¨:÷Ál<À7ù´Üµ /#¬"ìýÓY_E´S#°)¿v—i9$ò•)@n¯ªú¤Þ´ÐkÎ9,èÙ4ËëÍ>9éÅMú‚_eG¥È³bëûÆ›;=vŒŽÒêä˪Q[Oü/,÷฽:½©*xF:ŸÏ˜QÓ!\ø1vA ¾aI‚¾ÁÀ@c8&ìj€näIÄ4êÁ@õpZ0?†£:HÃéJ08 êU'1æÑÕÀ^\…’ênç”’CÊ(üºjÕ_iƒ» ™1qw]LI-l]ñ®Àôâ~Û˜öŧþznä÷1PÛ‹4Ñù}1!±Ü¼¤Ôµç ztÓº°˜|I)>öæP_Š„¥jÁlF<õUÙjÂ!ŒÖÎPAéÑÁ ÁO5Àȴ¢ût? Çf•PT\žÃ#ÑèZÑ(í$ÃaÙ7x/Œј¢Á$+\²ˆë¤«Óc¥#ñL­’‹á˜›÷n„gLÍê fëÁ½7Y?{wñÓQ0¾A7ÿæ(¾¿Jå<¾H¬¥×Ä|›"±Zí’ p4¥7i °Å·¢ßc­rÝl³f]`Ÿ`wÁÍt£¼NU\îFÜÌ…“º Ñ2V+ÍbàÁéÍUÂm J³Šø ¾¬ÔÒ9Ø»½`<°!Aäyã;O˜q“u׬’IÀmÿPbR 8Êù‘»X÷ߢ?1O{é¼tZ;°' Udòä]c#¹¥Ã¸29Åžn¤qêÿd”B™dŠ:“²dS\Ãà&Ä­Ñ(%Ãyà)S-¢ø¾8oªŽfã!³> 1sö:vÕ•ÑþÙ+ÕvóßšÄZw ¡JäBì¨3Ê¡:£¤Ó ¦ I'Q6Ë ¬ðG†S7 (r-†nÚ­ Ð9öŠÌ«0š© aYßyf~Ð ÙÔiEKýZY{ÑÂÿ*þwËÛá1­µZ[-Lµ[´²ÿZ¿uÝê·*Þ#ñiË!iË!Ú¶¶ õáL¸rFx'•k¢ÖÔJê]š E†ÞÒ¶\Í'‹;ÙŠ7ÁLÊÕŠã<‰*Î­Í »Su…á¸y²ªïêý×êÎÆvÍe¨­M²Rû¤Á =Ò|Þ²Âd¸mý ©³A{OÎ+¨cŽ!åŽ=÷ušî‹¬G™ûÎ I'™JèaÇNÆ’-È4h3µfNOæ¥êæ§A6E:Âú\P!½œäœy•ÈhŠ!qóv¯NJ)›‚5ç'íEá:ÃÃ:,Ç•ï‘E<ø7þP@ˆ\¶“`ªÎà u£¾;õ2DñÜzž¬ÀÜ é`-ÓCg7’–——£È¯“®r+ ’.cÁ`”ÛWEKK@ ×èÉrJ9²–Þ«UjHxWëËJµöà5)`W©åʲòp[dšÝWùµr+Eh+Tì«J¬Òî¨kv¶¤0+mUi ÄZ®©ª1 ˆõùÓxöFªÞ˜rß•@k¤€G»¢¤ ËZÇ ïä]Ñ÷ö*c×O¨Kêe±3¬¬*:¿|%œ*ßöª€ÆÅ¦á´N£œ6hF[teYnÄÁ÷‘¤K'GsÀ¤{«ätyÀš°\µtEÎm Àf–Ù ¹Í0³° o í=³|3=5.€Rm€öŠÑ.šlÍ­Ä+׈¯MН,ËŽ¼þ ŸèDÇe4"•|¨¼ ¨qÌÒЫË~ͼ¨@móV(OOg À6»Fôƒ ŒV.ü b6 «ÿ¥È}™1ä%ŽZf?†ÆŠt~ bÑ7…hã+!:9»ý¦ m}%H'n8ô¾)D?|%Dofß e(ZÀ.0‹‘•[ÈAhÜ‚- ïvZñL”߉çÊ<Ãɰ´aeÑF‰«~>f÷Ÿ:òc¿hTVœÌŽþ3œíÔhvWÍêŸþSƲ‘ËÓB›x™Á÷ʦ<È SC:ÈËÖðî¬èÊ”embL§̾âçbõb=a2¯P˜ª<¾Cì!…ä£v1Q ­\­¼OBm±úä½4G¡Ò¢‡TáÕmpÙèvÅʉzbèÉLá¬á¨“ŠdôS±t(¨—xµ#ÊÑð—¦ôò¨Ö.ýD=‚‰1#òc‡SWU¶>:çÞ}÷>Ñ£TŒ°tÛèÚ—Í'Ï/k¸|Šž¨ZgoÁ,ôA¡£OàXõVYÂXsøÞž=PÆé;ÎÚ¦ºãÅ}ÃbKSUQBµØ’êÙP íD—[¬&õMë7¡ßJLß4WñÖE ‹©­Ò‘-ƒšÖE$³®¡Þ;ÀÉÓ±ùƒfµ€ôªa_ÛHD¥´-0Ú’./_wØ6WÛQâ8ô{²‚t)ZLeÇA=6~ ~AZYx±aeO¤nRXC쪡÷¸|„`Õÿ©j %Ã'¬ñYÖM‡ q@?#^sUå—O”öý÷b(9z[d€Ú©;›Àô¶s&=¹¿bBðj jÌßÅ]nÚÔ°š–FÌ_Ôˆ1Fæ‚«ëÖKVª‘®¾ÅâÑPKæˆY¡MˆÄ«&EkÕ‚òqím³â­IQÿm–¥ð;©‰Ã¨°:¡Pó·‹GœxÑŸ)q-6×\F .qŸÜyºø°ÚÀ|¤ugÞ1Mû:p£Ñn¥iZT¾‘RÑ|2 ¦˜‰Ñc§þt§*¦j—^g–Aò^Rš—§îçD3[4 ¸o"c²D>‰s±w/³tÿ˜ùâfÖ°ª¹q˜ ôì±n#côcŽf”#ýÍ(¦9°™ôK®“fªÏÐò$ÓÈòRºl†5’1~M©«u•íŠÌrWÎS§Ý*­D'x¤Ê£•fº5\×]<ë6ĺlݘ‹2BÀ7DÔñµsïqpªtLŒ7²žîœ\`8†”³c9¬ŠåП…¨µÏ»²©j ;« ÿø‰íôçâ‰ÑL="FϪXŒg%ƒé­ÃZÌ3“Ò¾"Æ’¬„M|Ðn5[Ùš0©ÕÁ¼ÎÅYCV­ uQ=ÕÎz-¥: æ: ÖYë¹ðÉîÛ$à­˜z©S!Í„yÐ68®©ÔbYm™JK±ÏË« ýPÐ9©\Ói;²ÖS×¥Z&„bŒ $’Rfoå.±9œG*ÓØZØÙaVõÿWs%”Ibš¾÷Š™ð&*¦`šÌ2}D ¦Rce‹k"í·@dè´"8É á`–U–‰—\ˆÌØRK¤’SkC¯•.”5–0ÛF¥0™‰4Â>ò»0NÛÉŘdÃÂWeçÖFlÈÌo¦pÛòš¨ :åqeûšÙ°0;×q«,ßQ“¾®Vœ'Â{#A{#°O! ÆèemÅŸLgq3±LjéìGs´²"mÅjrѤ*Ÿ3ŠÕe“ÊÊ ¥Xý¤87‘pl5ò¤)ÂÛ¨u®«Yô€uO0< g¡J!ø@‚"SV ½Ê1î$©˜*Ó’‚<< „RWŠó[Ðð)d'~+¿8Vœ`Óc,a‹H™QºúA´¿Ï¢$\úŸs<ª¯¢¬ÿEc€m±‚ï/÷ß¾?9¼8tÞwÎίtÝH†  ”,Œ]q2¥2Ht:ªxxg×GT ê‚RK®A¬sÌÖl;E§8¼³´Šû 1„ K ‹š£IoªáÝBïý¥'o}<9]:Îá4`•rÚ[&+¢°R™FiH˼V±AÔÒè.̓ÿõd§³vÑñ#©Žl8¿Žèäü2Pâô®k¥zíúã"óiÛÛVžÓ4gÔ9J§§3uLJuÞËÓ3·®²ÇOnþMå 5±P}ÜÞb˜ o>/Õò4n’ZL¤ÛGX\gÐà#Ú.L˜7²o¹{g)–ìilÄÚÙ{÷á¯bÊß¿HÇß&姤:åO¡Ç…²Ösh)K.Ž/NItFhŠ¥$H–ÉNÄ·7§^¹Co/xÑWÅ£°xV½å¿»4n8ûcäÊ]¬ÖDsamQÙRð u²JG‡Ä:«Î˜4iD¡-V<7ràE™Š.˜˜Y£Â õC¿7ö~¤×9¼rí?Å8¸²°Xè¡x!νéØfy‹ax2þüð›°¸,­ØÌTlÌmU…eRWÇ¥”Ó…y¤ª¶›-ç{§Šï wŒŒÇHw[5g=…Y”—ðœÌ}cÎðµ“{’,¦[2Жö1íÔ¾ÊSXÕ~¥êî3™³,;Ðü.€×"¸2šˆ­U3X‡>þ)zÉ!;aVí ³Ò ]L0T¤Ñ6G/_*­þ‚ÿ!ìäÿcóµÒó¨™Óu¥K¶ŒÛÅ·š>=º®ÍÎaÿè5Æœ BÝtʸCQ™ïd!OK(£úÑžš9«3¦P¢Y½BN~KN>iˆ‘“„/uÅ–ëûÿÍ“õ-ÈÕgN… l7ãw¸/βù{à۲ɑ.‡U`cMѵé~¹”[Zµi©ÉÝ"©åÂù¡I"Ö1·¨Mò†àH‘Ajˆ*‡Ð‚låø¾MÇ>žŽÖ.ùå =šœØ½Á_ÍzìQ}’‘êt*å7©K Ýܤ÷duV&.'|WÙ©ÑIª›ív'°t»xh¯t»ø¶Û­0Éõ•JÝ®;c 8{ãŠv.QJç-õÙlµaS¶ÕJÚâO{sãùÖv{û9Ù~&_n´·šëñít©¯AÝñØ éç:ôº…LÞ]øÑœÎÿgÍyëÆ³¦øÎî3wè9A0¾ñãç|ä_Ç3fÞ³ØÃ–ÖJkÎ~0“¢Û©îל ¢±€8dôý> ~÷ú1ëÌâQîÜÎKo0Æ/b˜L [èGÍœŽ)«ö+¨÷áìÄL¿M«*‡ëøö4@㟅ټÆ0¡œ 3ò<çäxÿðíùaóâ— ‚ô°ý™Ãw~ØzþÂA€x{Óimïlµv6·~uÎc´BÚóÃAÛùßR ×eWìtD "$Ñá=LA£; ´A%”…Œ2™t½taCÖ¶1ÆP ëKŠ«}Í£©O¡3¨I B‰qƒk&Xà$¸‰Ÿt{E÷)#7rzj€š ¯Ç ;~Tòež Ç»Æsx}Gp>FÆsæN4ƒ†'J­Ofl²'·$n–J¥w“ä–#SLF%ôy *Þ3La\ 0å‡Ñ€»œ¹óÀXì l½9–.8híÍ@¤ÂÈZ€VÒ )òýšì„y{|”™(Ú¡0[ §LóUvPFã1FæýàPñÉÌ©x Á”Ö¶Í¢{¢Ì™ë²œQÑåÃTN#¹Éà› ÓO÷=$¹ƒhR €Ðn{òšœšºá{ ö<Ž¥¡Ù[@{æfTÑøŸƒEkêsÊ?É)1Gä(tá,,Â{‰3„[A*rþˆ¹Pæ¢QÚµ©È¡²B>–V]ÔS ÅV±Õ¼>R“FcÊ r4@B¹ÆãàAÄøòtgµ¤¡¦€wè“FÄWŠˆåLÊr+Ó|óÔ_;·³q죔W’f}"rõ2ùèFx÷#´nYÛ:#’iâ=7ûp—”r™öS§N|/2»É뢴¤ B­È÷ ëm,“°Îhnú£.IŽÒ}`Ö7È^Ü4jAÉ(o g2ù8¶ˆ;Á™i2î)$pô\à‡\Óå«J­oò,èycm› ØB0ñ̵+WtÞž6öÃÐp¨õEñzÇU%øú}Ú Íœs¢ç`â±O¹KX¥\±ÞÒÛ WíEâ ¶‹ ¹ô“7ï¸eŸÃþ÷gq´#%ôKÊàqõ[ì¼÷BÜ|©â„Œ)hÙÀŠûZd²j¤Wseùt©Ð,•Ìg¦$e ø‚Å?Êbx°‰¢ì—)Ëqp±¬fFÊ G”âK4¦^!%Ó£Q ªÑ»‹•.ÈD=‰¨(=T.Gø}Œ:S úôÈÇà“Ÿg~Lýó>ˆðîv íap–aW"bÛ!¢ä%¿.È w€™BѬã Ó‘ØzÿŽž<@Ÿ¼?rf? =c8HzLq‚ßBþõ\m?Ò[bf¡G[†ÿ…‰ÒÿŠvs ®¡OÑ‘@à›ˆP~,)?â& )žO=â•Ô™o¤x\ña&œÎhÇÑ…溂S‚S¯‹;?KØ E?!ñh‘HÃr<±c6vy’©.snX Ì­Á<…c¡©`[çå‹iÖŸÃRbb'%‘êaNqZp0!&ÏS„ˆÀQStñ(ªË~%zÉ6ÙûöÿASÍ mùˆ÷OA¨3lCN&éy9Ë\×ÔÞñ¡o$`CØm-ÒáXÕ|hÔ­A$‚×d KÎàÔ‘aš¤e’ÖTwl@~q“q¥CÕ0i©ªêÖÉ=õš„•I=î×)m¬:}CmZHN-ê µA‹O›6TÄiÓÌcŒA‘H-FKõÀ†šÞO⮦Ç͹¶!§Äv«ðí:·”5Lj}#Ö‘:m’)nmL¿éµ ׎À°¥[DI´ ÏDjæ´µøFï"K*7ˆ@>ï±·!_šœ…⇠R¥ßÏI\žIÆhj&Kz~)#o~1]»ž_JKšÅ”^)[ǵâ,TÇL»«gõH9:$©=´ö©‹OÂÈŸÈQŠË X8¬«°+wMp¬nQ•Ç:Ë(2ãÃ!'¿xtĹ4UÖ0bÃvèÜóLU—‹÷>Í{7¨R˜šнÜ{“õ³w?ãØF½&ïǯÒKˆ½vÑNôæ}qñŸ*ùSˆŒÛµË2þ,kVQ3˜ÒŬ;TËßc­rÝlÓrÝ ìàì-¸‹ÎBZ”ìÀ\Av#œKôÚN7!ZfogÁ1–ÍHÝ+§d¼8®àËJ-c÷.]eÒHpoýñ|×~ÏIl9(x±û}riÖ€K%H¼V–wÿ5ý¥̼[Ëd•— !¥³Öé‚ñt³8ywªN1æ¼;¸TÞîTÛ«gèVnR…ò;2å¼:ç)¡-“eØ™‡WÄñÊ3C§8WŽXW„d ä!¬’;¯bÍá¼¼#ö‹þW¬šáU Òš|»he#Gw¿uÝê·*™µ®®\µDÚ2û¹ÍC»mm!u‹l¿ßλTþW^§ªŠdO"7¦âÐnkQàëÎÆvÍÜêXó`ZhÿšSXsHð€5&PÍ~ÅÛ;köΜWP-åàSè P|.;}Ôj‡ó£‡a_à@OêáÅÉAsî\¤çÓÌÏps©å+«›YS%—Ì B^¸°„{å _ŠVÀà|ç¹p Oý¨¿¼Ç©j ‰UБª†1ÏT8j-)›ª º\бYQÙ0Y5DN4…íÔ>æ6Þ€&Çîmoà:^Ý5R|z;.G¼ned¤þîòúíEõŸ.¯¿•=xè: ëÁCÓÏÙ&Ü‚8‡™&ˆã¯V·.¤‘à>-}òç >è[£‹´ìŽ”[ q˜s-# ‚ ™á’ü£) ¯I _Hb»’Õ¥ÿ(C~˜x9¸ לVdu ‰7Z@L-]AhY6âµT®Éœ9üqálײ¬-Ó-käX×ýøL·´ó¥RÝÂæX)Ý‚¢Âï½ÚJ²³¤àá[ׂ >ø& íU`‚𠀇bùüqzIqþ_ ´F ¸%YÖ:f´–ñ¼ô¾·W»¾.©—ÅನdÔüup*;åU‹M#ÂiF98mÑF©è4ʲ6܈Í쑤ËG cÀ$úUr:©;#Rø"è li‹Àã3NvB¢<3ü®ÀnµyºÊB~+Ø©ˆÛ ¼Y1ŠÅëι•d%ÈÉ¢ú£(ÉʲÜ)&{àMZæ•×–W=5î†>jo¥š\»F,PÛÔ&s`›S£š5Æ+þ1›†ÕÿRDïeÌÇ;žÁºnÕ3»°Jv@ç·îxüM!ÚøJˆNé¶ù›‚´õ• ¸áÐû¦ý𕽙}[€6–¡h»ø°Z¹…„Æ-ÄI²(ÃЊç`¢ü6HÎØ+ó 'Ã6D/«Ár>þõ'B³Q°ƒó7*+â3–ðgŽ¢ý˜Qì®6Š#7ŠÿÌ1l=bO í¾e†Ú+›â¯(mä°¨Tk¨è*º¤dY›ÔAù/vP2ˆb=OØ)ƒîXÝ!ö’sŽÚÅd4è°2- µÅÐA„­÷ATZô9Ùæ:–£½¸FÕ=Ãä¥Û_F)]õõÖ{‰{÷Mú;ßÝÌ3™IÝ¿HÝÉ ®¨wÒ.m§h$ÏÜ ž6(udA5)Håõx4›Üx¼§ŒkxßkmÇŽrHÔl~͸3ï)Ðv[ÚtÔ–7ž¬x{ãÚ{jVMþ·-;З.zÃuT4„?1ªí¶A“qp“I4AEÇ/„j¹)Ý8ýÒÞ?*IüþÊŽÃ{ ÝjV’0ÿðbq¾¡ŠJEP‘Q„ %#¨$9¨s VõlÉöSIòuÄ "‘$ÿ™ik•d €ÃÕD‡ï¿¸ÍðgKL ÂrUʼn4·ÒGœì "&h® ®Av Ý>ú9KÓLté¦ ¡ð“å·2º¥- UÞ~+ãgñâ€5™ª&׌®nCD¶¶­ºT¼‰äÆEpåxdøHå-AÕ ÑK¯Åo4O«ÍQvwùŒ'øÈ`Ý@•ó[†‘zZ:|žA?’[JGÏn’Ò'œ#gT׉{tÒg’±)5>[¿iîRŒúƒ¾dYŽº2›ûN6?üœÏú(h9Š|ó¾\< =-(£ošÁ ²9<¯Mò’óa€…Ò}*ëŸ8__¦“Î,3Ù­N±Yy¢t&ãFÙ–.8iXÆ'¶m~‹æÌºf(T\à$—)àô ~›”ŸfÁ( ý8fˆŸºœàââøâäe 2[>Øü©å@§7§¨éz{Áƒ ´'ž…ųê-ÿݥъpú»  ¦¤ºú¬Qí:ãÏcÒÿ›â¿I!þýI!\9>Y]N’ë<™õs©òy¬Ä”r0Y(îñð,Ç“„ÀVˆÄTàþRåÈ‹(ÝÎÒ¦ Ë9/'î< ÍdÏI¶€lš"ß’ÎÞ`ÂLg õCÉt¤"¤ˆù &z©“­ 13 Õ[ó„+ô=üªæüe×iglBéóõ4—½liÁÙ–žª3\ˆSê^ý÷´m-¬‡!Ôp«¯Ù’vWÐIãçëôÒ„¯•uÓ Më§™ôµÛýü{†3ÊVEv^Â_ùÛjz‘£8ñij‰du{r•½zÉ.­·šuZ€Lt¥D€¶ F„jÖ yùM`®H…ñP a~í¹ ¸úV˜Ä»}ôÎgj43—FIRšV`²{"dšdWOeFL]Ñ¥Â[‰pfZC,…MÒ$ç1I_µ®Ò”ªëÕM(,e$Ð-»Xÿ/gë¡æY»¢3MYÎc–€¿ƒ¤1ƒ‰&èÆ{mИjß·›­õA:ð•1 "Mß Ö’ÙLÂVšyE:©ßß“ÔÏ’¸=Ñå%õÓ ]5•ßÃâÍ£‡§@y_ú׿oSEÏ•Œ Z=^¸¹Ë#Ë¡èì¸W3º²ˆNyÀAÖ 4[Â놘º¥Îïù…wj³_Ä¢|Æ0™Ý&F·™›6íÊ[CáF¼»v( cèMR &ÊY·I/;³yâî5f‡cð€p àLãÍdv—矞8šh{Ûyª§fCÃ&G~\(ïäµ ª~ÞCÖÇØÕ÷‹˜ŽØë$×YßвSÛ³¿5,œ'ú¶ùD&³þŠ4£ú0VbIѲô¢ ï0iܨv® 2Gœ·>"k©aGh"a…«VÊ«çX4;¡ëÃþ#ºÛ†¡e?‡]æAsÒÛ£ERÓý_– 3v8›à'±ÿL¯Þ ÂÄONdQ±‘õ f§° ÃÕ+@›‚,åì8¦™:~X y( Ͱi®^× "‹)ªÅîwé>÷Ÿ±…­9)0L· {3U°à‚5+å,Y³PÝ)·9Ƶ è§î'´~ÁÄKC¼de›…ó=²À>ÊW!ÛŒö1¼pùÖó7´ª+B¶µÕœš˜ Ïw ÁÊ~4¯„<ú%˜•xY0–E6ï+a¹¼ÙN17³Î lÓ¬h0Î …/f”®À<ÍÚÅØ§­Nqš£¢k@L<Ï9çá¯óå1W<¸z^‡u(­ãÿákJÁó:–#·t ŠªÅõrÁGñ-0yw|• À¼Æ¨ê‘ù$F»Á¾6üÄâ/y†C¹Äq||¥©èµøòòéîí¢Œ‹œˆ¬ËËKH”.-”S'ãò½, ¡ãÄ{ ”ŽKA#©W ¶ù‚ÆYm§’Óµ(ár]½M||ëNò*~Ä÷‘{Ÿ©H°ø¹õè5f@¥ŠéauÝäÕ¤ƒ`hV|<[6ÌÛ¹Q@lôIƼì:CBÓïÌ„æÈ\¬¼¥ k z'Ì& /€uEÀ!‘2Ó*Q¤€uÐ*b5Ê÷i¤~¬%FªPÚÇ<‘X¿Ý.²öJ·‹o»Ý 32âi¥R·ëŽÇXçy:­\•(@Òó–úl¶ÛpÜz¶ÕJÚâO{sãùÖv{û9Ù~&_ooµ›ëñít¹YƒØþxì…ôsúZ¿ÂÁuèýÑ…ÍéüÖ€ÝÀÞ$}âN†3wè9A0¾ñãç“(«xÑÑy?˜ÎùÖ¨º_s6 ßÆ€æ%L2æC…bY< Âç|v;»9¯GÁÙ/£!}ù[?мæ koÜà oMwö j}8;1³ÁcÂá:¾=ްx“Èsðj/äK9œ Ï99Þ?|{~ؼøåà<|:IPÏ%Û$?Ô„çƒø8…][”΀r@¸–hšŽƒ¸‹HëÖPÁ[ÅúbkºÆd:´­Ü}·')&rqÿA^z=hÞxsâ.w¶Åm…Ñ£Ð׃K.yÅœš¾Ó¦ÎõÓå£Ùm•]îøOQ•ÿ=œ—ZÍÖ:tó¶Êrª „˜Ä—’!qùšàö„ ‘ï“Àž(«~õ@'Âjùœ!(ëïæâÝ>Ì瘓¿÷BL5 dc”D¬ÉžõÆa¹‰ª pbœ5à—¨´4DmLÓ$ï¾hé­ û¿´´cP2šX²üSóÙ¼'‡¿ Û‹¢qôæ0¡ ûÇqEHÙÖùnË¥½±ÚÒÞ/Ÿ?Û.°´'^\te¿ùq/ îSœYàŸE‘cÀ­ë¼ü=Žøëß0Q*ÈÁ ¨„¬šà<¿ÒëïÝÙØÙÃŒò/§=øó·(ž (—üŸË(ßt'ƒ7ÁƒH]=vg“þÈ‹„Û<÷¸x©Ê™½…m¿;ùt0wÂÙÝÝkäL1U2+c·'÷™~0™;:8±Šá ÏýxDñr¢u2c³Q€ûRˆ·çÍÅÅ{NH«8­r4õúä‰ïÃn{ÞSc9”ˆÀ 2ÿK2ŽDži1žçÒŽœ€ÒàUÑ(ì6Œ£MÂÀœòtsØabs#Xøä¥¢ãûñCO¸»Ê4Ò•Nh=L4›bÇQéïp\g¿‘f©„SqÎã4Âb¨È¨¢LïL¤øI@1p·.L:àëÚ£¸¤æÍUýõǾljzÏÀlTåug\—$î8†ãV»Œã[ÊõNn2¢í¨Tú€æÊ;;¼ ˆžñ:ª,/Ñü3ú\¼º*•ÞMÙHG´Ñ9e£S ä¤Dß8Â7Ž0”nŠJcç%:'½âºã`ØÀŸâ¡jæ$F²f$oç1yý„²CÔY×âcŽp6#bBWô¦Rm Ž)‚ª“xŒnõÁtŠþÍ “#ÃD4ÇT3g3–ÈJt‡~/ößu×%ѼD°¢ùˆÛåz L/™ +`¸ùvW'BÎÁ;ÄÆ(®L—žÇGA<î'B´$¸D6r.Ù]»}O¤²_s.Þ¼Û‘å‚YÌã@OdÅ2Æ®ƒ[J$ è³3`Ùä®90ÿz.b ßzUùî$f6‚KÖT? 0x¸°Åáâ&bEέ`c¢wçƒ)ežD3Êrˆc eû“Ó¬ì) 桑þ7m*ÀOëOŒ&jâ€;"ÈŽR¸˜AidÀŠd¢lªÆÝ 4Y‹æJ›ºñ¨$åµ Ê¼wÃá?܃ìP² ~Ÿ<©«g¼~߆`ËäÚ³p<ö{²Ö³ öºÓñ,ÂȸÉ/ ̱äúâ ð/†íIþÕ7˜sØ`NÔ­ØÐ‹a5É_È´žmÉ_ý÷ ŒUÑ>ìÂ%CP½äHˆÅ:&_˜•\ÚÎ;É”j"˜À†d)Q Žü”ÎæÄƒ T©ÀÄú“þöf²é¤ŠxDIdqŸÅþX6€y™»#j»Î?âÐ…²ðë7Þû‡hñŽB]¿KЏî¨¨®?Æ]ÏT5zˆÝ‘„V×~ƯUÈú÷™ÍãåôÌ£…)Æiy¡GܦM~ 6F]k±Ø»ÄLÖ²‹AÓY âÝÕZ³¤•Aìó˜¬úy1Ð6Fì7è¡àÄW™èyƒÝoVì³Y‚_TRÆ(õÇn9§s¦hAÀU;]×v´0A÷Íaç 5‘½&šÝ *¼>¼H—ŽƒÍ´BlÒÔTÞà]¶w õ·¾†ªÑ”,+ço>\¼ûôÖ¹xs蜞}<<«d´•F³jDœqè¶,–¼tˆ½ñÉ!–¸âùñë‹Ã³Óf9S/ç°2|Öaÿô'f»æ’¼}vî1ÍxÞpCøKVÃDЫz0™0%\»w>ðÛ&ü“»¯âß ×XÌì@Œ^4ÑE©'u[eí²²ã€hŒS(™¦µc ®×JzNø¶/¡tÜ®4šÈ"Í\‰G°à¼¼¹´ò‚*.Ú"fï ¡~S¦+À8p¬CØ{&)½³†·&,‘É©Z¡4ó•âð"”›ñµ÷QŽsqzÂk›¶ûUÐ:£”^'iÌÏÄâ®<®(±Î: §%¨žñ÷fqçû,ñB@¿¸œ›~DÎíÐ}Î@<žo¨m&y1еPÕ¶ ç [sžË·ÕÁOååô^äpð¤G“°SY\ëeïUšFœ>…ëüsq{—…ÞkŒzÝ"ªí,¯ ½‰ÃÒ GVè8Pù‹õ‹.g¤æô0*–×wÒ+cÍám0ƒ§„ÐkæG4-°°ô-™¦<³høäD˦yn؇ÃaÚ~˜ÎŠƒÀ‹&ä)‘‹dNâÞ§Ÿ£•¸Ó”Æ©l*´¶å¿–k—í«šüù]ÙÊ.Uù]* {Bß«–Ÿ–ëNÙ)×ì¡Íɵ‚<*¨gô-2Ž!ÿp¡–kWiûe–rDÿ¦0S%º_ˆÑqÜ̦] Jõ8.%4§cÕqtã¸{CGá ¼ðༀ{%BC„¯„oÞö§—¨¾Ó@*4P”ÒªÂAW¦¡‘•Óô¯•$E§}+ckè`6†”8GeÑ‚® ÂÝKD+œ“˜æ*Í‚¼"ùåOiYÖåEÖ«Fû$”X÷üò> 9]xÊáïßY'¬aýÞ60ŒNƒJ]'ÒvòŹGfÑDáƒþ=™j…ÝTeE¾ ÇêépIl‘yÕV:4¡j$Ù ÝðLâ¶VáFSœ҆Ý<ˆÊ]!8K³hšbw%+Ä Ù:Á’-Ÿ™S" 4d˜@ùIä4à¿Ë'Ñ•ó$úmØÏb“X”€‡¨Ëçz«ï•ñày²‹'ÅE¥yˆOh€5:š1?…­ÐÁÊ›ˆÕ¼”ÖJ€§r”Z’@ìäWM»(GJÅùµÜÎËšŒŠ¬0ì ¦ }£¿nR0#²ñÔÈ4H']Íy¹ë<ßH™Áp4 ËèjÁQ….[;Ï7®ÐLLts í]‰ÞYPç|®nÂ?r¯a×z¶Å®2ÚŽ:÷ß½íîuΟmu:Úo¥ËJ§ÓÙët;‡ð/ö‚½Ÿ;£!|ÝÇ:?ã?Çô߱Ðʺ’ü<èv»?t^À¿·½ÍîýÁ»ðû÷÷×ÝÎÉ/wÝûýþsüýÞ¿¹ƒß{_ð·Û€ß?ýB!$*XhŽßA¥ã‹ Ú¦Bðûõ{üíâïþ‹çð-Õ÷~ß¼û¡ó)†ßG'øû'üMýLÃOðð¼ Ïã^·sx¿;Ûøût¿÷'ïà÷è ü~} å÷~ Ãún÷z³óþz“{q¿÷|ó®ßù÷6;‡á§Pé~wNá÷ÑÉ»ç÷ϳ:‡§½»ÎþëÛðûÎëw^4®?uÞ¼Ýîýt7øÌ½ëm~ÂBíáÁÝf´ÕyˆŸ¿ëìß¶¿ ÷Oã^«sþÇÆ x?¾ûy€þÐùðÇF»sð:ŽîlOá÷fäÆÃ?úÔXøóÁ§;÷Cçó³qçÝõëI~ÿz߉{Ÿú¿ŸÇðûuøëÏ/lœvŽçýß;{_&Ç÷w{~Úyû¬=‚òá/CöåöóΩ;öî÷ã)Üoàaøùì§ÎñÙ—óáÞs·túñOÐh¿ÝÏð;þ|¶ßù©ýüçáþØkw¦¿œnwþþð…'àçý¾ûå¦óûëÓhÄ=ýyï˧ß;Ð;ýþéç½ç磲Ï__÷ùocÿmìßÕØë£ÑÖç“ÎgýÙ›×{[Ãã½ÏŸö†ÉãÑðËÏg[Û§þÃëÓŸõÆö~þ|qÔúÔû2¢ícïèãáð§£Ã‡ÁÑëíÞüþÓþý^gðæìïûÃéÉ/g£oóøîØ?>¸Ø˜¶>v#jìz²ý€ôýÎýÑÑÛ?ÜÉÏ›{÷ÁOû£ÏŸ ¢ý‡Ïîäìw|öþÓdz_ÏF§ŸßøÏ·ã?“_7“Ѷ÷Kû55àzG‡w/ÚçA^øéËðù'¨ðºu.nO_|ø}xÿËA¼áEÇGÃ>ñàìàäCÐèMF¿¦p6¼?;o÷ßmþºñ)ÁÉÁ‡‹_ßô>þ½5hÝ$ˆ:8zÿycúǯ‡IAÜ;ï»]Ñì2÷Nä»í¾Þý2ÄB[Pè®+ßS¥ÓÍü¾O~Ÿ4ä{jìázk·rUb;Sufšž±” ÕÇÆ§Kªã^®iT‚IVÚÐ)«Z©p4‹¼Ar‚„'ìÙÄi¾ÛfxÄGh­ª´/²ÃÇÃøñ•*UrD%_ ž8;qøbz!z³8Î;ÌO75ÌÆcsâDûûÿðÑFï8Ù_GÄí6RœLQŒ…Ù7çð>OQiã è=íÿ£cÏüÓ@ÇoK.´ðšq6¦…³É4ôðF ¯&é0†ÖŽpJpzèù ÊšEžˆƒÀÆ ôc»We5UÕjB׎5ꢭ(Àã¼¼°„‰!í~¹whùK&Ù*IAσÇ~0(Xá Nä:žëÓ*txI‰" .¼ý‰”y ÐðÏ%<\Éx.´ÖðÊÀÁÓ0³Z"YyÇx5„G"a8 šó× ¸Gm Vã9p2Ò@õõ€—»èÐÃRœÐ•ˆÄ• TÎG3º…®°J‡• x¥ï¦õt.‹„ó½è]´0Ô¼¦à¨{ síl/€$)‘¼ˆÃHÌ&×pô‹ß9Q¢ ;n4aó0?J(=°Î³ Ë¡D1}„hÊG½N߃º‘¼Èqªb Ö˜¨Ba$ Ö Z•#ã›;œHÑÖ-ª‚°# ùÏÈs0.”¼žŽÌaœûêK_Бn]‡à[d …*Þ¢Ç;ÞDhǘj€¬ó)m¨'î„…ZB»WQ¯”¬nyµˆšŸdqK«×sà©S¬5T^‰VùfÂ~3ù’»F:õk•àÌïÂÁ¿¡ÚÎ-pÈ©!Pov}MÎZP+Š»ç¿#W库Žp5šQ<ðÂPèSX“…l»užDê>šÝßåe9% HÓ1¹ ;ºkÛ¶ ôõ*#q?¸ÎbÞ8q¢™^5nFå`?F=]ÆV Uó˜¿Ú+56¬)³h>ÙHl7±h¢S‰vu"zír l‰öªGÝhK™@Ÿ§ÖYS##h%1Œ¨‚`R§Ý ÖTên¹–"¢ òlj1ózJxqÏò¢]TíÎ CeÁH #¬©p ¥ 5‹áÒŒžæ±Ó˜F?yó^à†ƒc•™'rêFQÒn(æ”°P'¸%³³£TcÐpµ–3¼{×ÈPˆzÀf4ö¼i•Vá'Lb a؃·0LŠÌz•`@vD |FèèƒF;öŃvˆf=¸QFºD?œð¾&ßëá`Ðrq×Ù8‹ŽL°ëˆÕt|Aߪ€iwa´¢ßbˆÑ )0.• û>.ô‹\s>ÀØa |›Ù84mªzi–ØÞ6‘; ÈÂÐíß ¾¬p¹`4²)¦nï'¬ÃþŸoª5b‚²nÂ2õûÌSÍõØ„2Ra ÿ»Ñ%v ³Ü$ÌxÄ!Ùû|wJº‰A[‰io9Ò"yÐ,‰èjQ6§±§â»I5®¢·êý¤ùö݇·õÊÛJ½2ƒLESzãÛ‡g{õÊGx øë¥ßvþ^¯ü^ºƒß³ï>Ö+gôî®R»*%@ݺqTUà¥fÄu9©±8¦9„¨ª::àU {9¼a’§·lI DáÇ”7޽Û)›.$æ]D]r³Åg³`ÒÀóõ { X­*ÌÓx`˜FM"Í(@½¤ðÐ.í²xûJÿµ!Îp8ÄI\’;ûù‡½ó‹ÎÛýC­Ò3íiRóyéôðtïðL+øƒ|””zQBºî¾>{÷á=ÂØ*¿•¨a´7Œ§j@›¥ý·s¬ pžœ¿ëžÒ/€óøôø¤s†?ž•ß^tŽONáþ~^ê¼½-ÿP::뜞ã÷%hJò«VI l¿sqøúÝÙg|(‘øá¼ó»Úذ`u3]h+ÛR»]*žì\P½“ÏÐÄ ¼?è½;#¶K0ìƒã³Ãý 5ltã™ØnÇh–ˆ¾Ò YÇó*[FÂf®nw=Æ5‚t›ºB⺼²_3¸Šãâzª5öoBEÔ»K ÊÓ©€ mÒHèD=&?„ ‡s%%‹ÐëÍüñ€ÂfTƒÐÒ71’ Ÿ Ôcil #zï¤už€­:¡ÓÆ¥ÑAÌÒÒ”\éZªÐsDVŸé˜¯í˜nµ¯akà7Žª•hÕ\‰V/Ž\a+)4ÔÆÛ`|‰vÑÑ ­®?gÍ1Îo;¨æ“>@<ɰDÑi·Å¼édSÕªX|Ù[ŠròÛ‹E袟½«­fÖ4´{,±%¨K:~ÛÝÅ=Éu(LéQû‹áçRyH¯CÁ Pi‰þèûؽí \çaÇy0 ›Šœ¢µìTÎQTé…vdU³L2…gÕVADk(1¢ŠTMžÇίèFÀ92ÛxݱáRßr¾÷xRã4€²EWµO”¾}ÉÆa§.Ú¸,jo<³ùá¤Ìz¿…ÆÉòK’ÙR¾Š )ékñ¸Ì::å ÅFÄ(– ÇÜø“.Š Dt±xJPÖ;i{KvU¹œ6YR¨;åÎ$¦¾sT2 ·b±ÎÇ;°»¹¢\æ–ˆÄúw´…N—övÞºýZ¸FÓ]Ñ4ªV©C²í!(ÂYö…¼Š@ž=~+Ôe¶¬e ÿÿ'ÛHŠD®S’8Ñbà“Å#ó’rö±‘( î»x¯±°­>ÉYqBŒGª!wOÏ[Ø:°t¡PNsÉéügçu̦‹áÃvwHåì­þç®ÒtË Vmçàïx÷a{Þ=ï\üû´8Ñäsë]¼\8oëÆ}ÞÖ0MOì÷}<õט¥©ç4Ø¥ƒ¥k˜&¼¶"MŽE¸ÇC”,`l ðä®ÖÂH˜ª†rз¯}Ͷö‹lÑFÐ9øh&Çú5ÏMÇ “£Kå™ß¿©j°ríFq=Ò^kFÝy rÉs%2µ£Ÿû/¦Ø¬‡Æý„ªÐÉ oÿYÊ8€ËÀÈ9âmùÃdD‘¸Æ+š—ë\ôåºb/è{C³òhÿ«ÐÝqÂòU ñ®{ªÈ©XÇúÐÌ:#Ÿ•¼èpÂþ ²ÜhóÕ9¹Ž8˜] ŠoB¥ÙøÕ˱Ï^?Êž'Rº;t1éSpïC¨Ui0‹§³X¿-.aÐÈ™3ºu¦/žÈøÄ+ì@wÉÚ2®~Ù{EncèÀÓ|¹•$T¢(Ü;Ü ÎôÀÇûRÔÞ²·H€Òž×gãlhi8Â&K~Ókj5ðÞ•´RËL%ývÆc̘ꅈ€M;à“8„?57 F¤‚ˆïZD ½Ý)Ñøè;G¬À;%¨´ŸØ#¨Nß CkÏâc:|%á9žŽÌ Á­5LmÊÉ"·r²b‘A3ä\G" #”$pz·?ò% Yàò85ˆÕ3Nhжx¡¦EÈ&IûnîåìÕùÎËõÙ+¬‚³›†Ê-F‚+Õ^Í’¥MìOU!v‘iœšãfHÝ( ú>J<%A,:í!º Ò•®û@[8HXεïÚÕvœP,o&°º_Qâ³u6‚£Z7ÞE%²K‘MòÒ¤×l¤"aYŸ7 EìîJ»Õz"¸Ai¹l¢;(nâv%—2¥¸cL%¹‡U «';Ê=qà•(Ù§µš.ËO©‚øjÖ½»ê¤žª5z2 *øËs‰úDDêú± #O÷åS?ÕÃØO—sÁqªät(öYä†B‘[ùÚ…ý¡p*%¿ xbÕ’E³û\Åᬯ’üJS#"ºt„æ 4|æoTFÅe!®nPåj´¹ô¥7…U¸)4Õ‚pUü iŒë©ÆU;ÆxuNÒ ßö|Þ"D5–d3yRø±;“RS)Jž¹Ù8=s@ÈhÒOÙI‹Ân¯'ÂÐ0ãcؾ\TTúÑMTçÛ.üKñ·™(*¢7Iм€êt¿o·°ãïåGƒ†¾ó+yP .UÃ]ºÄõw…k)IS’r¥:JëÎEäÂo­‚G)hÿ·IEhý»äþI\GH[]à ™SOÊhÈ2„“ž°Ne&Ç^Ê–s¶ëž'‘Áòã¯n°ž c ogá‘Ü!c¨Ýn0ñ$ЦP+ª(¦m]€ >Ñ$;bUb`1Hq³nY^$–ÏBÚ½SÄo]IÜàŽºÑÊËWú ½R2ˆݹ‘óÅŠØUé ¾šäöÒÌt§7ŠV ŽQ&«ò}׈#‘ZÆ)¤¡Ú& ÚŒqä,áT¤ZI,|ÍŒŸ1ϰ› 696¼ âc”ûniâs™ˆ…HÊâÞ ¢˜­ ~S®™ h‡Ž:]=ó¿°W{ÂFÕýË®(TNžá9¬Áí$Ifoè鞈̨´~+/:ëa|˜æÀó¦ø¥zæ‘܈ùu4¢×ˆ]Úß\7E Nc! ?®$3Œ Jž:åÓ7i´ªÕEfÒ‡ˆu¨Ù8™í1ŸÓ&Ž…bÏtªO¢šCò­{ãu…‹1Js@~×u£ÉZqTK¢6óRèÒá¶z¯‚VL÷ÊÉ»ÒÅË%GK™‡bšp¦@Ó1yk½ûåAÜ/rêåùÑf¤fkÈ>¾{CÒy H ÙÚìÒ_ç])Ë›è!â¼ÌU’‹ƒ„–Ä ¸ó'>;H'À–tÊ¢ÿòoå'Ñoe¬ÍµFMqð‚ž +/t ž:Ý4¥,ã̼÷EÐfd°wŒ°($aÑ -Æt»ÄÁjv'!tÌ&Pæh@&ÿt:㆕$ÂÖ6R®b΀"4äàbu!)ÍI|`kÇåÒ+¤úˆË¢I½VúLËK_Û25t¡F‹±ºêĘ[æ*çí;¦y<|©]Î.—>fãÔD2x£mã<ѵA昖ÝóvD¼«Å[l"ßð!Yâø0tžl¯‹:-¾Ñ’?|ÞV«ÛRT¢ÔFoÛf%§mqEV.H[zŠÃÙ÷œÄG…ðjhšãPÚ”Ø.φ•ñføeÒ}Èœ´bv7Ø24~mvIά=¦€nê[¥­%Ú¤ÓÑ+Hn™†AÏÅx®0DÉÛõE24Q–+YQª•HÞ½\S*â¶zžÆ¾š¶Á”‰!clÉÜ ä§š5£3ËZ$Ó»ÌêÌ¢PDõ )21ÞÏ-Ü;Óšà$Ì—«ØM­CÑ^#øéǨ ÛI¦ä) ú‹ƒ¬Šð¬¦Ñ$” Yµ:èuÙ­±ø £‰jY™†TÊ»šJ Íæñ“!Ù M!–UÍÏÔbR±€±Ö! ,w úšíBÔ¤i10½Z/5–΢僎²ãÖé£lÐG9C‚<˜:×¥4ÏÑ:çw ÷â°®í­Ø]õ~±×Ìl?pÇÖÛIf™Í›µÚµó€‰Œþ‚æ…53•R&hCã1‰0 ?ãH)Ù¥ƒ³Æ(Ï ëLH•C®J ­ðÝu(ÚMÉîœhwý.ÊzÝ®ÌzmeÞ¦¥‚ÚùIi€„ÞSå'‹ŸD'íd¤<ò a°”i7E‘À妚,¾Då,E5`•éµ™È>ë‘I߈,ŠGò^Ž®a¢ ³‰d‡÷1µíúxWågpÚk|Z?ÚòÄj*á¦wj‡cÇY“ÃnfXÉLʰ&fØF½³C*Á§ðd²ü M—×´˜È> ÇÎNì‹ëMáÄÉ;LiîG˜xi"ƒ\²ÇS4­‰)E9`¦¡‡9\\øïH£[Šk.]îB>07iCËÊLó|@‹zÄcÜ•áU›ƒÙí4ªVÕ,Ôíx‡Ý¢Ñ®¥yQ~dêFÛ]òØ8-9ÉÌåãœÉ» 6Ø']]ÏYoäQ%R¢D†"i{l&‹Åtu¸g¢B ÿäÍi÷²UÔÛ±˜>ž«fP˜ìy“Aôvâñ¯µU5±#}Á´¼¼ÈѪ˜EbFÖÜÁ@k*³)A9“mÌñâ™(£Û!sbµ£Ezvó°›émCÌÈõ<‚q„|6)X¬‘nÙÀ#F9jBWq)¹¦Ìmð€J[öJí¢B@2*+f³A®c,mß5V2ÜAdT£*p2RhFĺ-ÃiªòÒu0|Þ®Œ¹‚- 7._^¡<§äAÑ€Àª-Ðà"¥ˆq“è?š‹Ïíx´a³ÅxyˆíˆÝƒÂÌÕ ¨:Þɇ ”'¤f@Ag ^¼yE"¶¿á®1AGΧ­ådqhVÐÜuøͦåI›P¬9ïRË’ ˆÿäÜ=ÈMU0ƒdBXŸ.ýÐ3ÀësƒÍóÜ$–†˜†è Æwž>M\éÿŠiJÝ Ú¦)ÃihJ2³•zÿß)û³§LÅ8²¦ªŠ×¬èØÕ£ÞWQOøt$„iÕµR¯˜ï.ïùD;ãå}yö´Z“ÿäb€tîÝJNôWú¨v¨m3ô)Ý#àñ¯\¾’ê6JlEIIÔÒ­ßÀ{œ2œU¹Õòû±‡±Ž9÷Æ\åÐùEžˆšZJC!¨ãfâúDëqL ÜÜ!e®¨T Ή&^ãk©I¤;¸Ô'{2€yt#VxôJ ùº0®?j2N©C”·A8Í«÷¬šÖ%*CÑÆñ Jšþ$¡‡Kø–Íw2ë/>ávÉMÍtÈëX éQJN üF±@uZ|ÂF½œk£^vÊ…lÔËÊF0ûò/Æ’„€HåÇ2ÕŽó:t§#Jv›ZBg.”)P¯ðØœz‹’ê劦t ‰&6vOš(jx¿Äî^k° þ÷dÿî|/íð|+Zã¿mØ»ÆõP„³iXK‘ö<0Ð]€J_Ü3,|3o s“‰8È…eìßxFé)ÓE*`O˜êK ø¾8ªKU+€BÍù¨2À”8 Çç`ÆìØIÆA™Puäô$"J’áó'²Éž¤8"UàSÊñ<¼Sð&2g€áÑü}J¨F’ÄpAèhG2Hbºÿ2Z0挂[>ÅýK…\\´‰aÐûšf{ƒÙzà,,‚ƒB=R“Ö Æ•Jƒ¥"ÀHàÓáÚ)¥A¢e™ZÉ Ñ¢ <2`¼TÀŒ8=÷’$bЃ¡¤»‹žÈˆ¨¦„S–E°" 4ˆì£àÞ”ØB^Š;Pä£ïÝÓÃhÖ»õ&31w£M2¨ç-á˜ÂkGÅzÒ¬Þã³ðœã+ Éš~ 'c‹U¾fOúR“I&ù(ÅRTIåÜc‰/Ü<ø¶„D€>Œq¸ø ÙKì³¶GïSÝCwáÄsøÍ¤bgÇÓª(r>ž`âa6¦.ö0ŠIF2üb9¶…í³{œ%Å.0æ,S'çð’˜=pë úì=èй1 €WHÿýxÆjn´ ?ŽÞ«ë“cïœ0åF|Cõ1÷¡ôR¸IJB+ ø”ê”_!91-î+7ˆièaÐш=LèîIÂ,Þà(ðZ‘Ë'pb*ÃhW1r>”QZHÔÍ5pwÆñÓï:î€t¯\24‹x5 to@¾GlhÅ}ûº ¼À!Š%^2}Ø Ó\7oC?%_fÂuù Î<²”öâ#f‡ìn›NG»2r)É ®^<Œ•0f«d„çíˆög„‹žšÎ¡«9³H£Þ ü¨¤ùÆ™G%‰o„0í“Ío{°™&Nht"&ò„~œyJ¦.’Ø8 BÅ¢LÓ£(…ªÏ^F³©Fjܲ¶+F%›¬”ß”¬Î~Sš“˜(Ÿònš°“—°R‰õÐxš Ö<…U/’ÊtÁ+&T¬˜Súô*γŽËšÞä®&(?xŒ_æ¤ø¶œRŸy¨¢ÉÞ&œÑIˆ6Ð}Â: ”L¦uV<›–_e!O©ó¢c&ôÊ`œÓiŸ¢Ë]ßôvT»lrQ™*B&YÚ²•…º#….i=ùŽI–Ë7G5å㑦™1™­S{ÈþkÚxèõ±7ÕVJ}TňЪèNùß!éFrÆ ðIg~È ·™{iKÞsÈ ^‰VR¦’,Ž29% ’2ô=ÚÖ#CÒ07~œox²¼8}0 Ô¡ædÛtô #_ñ§j×Ú=á™F&xI!ÒÌ4?K'³­·`gßTsؽ¶ðèÁŠA ~ÖïIÖàÌÞ8M‚‚Iý{}=Ô®p}ï÷ÉZ…1ôAá’r&k¬¬†aŒ WeµJé^{j·a•^ª_ø¯Cõªÿop¨þ÷ûS¿E ø—;U«Hq Mù›SWr›ÅƒjYø¥Å:ä8ºªNÒ9 ÑÀÎí¤œ\¨¼E¥~ù¯~r%ý$&õÖyyq|qrhUñ½\çwRqÈ5K/å$8Xl·üüÉ“ú‹Í'OÊ\_;QØß-£í<Å4)³Cµ¥ˆ–´SÅ}çVM6~ç‰Ö®Ž ªMôÿ|qAŒÆ¼í°ÜXÊÊkÃÂZQ÷­JvóÕîL-)MÑO5F™Zp·Ç©ÑáNÊ9_ptJ¤y$r^8¢8ß4êI+9ã.ÜÇ_&üku¥uúµk´Èú\ymf®Ä M_ ”õ „²º@([.ü(hüðÃö‹F»ìh ‰+„ú;p‚þ ]Ÿ«yS@ßYøäÅ‹J€i v˯/Êd¶LveÆfGÄï—+Y̾°êÁYÜùnÒ‹¦?¾ô'$b2XCÙñ¡eaÿƒËNä—íVwbRŠ›Íªuq›*³²`5ÉoÕÙ®h‡KóðøPUm£†×‡ÖïÜñ ~òF,ÙÜ:¢JúkbF÷Í‹Tqs“jO¢R&øÑrF(Ÿ(þ&Õ ¿•Ïß|¸8x÷é­sñæÐ9?<ûxxö[Yµ°o·Ì4¨Ù e¥Qº$˜åÌv(‘7åÂÿå!:?Ðݧ8ɤ’ŒQžÖnwô»]nÚNeîN¤Nð‘ƒLCTN5OÊ*üŠêN·ÆùÉáW“ÿTa~î0ÖuÝ)w¦;Ñhi.ç²,òšì–ÉK?Œé ³[JŠ„?q4eáBH©w“ä_fžº»§çtYœyª%¯‘ihð1J»8®:ÓbΛ0LÃø Ð{c\®9¢?hˆvÓÙÁÍî0j·¯ ÑU~ÒÎ4Ó07ݪ@æ}[Ü\”iNGlªU‘¸gæ5;Ê4K”jOŸjM.:õB“&¢<›™¤®Î¶–lMÂÑÎG¿*üÞíRÞ…nƒCw»"ýÑx©Ô%#©.$VàQåªÔzNÿ©Ïæf»Õzöl«•þ´ÅŸöæÆó­íöös*²ý\¼„'Ïšëñí”®Õ2GH?ס¯õûqØ…_ÍéüÖVÛ¬×ø_¡zíÑ{trû½;;{t-3íÁŸ¿Eñ µ+Q³Áùw6踇W˜Mw†mëk…7ô$ýKa¼0[ 7'•ûЛpX2²äkL|'YxÏÆúØû‡÷\›5¸3VáEê¹”éM¥Y¡§G b¬Ðú9œÞÄŒp¹CVê{íâBÄPü½ ¸q>ïà=¶¸Æ&í'Ñ’7qÈ…!ÅÒ+’õO½ÉL~?Â,[*Î׫Ÿ#÷ÎûèuÞ¾#E·w/+]ˆ>õŠXü;R3ü(ór%—°|=„æÆ\ÙÔ{ì!œï(…úwdü˜Ø9œ¤ [9‹b£0 N3Ýyß <ñeB¡œ¬˜Õœ*ŽÑà”œ‘ºñZz†uS¹Á×mïß'¥ºMéF‰Îö»6ýÿeïMÛ8Ž5ÐóO1¢®À!‘’¬„1åP-1ÑvEʲ#ñð€!9!0ƒ`QHœw¿õUUo³€ lŸ‰)`¦»ºz«­««\Uîw9Ó™ABˆT9OMí ÷VMDæVSš³è33*³áM+*³DÂ’ÃhšÌ¯ÜI"&ï˜3Ÿa.iiÏa  «ëÝ0¦IÎiî“UOeË•3 ß‚²¶œ"³ÙÆŽ9z&­k¡× ’ƒ ²*¬Ì'þ0f«²1R"3ˆ_€zTN7nÇA0ÈÌ š~ê «ØÇŠ f„˜lvø^Ÿ‚÷<û/Ò‘\6ëÙi Wé×µéGQ‰½F¦ƒÃœÃžNòb^êE¸?J,¨ìÜáK1nÒÂo‹ò¬‰µÒ ‹&yá¾õÙ)PË¢%¾XDæ!ò¬™r9Õþ¢6Á*¤Òb6§Síï "˜§Óñ‚öæ"s¿¤lJ|žS…)ΡµK‹ãÕg÷•ìw¹îžSsý«ÏÓãø¼%`=n‡÷»8a;¥¡;ÿeXXÅϸ˜^K÷âåõ‚øx½&?bSÓ;W"z~ßhNB¶OûOL°yÙóœÀºFÄ>ç` `YèË¥¤í,Dyàjñ ùÅ&ñìÒ¸Á´üñÇ6’—ÑsúºÃÉ"y³R=Í¢É!c=¨AÚV¦ CöV{ꥂc!xªÐdœ_læ =SÕ¸¤’È]¾Â¤~$rœ~ ØÜsÊz6®ÔÞÚ-–/%Ûnä׸ãÔ(š]—8×ë§Ä(:ÌÙ2ãT$k’© r[¶´Ð)\ש…þ«Õ²>úyN‰áá5"`è ô#É>¥³<ûÐ~þúåÒ•l°œ´ÑmÕºàëË]a=>U¶©UÍEˆ/sTãþj4†‹=hI—N!>Ÿªr Ö¿V‹#¥0g g8 ‚˜µ§—ã6nxݾ­]zùîèX|ŽØÿ=Å8”Q¶äŽiÎ1¾ÇõùCœù"ž|‹ë€Óz“TöþõÛ§¯ŽOŸ>9=|õýëÓï_¼Ù;~Þ´^j\ÒÛØLâåò9=‘ÑÞ²ï~{É%K!ˆ‘кíEéÈSŠJµûujã…W>Å|Ij3Q©WÔÉw/^œ¾¥~¾:>}ýоþõÀé…|AX@@;þ¾„ÐqÊÂÉ®<™ëÔ^ 2b?þžøÐ¶•Ú'a+íN!Ö9¸{ȰùâìøRàoê…Xx½ l_ø£D2(þàJH$ó¦ÿ,™?ÇeO/©g<8ç§bn¡o¾þ^~ó†Êü#bnÀý:w'¸(e›åd²ý}ÉOý4¿ÊÊw½«….Ò³y]ÑÒÄáB}õúøàÉë×9}³÷ìàtÿùÞ«gO5Öë C¾Ï´¡,]ãðÕ³*08V]G+ù,¬íÊS¸×ãÑQ¢Ž¨^§$?½-UW¤ààÓ†hÖ• z‡Öní‚‹Ý 5|‰Ê¨‘=ÊÌœŠy‹ÈÙ—)y†¬‚="UÓ™NàÀ›œ!”™7JG#úŸ?ra× –•áæYÿ€/ìu yq…ÀLÅ\:ÖÀ©B-~±€ZÅÂB1ƒ#¢¸âp“«( ’¯€¶{Q¨yŠ*P`VÚ~´;Úíóùåv›/}î¿QChpÒaÀU9p·UzýV’ö±³` °°ÙæûŽ_žß'°–€ÓnnF_í¦Ö„ =‰g¤âEü!uÑ@ð¹·Þ·›¼"yþÀŠõDÉÞ €Ëe×O#^Y Á[îÝÞhÄK•ŠºitsLŒyXC†JiÕÆµud=ÐßZ °Ú³jâôò•w”Ï |84׃/ï·x/®\hS05,ü7q–ŒÁÂøK3·çå2{®QæôºŒ™ÑßÔ,&.„j)n•õODû)’9Û³¥OW£?# 2îcç\_è é¾Rj zfU ‹T,‹>{ÉÜ;é"c¼¿®Ä¿A*Ö­-δè×eÜk*¹íªýÃrE¶,ÒX ÒŠû\~–ÐZw[¹" ÍØMKؘ7âbùÁ!=»p¹ê ÿŒD$gåŸH<›aAüpðöøpï…_x1 ¢§[ÕÂÏ_¿=üëëWÇaqAKö€Öa·3Kg‘JÓ\ò¶o¯¯ž¼;>~ýJå©<;eÀbœ êVÐQéezÑÃÐyööõûèg|Û{ñ¢m—‘7;¬ yvØ»)â ÔCÜ4² qSf ÄW ‹óð›`Šó›^ÔŒÚ/FH/ä.Ó)º7A÷øTê—#9ŒI‰Fd퉊è{Án'ªO!êöº¡y{/Ÿ™;|öüøgûóÉkêÀË< ƒu—¥'' Ò@w>û(·= ||púæíëýƒ££ÓƒWÇoW òñÁÇR¨~ ËtQ À¾8øþøy˜ Ì Œp§‹éªUï£Q¸·ÖØ Ðz‘«WÂ5àäá‰n€"åzÑ–¤jua2h*VM,{‹yþ"^拹Æ!²\–â2YžÂÇ´A…©×+´ÞbzÓZp©¾i3U õ*ó~ä ZX‹¾@ÝØŠ¥ç ÍI)Td±r¦‰#Éú‘¸¿ƒÕ‘óôÒ/"*ìë,Šø|î-«µ~0-” 3ïøå}á»Çü C@»5’©V(‹Î õI€ Õ@*‰ŒÕ#«ÛÑ[5¯Úë×vƒ‚%´KƒáOχͭ“ÆN•´…F;e +ÖÃjáùz¡¼i–Êaêý²•ƒ(zvÝ!TÝΩ;M+ãšK gùxÜác`§Yd½æ¯¨ÿ%;Í_L&v&C®©R^O«¶” :yÓíäÏÔ•&ÄK$¢ÄÖÎÊÍ´¶&z ZFïàf‚Œ¼Š1Šò¦¥4y@XÀ’ã§X¾OÄõ^ü*-âq¶˜tlRר)ŸÂ˜¤zŸ5ŒÆò%+ÅÜÏxÄõàçOÿ³ß7Ûe_ãÌœn¯!8 ´™fYµ¦L-žûp@.{s„ÓRW§TFæÄ/V3fkN€·ºMxåòäð µk'¦§á[+î‹¥u\ë!èÕø ù–·Ïo¼MŽFµÕu¼&욨é£, ×nùh¹DãüNþ"¶†r§ÿ5x›Eå?šÁís<¢!WpTóðÚ‚l?[ÍXt»óÜ­?ˆ×“ýn#ý÷J7mÁ›ó¡ºMú>Ê¡©¡ß{ÆØ>êÝ6™AƒógÕY­@ë[ˆ x*fJ‚…[/%³$Ør¯½™ÈOá#(ú{²þØj¯¾5Óï÷?²MãÎkß&t××ãË·]ö WõÈ«T:’;ùÌÉ ¨§õàÙŠêà¾iûÕøòNS=~y4’Ïõ >§ó2¸h©Aå~‡ÑkW¦ù}u"øìÜ”Ú{å]'Òn{í+îã–R¹êûÊØZuƒ \¹¡…•¢ Ú|-©½$ :„˜_•äÚ1=ÝÆ¨šKYEehëV̦ÛÕUAšµŽ BE{ÌÃE”´úú0;kÝ·|±\¹¶#Ü Þ"Ï…>Wzr¿¶'©Û«÷=4*³Ð>Ôëbr› F1EòCþöj1™Æ#÷ûÝ´ŒúhÖÚO“†–6£RSÞxöVF¹¾5bvΪÀ¿Wäû5‹ø6_6 ¼Ùå"`9Nzd®…hôÖ¾»õ9œ¦õ˜Uoܵ»®^#R¦@qZ¬€Ëþý"©hß]·¬îcYý D.,ù v9]œ^”N'.©ËíáB Ñâë{ÄhÅ$kØ•©Y.âÛ®_¼zç ׸‰É÷5zàb+,éa5Üj9ÁEOãµ+1KÒ[¢§7¨ÅL§gXÓÚq©†T«²»¹tZˆ=b#k×áK¬Rk¸~-a=e 7 UEäë ºfOéäëÚ5Ùå—=ε«Y'Ùhàúx¡ÒjªýÄç:L‰6XÎÀ›®?ƒÐØäš;6A±vEk8évܬ_>C½ØÛàñp˜ŒçæÀo¿&“Ǭj|X)_ ±½ýýƒ§ûÇo_ô x¶IBí-€Úq8êt¯qk¬ƒ@‚jÃ/€@"iOèÄ—¡_ïüËéÞÓ§½ôK»¢0^½{ùfïé¯êÝ›_ âèÝ“ã·{ûǽѯӭ_ ‚×|9žtGzÙ/‚ðŽ^qc{/ŽM?p´Ûêú‹Æã—ÀñÐá3÷žÐì_„Ï/ä#tpüîí«žp®5Ôˆ,BeèØ*ÙnVŽ&ŠÐ,©¹ Ò×?T*Á?wE•ý¯*u˜ÕW=¶3v¦uÞgrþþŸx(îaÛPéοüK±ÿâ£.‹æÚ^¶†ìõÇ]AË7:ôZÃKÊk!(-ånä‚r¥k@Y•w­í‚~×Ý ½³Bµ¯ÔÍT«0ÿ"¨%LE#^Ic1Ѧ\‰B;ÍøZ…9Eô8ׂÕ‘íOˆ|ÛOTØ®ÅZsA£Y>É?w\ñòÙ¬?$UÔþºd¢øºýˈʚ;4Ü •„®#4…/ i ,ÚåŠ~>E`U0‘ø2‡¯k2¼’4øúéëD‘BØŽ†Pw³ÅÐ}’`y£˜GÃÙ+aÕñ¢iNWN«™)læ ¾0TwÚµ‘EÚ’?î+Î&€p¡]îÔ‡&€€¬õ£· O>ìÇo7BÒtu’}‚ƒ•¥ó>ŠûA›ê–E «@•2ÑV×ÒÉàØJ’d˜³‰h "¤ÈÖ¬LœtÓÆËéÛ]àö›9Wµigñ©¬=¸%ZO6ÂÍÁ‹N{´Ýå[˜ oiW³8'éÚgR"Ôd_­L[ÆÌä8­ÜóÜ“*&nÃKÇ—†)Ÿqô×ÖøÎ˜Ü0¨Q;ÈÙ®Ô'½ôôžFé#²Q‡ v«ÅÎLÉ]R&ê3-'³ç!>ûɳ¥jÀ#;LóV!|Vð~ÓHE|J2—ßÉ­ “m ìp$Z×_µõ¶Ãz&`vMi/†Ënq‡ëŸÔ¬8^U\Ë܃g²­·}‡qr-•¿ˆèW‡uMÂ_ªxƒ¨/z©®6FÐõpѳ){bÎg|çŒ~«pLê$Ãw${M“!löœølÐßi&’+?B¨ò#è1o§–8÷h|.s4þ4ǹ¹u}•ŽGÃX·9èÀ×ëÞ’yýæà®ÇÈûÓ§‡oÑŸšìãXòe>ŠÇe7>!I\†=­ˆt5“­ªêbÝÑéòðÓ„æ-_7º­®|ãñٕѯ;:G{?„£ƒ_¯8xûþí¡\éyùæø7± ’«ÖèÁd:_òº„%§.ÀQÝø³0[Ùîë:ÏUX¨öá×c£vÔX)>A†m˜°jú~ÖçPépÝû²ŒˆO=ÑçkÑâÿ0º‚O=mñK4ì*k»jÚT èpP vV›=xXŽ|÷öE{ý­@M¹P¶yÔÛ B‘M<ˆðt-•(©t¾ªD+5QMGÜ/äÒ—ï ;X\u\LFª´ç§±7›ê¶ç#Ò6á\¶Ø¦ýû{÷ºáÐxpÝ©-±4ZÞ(èg|…I#A,ßN{‘áß‘ïF"ªÐÍG³¦en–e @ pw&ÏRœÉ_¯mnÂo]?á»É¹OÕDn–zS$-„x±AAÓb^Tìð¾uq¥µ~æ[ëKÁXÕxÝ!(ͦúP¨†tUœ½8XFööËÖC(GÏÚÚràìÉÀ'+ðG2§æÍq a T™_‰ÇŽ&Y8e㓌tÔ‡'ýS¯R“ŠŒxä£t’d‘ÜKÏ:H†1&dÐcçsU•ok&(6¢ÏrÊ@ 4?‰?›©óĤ¦Ÿ= oîŠÜ¶áyÙœ0C–Ÿ™FÅ'§Wéˆø#¾]$ì´èBðŽËę۾)µéa¦smI@_pz»žÆ,-’ØqLo›P@bJÄaæ` #¿o홉i(fš7“#t㵺® â n– l÷4ýL+¸fÑdÉ–£Îû4ûñMÏ5¾Ýÿ}7ú¼»»ÜÝÝ|` hŒyþé¸ âï.Ml‘n‡[01Âo›óBÄhÄY¬)gýÍ’sRðé g')T¢PºÎ4[4²2p'I" a’D¢‹.W.­Èx1žß2+þsômtÏ g¦òÒvôuôÙ¾‚Só=SmY­¦}×zΚµ´õn#£XÆ©y°ÈãåÜÖÕGÞ´i_ÿÅR»æl7›CÆSoG¯ò+Ò¥çmä„çM†õ†ˆÝI›õîTD¤ut½åÛØ$—‘`wA31hf;Äë ¯Úºa`»8wîbneÏYDQD~ô¼9ë:,ï(´ÇÄŽ‡âæ®WfÓ•ñ¥¶RWº»â ƒsÇôè±×‹ohO¯Ô¦WÊÇtYÅ´n¤×ÁêÔ|©$Ê¿#o4ò@§ã/¨n©„% Þ yy8Ì3n% ¯¨ÍE:ZÊþ§ù4ßÿ(IUª5à ¼ÿ!Iùþ/ƒæš4Ëiú* úÚ~‚¾¶MÐ×®Iз(6ãb˜¦í ˜új-ƾýÖX†ƒ´}6oçì‹çÃ|œÏvÛ·¿ˆÿµ# ïv[RAµ±¶ë›ÒlÏÛ6!wm)äž³ƒÊ/xmt³†Üé‡$ ™¼)ƒ„Ó¨œåJOe]´‘á}`ÓÌ FŒ’”ÌC¶(Ï/4­H-‚ $Žùœa\ƒLcÇÀ×óŒd™˜“ª€ŸKàY‹É2s¢Î7ÇÅ·ƒÇ$Th‚uÙ/¬ÕlŽs,=$C@^lä¯>$CPtd!ÖŒLtarwï A³Ò¥+?Ù€“Ñâ.§Çæ¥n3È{-Ù‘SÈ®I¿)¿ÞK¥í§ç,ëR¿ÓQs#3;¦ê6-i+8Ó1Äk³œ²áx1¢^ä¸ Õœc^²°ÈAnQðCäcL²…ŽñÅ}^·ïØ’-M`.Kz!mê+YÌThþ:‘˜•¡Û‚’ˆ¦Å-&é8‘læ*!E4§¬srβ„\;Œky!½§b’”M¼-„š%ÆsôØsÙ³0å(@4¥ÕÁ¢“Tâã"ïzxÛô[š]–ܧ}2ï˜2Å)’… É+›êÈD)wH“9HZ‚õˆ—,/vBøfÚ±l$ͶNØKV-> -”✃ÉÉÒ[9ÏŽÍp¥Ã£:ä?Ò&5’¤€'ŽsVøJÖ+»Vᯥ )c&¹b²§y¡8å:ol«”^ÜËÅŠ©I‹#6@P¡¥¤b2$Û¦÷ŒùéÅ5=kO’ÆúÛ]Ò-åØÔ<ê¸S΃ŸÏ⬒“aWqŠ’xUìD‘kžžÅ+剨pÌFL†»ˆIà˺4óhÙgm™ËÔ˲5¦‹dóp “æ ÛpzAœôœÅ"3Ô¿ùBrŠFÄç9AýàñÙ‹ÃäH(?2OEù Bï1yÙ°ö ÞÚ&ëÆK®³ñdÉWûѾÍín# qÒU¤ø!ʬô¹¾dpÇD§Ôd¡Óž!+R"[–— UâƒÂšâ †¹äÝNñ7¸I¡šéÄŸn!½fò’%i3ÏŽªYÊ=ƒiMÌS`²zù‹Æé«ØêCÕ>2J2¡ÛÉÜfÌkÑr°¹Ä8Ù8KxÀKÔó~t{W\mB3–%ˆÖPCœ7O¶ZìòèDÑ&=Ãà’Œ{Nk,ÎZñbžOXÂ7éÚ˜ År?¾DµaIîèšÁ”ì§ZD‰X·ÌxC]0ÒÝ)Ê&Ç@g‹h™/¸6§H¢Ê<ü˜“ ›˜Û%åT§ˆ*È(JcÂõA ¢Þr°ývÜ,®H•“I6ΥƦÍBÅ;M2‰œq^S>Ät[RsSºóš[Ôό¾ÀVÔ™!é„Z„-ù©g:l-š_å–Uóòï) «ÙW¥"ôŒŽhȶÓÞH,¦+@MzQ cü#-Ñž¬Ð!ûl±SÛTZc%Â0/ ˆÌniØù@+Î4åln=š›ÕåO´sÛv«%TaL[euÆÉgü®—>p– Ñšm¿ªThŠ“p(íŠÓåÍCŸf®ƒ­ÿÞßú)qèä<Í„ny›=ÕÙ“­ne{#\âö÷ERxk™$ \âŽØ7ޏ“>ŸbIa*x[ñ4Å,µc¾ÓìSŽÅ T·e42&³XãK—¢‘¡BFš¹vÜ¢ñ¤Ùžæ狼L–ƒnœwv¸˜÷ éÌøB&°'œðܽ³Š&;q+WHìlJ»âIØ„/ˆ•žVYíc[ §²UªPdn©ÅÓåë,-åð‡°KjYá ›±~ §ßà Ð3]”Ò4u³%K3^S±Q>ÙÒÃ9©ùUÀpΕŒz-9úEÉ|ØAd©ù.¹6ûVDRŒx:-®kF ûûŸó1šÿ†‰šiL-™_jzªÑ“#žØØÌx¢j8—a‚–ÙL7…u•Äk…sÜñÈTp{J{ãäš ÊÕc_×Ä=0ڤʌix¸¯ç]VKó‡ŠX«¤ˆ94®5A(×4´¶g¤' çÉå<àL; AÙÏ×uB°[—©™ŠÿÑdk1L¡Œ9ÇÕQæË[^Œ52ígfÿÂO/phöÂ-ÈÖE7A%àbèbû5LŠFéaˆcÐÂ%Ÿˆ A£’ìì.Fèâx,U¡W´}I{ªæ'd9ï[CÎ_ c8RÆPz²;ݬpÂH“+õï œz‘/²áEKÎ芔É20yÌú! »[œDl ]4;·¿‹i<4¿‹Im³åî ì$Òx¤çÙîÆÉJãÃù .Wzùí]zʯ^ÔôÑ]©ÃF%è=Éï×b¹×‡5å\ù x›Ðþ¢¹âW‘Ý;‚šÍÕŽ+øv˜ˆw=šØTÿ½«¯ñ’í[£ºÍÄYBPX–²á´Ë‡²\®…Ê 0›`Ïk€¯ÚFZŠ:[]oš@Œ|ù®ÊŽ*iíá#„ÇöñgSI¹‰,à6+FÖ@Š:ÛÝ_ hqt¦^Ì—n€Óxæ ‘º)(„bºëÇaºëaŠ:÷=Ø6˜“e>Í@7} úÃF[ ÁÚÈMk€½uø½ÉqŠïJ«,à^¥»LéËhŒ?óǼ@é_ú1zÌîþ´/õ…Jêå&H¶"­°ß ‹iÄXצ©I!›FÚÆvÐî—@¦Ì$q7'_&•tJJñBŒ„‹b_.èYƒ w‚EÏœ:\oýÈ&­‰Xµ;æ±N@sÖ /ÈT:„Ìn:5Ù^Ü>u~œòqƒ„3µ3CsVV‡³ ÍKžž…0ü¾E£Ü„[¼D]H bm!^êÙýpÈTÉ%IÂ:Á™¶Àö:©Ã‡¹iù»`³„M:rè¨òó!n›4Ôx:ϧiæ²²ÖÃú¬¦Ð èIb?zN;[ÀŸHô»Y1oñPú ¤é‚ˆ¤š`f ”¦×r¤»…žnð×$³|ƒ»"¿óņu——ú]ªâ_97Çé<ÿÇÓé*÷A~AeÄËôÍòˆSÉîMM€Û:gDu8˜D»AKÄfíù×zºSé' ¸ÁATël4†0„\ëè®_‘@ÁÒÌ&­ ¬=g Íú‡Áü·“dÄ›OR{!ðV®¾<¬Ý¤|n$†øxl¼C¦šÞ«_,‹y‚ðÆð…WžK|:Ìe€c§}¢'YŸþ´9QÄ“Ãã—{oNáípz¸ÿÚyxÐHÀ+ƒ«Qiy±úFtûî|m;’õ·]îyW\‚B•ûÍþgWòšˆ¥Š]ò™FHWÝ~Xß Kf9=7ÐÙOïùT*®òŒ­«cRÛÝ뮬ÊÃN8y?­_‹gÛàl9éš…O•ÒìEžsl‡VŠÈ«°Ýžžò29=ÐëÓS]'¼™Z­ÓÓxÉÞÄ5QœÎøç]ÎÃ9]þÛíÎ0GL¦hùj_Žs‘̱s†DóˆÒoÙsDٺݺ½ž/Ìmës4Ç-âèIJ«ïÛbð§aAdq‘¥“d<ÀÉ~?^X‚SÑ”|ž·‰Òh£\}gCú¶ÕÒs»fŒSðl“ÏÛj mhÛ´ûš¬-+ÉΩ[›—û$ zqÀ2ÇÉ_¡ò£$'f´ý楈ÌÚ<—çyûóëÃWÚÈ7,v‘hp@òg{ vðçþÀ ÿžØ'Ãq>uÿºöì .ú—ÃWÏðïÞÛãçïÞZ˜ï/òÿ²Z/·Ú‚Å#îêÒšgŒíiv)SÔßo¶ðwI$Ã>é+¯¿p;ÃS¤kñ×,¬ZǦÎþþ³2‡Þ“ƒÑ9×ä—ñ™÷sk@lDþ‰éPc—yt£ÿ@iÑööö£íÕ?ž¦×îùýgv‡$Å—=.éŠöh¾I:˜c€¥ ËN>zÊ Ûms{›íCÛ]¶>y¤â\èCÑj«ÅW.£Á{aÔ’=@gé$…0ÄåÁ˜ÓâÔ>,ßÙŸaáE{4L3ô‘C´qÒ‰J—øÔ‡TU}ã®U´[kbÛC ;õÑ4ïÿs±ü•&¡ÜÇì“æþ:Ž/ϺCÍõ,VÝÐs½§ž*•­Pß`î%rÃJ­T¦/îx+ôÚ3mؾڦ4[ÐôoÔû§ =™ˆ[åE"ÎÅW¹¿AŒH–ž¥É¨¯õ¢M;›8`ÑÙĵôLN´Bµ|,¦7\Kï9†O·ßUXßlA ý>Ø¥ŒŸ]ýÚãâm÷ws1§š­QìðÙMkÁ$i>?=\)"Ý~EÕÓÓádj2ý&ˆzMÑËÓJ<“_»tñ·jE©9•þ6{Må5ßçâ3£ò³”Ûo`hfsÉŒ¼ÃÁ3þ<û›™Íˆ/ƒ©oú’om¢;­Çû`מ$ì G3†´€øÍ㤲ˆÓIüùð)ߨö-ÌwGœàü»rÚ"I•‡T2åM¿+qý*Ô;%Å…6l+?‹ÐŸà ?qÐw¨ð€˜{žÝñœP©îqŸ>±ŸÜr–óy˲ —íO¿‘ýj#ÿò»a¿s`™ÓÔÆ€Æic:*Gž9µÈÅ|÷7á^$kCÃ†ÄÆc –o=@Bàˆ,ýûÂï’¨ÂíIEa8ö‰)ýêÛ¹ó~܉¶Ü"×P|îí¦¾\“;òÐ Ú³R ؼ"‚ ¹©BŸ”²8wtcZÇj¢&^ƒ•y¦îriÈ+XbhæVƒ™…^vϰ-J›´¼‚°_™S±.»mù8VBš~B~HYl5áÕÌÆó2R¨àƱÄb¢2é&£Ã·î쬕ù=W´ÕJã&^µÔºa¨å¾§ê™Ñ–§@¶É‡ÝÞ û«ýúàóž“r¯Á´½Õ3jX9fû4mcÓx ¡pÃ&.ÑÂÞ­Ås³ºÏ&^ ÷tÈd n$ðÒBè^Ë(¥Ò6Ðí{vç*+o×3¯ãDr!KK‹÷pð-¿‡[öZ1Nõõ‚±g”ˆù:”^\°ºŠ‰w´´^ºNÎ ö•/A… Ù5 å/í{*€àui)Ìñ)WB¤Z¿¶OµqFÇÂë³—€Ò‰â½r 2&»QûnÛ¯i|ñתúñ£'Ý’‡\S1¬ Ý.ס}»gEZ@<‡+3gn··ûm‚‰ÛòaJ¨t·¼Ì*£…ø׈¾pàµ:_L•¨Ø¶CQ£^hÀcb€%yuVÒÓZò¯ˆ³êÌ[ù¬¨ÖÂ…‰¸—gÊ.Ð8ìQîJ¸'”ذ×9‚RíœÙ¨×­Ý(n5ƒ¹†ó·‚Tkß-W67FöµÖÝ/2XÚ3l qµÖ?6æ¸Z±?t†‹ùO †á+G3<[¡§þ¢Üp³TÆ%¹l÷ÃIE4ãRP<ñïZâÙMÔQÖ#m´T¶^¨ªãêe E_YÍ nÿxÈB¹PÕ5®®’Õ¦Ùúfau/ç›Pm¬^§ºÄ¼ˆŸ|eÒ-9¹o%5¹–l­æÅ‰ˆ¸>ÖÚï‚Wà'EJ£6ißM ÎÍW£‹ÖîiæÊ^e_*i#DGä™<©V–yîÝÞü.¥Óaò¢º’þ*`)iâæ^#4õÎïÂ5ÒÉéÌÒwõ¹ã퀒°W„» v•ÝÞHõZÿ2þjY“è?œÔKk–[r{+¸©¼4|Ëod3~oo.ݘßhÔeM'n%Ñ~ß _f¥9bâkðŽiôˆD‹í{™kÇY½n}ƒ!à=jÕÿ _5aÆQzNè9ým4Ü„¿ÜØÜYaH×ñ<¨#û3séŸ=j9B<·±-‘ÇZ ä B!)¥E]”Á¬;ºz àŸ:ÒvÔû~” Ë0Û!_]XæÊÎ[sa3bÕe¯ö\ÏÉ£þÈîy†„:#Q^£‘‡Ì{¹•s mÈ$üÁs <–…'ÞáÄ—0°_ù볡ˆƒItèXZIÀ3 f²\ƒõJ¶jæÖ®µ²ùbd~VV1hœû¸[iŠtº¥B•ÜhÐØ 7Ž„%Q×A-7ô–;›õHrþ’ «UìijUÒH‘/D^'AºÂëØý~ï®n¨/`ÞÁ¶súÑjÎóÙ«Õœ­Ú|C6Þ„ÇJ²ZÖ;e¦raUsÝj6ºW©÷næ*¥U·}óÍjoÌ»ì·÷¿ÇA6ë–¨l ñ­$Æ,£âöwdÊô§ðfS¸Î·¦¦¨¿»ýVë031ÅrM‚´'4Ð ±No³^7}FÅ^Û>;LüZ àÓYþ)Õðaâ\ƒÈ|=•Do©ôQ2o1,i‘ùÉWéw°¶{A°7¬[­¹«ÀCO>ÛkØaô’ùcÚx«æ`nnšÌ4Rç–`¾Qaa÷³A‚‹ŸøÑùÖÜ`{Ü/H¸˜ó!×û Ä©â“T#¬8—\)Ò›úš"¡d³h©B©9+GRÃämàØ8I6Æòäˆk=[ˆ›øñl¼lyÊf:—HÐF;e@ª´j‡ýïŸi¯Hò‘xr q¼Ôœ’.d<Þ±óÅÞ×4®O“Yú‰1î|Ë0~ò™¥m¸k°ß,u›†Xξ ÉT*þÛq”ÑRo0ØŒ,ð>VáC,%°οÁñ¥Îbõ怰ñg aÂ+&–#¿©\3$Âs–Ë—è@?ž¦%·_ïeàl• <|Ëgp=ìEüÄU¶—=ilzfÆå†¼¾Ñ%Ùì“L„áI,GÜ3nHç‘+.õA¢Œ# DnMáÕ“X#…TuÑŸùJ<Õ”à´PM©‰¦œæ—) 0,’}ç݆¢\¢ªœÓ83&0xÙ”õ¼‡€CÿÜÄ"¶àt»øÇhŒàa_!¢ÉŽÄ’=µ\[̢ĥ|´±Ø‡{'¶d’šËm¹rã$;Ÿ_¬*mjúæR ³J¿:Nåe4ïùjÎr¾TÆz”h±4k(è”1ñÀù\n-” ojØãî-®;´³^è*þMY½ñ•²¯âŠ †1Y-¿ñê2B[Ƀ~~©-fB]ÏÿÎø±»t…e¨&-áѹxRÇì{çS_¿zu³OóB÷:àTö:½†[X^¶E¬ÚéŒÐ.ãõìôŽE¬g¿ÝÙê®±íM/Vmyô×ø5nù­ûÚ Áÿ€ýí C|ÖÜÑ_ac›Eÿ›mîu÷ömm·}lxCÙ]¾6™£S‘Bj·»õp`/[ÏÞÈžMì |”"‚C‘6w$\si‚YrUzõîåÁÓgG´‘·H‡ƒQ=sàw½¶ÃÚ”xÑþ[L4^ž¦$ãèJâ¾ôÌÍ-dzù)H^/b5¿{HÈñ,JE¿“J Ùd€h‹¡ó÷gÇ‚FÊX9ópMô…¸tkM ö‘‡ÆþE2¼4÷,àÕ/h°JeXYà $}¯ê!‘²¼gœl“šÛ Œ‘ßòØ÷Õ ½Ž™uïõKÕé¸t#ØèZ¸øÀCEh}©›sרƒäIé ÇÚ…uÛv·Œ]múwÙD{H¢î˜æ{®=o<êÆgÉàL]5_4)BÞVè`@m”aÇ£‹u$½ƒ¯¯$þû>Qf‘s4-Z_ëš´ÙlÀëñ2Ò œštOuJçb9‹ €]¯Xì#¹ðéî‹Õ—Ç«KÖ·Q7Ìu>sÅï$3Ò€ p@iÞ_‘ö²™XÕS+7˜5Ê úëQ _œØg¿Õ6lÚ‚Þvy£Ý¶ÎN«Û_5{ë‹7„r\sƒô—®?ãã,Àìò V Åõÿ– – à¯ºàß|²z2á+ír¾ìy Š»8†Õ[=vÐoy7»Õ¬Š[êµ"dõŠ8 |sF÷Z5Ê»'-ïBøšHx5¾‰_È“ÏÔÿÔyÍøU¾¼õ#´mº–#R;õ¤Š€´D´¨5Í‹å>´¾S#5kçNT3—wzW7jw¢ro<ƒ…oŸ—*V ¶¼ªe‹ÖI! bå¬ÇÈÁ,RbÕLꮹâ§ÖØžj@ì“Ý{ÅŸìZûmø^$1X–­ “Þñ¿¾–,G3«kÓÞ’o4;µæÿô—(ã@k~1q*½w\!}œç— ®Bg»Ðí½¦äú”óŽür¤Q à»vÎ:~)WŒP´ç×áØøê‰»,é Pý€a àg°ŒÔ:B‘³ÅŠñ'> ­ªØ=¨پó¼Ñªl„` ×+ÖˆÆåù6¹rÐLJ=Ûˆ¬a 4 G|7R1„c÷Dy<‚áà"v4¶{Õñ¸³U3"’CËÖ»WSo +•C$ŒG$<› Y˜‰*ÊM’ËȨš5P²ck“cÆÝi€í&4hScéÚ®t±¶ÈDn¯r¹; »¶é4~¹Zäð©}¬ÙT`Î6ûϪÑGj0iÒ 14ÔC¡ê=`¹ >·96$+üÁ¬±§ó’=a%DÆ Ü"õñ5^™åƒ>G£dt:Xšó›{×ÖCƒÈ¶2’Põ¨×÷„Yciƒ÷0è¢t_3(«0º³ëÝövæëÅ|ª99œ6Ë4´(f=RäwÈeeâ;ÙI™ÏuøoãÙ™R`¯C\¾__ʘÇ÷ÍѸ°9/fD}å3z—‚y¶@t¾ñ²§yuÌÉm!Çú’âHûgîÍf€\R û'Ú±¹ëì7–J’ºy®€ù²­wóÊ c¢ÝâiìÖ½h’Lr ¯{Ê CmŬ”ªõÕÁ‹(u∱t|×wvÅÂç~[žš‘ LÐAÛF{×Cš+ôq~Ñí}Py€§óƒ¯Pœ„ÌEE<0>œ 4¾AðÐZ©fÐ{°PšàyÀv†Ó±ÝúËt<:æ|~| î/½)uYŠ /òt(ck¤ÌìpºrF×"Ã)“Ééø$(žòyK'h¾=ŽîIˆX’@Ãwî°:Ëo66j¨H¥'Áƒ°uF ¯g’zOU:N¨tj^”«¶HóIíøs õÚÓÃrXÐH(èÞ ¡ú=‹’/K‚k—iš6ÒÈ ?Þ7`©å¶'úHÏž&sÂЋ…æ©ç˜ Áê/“%³Þö§xfÖ&®¢#á*³$’†»é|›ï»Û×^ˆ ~uM´û=úç.ÿLÄó”^w^ÑØtïvŽhÔt³!R6^ÉøÝö‚hšŸšHÙNÒa:ç{-ÖÝ^ž°Ún€Ü!5УãøgÏõ—ÔX³<ŸxøJ6­³,zO1[ÍOZ{]‰ ¿V(Ú8;—̨P£Ð²Ûµ”Y4¾4«sgÞ8Ôþp0®‰ÞZe|6Œ“s/ºïQÆ©Áu¯Í{#©^غYe-¸ýàþý×\pOÿ+î °á;pkVƒDs°â0üª ðbÎ=AOô‚è}G"Ù/“Ló´t9~ ŨRB"™½ÕÇÅݸÄ5}½O){‰Ü4ýÀ´’‘\è¿2w«kxPªû”ÿ¸$×1Üééo±˜L]¤žI3¼×Ç‚l^+ëžÔöÅó²gÛÿ­›÷ƒ~­»ÈßÏ EϬȸ°-7­îCd`…{Kd|ñWè§'àéˆ8߈Ã7‚¥…gfó×½YÎ(ärázŠ&÷‰5¦Ü\#®úûc%Á†Ô¨lIm˜Œ¦]' «·þƒš=‰¼éo‚u½¿´¿ï¯8ª¢\µH‘Y&RPÒÃHt±/»AC•dþºrà46 Äœ`M¦{Úðær‚ËFŒÒ ÑJ†q!þºÀæW®Æ1³Ææì£`G@ɳ³ôó. 7qÏt¥\?vØRig/ü¥´TÖ]©g÷ ÿE=-àÄœQåSÂگѭP³•y'‘+Ý]jO/ćĄ¹ôæl•Iûq;¸~éy’à¹7Öe鍊ï‰c‘Å¥‰ôLùư(«ì5Ÿ7ý÷X…¿Í äJዖ௽}|š–àºËïÛµ—Ÿ©YCkÒXtš9‰q×Ðéæˆž×„ú§ªðI⛀ýñîOÑOÐŒ:eä÷ÔvúP›}P~õV*ãK†Í‹i¥,¦¦)SfUƒjHïiD‚¸¶]0¬0äM€ê*©íƈº5`×ýbCÿõhúr%E{½fd]ÛVøF}5´Æºk¾Ý+±nVQhêW•;ÈŠÅL¯Q°¬h$…¹ løx²Ê¯JÄël¼¬ò {‚GbÇ è{Y»å¦E:½¶m@ã¦Í_Ù6?rÛ˜®Üúž;Ç“ÙÔàZ´ªˆÎ¥ÚãN«)Eõ|¼Ñë¶j\=÷Ù_£9Ä\¼ÚÄ:ëÓ7Ž"ˆÛ¹ˆg“KšaÖ© CHþjŽð”kdíêªr¬ùèZ~¸NKuSÜSnÈ 3giòÉ3ülNò‘>çøºD¯—…^97vùøŠ×RšeFe³;Y¦òŠcÎñG•ëìpAè)êO»Fn)óq{ïz&nlü® o÷á£ÕÆ{еÜÖ'n•(V´’¬c+¥¢ÞqºúÌtj*{u'âÁ€\9XÌ.gŽŸJæØq`Zxì¾èEW »Cù½ø1úéã4Ño»$­tèW×ìmþ5ÞüÇÉnçãýÑ;¹óñ¤ûx ãÎ:²ÏU¢ÅÅžºmJ¼Y‡ß‘ JKäB!ñŠ*_‡Hç»2&ÝNÿëî t·I­UEˆØ›NëÚþøñîI·ó¡ß;ùÎþã5©­÷]ùþZ5Ó_‘çñX.šñ}\ö_RÓ/øžAC·Oi>_wvv~þ°¹{rç1ÿêßq½¦!œ²#¼˜¾d¹ñ5 ‚¸1!p†ýï·O¾îÒxÞ¦¾˜é V¤^t(‚Ç^`Q¹xN®ÌçmbdhÈ{¨°NLŽêM ¸§²ër}Y—L?¢„qħúß›¯ùYù&!¡3“B{惽[Á¹²í%ù°^³¯’\éwø÷ "=m«óXCT³¡ñ›ºœÚ:4»Þ8•JÙîïÚ‘(•0]ß5ƒàd VM%49">C ©Žƒ^"ÇPyꙫÁƒÁ—Ö¢•h«r-¹®mïjŒ#:Ór=Á¶ëf­oÍÅÐVŽV¥„óÒm—_~” ççæªË I±˜—’¦3~h$Çd”dž'–ŽIÿ2YV²U»(@UÕ¯B‚] ­ÂÇl£UÿJZ£"0l´‚Rµ(4w13q í¤ŠÊjDKÈF?G•2Õ¤‚e,C l®¹´ZY óVÈl•dK°ã*…Û¬²¤sNÄ0>^[A¬H˜H f$t%sHók)æN˜.æ"O3'Z¾#ÜÏž1jðìÃÖŽl’±hà±¼:ö“&óE— fè˜<’˜®vÚÝv¨ÖV tÚ¥E‡»:sÎVØå{™>²ü4\¢7ýR «R“3¶R@Mts7w<üÊC öA÷ŒUÞB¦×=B…ÿ¦+¢`Ì<òÒrñ±­ÎŒøA0?¶ÛÕ¢¯]ȈAù¬NhZ!*™¨ËGðHjòS6Ï¡½—Z³Êƒ®•·ú\Ö”éŸÏòÅ´èø²M)1™‘¢äª3ŠøÎPT2<2wyAéÍtÿð”„Ò““H•ÑÔäo´-Ù©çÿ®r~L onèŵv¯ŒÉ‡‹Ò5ÍW9÷ðb‘]µbéëöpÿð̪ûVc÷/CZ¾Ç·!{Z‹xÖוӹ—YÃ9D©â™é¼ A£Œ¦ ‚±ì}½Å¯«6—ô¸Þ/wµÅN·´ee÷!³2:ì8¯ È`q³ÈÊ›4³‹k²#A¶Ê“h>H• ¥W#Æ‚-@¬ Hbµ¢¿¾½êCghúqRá(Œw±Ú7ýžhÀ e·Òʵ³Qm£fq#=…ÖuƒÜ@ÔŽ|úeB=€~±‘È*#¢‚÷˜Õ,ø>Ó:FÛÞýÍò©¬Y^¢Žšñ y·E,âPCêöEH%âÅù›õªaù n9Õ¯h¦ =Í¿È&U‹OÙG•r—]‰˜GZ‰5®blþµ±ìÉâó"êÀZ¾xÀp‹‰Š5SwŽ {¢‚ ÕòÖH…¤Ù· Ls>äŽDµ­7Áf0× U–i\ŸcUˉÉ(\EǨLAÉ©XÇ>ÜÛ¹l2/ðâ¾ý©y†BÁçºIbXȲö &ª™`ùÅIðÆÒvÕÖîeÕ)3´jf'ò¶»ÔƯîÍž†ÕÉ>‰BºH\ÊË«&=ų@E q™+î¾*¬Ñ¸<Å÷Ÿÿ tü]ßÇñ)ݽwl: yʳ ‰5Lm+δ">1¼ ï¶:æ`Bé<áÐî†ácÙÂü¢SÁÝÎѱ>ÆgÚ ––T ·>ÐBÒÌSÕÊ•‚¹³Y^‘HŰl*ýRéc«ãÈPÕªç¥:8°Öxã‡7A‰Kñ¥üÓŒG¤±í'ÃÚFH¨ÛÿÊÊë˜[Y£Ö9RÂÎN»*ŽÙà ]p“â…©”°²•¨÷ˆÄb%¿\½Ž®ÑA®ÅÀ¼áân”ˆ–ÅÌêõJq<ó£Ch«Œp­V‹—l8—µÒ™ËáE¾9ˆ¤¡wÛêçÓqˆ¡ì!ãÜ™' –­åèo‹ìréŠ[\{ !oÑ‹N< Á%¦BH¸‚Þ0¨HŸ:ûÐDáO WÜt¥Ç¤é¯Ô|ç<™ð!äUZŒò =Ñ«F:¿F×0µô‚PFí÷,3iYZððG¸µqzÉ‘ˆh\Ò¿ßµ•¬Ï%Äá.ÇÍx§î¤.Œº)WÙµ£è«ÑNôUÑŽ¾¢A½³ÕLÆN¬‰J´tœU*Ìi!þ$í¬ JÄþ[› À¦yç£%ñoqð±,HÈ'ݪ?# kÒ~UE’ }ÓôñÇÄÁAò¦mF[Ý•=æ£V.W(vT"¶©íø7²³!ÔÀFæá®˜Ã„”|cðÔBuÎÞŒ˜·ßÐÿ¯a#¼®ã!¼»~Kb?#ö#„èÛd4Æ—?‘ØŽ68Qû,©ç‚ ·žŸ)»*4±COC±`Š'|M¾9Y$çv+{-™¸EÖ}ÃøÐ†’Ù!+Λ“×¢q:ÑÆû Ò~s!~÷ÕÖwBY6Þ3½LaÚd~{‘Œ§\6 ¯èu’y¡_,,ñ%•業ìc{Îe|4ž Ú¦è’Χí·ó&™]ÄÓ"âÈ• .{(a'¿Úê›B!¬+¸–O˜Ü×á3ŒŸÃ:l î—dZ¸ yR‡g\F.aè9~žã6£ÜÕ.]fù­-*Éè€x0ŸÃ£ÔÒGG!ö‡wóçW§“Û`Äâ†Qô±[à>£!;”ï]OR ÍÆÌPAy<±eÂqLGÚˆLý$Øvi¤{*è3á;ÎI@»0@)•ÐÞ%ɘ&¤f@ª« ó±=©R‚Ly’ / ì»ÒòI²¿åK¶»_æHî>aí°'~!³†LG³mª2)$èÄe˜ŽÇ·²A€ˆÒÍÃ0|†¢©·aÒ‰RàØwËâòˆÊê¤Ù÷ûQÞ>ñ¼‰–ÄÅe0k²†pžWÉL®TRuCS¾ V\s›€Õ°Þ‹Å7ëW¹k™~΢ü*‹þ¾H vÛšNyàu¾tóQgh„ˆ<Ñuì#sX8sTÅŸ"Ͼ7q¼)ÜÎ Ã[&“Àsì‹®!¹YØò %;xá\äIΠѺQ‘ÏfËócfS²3£–Hþàp|GõœÂÀ¶”cÏdT&Ø<ÌQux÷r]^\9ýG ç´wò`"ñ ßïG‡´mÏÇñÈ£N81å¸ìù(^ÚÆŸ§èY‚J´ bÝL\Ê->ÀíaS0¤`²_k˜Á(_ ‚ UDz GêŸ,ì2’È5ûŸgãŒ(n2åc¥,“œÃ´æj¦E oüu”¥uN@-et5`y•õ¥|X¹Ž!cÒN,¡æ/ò|¤ B”J ƒåOØtI2¡”Îî¡Åìõ_$j<6SSOrI¤ ÒyÍ0IÌCÍ“ºÆ‡ÓŒé$ 7L:là²Ééw’ך¡—Y‰y^¬fÌmæ„Ç<ÉÄFak–V#Ò(L¤LajÈr:oÍd×mtx°$¼”VÊ&‹Ø&w AzAé\W;¥$=SI„ŽSG Á[æ¹6Ç S¸«ç’Š@DΨ &#ê·¼¯Ë"äñœg¡ðÆë°*½0NØâ®h°Žû厈uXÞ}@"”û|–!„¥GuÊâ¥y³®LhF£´T j`£fB¾o–;iPqaˆž–åY m`ý‚@&&ú³×9¯¨ÇÃR¿ÖJO>¶›{õ«"¿\×Qìÿ«¤Ú<Ÿ>N’2íVÝ"Ü|†ÌiW*Í×ߥ®Š¹Lüfž¹ìYJê‘m‰;ÑÆÌì|’Á6ôì7æq*%ñxaŸµG¦è‚¾Ú§Ÿ’ óô“}*ËOÛã±-K_õñdiñZXÄp°© o}(êÿ¦ÁÃOúðSâ?Ek;Ú¬{!¹,BysKÖŸØ™Š8Ã4šçpG  a˜K’ÝÕ«o–/Aqð‰&‹áÅ­`z/~КhÚ4 ™=‹¼UnBoº &î‰%ŸbúKÌx -Ö{ïÕ¥!‰*õo‡Š¢”6ì³ÃÊÐýŒDÕåÏýÿj[:ï5ožó!gì#&ôرh«Å|qvF43íO¢t¡XôŸeBiÉ$?’9ß‹+MæbÆž> ”8öðyže±=¹‚' 9T…ßë}ýGe9QZ;«ÞèÁMÆßR²‘{5îCâ‹?Ϻ™t(“‚‚Ø aâk•d¯±I¼Ðâ¡-^\,ˆ! [8¶òøôqÐìUàN`ÈSdOÎ&fhNÔÿDjš¹Ä¥äÐM „y€)J5ŒÂa//|On“þ±txËßÌf;Ct‘þ|e„þV–ر"ÙÄ}‚šVSfW_xž¥júÉ©“ìÿ?âl|xvzÒÉ’[þF#¹;ZxuhX ,0gº²I*&%âsht´í³[•Õ[MÀL²¥å×uÌG–ªhíf¸Å?á–•Zh;êîñr;Š×£ÏÛøïa}T¢oNpE * P#H)š/½iÔžrièyÖáyõÚ•.©×â)]ýëI~ü?CÍÆfzÄ¿Eõr‘>n©z ßJõÚºuÕëŠnýèFºõSO®Ñ­áÉ~jý×_¦Y¿Á±èhšŽÙïuZÈ·ÿLÅ%eµ¢Ž¼O|ÒÅ,ø½¨}•´ÝæŒÌ¡jš ÇpýŸ$â©îS6ñ™†–ÅÙÆœŠÆjß7˜üäœ,+Is––°ñ ¢=_ÀÚ4ê÷ËhT€{‘k“äÌzÇú3ŠÀÍ!MØ<ùzDØ3WNþ–Ä£`¾Lzô ÿÁ¸þý,Î.ÇËžõ©vC•>÷ç¡|$Ø—_‚&ºwÂSÚn< þ‡*ßò}‡ó\L¶$æœ#`²9rÀ/s‚åû’t(ø<Í©Gcl$¢$.q)b”Ærß§ç ?4Þ¾ K¹e=ä daO­±&|JaÿÁ‚ËX•Ü?ÅF²4â1îâZÎŒƒ~Eì")™„¤i• Ä쪌å3³€²sD#…×ÞY’àÌðÕ‹7ÈÂN£Š‹ßEGºmಳP‘ÎPGöb!/ØìÅ£ÿÚè…þ¤¹×|`ÕsÔƒCœg`?ús2ÔþŽÏHÂ=OaîW÷JÈ,ú;|cÊÙK5z’°²$Ú\õ؉[×€ ¯èüb–ŠÅšH#®]Ð@ÍëXß¾«i$0H!ç hÐR¾rS¿©|Ò×£&{›oµ(‰pgK—Ošþ[I¸‡Æ[Ü |Ç*]_…\\VʸÞ|©ˆËW©op|d?ØÚztˆ+w0¯rYt\,6Ÿ|áñÍöóE1Ï3”}ô˱|rT,Ø*¯÷ƒ ©ýï‹dfÓ‰îh¿Ì]ÓVëÿ]ä¸1Ì`õ%ÄjÙ›ñ}«÷$´´Žõf™¸ú¾8|ýêàEôìðÅÁQ/zÙßëG[غ×zN;QJ`Õ?[Ì“lÌÎíøx½?7/yä<ɉã­Ê!~åxþ͘˜¼y̓—´^?¥CŽÿ=HôÀ|Aó›xN!þýãw'_¿†_꺰±'V•,÷mÖ^k÷6ÿprÇ9lˆƒ1;n-Ƭ&\ųD-Æ6Ÿá´ônJ<’,™€Ð&î±Ñb1›±Ôy‘N8+ÈY*FS žÏãá¥}=¿Â]ظÀÓ„j=‘3>!¦%šgsð*7=s <Ÿ" ˜Ün×q¶„vwos?Þžx–*¼Ž®$ª}û”‚êǦabzz4§f‡–Ö.U9‹Å•ÐJ%ÃdêϸÑüL%’9JÉ•«9oiŽ,d=Il€0›‹—F¨ø‰Ül@8ÀÂQW1Ÿ%ÙùœÈrò)¦aBIלq›Ðr=gã-‘÷ù,òÑŽFá$„LLÓ1q Ñ4hÉß’M#u:¯ò=;}zYRÐ[d÷®¿@•×Kõ˜fYòó”Ã÷‡³È9lFd¹Á×—Ùÿ‰ù2­²³Ôê.žá$P¦ ï¯ê§›ß6ÏÊÓ\\RÿÎÐLÆÄ^gâûÁAd9ž`ŒYÉ9ÚÕ'¼mÀ@ôS¶ÀžÍ«øGÇ㜆}ä×y›n=aµ ŠÈdÂaþ'˜à‘CÔÇÍ3躘ó^²3©»N‡ÈS¯N6ç¸ÔåF`0k½§9´°,–vú ;Ò9Z(a˜ dK*§$óšÏ‘?PÓPwá&F36Ä-Uš—?ÖÚé3ÄGs@µ Åͤ)û\9Ý/¹\o°"2÷‰¤Þ¦œÛ§ý•t{/IÔnÙ5À‘Ô,?„â4+tñb¸‰ÍWyÙÓt”Œ6ßÿ£Ê)²¿ÆÙeÔž“+ˆBI WЄB4k5òÖ÷øxS/âÂÍê0žÆCPî£ðV–©·ê¨c;€ÝÐ èÃËÍדÍܧêsÏDÇáøæ†üLº°4q 8¿/M’e‘".ˆf en¡H‚cnÌÓmšäE|$ëw5ñÐæ1¡%–s `Ž#¤´®#M¸þdb8õºÂ‹Å¶Â{UZÔ}¸˜$ÿHÇO)kÊFÖÆBq»-¶›ÈÕ:K®H/Èvôz£fø‹­–À|Ü ´`1€t¦`Š`úÞl¾nÎüé;‚lË+ƒUÌuT8úØ8~fzˆXæ<$î!¼ D »q‘Lå—Y’úfLHzÒ¯B”k€È›¦/#D§yˆ +Vzþ£²m Ø*Û5prC!5¦`°› øE^¢Å=ä²Üú(ö£—•gh¹ñ] úHZ\ò&·6…Žc͵ûÊtÅF§3Wßc¯”‘sÛvOa±¹“‰JΔ‘\°E ÖXH€IõrÁ2ñ…«„© ‡`Y¶Aì/±“JÃEØÐƒs_7Àò̳٢¸0R¼ÅÛ.Q{1 Å?×l4Sš—MZhW1óA–;$Rƒ¼>`9»}Æôu‚5”Lƒex´ù®Ø\øËð½¹ØO“™²—©²o;惕²XéËäX„y>|É0Í" ³°+òIšeÖ3z¹[X/X:âXÜi(IfëUÕ,Þ<· ‹ÉÀiöæ’…„(é!乯½¬ Xa¨¹/›i]d¹›ë¶Ÿ¦é©•j?´>eNrN$*Y`eBÑJø¹·¾ "Γsƒñ+’äÒì‘(>›'~%³ŽrÙõD†0 )pÆÊ¥ªø‹ ̽1Ÿ¡rFA>PX}U.àžZm¥„É¢à ¤ô ÁG—É\ ¥äi3éõF(jœãÏ,‹4C›$ã|”Ê‚ŒeV^Zƒ„‡Ü^…8ž«®¯äQ9€ QQ„IØè4ƒ3*V\¥gs‰'Á[²X©Y{»â‡Í¿~ÚüÇIÅÀ›@%9)ŸÉ1ãˆF¾FDâ‘ÉÝÝ'>K@›D†^˜Í¾ñš kF’€Ì5ôd‰~ô=Ø=dÅ\¼éXùdò`-lHA O ¬‰t™9ƒôs°äûãLIzê8ƒWg‰­dXŸ,Ã%㛳YuHû)'åIç§Q’DfjxŽkå¨Ð±|´h 4±J9ÜhòµFˆò7’üÙRÒX&BæC‰W°9Ê­rÊcÆûSžêu–”·ZÎ|€w‚Å]²%ɼ¿"-\!þ«Š6SUè? ÝàIPÛqgRö4Áq‹6}&j™Û5®¨S=ü5V FÓ56‘”OÄ ÿ“ž`xa×qçñŠÿ·>ÄøI‚5\Òî3—C5=¾eåF0 ëaTÏ0¶¾ÌMçÑÖ½íkÎ0`ÿ_3ŽkônáÑ›ÿ/ ë ºšš’EÉ¥å4ßF>©»ïâßv‰õbKímÿ® n|È3w×ÅÞÓ7oì}ãl^ØË-FÍ‘Þõ¾}«×[ü{,‡\ï"Ë¡µz‘¥öKÍ%ÿ ÞâЦÄN ?V³"FUúµûÏy87¼ly‡ÓòIìlú(zÃgLÌ£m@A‰Á[xáu ¨ÑA<¼°M¤^®ZR¦rC¼^øCXZMÀè~ã*Â)†Ev¦ôÔ*6 ÇG$UõÂ6»*þ¡H¸A„Ê4dㇶwÿ“Ûøj«Ý‹Úæ4‹R$<˜W'ÔúK5¨[˜6'¼Íh¶DŒ{êÉm4¾°}aÎg‡±€Ï7WŸNYà´QVT^znDkkEsÑ¥3_mõ]b•?±£/"tÿÄÐü‹SQÿ“sñüËDRµ‰)yìcÌv ²·ÆÙ>*0ƒ2.õç ™_AòyZìå|ìAÌ¡¼t¼â ­¢%¤÷ùW°¢íMÝ©g¤:^úÕÏØýÃg¯^¿=Øß;:èö–’ñ‚Þ,9l-W;)Á i—÷K½M"ØùŽèSÄô'YC•RNsá'–H2Çnÿú Ûy »¼+å {d‹jæT£›|/Ã!=V»ïÜä7;K“‘xioÁõd# "–š÷¥ §f0üÒZeªùí±KËWém8‰e(Á4JïÂÔžPW1¢6I f$Ñ,1qÚj>ÏÊLöIË9½L–RâT¿}DF.×󒨆¨ÞÙÚR~Ù4­øhâ .Òr3_·!tFp³ÁzB>üÑya3SöI€ulÙöWí®OÆ|=Þî…ý!êá`Äó<µ0>P…;[;ø»}RÎ+§iwí×(xB]üX.Mýê2÷CR´t¨ýn·¶Z€ÇöN8®ëvÝ&V”—n˜ÕÇãÚMõLÂ3ÇÒ^ëö‡2IDMñÿ´=q›t2¼4a™@»££Ý‚êºd²B܃æIâd,\ÍK\f¬½ÍQP<¦%5¿+ï2~ZÝK@ˈeÁ¾èî8 ¹iJ‹µŒ›ËPÒ#·‹ðØì›åCœ4Ù­  ÛÄy>G“ŸÞ{JUd­¾£JÉ„:ý°I+•“}×oïØÞáÍÎæ6öG[5°Õ•¿k¨ü]µ²·Ä Óa»G¬«‹ØÅy‰g÷Õád#@[ÜÑORËyÂ+4E^ÜÚåz夲Ro‚óÙrǾÅW§ü½³ñx£”Úè3¼T¢ƒ×ßK¤÷JÇEeåÚåÅÆk·CúÃæÖ ÖÿÆ­þ†CG^! \Ck¼¸ íàÒ5ŽwÛ7SZõßíû÷>¸FiýG’ýÆWKö&K,Ÿ1¤®oãÉrvñ³ÁCšÝhYv1yðRLÑœŠ#UÝ£ˆ%=He‘{ÀvZÝßÁì½OÆCvh¡îqdŸ~k’ðņÚ,ϳ¾©°Ç·wÜä"ixݸ¸´@àØ;äÐD~ùÂsh?1PkB Çz:=¡]l8¦Ö÷ÈvË Ûñ+¥ý ú>©Á–è1ÑÙÖ˜ÏÆ#ŽüÍÑ  ‹×{>(¬DRöƒ´k!¸®ÀàËçŽaµýŸ2EáŠ-wÀ6*eo²Å!‰˜ˆ ¦þ¡ æ³{îìÜ+Lf˜ )'Ú(ŒŽ#$³diÁùaäm_â")?gÀwO—–XëØ©»`Í‹á|Qò ãÑÉ%eH_Ò9^ð¡Iç ŠV]&/‰»XٵŞÊÕÒ~Ò”)àõ¾@$%b˜ø+Ý–,²퇹zÖð霞`!–K›½–ÕÚØ†Ê§¸c$+KFý*øš€‘º:ä´9>_$¸ý‘ÏZ&ŸðØN Lkœ:§8~†oUx,(\üMZq!«áQnG!òF©žçS^Qù|甆ólSuœ¡#‚2Ò*†õ{†´ßÆRÀ¯{+mê§©‘7€ìl{;5d¡+\VhN´Q%A¼–yt½^Ù(Y1üè>âB<Üq2Ù‰Úp¾jÛÎl°/Ö$ŸaŠ7`¸Û0Ñ´6¢d>”#öç³49»µñÆÒ[ç&ÑQĹ*aª»ípg]G[Õ TKãéô.1›KÝŸ;Üâ‡xó{›=¹ã]V#Â…œÈxDD¼[q„“á%!—ŒÝµ„gÚhÑgZ‘ê©ÌTÇBÝF.þW®þ­†îyÀ֣󮼆‹Ò0uj*?låÅ “!É16x´7$\䆸Á!b ­”­Aq$Wœ;ÉyÔ¹%æv$¾p±ÄéãWzáfÅ.swÇu‰˜™ãS˜e)È&¡u[­(LÁˆM‰· Ñ“W“Ä`ª<1å{mœøJäF6½ñ¥ÊFÅd¬ DË |¯]åŤ Ã°¾‚ãdÁ¡‡‡u¦8’V`I‘Cc™yN–OTªƒ^WbZÊK 﫭/í“·DºsºÉp"œÎ’ R´?Æ¡ª[žðØÑ#>0„ Íû=Ç®óŠfç Œ¬ç ]9D3~EᥴTÂ52ZÖ´(—#Ü(-ØéÚ9ýΛ6¦‡'d›¹*p ŒlñÂ × \ö‘òwb½ØûLæ x|ñ™¥fÜ_&Tãi³7¨B¿(¶q1\)Ù‡|Uê» ÉOmÅÑImUcj÷êªààW¿ª†L´ú^}”5ç½—Ëh0^HIݤ…Éôm·âÆû k‹»1ì8‡^Ä@*¹éï{¬gq8»0üq;®w¾I[†¢Ëma)[Q‚÷@dÉy\ @ѵx¥£4*‡+ú)_|µõÕ¶·Hühâ¼Åm N,…ôP¸ô² ˜‘jùÀ¿ÚŽ¿ÚòH~0„(Z߇{hœmÍpc}ú7“W„ ¦Q•!3ë F’qòž{J;d1¼Ü0X˜ÇeLXp]°Rk —Õ…F"—N´&žåpšB>½Œ/ц˜a—‚~{×i± ^“šùÔÙZSÈ&ÄT˜ pc×H¾.¾K‹“:KzvsÆŽD«\»±\ìPG³§Ü–:²ê†\P0ÁÈX$ésKCï û¯«]ê:˜+(¢ÄßÝ8e¶üÿ³÷®mmIÃðýY¿bëgFFÀ‡øyXÀ‹1Ù%K°/'Ù³Ê 0kV32&„÷·¿]‡î®î™‘8Ùͽ6©»º»ºººªºà[Z«H¸ IœõLt¢9‘ú²5Ì´ï«à¹B“qJ…©ùh·»{pÓ–¤‹wL¦C¤5ýw×M*@eÔ21±ºaùlšb]È,³Ø×RªØRù(%ß«û‘Û–j;ô0úššOTà ‚ûV»LÎÒ 1Ç–þ‚‡hìžÂ7•¼’¦¬³ëŒjWú š Å…càiä-ÖóÖU¨<Ó`¸Ëc ¤¾ƒ÷$¨ê~Ѳüz¤…¾‰¬Â³ã®’(Œ@âV1tª7ÓªÿH”éŸÓײ)öÝqƒ~^f+8Že*æëÚøåËäJ+4KÐç™ÎÛ¯øàܧôëøu hÚù¥ãÄÊ;Uð7’À/FÅtRþ—vÊGéÆHEgF§bLvnþ¤¬¨:Iû.MRÅK“`\íž[šv4gMÜFÇóyéß{fãM„~k@ 91ý=â×"å±uJÉRáõTíý4eoYàa¹IÂ-ŸüšÞQ¸/éG6O¶FßõPáf¤´t0béâW6xÏn’u`€(Ì!}†&9ò)ð¬.m.ý=Ev‹þÞ‹6O—$V)±Ûq~ò\iÏìU ÿ¢d›ðIÓ=O¶ÚQK0‰ÇyÇ>xÄú‰þ0%QŸ2ƒË{J©9I_ç(¯†aY©ñn€¸.¡ T<ÑÄé`Dq¥Œë½¾ýÄG„YI t0€¹ ”7,Zµp(©G,ã gŽo „»¯ãSLVߦÅÚ¼u,`bÔ«d݆bËŒÆn¹¨©úr>1ì¡5ߺ«†³·˜á'VÝàÌ÷¯xP8±{à “Nâ&x ORزÝß™"¸g¯ü©yꤩQP1÷øï3{ñµ?ì(Õ?ª$™[qù褙 80ÂéÑŸôÈ: æY™u“êT¤…Þ³Ùui³Ô|ë˜m›-5V|é2Ån°¦Úµ¥ú5Ífùl3BN ú¤ ™MFlð£ëÃÅBÞàw3ôcˆcMlÝ¢k†Sx$ÃRv#…ÂwvXŽß3¡‹Ô,siØC[È_ï—©¼ÝÉúL–ŒWGË‚ÂÀX J+hê袙$Š–'(ˆËj‰Š'©Õ2ûÉWó¬ûae×¶"¶eQ²Kuk-YÇ`LçÂÒD332?P´ÜÕ`\ŒèUÊjâ%ÃA…ª¡/×;Æø£«Fˆ£ò}$ìSUlBFÚµiDS(ïŸn?Éèy ¼ñ?h²É,C6dðÝèò :L‡£/œ»ç )R“mÍ”:¨EΉÏ1ô^”Ndæƒ67xçHú‹†éj­Çær81ÓÓßíCVÆž"m3ÅS¿6¦eF š‹EÁx;‚àEú(­vG“‰b¿ Š›-P}Õ5?‰Õ™ZRsás_°\ìÁÍ\Òü ïhºŸ!ïmè¦%‚Ü=8Òu"I¨SBÍüz…‘È+3ðR‚S÷‚ãSŒ.eT`!‰¦øçPWBO&ÐMµ†ÛWgt£x?íCeÚðDÉúqp 8\*rÌÁ,¥él‰°²/æãÄ’O»,qc¿r1Ä­ýº¥‰ Ö¿8§¯>ø³%KV$XZ°¿T3É9Ç ¡ÿèHC"’€ÃÓO{ç©ÐhÃ}pq…œÂê¿W°Lv¼Ìr¯n–3¸gðªÇ 9fÆfà‹¨Ôà-ãè⊆؟d¹+FJnW‡!¤ê¿ƒ+øO6„ß3Oöõf iéh²÷Ï «p²¬º•Ñ…W§¯ÁWæz”x1ƒ"<ˆóv4¹âg]Ê~ƒOj9Y‚pc*¥K80¦³xò¿9¥  cN(½£èç9œ‘§aœhõ) ¾á˜ÚX"Œ‡WdÜg©x„Þ?`ˆ"È1€@” Ìe˜Ú7þ€ëïè“¢ÄMxè;½¢ôdïH?“ÄñÜàY¥Ÿd®â¯Ô>A¢_BG§j‰Ý¾¹4a²ò”šÆ†R‹â-~Ž¡ ¼D–F ¸2$> ¾#ÊèNÇyË„ õéÎÿ÷4KáwÊjpUÅÝþ~x„Œ\Ñ2qçpL}‘ÚD0’£ …CÍÓdZd˜Ùr|;Ùúàš9€`XO;@ó'$)¦ƒ1Vú•‘|n‰J7ÿÑ`ؾ|ùR0眧€G±Ûä4›ëmzyÛaR;È£–æ¤_D%i’¢óXÄOÏÌ 9÷ÛÏ'®9Ž,û²HJcf"*#<S$u»)½œc“Õ‡èé¢ë)œøq[¾»¥Û»[áÃðá³§âƒ%ñÂÿÅRÜ…#8‰ž=mÎê.a2UN„AìÌ™:0v«Mé&ñ]ˈ•KaõK )ÆhÔØu~Š—¼nà‘JÁ¬• 5Å ˆ¦õ9}Ê(¨ða ‡ó™aa×wNk÷xíÉÚ³gO×üŸgÚ»îÉ£Çë(ýÝÚšã…7Ë»n:üð?¥±ž,æÉ÷ôÙ—Ôœ?~¶þäñœ±VudÎü4ÕZ¡óN)ô‹<Ó?{=ˆUFcûJ{ótò—óIÒƒèô¸›åñtœ‡Ðõ3z.“à.„1q~$JÏqĘd BâÙEFÛÀ_û,JÉ4Sl¿Û@ò<ˆPÉJÈ]KµŸƒñÅDýž£Çótgó„·äB]Tq@nVv΄ÿý5nDê@ÈÆ HöüüiréÙeXì g îJ~“çþÅ£q=ázC?ˆx׈©6%ÀMÇ8¤Á{󎙥ùø†k^©¾ it}H9b‡&Ä1íäýD0È­rzJãš¾ z‘E:¸ê:_¶82Ì™î˜ú jBR´x ºfrÉj¹’MŠŒ ˆ–SSÚØÀ0Þ·{‡G{‡»{Ä¢Ã׊òö·‰1n*ÛâsÕ};ØTðÔDÝj4,7‚Q¶ÕµÑ8²¾yIiE§8 ½Û´waüð®àv„I7¼Ža\÷!IX¾y¤ÓïÛ7Œ“¯áî5ИºÒ°»úrÔƒ4`Šw­¨@ xmöo0h˜aZÀzC'R$-Q»}pý&=W"°€ 8þ®-Ì×èÞØðbEQ±K ;Äñ®÷&øV©µ)ÀnL J–B‘‰è¢ÓÅÊ=R ß6¥¹5¶üŸFƒñ—BIˆüðVˆJŒe§ßÀÕpæ),º –ôd‰æûE®ç¢wcÔP”„M‡´Ëè pÛœ¹zÀÞcã»üh6Œ§L~èz8 Àì“tK¦³¼Á D}?-\ïŠAâc¨ PÅƨˆöoÝä: ÅŸ8²“¢@Q%&¹kÜ7®¬ÐËþ iÒe0t‹…Ûsu?ØD*ÎÑiÙ¨IÒñS8-»ÄŒ3dQ§\zMìÛ}`ß+bFqÃáaüú‰‡XýS9†‰—Н5øJA‹ î´1ƒH:9 J_ÿÕ'=–ÚÐŒ—‘c6˜öÕ‘NGÓÞ±q$´8ÊÛ ³¼a¼g™h"ëèZ¸«–Õb=°áKò½‡°§~ÜtŽ(KÞh{?JšÂ, z¯6BÞ<.f:÷°’®æšÚG™nFHYôvEù»stdqy`Ôˆúà \éÃÚšIÉDÁìŽYNñéÆAy4âpj‚à Á¦[Mƒ†+é»Q‰ ëQ^‡ÎiTzƒh ÃM8Ø—,ɧ]’˜ô²ê†oà%l ¼åÐ!¶aƒ$÷¶fß¼ º§±ž}~ÉÝ8¡Øh=YÖÑì”c1³³ÏñÝPÌ’ÜÒ¡ž'óá•9eØ ³Œ` û›tržòl]$àH€‰òPvŽ… G¶û[ˆ4­ìŽ1¨– ±˜Å95ybùuƒÔ½OcuàÒ³B¬9Å »„mVd;SíâazÉ@§C-hšô®,Wõ@7 R|ÖQPIñ&9^s¸xãÿ?ÕLY°>ŸÈìô»º&!®è0ôV`dmuq›ñÔŠ¿ñ™}ÅèF}/ÒË]rŽ èZÐ9Ý7øÌ“0Ú|y´ýÓæ×_«ÿn7WBý üéü?ÜIÐÓa]ÌJAîyœøz5‡†3}l÷‡ÎüZŠg…›á vR¿nï]´ ›\©’ sMwЧzcc#†˜ˆíÄq¨ÈÌ»&·éÎp@(Ò—}6ASßwY‘R‘ÓÌ] :öA–N”sÞÊ+¬?44%ý{š}Lúü„Î-~¢ïg *®é}üÏë›Íí“ð¦„(ØE‹)`9|¨¶1Í¢ Fu¬°t¤°­@žžÆ£Iª-q sU@‘9j C=g¸WêiG½ÇÉ»W yW¿ž¤}LŠ –ë”ò"Ó+3qÏZ‹y­¢¨Æ„\èàñÚëC«”§¡¥Ÿ·ÕAÆàé>¾ã¨Sõ‰n½<-9JÞ‹YÅÉÞ<*§@6H6š4i§­53<•Çd”ž+hcòþÔCh£.³”gCÀÅ^«+«òn†˜ê)F xnÇ,Ðù“ä B¹;%ÆÕKÇàóvgrУܻ̃”v=È~ž,4耜WlF<›d È€Y„Þ°¯úp{R×¾xÈ@!¨¬¡ØMDÜ0 ¹E¡¢ AÒ̓£lÜôÃYŒOªїåc‰(ˆ 0莗K£;ê+]ðG_7È­M’qTÙÀ—F!nL;‘CxQƒ²âä£ùD¿EK»ínðÅRS3éŒï‘Ž)Ž”\ˆZ NAnm¶ÏÔ¥ÞîºÚÒ+2Ì6´5,Œ¢¥ÍãnŸ¬l/kAhÛ*LoƒB)£–£¥ã뛯¹fm°uR—¦™VÒëµ/ÀúÊyá'Q?œöµ†  ¨­@¨M€Ñ`ïP¹àŽ©@'îŽÆ,?Ó^í©+2¶b:ØVrõé—s Yp2™•t©°Õ‡„¿6[ ›ì)}YcIíaûS°ÛNo–!þÃV\33ÊLɘÉthžéÅÞ*Ú#qLÈÐeåœÁtñ¨8á\p»u¾;¦Ê öች¬˜µ^¦Ú<£(ìcl^³D…ÔÝâSæúsÎY!r¨IZyè™/@ðW áëLÞœ}Ð@A—˜Í5-ru=ÀÊÒ´è4 'r¨LŸaùµ+;¶Ï ìÂÇbå\ÛK5~A … ë‡V ã=««ñ½fHRèVž[ž¥á™ª /B§sª»àá¹ ꤓÙHŒUµ /^*070çHïÁï´…£¡Ei¡_€’iÁtÝø1ŒÚT`jB¥zçQÔBù8vÿöîðv^ïí½9Ü@€pÙÜ4þòQM¤¢ÅNÕ×4”ÃÕåNð¦¦C-Ò\©Cõd,k2Îl(kEt+~D!¯Žpá•$8Æ!–q5 Ó²-i, [:%}@î&aéz…÷súPÿMýÂ!T!RÍ Á)È>û ¶87à·Ú –ãÓD]1¯_½„œ§“FçÅþáΛt÷:ˆÜ½7ja—Ÿ5VÕùQ¬Å€M'«)¥Bì¨uNQP‰!H? oÞíïì¼}{7x`gÉPo60á‰J·ŽpνŽ6ïÒð[_)ºMùK'‡y—§øìæÀ9=ö¯ìå ¿0qÁÓ@Áê)Þ¹ØÙÚ’Hý5fe^ÛÚñ”\D³±i-h‡V¢°PF5¶—¥Y½*‘ÚmU#|Úé%EC]‘ÈB·ûâðW1J.BùAÐ%$ÝÄÄ!ÿ,h·(G0•h3 Wªb,S¿Å]¢½pöÊ Am×ògùi,_/á¡ ÔŒÞçмWŒpµ˜gŠÁtvDÍëö#É [¢©¥4 2,›šköš  >•xêÐ¥öµð#É™‚Z°ô´ýôVOÛ&³êã'Ͼœ÷´­.¹…^µõÓj°ózÿ.ÏÛ·}¯Ö?‹<‹àrâp3 7?gŠÖåÕ;ý¨Ž£Ž·xßÜýw†)β}†˜ìu¶VhÐ(¦É~Äÿz|zGŠºÎ+åÌ×|~@ÏÈj[ùT¯4.=0¹¯ÉÄZP0Plë&¿RÒlz§ËŠi‚ÅÌjýÔ0ï†à-ŠÝ®íÓë®û(²†Ú7©~Õ(ìs®~u@\‰ëÛ”rô€ç .HbÇäÈ7guvº¿7ß`'3Ð×*ã­œAÀLwÊ߉÷y T%²£\uÔ“ÒÕé{ó#¶à:lj {ó¥‚Ò·†!²”²rº»ƒkx0¿©\5IöÁZ ”JÁÉ)g50T·dÓþiµ”‘{>ê÷ê²ivÉÆ©"”¤‘t¯œweù¹Î¢2®éWy»î7é`ô1µ½´úë4ç×CÅÔéy  GDŒÙr·L>¼oâ Æv1æ¶¶_m[4rM6íÇÓQlÙ•)_”åúp¥p &¥¼±°o#Ýýb²¡v’hô‹M;ï$Ù¹úyFÒÄÖ+w”Xp$–”*{GÖ Cå.ÐWEa1ÂÚ#AÆøßˆ[·(QcþŒ?"küq…1`Ï_éÂþòV6'•[{ôäñ“¹6ÊÌ’™ÿ+oîÏ™kî°BA oºz‰¶’šÅõ˜¢c·Ú£ÇU z¸CöŽDC¢«²®ïýÖ¹c9«Çæ“É"× ‚j:r­”I¡Ý/Ðb²‰^°9ø_‘«Q9‹ ÓWP4: ú¤P;ßVŽGhÍt4Oóí¢ôß(L‡mu]fÝÌ,î 6|„Z?P7CŒn}ðœ7Á´ôôµ',Øõ‚-f¾Åº|Qzéͱ¨àç³'›ÞΙ ¶ð¿-÷ÈPÓiÖ肋-Ê8Zþ¢)²ú–¿­™3³œú7h˰“±qD³RâÄùh’ƒ­pž'ƒÐ蜟ódª &ÃÎJ¾uòì|l­ß \³éÖ#bgv©P G‰^Z¤Iï2Ð×K?µ )+ç´ÊÔÙLœœósœ]uÔŸ‘Ûv]¤)Æ$ êSð,öR+õé#¿Š?!ux©) «[Ò_• á©Î6¤¿œ†i_Îj½vVrUíuÀç¯n–'·ÐäkqÐ×OªÁÎ])ï œg% JLH[ÇgY¾ÓëÑì^÷EJ}×GÕ]«6[nÓG'•tÝO‡¨JªyâPÆkRÉ¡_«áLî6lYª®Þ\¸þ±e³BÅÒ?:l4ÁÕA»Í9éµ#´±.UvGŸëá´\êV¨·‹f¡Ä¨V n#ŠMÊi–©MâoKíŽ×7Nª§ï ‡7PÄ>g„j+O¸‡»LRÐKJZŒàa•®Ã¯µÿßÉJô>Æ_6ŸÿD¿ð+RŒÈ+_Ä#„C@Êx -Ô÷ßÍꈥP*ûC†oÈô=sàébÛ«Ìë %j{¿¿œÕu}J{²¯Ü Ý3.„Œy©Þåû¹Ãö¾U\Ow_úvÉÂ6ÜKP…Š?‚nŒ°Hð.½ØÃ…:$øÉÇi ÂCuÏé€ë‚W…!dm`eÞž ¸Ñ5CÏ2 óÓËÞ(„_åLuÃô’EˆòÝå0 h%ŠÁ܆Yðšôq {tp<÷ø °0=5cã/åóÄp%LJ¸ßSžŽFK ÂoÔlv*ЙcÍ¢ tÔ‹v ªè_¾${R2ôÙ Ø ÉDa6ÿ;§ªW¾Šã Gèk‡ÿ I@Õ zX´ñ,ëS<Ù(/“þ¦\Í >!¿Q)Ž6©Šž™B¡•¬¦p†¸2 ò;©áÎÄXq~>€CQÉKqc©æ®EÀÔz0ÓÇ¿FJ¥Ÿ5}5w=ü\eiŸ‘*›¸»‚ÃÀ+ü"!sžˆ ¶®çB'Wwi³±&áo8ŠüÕJƒOñ¤×? ú¡v·{“&®> hTØóTÊø‘qåDý‡½¸¡ëV°wÄÏHPT"â 4›±¸Šìyå' …_Õ‹¢%ý~¤8gw /«ô½”ÕŠ+(0Åï>XE-¤?:ðà6cX¥CÎÖ„ ßM7‡{r _$„ `ô Ì< þñzO‘åJÆ=ÜùfOé’šö0šóp/w äXŸ©ˆ4š/¤wƒÚH Cÿ^EöÌÓönåùë®+ëngÞÁiZ¿_)ø[ŽŒvÏ1%[eà qnhµ…¥B,Å$uð­œdãàûo0@IW~ÁdO”€½;šbø Õ™'çê^Öe²âaàrz…›Ñó/VŸí}Ô$×vpìTÿ'Œñ/úà‰)Wtйp¯’sø&ôß6R® .îÑÀÞ‡a>ŒÀ¢f{@צúíËfÕœŽßçïßž<$¨À) žžTÕ*V±-w¬ZÂ[¾ÂƒOGE%*–~øaIu –Âú–ÂÚ,.xqFr~/ƒ“„woÞJ[Hú„dinKa7=t*䲇{:kñá^SKŠC¡úÇ·‡¸|Õxk¾ ?Cÿ,Í)M ¥ˆ˜©•«a›A¶A§MMù•uÎù"^¦I_T¡MóÜx€jÏ„V¶ÆqCZØßKÆøêgã<ŠBÿl#~D—­  ötøå¼@úü1×Í™©«‹Ç㼨´veT]@YWr)´×Ÿæú¿ù¤qHõÿ˲Àˆ}n7´cCA¸QÄ?U†$ƒæg(•èºåôÉi¼:Ö¤fáJ²äÙAVmøÁc®9ŠŸBØéŠ™Ö[œÄ!ñ^ÕŠ ü÷Òa³Ò-`¡îê´)Iâ® NO‡÷™Á`Ú¥î'bý˜ýS­ßUÚnHC=  ÌQÞɰ]‚làžÈÝ8â]0Ðû§-²#4Y¢ê‹QU› NŒîÆÂ'·ÜWè¢ñ‚Ý«Ñb! ¬à‡óbžXy–W²ÇѸ5Ò;¯ä‡~Ã-錗•Vn´‘™à,Èk î¸Î Ï4­÷áC ®K‰ºNoÔsd™¨O8õá§ê€Œ¾v°D.y&J©óÀD$©Ó¯0-Ñõ6ùh’'Q4ËHî «…ªGÑZ…øJ1²ËSæc4JÜ›oV®jÖ ÚëMÝ=Æ0K©„h/Åq£”´/ÔIûBmÇú&ù@•5­C4ØÓΧInsDp€ÈÏQNú&j¯7Á ·’9ÆXþFaŠ«–<,ŸÝ.Ûß#úôéãõµ§ó<,)¤ožoåŽ3ܳq†»×ö»ŠàmV'G»9‚ª%#¤'æšáîá}ùâ1(ˆýA"°‚`EtZJaQÛ¦ÄA˜³ÈħÕ$ãüeØŸ#¥jòsÑ,09šIZcŠÓ³î‹q½ŽG{Uò²#ªDGIß(• ä>"YÀíÓIÒýp÷QZ¶u nb†Ñy*è}íTIÜàÁ;CÍ˸¯Ìè ‚ôs éA k d¹ÙüöÅáöæþá6}ŒÅòo_¼ÜVŸPW˜Hx’bIB*žÈ/sÌDP³¥=ä!û à‡»›àòTt¦Ï ¬ß»{£õ¦Y›üÍÜW:¸ÿsäm†Ÿ¾Ê”ÙÔyuLu×ÝëOpÉ=ÃM’ÅDPú¬3”P6G"~º*8%å`ƹå~"°\;E OqŽj€X Ú[ÜØ?“•‹Ûî_+<ÜpV0N"YCGZŽ´Ä ¼ÐQ†àš7-þ/DÓ7G:A¤ébvfÒÝ«½­™RGÿ^_{œÄ|ÈŽ„:ÀŽÅ}ãuœ¥ÝÔ ð¡‚Èõ±°iH›ûº?¦Ú®é ó´á’¼Tm^/=ž8ü5è«‹­)…Rfa:o£ºšz£®xOç‚Öhû‡t+`¾Ë¬§ö„ƒöuð¿æšJ201Œ+ØBú)*Ìi’;CÉt0Hì¼ÿKŒœ™‘õ…có!ìþm"í'áñ?ß_¿Ç,In ü2Ú|¯<ßn‚lêöµüÊÇ$u|¾€O¾¿i†¶Ù~e³›“‡ÑûëŸþÔlš—#“÷_‡t>¦Å+ð{ ¼¿†)ªaVž;³Ô ÷uChVÝèÛƒý—d'†L<ð¼4 ÿ© ?¿¿yÞ|øüON/¢ò΋7;»ß;zëöd„ž¬„ºÙÎÁÎáîÞËšö¸5ý‡ }×{×µ )©CÞ¢ÐÁ°µ^ï„Bͱ´ñ¥ ±èD¦ž`Mrú d÷Úñqº)‘ÉùÓ¨Œ s™«N„8RÌáÙt‚‰8jbÅê7€¹ûÃ8ßc¼ü  ¿Á²#t1‡>‡WP²U7}—® 5¬µ8¥¤ÉBh\Îôæ1£ ¬m@¾“îEÚýW|½¾¡ð Ž:6ÀG¬¨"¶*z<šÝƒ/fºâ6øTeÁœ îâãò[pè¹CŠ·Ïi÷̆eKÔ-xƒ×´EžuŽ-»ÐZ?Ö%Y(EÕJä8N§³MÏYJ¾Š¹ë Ñ!r4î×›Cýè2á5åëÕmx¼a=¿0•±ñÎd7Ò¦ô’€N<ð Ø*6Ã`%·7Cz£˜Mü$ôÚÓ-»%O“ë³ig]30œQšxú¶A`0ÈÃî9°~RÇ"‚‚â¬[ :kUø£ØÞ.6ˆÛòK€ÇwrªeJÛ¼VQªý YžÁª‚išWØ–éÇbn1Fœ’éïnB ÇsƒY.[Œ’xo J -0[a=Š¿ÅµÄ9 ‘}s÷š ¤ÌÄÅt€‡ŸŽFêx KlÜi¦Ë¶êÇêYo Å€V«Ù+²Ì¹Ï’,” ƒ:èXŒ#Î^Ú•Ë÷¥EÐöÍÊmâ’Ū÷¯/77¼;¨Ö¢ÜÛÞœÊk©Øž‚¤^|AÒó#q­@]1:`äž8TƒX÷ÉwOYY^s_Õñ΋£ºÀ:‡tOñ£5çÈÇTMxNÕìm™I2>@ÉÌq©¿äÇ1¹˜ä,öòó_7Ëôȹ Lž…ž™ó¼Ã©o!5öÆûaÀOB¹cÇ 6 ³âqy@H"ÚµjÌ& cð,¨·TÑõ9¹’%ì8á­eó<&å|’&™£ßu™ìì°2™Ë:£WË—äU®î[s†ni/Z_‡_ ¨¹ õW¼ Q@è¾Ð­Ž³‡ÐlƒþYÿ¸1oU[^~õö#ô °ÏO Ò‚š½àj›[kÚý¥|¡Á:øhÂ#>k+5е¨Ä{óx}C†BGîÅBW7þZy““¾ñ‹-ô‹ž ^ 4޽PÄl¸bž££À]¼¢ºÖ\jNª_XÄ&@@(“DI×(³Ë}4œºx— ‹›ÄåÇUÕ\Ü2„´´ B=} ®z”½ZÁº+ð.çlŸIÙwAÅprŒÖ!»¤E ¦»EŒGw ‡l¥TJÚ6ç+‡ Q‡ý4lYá±éÙ˜–¡:yɹ©£Rr8FÇ ï ôœÙq‹¿CÏ·HI§ÍÒvµ-ç„PüäEQ°‚;½ïMPa*nù×o¨‹ª GêæºAkÊoÉ)/NÀû·²%{ú+yG8ÞÞ¡G¹°ó+[ŒßFi$IÀ°=X¦K׸Š8ÈŠi!EïܺK.%/=©.iBb8\U;ã%—Ë EfLuy|v[º¾Y‚gB›`lÀi™µ_¸B‰Ð°ö$¨h#Ò½]+e\µÀDTv¯vµ  /o;.³ë§çIßB. >ë¾àÓ’ Ú\Åk4e§<”˜º0ZåÂðÔ5–Íâµâ¥¦¿ÒÙÒ‚çά·À²ÐI Á•L*ûkXІ{×—nËM1ÄFÎ}³ ·Ëʈ\¶ö×wá>È·AÙùÁàó‰Lõ÷gÀ%mÿBè +1é?¸z F…%‰] ã„óÏ)ÏÒ2˜`(ý<¤zŸŽÍsù,ˈ.ÚìN;¿1 M‘®–æ<Œ¹9ßúM¥ŽG€jgŽ6$,åKNï×øÅi纘¯ GØ&ä!»qvcõ»OnðÑgpºõRI²ûúÝxÖù:ß*^b/T.þ,Ä+ñ2_®ˆPt‘Ïr=°r*ûFTQèlñÒcàžåÇÄ}J!YU–!kÙIé«Ò˜…00±F2tÙé’D‡u¬|`Pâà+d'UPGùþš_öˆ‰a(¾_'QÝJyšL°0P…Sp½ æ:wK•¡;diòŽB‘kñžˆzÔ¬ˆœOpJ«ZÐ1<1’Œ¾s+åé *A<×P!™ƒqQí»—æÝI6&5WÇô€@=C_H½DV6F¦E|‚gOÁ^I[ÓIÞtàñÃ2º‘róI Ç ª,R-@i*-C¼ûÍV­æ«Td¶OÛê‚)©T¬ü`e­,×RºP«VUá à «ˆâ<œU½6xzK÷¤‡Õê*Ôú¨zc¼Š!:W'ž[D ò20…ÙGøò)ÕGÐÜrÕGÑÎe¡âyiy5YÊSȵöÆUOXDÎøÅð´¤ÂÖW]TŸÓ)v'pŽÍä]³S‰AÖ"ÚéWôÅí@)!½NF2ž±|þ~‡™ËX¦¢Îü*ÄPŸ¥žŸ„–G'í{]ñô¬\k4­Ò4úk¹>=ø™ÁP*ƒ5c»qßûW‚íêßR/~±¢;Ö“vw÷Ae9ø{šŽÑ޶ õÓ¬ zåeŠw{6Tœ J“Õ˜ÐF9M*\X˜,t%ýˆ½ðLo’hLu­@G®3ÇkÔßÂK*?l@ Ò„S8áLo×<òÑHØðææ“‘mw$,êäÍ’<ÎnóQ‰åÛìÞÔ¡ÄÖêª §%ˆO¼Þ âï©õ%·<‡Ò­ ~æœC“•Éœ_Ü€“«ÐüSýKê~°éñlÔù÷úöEüPi÷ííôýpÛëê];Þ¨¥_¾n[!Ñý™ ŸìÑOr~ÒíbH§·ÔN V}ŽæÒ¾Ÿ±QX±ê•$Æ|þ°RBàÄ6÷X ·õ+du–.N W' ÑSZâ‹AäóŸhŒŒÙÅíê E­À-“HG™\¸ªZÚs ÀÜŽúg™_=ÄD»jwí´ËÊíƒÅvT¸²ÝõhGE±iøª†öÏjô/mþàƒ±#„0Ê“apBþØ”£ g†Ò£Ýë囦ÄP­LìÁÒp•#Ðo·Ñü²½½]’bi×É‚öœê»®Üˆ—ÄMI}²‘0©¹†,­8Á™³”ªyR¦·BJø~YŒ4 Q€ÜÀÃùõ&¬j;zÿ>þéøŸË'͇Mø!n/Ç›Ï]g ¼x¶‚AŒÖö(D¬4c Í±È+«ç´D¦)6AZCðC8)u«äjÎÁ©R€ÀW‰²E4×¢%¶ˆbì>ÉrXðÃ~0ËEü9öDÎ8>øÞ“oFÆm‚ºI‡ƒÚëôÙME&Ef8Öîƒ=У¥‘U‘QuS1Ôõ¬¡²[ Þ¨µÀ{%ª îa‹ã(·èˆñC¼êV~jå9˜rå‘è?s×7wšÄõMý$LÉõE'!rÅýóúæäá5ýsCÿ€mR«p"ðL¥f–¾ÇvžKß©ëLiÇÀ¸¢2ÜÚÙW½à—©|¿Ï%.o°ß‘;?¨-:V¬Åv½>ÆB£s½äÇ GÄMò¸ç¢–Q°¦pØN¥NÎÆP6yè·W¡íjëÜ,Å^ ôh_¯t’4-jj'ºPr–›ÂæIÒ0]áó4ÅÓñ6£3¼–À4B8dß'êÆ¾ŸÎ3̈¢àÖ.³a†áù“{:Ž?CQvZiI6Ì+6£J»©Þ«UøÀâî²Q.ß'9¶fÃþ;­:O2-kK¢^HÂe ¸®íykY_?QuµyùquV û#3ýh,á§ä‹V5–Çðúý9/•‰Ôýš#'jÕì·¥‘…›Uª˜ÑÁ ÄÜÙýn ¶»S(b%[ÁþúmÑK;Õ\ ] îâpœÛ‚—Ã* ‡Nܤ¸˜æt£à —#{«äw»8ªv±êÞÈ~÷ÆïwÿÚX`ËJÛåÙ¢æmÅåoââÉþ“Ï»{]<7xñÀ®SOö{¸x²….žwÃ{++æê©ÕW~Æ«[?-thÃÜû‡xÕtئöúAæç¹.rµÆ<;Èu‡®B´]÷¯œ¹¿¿vôŠ÷7óØûÜýöÚh¿!þ,NÍo™C»ËœÅ£­Í"˜AWyØ  ȪXtÏ`¼s9i¶AÇÍbaÛ“`3È%(¹SÊ¥m4›)š i/#ôÓK—o•pÆizž ‡AÐÜùÁ>Oã S7¬0 Þp%œÝKJfÜ0óŽsàè{Ü)†Ê!å%س\Êmå­BHR_;TëbáósÏÔnºÞ"§ÛîµÄ›…hÀBC”iæ|Rðô•2YÌ% »'þäglI¹©Ü<ÿ "qöùvb÷ºD‘7Ÿ{Gàç¿RYö†nPˆÈî ù/%ÎÁûñ2îÅç±b¤g ÐÈVñÓàs+—õÂ8¢ávÓÛF2ä÷çíWˆ85‹O¦N-¦yK›Ü±*túð!µ®÷‘×£D5ÿ³ùª:Œ¢Ü’ì¶Ô¿wÕ×Kš£´/Žª;ɇ¸¬ßxh.@zyôÓ×_c=ø ~ýM ŠQè¬Xˉ>‘ýé²[Ï5GD»÷„iŸ¶ï(a’‹Æ¯@Â46MB un0›n¹ZH@ÆJk·éðr ›­@§tAbêÓ´¸L‘%§#zxž×B´4TPo¬ø¯1çš/ü—æ¤Æ>Oèôèb1¹qÒp(à ¨„…sjô‰ùï–6?ÛÞ”eתÃ:[„Ä q`Üvsô†øÛ¼èæü!kþ!kú²¦‘4«‹ÿéŸ_¨91²æÍoé±È܉$m’œùòè·'dÊ…þ*èÁ‹“/n„¹yÌ´éÚR÷$ß¾Ô7é@$Œ4,;:IÏ0?—û¦´ˆo¨^ÓW[$ â¨Ð·E7§$TWP7€FÑÕªxÐP×6˜'Íóì]Ñá|TfGt˜*»ß6X°|ìê=+ê&=O­ì²ãÄòU@ =7?R÷RÕªžt>^› Ì‚æ Ó% 3–RÑvæÌtÊ2²¦[R?Æë¿â”/[àb CgÖƒÒég3ïˆéÌ:ƒ¤—j½¢R¡¨%¦¹ÊÞgØW÷¾ýCæ¾›Ì]±¥·’½K›wɽzU|EK-ÉW|åA(MÕ”Zœ§T[ûÝâZ®å(wÍ­ÝÖ»© å ¾Êq‡y-˜¸B¦nÝž"Q…µ²ßV×À;*N•,@ÁmZÃùz–„5K0iñÿ±dBÔpÅ…Zu<¦U¤þ(Ü3“þ‘ºˆ_3ú Ó‚ÈU0ï‘X™v™³f›ZºZØ@Q6®[Dœa&v;Héj+„®ÎŸˆ›:.ò <¸a+‹‹Úàn-îèAþÓæƒ|»ù°ù§yl~"[×\)©s›²9Üçâ`Ž<2£” £Íè]Pžln7ý.zö\޹vêºÆ°/1p6ÑòÝ ?嬙/‡l@ðy¯jJš7-ÅKÌ/ÍØ»Ûys³ïÒRèäLjãíbª¤×˃ë›ã“ÍmSbçÒÔ+Yú:›ª‰–Ç ^¿ÙCÁ²ã`ýÑè颸ÊöLÙ"Ÿš@ )|fκ)Ùu{#‚†Þè{™LÔ´"\ÀK.Ò¤'6uÈ I}S(„`•\X WÐÔW85…·oöb£+ñÜ FU”; ú9èmP¨¿ŸÇÜ+’ÚIòaü¯Q¦tÞœµ]¥Œ}Dš³I ÒÝ?êÄm{^·U;íô±‰¡×ûè½úi>Œþô“%ߟ4‘Äq.-Ó­úhØñl»†X¢´+ÜS=ÊîkȘ=ùœµ¶b$~ß/¿ß”9Œ¢'Lã=‰½ Yì2†çüê Ÿ~ Šq~cLúü„²²{àN'Úz¡1% €Í² “ sqj#ª*œ­Š*¡R‰9H™TOH¿„ø™ ®¬(/H&a2B¥ZSoËL¡F<7¢>Š¢<%ßèG¨×”£û.å¿ Q»mÖ2ÓÕI[^©–“ÿ£Å²¾ Ûlê³Åì ³L€`­¶JjyÏc.Y]˜¶jv`­ðk–Ê:S÷a [Ie)upšp{Kº! ÁG"µW’ùÄ´?å°ÅJm‘YÇÃÉPŽZ€£fÃÂÏ_ ê–ž%PHª¯øE©V}—Éê ó£pbn½öÃ×(ùÏ·ð ¹µ¶ðÛRÅþÕ=,-Œ\ìÂOÁòüW"™‹Ö9š¿.´âRU¢QLöz˜í°SµHšûTD`‚ðȧӴrfjPgjÄþÔ,&išë+ò˜™kºÜ MÀéM¶´SÍopÖ 1ÙЙ±2OEmCÏ6÷9[:|½Do0ÀÕb¦J2½˜@ $;šbfe‰3MÃLÉX¨P7YT3 âSzMiJ¥#$'c‘|*çséèÂE÷_!»×k\^ÌØpd`ަÅxZüÙ±{½^îUü^ÌKAñ5ÂEvåI]Jx8EïBȬ stt(Ïî}‡ˆ‚Üs©gK#'nÉ– }ï;á_…ÍávªªXµôJf‹Ç[×–—-óž0׬a¤ßpð·° –Ú¿΀‹&Ä«˜¹ßéñv÷š§gi ‹:MS~ž+mˆÞˆ*Æ’ß©`•¶’›ŠUV1ôëŒ.\åMg38@”/nš|w‘‚/…ÆŽ='Üb~ùo¹‰ã ¯‡ËJSWÐ6Bï ½fó1ð\_6”ÄëÕG€M"’ÒJ¥EôzJFa=#. S¬æJDáÊðÑ+aSÌ~ªëAÔ‚òz׬Õ¥ìÖ“_èt4~Arœ" ¬ÏH2f#¸ª”Ňn-#Álêþ„A`úGꞯ"t£Š›¬\tFUG›ô½xû]]€Å¢7 ‹S(t8PÀª:Éz¬,ó'¤åR‰D¤-ˆHýÓÕBðhRYÐÛg[;¯´³0i)BuáˆÞ‰H^…Wƒñ‰†ÀÃÑp4lK϶¾úÏTIÚYוˆ¡jTË~‰”ôX'ÍSÔdh6./ÕÎH=VlKkê¥ãtØ3®”?º´´Qk˜Um¥Î~ªýhÔ&”lZsO83ãï’ ø&núM±а˔Ùì;¹‚L+8>©(£Uªˆ%g;‚×úÙ)_;lã®í¬$§Êi8K.Oh9x—“–£5E"m$êç,ämÀÀ6ôŸR´ì_³Gõ½àÍ.úbº|h¥¶64.*ëÈi¹hÑG{+ˆðÛíu÷fSî¸ìÜŠ“vJ·6uw¯[{T`VL_;t!¡ZÝ„ÉJ÷‹‹QGÿ®šF¾`-ÜKe³}K˜­T#{¦¥‚g-¢’×Vê¤C.싸ÆX®8vÁX?ÓÆ»J[”¯›V,`i³ŠLþ¨$\¾½<@–#–긩A^ª&èÙ>?Ÿcy á-ÍYÚÆûaåâ4ŒA29ÏÀ^½v;ñÊô$Ÿ"ú£…£H¹X8q©ÃÏ]6ƒÇO]XdÍ€ÅÀ_–‚•à‡6]yÜüAý½”?ÈÕJftˆjMŸ»ýZ[¬lixgE­€†Ò{E~ E0÷3éãŽU’BÄg2ç/bÉ?§ëÌ÷äw,Áiû=K BJÝhNù1ÐÙÚiz‘|ÌÔ¶hg] ÁÜÈ&¸¥¿d9ÖóXІ_e¿G56|‹p¶ä¹>1j÷»<9·6­qþ;|m*¥Bø’­HkJÒöRµ%Mèu éý+íJ5Œ÷fóø}üíþ P½¶3./«6:ÙÌ·­`¿³5¼kJc½V1J~§/Qx³1=iæ×7äçB=E2yˆ:5«î«j% ].6]Ç’†íŠ¡LЇAŒãF§ŸÒî´ 1x4éá Ø³ÑDé¸4mýÞ7H®À32êM»VZ8G•drEJÀž‡PO8èCä©«Ñ€ 4ïÑ -²=‰”}Ímè+E(¹ÿ‡"Ë–û¢Ž ÊÇ'’â´“#2êN«¤T¢DÒÅúºJ2)¨; ÖèPŸe;1)§}mD2φSzäÖvzKŒÏëtIÂÞ N ÑOâÑKŸ Ü0}~èhæ£êm ºIÞMôK‘dkè#Æ ‰è=«LêªËSˆ83æp&?K‹6i§?ÊqÉ È…a §ƒÓÙŸwØya¼n 9Óú/€R·x« y?¸h0©6…}P¾TÛ^¦`r• ޤťCŠð2¦ìįXx’Jž/ôÅß¡r ¿ùÕàtÔ×”§÷)B•q4*´é_\¥Ú… _KšÞÀçô°ä¿ŸéoäûœVé»’Óœ«eÍxZãöþ[š"ãñÖúÜ7µr=R¢Š–®_¦ýèu…N“†GW$ýô­A<[-ú~¤Ÿ\«AX7@Ýa¡× ÿiÇiŠàtH|Û–é~ G€{å ‚tS$ýrLšB>¿>™SDL*EMáMFÓó é“{ŒϦö{²·}†§YJ|·‚ãïͶJôÇ>ùjkÀuHýcUu¥Id o ªR†#s*A^%ºiVYð´é£ÒYæó;Ë’°|×;ƒk,'ïÒ¬0±œ#”8×¼§>é¤Xrî}Rr0\¬**r×'µ¤ÕåkÌ’?¡SÖ7†WÐìXÅ5zbñŸ^l´bm°cÝ‘š+ùÕéY»­aԴήšQ”¦È~}bÃTVyy%Áág±‰xd¦iÉ2rwókع&jþ·´?Vçäl:ìâý ‹×ĆY•¢ž’XšöìaŽ¥‚lÅé_À`¿s´WºÓ Êu…ô÷ÃÐÃ!6ÜÂbz¹XóˆûP]LˆBñs4×A$cˆèTÝÝd¬ˆÕëÓ—}‡P·¡åØÛÑû÷ñOÇÿÜ8i>lF§–º?DG:˜\é!oP¦p×T—8E™Ó,É„Ÿÿ‚ˆT]6N<<¹Ä‘ôzD {È(ìÎ8ývÜ·xÓ½Ê#ÐF b4ºG¡X}جÄ<üTl¸·q &ù±¼}{û!§ýDIÿGKíѰ…pr¹ðáÖVLJ1ÊàØ©6oYÑÐýÀ_[û¯p)ðÔ¦Ät?ÑCLl¸Ï޹Uìç ŠëñÿwÍtÛÁq‡ªìv—sù(-hQRÚ,|€™¶Ö~›½áïpG5½2É—›Åuc«Ã X”žƒþßôx>ë[Žœe1ÀÏ MÚù1Ò¯‡Ï­Š#$³&š˜Ø´{<ÁÚ—GÑ*ØQ›tÝ ù—|¹…ŸˆJW‚¬þÕÁ_&1j­dGÔ¾Ävš¿ô¿ùÍW<Âðî–$⺇a,ð,§[ãÉ࢟$ÃsýR’p³d÷6†¡sâÊ<Å{‚d¶E°êèyGZ¾tqËŸ÷A¨ÚÞ|çW 2,óVBóÞèß÷!È;JŸéQhîJKÏBz­LÍ»Ð"”†Ï'L[cMxÑ1î³ÉýžJ^¦<¹Ú#¶—Þã½ö¹§vRÅ6èThK °H、<4©EÀr¨æ|â™{ø½¢AûMo JJo4g3¢G’C$6Öì^cBç•ahOÜD‰Ž®r;KŸi ó€­—„ N™ªà+Æã\9¹âd:Ì…aʹbGC8{—éšD ËEx"FÇ\Ê~ÐûGÙ<£Ðgõ[Ò盧ö M Ö±I׈:L£ Ã({²°n nçAIÒi*šavH5L0Úá’]#¥X¬¯!4[Q'¥>5=Ìçx¬f¥îpm4B!NøÖYaåp–k“˜Nt¦~²s£51¿Á‘¸š„Çû\uM§#¦I"ÂtÙŒ&x´VûÅx·uÖý*&I‡¾‡ûPÇÜíÑpà8;qŸ±ahA{Çö|hÍŽ— :’[*•`òÒm…jÁ·¬—•¯¥±`®¿J‘nq>ê÷Ì,!ú¾˜<*È¥‡úX`Åè„-oaŸ¸Ÿ&Ó\Œ[¯!972zQ¼BhêkYxÈX”Ÿ߯h!̦‹ÂûL­†+Q¸>|ö´¹¾ÊïÛjÛÖœ’h…mÕñYSöy­OíFðài¼~öà¦Ù³3ç:j>\_[Sk ƒðá“–„ò|-âNÿ/þ_µ¿!&Q â¬Ãl„¡`oM߯ æD‘Ù™Ä01“¨Db!õvf;[U½û”]û†Üãxc}ÍÓyˆ qÉÙ+‚Ë¡aãEçΠl+V3LÇ¡ÆÈ¾Çäªba^LË8·jÀT#È4øyq$æ1MH=âcu’ĹZÇÊÊQÉã¢çÈÚ–[)‰Îøœ©ãÉ‚J0„½3à°:HáðÚÜÈ J1šdiqe<Ê ^Ü0›ô<“•!g°ÿ–àò8£-èñÿ>†Õ¬¾éÎÕ'jnàÂ+!AÔè‡\}ÿ¿êóo’É•žØø› æ[7]=†ASÅC ùÁž¹u骕ì>˜Ñ™[[þD“Ñáë ü³¬>w,kè˜ñ|ó믷BÚŠjTòi ç$ð˜È …¾í•w &±ÉäPÛ\ϯ;6·¥ã‹ä¼CÐ÷Bú³Ð=kÙfVàxâ/Ÿ^SO¯DÀ(¾~›\é./ª›ÁÉSex…jœBÈ„»\g•™€¸z'¾+Æy…]Uè`þ’˜ù »ý-B9z¥ |ûB xY}£FÜ—£Iï\’šÅCwŸ+·YMíª0ÞWõ[mÝ›ÚèJ'˜g.ÍH {½Æyïnoªsã8f½$Š•–,<õÅ òU÷Ÿ^¿æÕ—v@õ=|Mû÷úõçÝ#¹CºOÖ‡´úÓÜ\£a¿ÿhmmMp@ÁΖ\}YÝ;䬯ß—ì•Z7ç¥%)yRƒ¤Û*Éø çl¦³FÔ‘YóÖùŸõÇž=yºþôÙ—ðçSþVýûx=^-ãUà¸mt¥è÷Ó þ¹Š¸]Y?_ý’ø’B›~p ϧðPx4õ?d`ÑÁ³ÎŽí]“>2o,+¿;_‘×J´Û ÔI[o?R Žþ¼žŒ@@PÍv¦ÅdïÚëA‚¡àŽʹׇ_þ¢6µ—Aîc¥ëÇÓq:Æioº­ú韷8Á/2Õy3?ýKW©¤ñt˜ Òþ)´“év)†ì ½…¦ðîÍÁF°yQãÕÕËËËoŸÑä|À…ÏÙÝt˜§2=$ïLƒƒýݽ÷{ñÑ÷Gjë袚¤ú7λИa¾âßéËä<ds‡dœé¦ïgÆÞûvçàÝÎÑþ«Ã{X¯í")¤í4³¹’<èè?P)µl–\z[h§ò´T4Á¡^jÉœ Š’s0KSjl4Ž¿OOÑ)EàO?¨2‡…õ€”ýØÎËÍ §ÖƒÖ8÷&H ½t_åN3 ª¦Í‘X9.òÕ­½LØGRÍyƒÍç úâ¨üdL!¡z cƒ5ϸK#ÃcÇ^ÙÂ×ìGx+Ã'¶cÓ¥º¹™J—æÿúêàåFزC©K}íÄir´÷öšðÔZ𣒡ïȀ͛l ’éb~?|f‚K*ØN8ù%8ªÁþQò1ø«‚qªœ`óJýy®8V|z®øT7ÎúÛ-à”_¶³\ ³l·Ñu#réMÀM€N9xE«zŸ“ ¤Ã‚„kœ¶b"rç·/6Àzïm|ÓvÕU2"a)b}®º‰z,dB/ €eJ §öh`' ®Ý(ö¬ %BÐKô޺œŸÉ[LoZÂQ×" ¡T=ý˜gŸH­y¶Uz€iÒéÙ/0SºT(®pªsSñS”a5ÚñAÍCÇM ¶“ÿ™Y 8j$À_]Ùù0;ËÎÌr¬f”ÑÃMÍÂñÙÁÀ¥¨¾§³·ÿnÊ:¤ÆõˆE¤¿Î Ûœþš£‘ð9-×ÞóB©ðòk³ÄÁ:}1/§(q²üÁƒ»ÙÜרÌoÐðȉ‹é À¾e %e¹LO¾½½í¾ð8o;²Zkys³¡þµ£ñ'ž›ã8&Î4Ukª|DÑïzúÙ¥Yê[zBá.-‚êNM¿4ú¥A?“”ÞL7Û6X‹ÿï£Ç|!aðdiTz_ð÷ô ähEçSpàË4ºƒäS§wÒO x9r¨ƒFáMÔÕ¢vE·!G ÇC†äüìîwúÉ9ÁŠ9Ó¯(@ úizC¶¢Z:£Bs_@ƒ‡ˆæ'€ðÌÌ|Ìœõl0ÇüCñ!O 5$'µã¦&HŠšVqEJöíØGêÙí‘z¦8˰jøÕ ¼šwêÝk¤½´§;¨· Œ…¶-.¼"I?bÌ…aÕØÂ[ê¹`ÝeKu_k‡÷ÞÚazž,´µô®¶Ø¾zDàì+Ãù¼›z»sZ±© ¿ä¦ŠÍ´nª5™•!6²K¾6”£H8‚Ðu€=!¡Üî/)ƒçI ã‡áMeq·0À×* }é\Žc/$êÌHnVGˆ)â`‡ÝåÐÙ|@gRh ¤á|HC’¡ðFy…µo•é*ù›C‡^ê–Wå¬eæÚjÁ «Á˜…Í­Þöð!ÝÁâñš3v˜Wê<•Ò¹D¬²æÛb\óÅYíÚ/èîW_Ò4ãsÕ&ô%ƒg·çYUϳ…z«zçô´‡Õëm¿€'zuØÊ(ƒtü 5Êê¾Ö|ÑM±Rÿ9(b[×âÊŨP¬Š¾öñBì)?LÓÞ»qÿ­à+¼5,yMñ ùJÙ]²³ ªæ:ö:”$Iîÿ¸Zîô$‰¸í¨èS§=o³eÖ‹´7«`—ÚU¶–Û +Ú-² $ÉsP#ϧ½ÏÝ¿¢—c¢›÷¯ŒGV¤iäyáxoù¡þR»ÔK:#•‚AfÂôÐÅuÍÎ1jç±Éúsgý%ÊÖ¬x5Â~«cu@¢õ6þÑ\eïCÓì ¾PµUcêðÌ=åpÍ\×dâH( ¦SÛV eÇTdÓ™™Xð´±/fWD'˜! „þEœ‹ê.›ÉËÐE”Jªcù¸{¼~âùºç'°y½,ñõÏïîXÿ÷ÿÔ.8ȵ Îvý§vaü³ìÂ`‚ =¿bnÎ*|•´˜û!Í Œ‰æEX…1åó#¬¤Ã}&„u:JË)‡2Ï9® ]+ñˆQnS@»\ .^'n¤›µÓÈ­ÍmD¸òƒ695X ™Ûá-¿X† ¿í2à稢7“¤'Ý8¡³ óÄÂþJTÏ92gI—¬ät4-¬ÜãJJÓÜxZ9®ADU¡õKVÔ¼)Í@¼^ª¯›"ÏBÅVEKÖM‹&‚që+%M8¤¿ Oî°²jB)ÐN“Q†)ú¯Y IÄ3ªŸ H~8c=(ÔH? œ°ÆfÛhÁX ~`£ðsh,Sù\ã)CÕI^-Át˜ýdÒ¥‘LNóQ<é ÷5–ENOtS®èC(Ìo\Ñä&ŒmtdLc–7¼Nx¬u•NÜÕ1˜‹äaØ‚¢Í¼4fG'¡²·f,&!8ª‘3ÓDO\ͳ©{ž©¡~LTÖz)ÚR„íÊþ%å r܉솕~~i\žÜ;iz z—H?¹ôv ìšã­p5ôÓ@¶XÐ8tf¹³€KØ^„x6äbj¼!@ Ýã™`Æ&¹-'9!ö´Ëý&œ‘UÃüßSø@—ͅʃÇqŸÀ¾Cñd¢ ÿõ³AÆISmè–­Àˆ$')ĀƿާÔ«ûPA÷æÆŒ@I.GN_…‘~’_˜Ñ逑‹ ðòã)¡Ñ®£¶®/åx­J&Z™ãÃ÷ç,¹ˆê¢w¦Ú>Qcye'@^SªY¾c ä4íàÐ6zÈUÙ­º´“ʳÕ$p¹=øºÊqƇõ»Wo^v^½é¼x³³û÷½#ÊOóJ‡Â«Ç?½?j«ÇïOÞç'+!›3è~ŽñNíËC(ç°Úio*ˆ*âšDò©—ƒU±§[staøþxí}üÃÇA©fÕ„ì^ë‹$ñz74™7ŽÁ°{‘L‚=T§Y`8Iä±ÚG."Ù›¬[bptÜ^?ÑLÍ x6mj¾W‡Ñ]õɼU?ºåªO_5Mt<’›RæªhƒE“*•fSB µÉ&ÕžJÿÿ™ôt¬H–ž¨Ièp°Y¤1#¯›„ÑÔŠéºÀ9­aÆœå`÷ÕáÁA£Ñ9Ø?Üë¼ÙsR½»Ò|Ÿ¯˜•€tÒl?Çž«cEªæ°ß/_82Ì-_ã¿}6ÝûÇ»vv¹4|ÂO3 1sƒ{¼^Áµ<€b+lUÃJ™Q÷qX„õ„ƒÎáëÄ+Á•ðíë&Ý ”F8g È€c²F]šY—úôm©Û…!zÙY˜ ÓŒ«Y±ô÷ŸÍ‹qþÒë?˜Ïé§î ¨Ì}+j9²“Å¥† ‹ûÌÓV¹d*§(%NÑ´úK…vÁ ~MÁ¹êëË̽úDV4s Ìd熟CF£á¨eÒž¦èN¦6>ÊeÖ×Å“*м§UI-—ƒ—)æk*0AöP¼‹Ó²èsÅb¥›¬ šà]SÅxüS®Nþ >ð¸>á¸:Ó3Q§òY‹2€ËÓØt/ª.ó~1sô Ööùd1}+fŒâ»‡øÒ–òõZºðP7ò¢ˆjï¼X6·w<*“ùØ©F÷©ãÔ^–PÁGuÖîÇmdß¹V7Ð'U/ iŠgákÂoS\†U–ƒ»ªTå²C äpM®00B«Á®F:IϲO®åEÛu  Œ!ÕÁžÉ2AR=畺þ)³Fô¹ƒÏÇR > Ê”ž¢ž–YËñúI‹—½¢ñÓ,»VXÄì Ä,PåÏ †Çß`ì¥WK®H=Vƒ?ÝLbU"Jd$U!Ù-Q·`·ØB¹šãž¶›Ó‡Q.ß݆DùHG9Þg¸8ß”<’«IF¦[TߨÈé` èK/’až®œ·´ˆÔ¿ ´›ÐíX¦/@†Ã²hyMOƒÒKq²k¿hû)Ú%8‹NÝùøÑ†¢—cgœ“…FÚ/·;Ü=r¹µ…o·^b~ø XrËîlÓÏÔ«'mîA¸å|+ÎüŒ»ÇæÕo ·‰/Ú+›Ü÷õÃôÜ‚Lðö~ÖG-ó°ù`6cs>„)¼Ü[VmÜ­/YV¡~Ž{#5Q|æAÈÛ­„ø±ÉŽš,vïVe¸Ä´ùpÛ²&ƒÉäP~¡‡­ØZÂyÀ¦¨¯šK` Å'—,M'Ååz˜d$*ðk/]Ø5WDÔótÉRþD­ñ›Ütˆg¨O)ƒŠÀªÝ¸ŸP‰Ú$0~½p”/uým…Úq–vÓΓž§Ö`n6V¶Á$œož¬€D#sMB/HeC½KfÜ’X >NÐÖ¹2æý;~ ÁÙ0¶ €òF³R,ü½úQsÆ.¥ŽVfJ~Àvƒ­ÍWÍ̶ÎQÏL»mðª@DZ‰©§./`,{08ÓÞ›7¯Þl,iŒø­ü+LQ›0ÿí蛃»Øn½ cI>ÚöÒO¬†Ø!›s.Õ.U  }E)ΔͷIª<è5ÃàÁ f†fó÷í¬ÇÞTÂs1úv)ŽQcé6¼ÚÅü3`ë}õæ¯;‡ûÿkþ~½÷æ-ýöòÝþÔ_[¼šìî¼y¹¸sÀw÷à×o^îýÙÛyûîÍ^x² 5Ù¾bš‡Iç “¼É “‰« ÈiÁª ›Ÿ=ƒgì<_BJŠcIͼ´”V QÒ !­@£Cý¦Vß ôÚ ;¼úV€k'Ã5¯¾ü¼è›c…AÖž(%øU*m´L·²Î‚}åÔè4†a%#`’Hæ~¦_YÓLnJèúÀ)y .C"‹£ÍŠb9ÕÇ‚rÀ0xºtïÆqÌ·-ÍÓò`’˜˜çæ´ J—ʵCš! *Ü(]™–ÝÂ÷âiÃSªC¯Ó>¨h†ŒÛmȼÜkª#§±ùÐk¾ê«X67Ÿ¹|}̾—Éõ÷n2FR:Ü3eEÍ­^ŒF_¸³£ÔâÊŽZ‹/"%—1ëÔ<3¤–†›$™d0Á¯3Å`SLÛûm¦ÎàÛZUwdðåúêîËà*M&˜X}>ê÷ Ñ0| Îê7/Å!y¯IÛ{:J&=“g˜XŽHôE<@Û9ÈìZïšF™Ê®ˆÇøK$>+R§LiBŠ“«y¼h¾n€×@2QjŒšÏ>|0žŒŠQwÔW²„Z~4‚lÎÁ·/^«.ß¾n¨Ý=z¥~}­~½°ÆIØ Ðòß¼^50ýþ&›cÖ!õ)¶=…Tô_Íã(˜Œ¯ $²­§{**{ýæ5ýŽnff JrªBȯôí èÓí% ;€+ïî.ý޶|=9·‹‘ÒõèŠ;ïÒrs(ŒŒDƒË cŒí—_ÃB Ô§ZÊÕ?‰ƒX­Ü°7TÛL ¬Ÿ@›N²BÁn5ó;›lRëÅ”Iô^’ŸsÚSúF’›£‡ÛšZ0a9áúÚ“µgÏž®y?ëOu2Â'¯?Z{JÙ Ÿ<mf$#„Ä?ÙÙÕÿ”†{´XîÃ/×aÎÃgúÛ/׿œ?ܪŽêŸ›‘:dóÈzø¹’ÝâÔ9A3˜Ã'gJÓ¢ØL|¦wk,5Q–ÆúßæA4š4¾¿¦ÌLôzcQ>'¨ëÍ ¹’ 92éTT$8ä²EQ¦(¸ úÅEˆuzÕŸ9–d ªÀ³ÎH…ÅGŠQƒs¬‰|Ô( †ª_c—.gFOòÚ?•“´Ýj°Jb€³×ÆIˆ)9Æ*Ô*ãR›dpšOáeÒ˜’ šñ9£ÉÝT‚¼ «:YS„)Ff!Œ/”H5¶¯,:ËLV+ ©J3%i!µ©%}•&èEÞØâŸÆ>—4½´ ä¤'ÎnsýîÉH1 àB$¿¡4Ø@i•€_À/ˆéžñà¡qóÂ7<Ì?–§Š”tƒ¤˜BÓ‘âž#µ(XÎæ¹#‚Å$-z¶ ø}yCBfºë§Yv)¨Ò»$Š{Ý]44]¨µéÝë x°ÁG"±9ryÖT¦)é^4ÈPHù}à ]Œ.ÕŠ (‡õÐᇭXm$`»Eiaw 6 AjÚyw’r¡=³X-¬ër ¥iqÓâûv2Pȃõ'“+šÔ s.;ÓUµ q泜åàù•l…£³ç/lÖ$ì6¡õvÉÝ$ˆçi½êUª1öt2%vKo(¦Oï€Âð¦ÅT¨ç{ d¢Câr÷z`¡'¡ºmø4êg§…‡¦b;.Òa)KRŽùŠs-µ½`b@ uÈn'•B–UÜþÔ%ÿÔèâƒUi¡.áM#Bq @–ØÍ[µjbª>¿3•#6I©aæ8ÔTañ¯°ˆz9" SdŽO©ŸŠ&ªq—Ùlqx&Hù¹Ý(¸%¨Ÿ†$_ÉØÙ¥·ß]¢ºéOœß r¹¥Q^`$*"A¿À#oÒËeÝt°ñ|?¬GÀdý‘î dRžšFglZ£Ã@:z‘þ„:û ëEpÀ£ãÈ£õ9˜‚Ë&œ ç‡K˜§¹¨¿òfä–ºú=¥SNxÜéÓô ¸<Õ¨>K{ ᦱþ]}Y!€¿NsÄŒ¨–ê–0Ã3ˆ£…œ³º] Ÿä‘·rÕ33èF8¯ìÇ´Aó¦Ó–DcÕ]yyÅú"*QÞ$G”îSu,à²C¬ØH21²Eü£â îÁFÍ„“Ìm˜¦½\_Ž>8OrsW¢ØM+gô²œÄ:oÑwo_6ù ƒCʧ­AQd‰HJ£ç“-“Lïäsã38ëµö¬ºç„| ùÖãT£õDzÍÕ4mRÕ‡þuéYA ×ÃÓ—Z'ç) *¨ÎÑeÞ³ûi—ŒKõèI,c+¸¾q¿s‹©’OÑCi?nVÃsa‡°†6Ï\ŸL=£ãìäD¦…©‡iA*Búuåq‹“q͸æœ9 ún; WÀÀQGbÂvE©mUü4¾*ˆRVĸŽiÖÇ’ÅÄÚ'”ßWsbýÚìÛ°é ÍÅûþýµ"œø»¤‰4T7Ÿ“§TÄ+ÞÉÊKSÉ?蓪"Òeú!±9Úñ£Azž DÞör*wüü¼½†‰¢O“«4÷¾Ð¢©LÏ_MŠ´cä};h§£ø3(íÁ1ìnûFÃÃïBûÕ>ZÐá%Üù ›•àpïC˜õ ˜µý mù^cè¾Ð¤³Ï6ß`wû^d ,Ð7zrœM§¢üM¸ ÑL´K{ŸÆJ T[–ô]xT;4‰‘“?¦íã …OÜXµ)ÛßÎôˆ?þòÉúÓ Aê/n²§ìwUXãþÝ,H[Æ‚Ôh\ FtC «)â`,U8Ì÷×ô†ÔÖ ®‚W1©P,´)BŽëöR¯ŒÜÓñ*‰E+›ÂÖmNåEª[î¡ gj#T}G §aÊ?°€è¹áÒ|F;{}»×è¢ô_·¼ÓÉÈXvloº%mÍ\ š±˜Ûôàò…ö½æ šd½^:¼O“ksüؤ#.º»—=áJv›êJŒ'#¸c8"Iì\å0Ožf? YØÄa@uûÀO­îe<ÌríI‰9lÓótrà Mt¾þ!ˆƒ)¼œ „”Åo¦ :oô(…J´£«Ím”ëúƒi»¯ã×¾Ö$‚ýßNO»lKÆ–=ÔmõS];8¸Öæù,Í0EÀÁµQ·šhö=¸>ÅXù1`ætT\ègÉÒ ÉUiL_mÐi§fÒwîH2Ë/¯`ÑTcZhõ9Ú¬uª+¡t (%þÈlc³r`ráPòÒ>ˆÄ”lÉ—C¤í3«¤ä^Vv_d5X¹\š‰bu 9XåÕ`ûÊq²3Á(õ€K¾ëŒHâQʱÁIó˜+¸ÀOW딹Ôq[¼%Ø?9%ÉVéâäâÔ‚5A88tæ8 Ÿ×Jz8}©€î›ÈiÅ`±ùn»:ÓW-ÎÏE¼‹ŸÏ}os}ôç5øßC°ÇÁ5ÎSmm­JiDÒøYòôéú#jò%»þ?zöhíÑO°ÒbÞ[¬v@Àø×_©w>ÌôOû½ ᢖ­¦ûåc˜ó“öÚ³öú³àÑúÆÚ£GOþ7à™¢§ØÇgyÔƒÔnT‡þÝ:¹ÉùƉ{)9 úò±sÚi’g9ù¡Š $Ô„{:I†Ý LO¤O º¢ëÇÿ¼12ÏŽ°ä]FÉm½n4!š úÙKŸ_ ô½hлŠqö©“ó3O•?[§ªV"!ù47Œ,æ•zÄ ‡ž„[¨Ý2¾;¹ùç‚Õi²”ÒarY8åè„r%8IO¸¡ô7ÄU^…<*ÚùTŸN”¸Ïw‡ó¹â"ÁX}L04Ç~d‘ăp:Âߤº-F²ˆŽì$1;£?lì/^b’³BÉ;Ï#’å²,wFu ƒ(Œ¾—g£Ii³9"+ኣ¢W"ЊKµîà ³YÝD©aA9'O¯,ƒÅUÜøÈ¡<[ÎdóŠ"¹è´Ðƒœ(ó(ùQnöwwŠYѨ4¬° ŽC”ÊÎ–× >¤W ^9Pv¯ ªntÞŒåNô‡7.怶Sôªß±Ìr ¢¬-ï¡P×"Ør¨Ãkb橚™ßKm©Ø³Úzµ’e¶-VJiŽV6åÂìà"ÅáF‰dz%ªrºÄD_‘gõ±ÃÇé'ŠzE¬Í½Íª)ÐÚj'às:UÀeyˆ«FµhÞò9yÙ^Ɗߟm”§M;[ïR† ª¨ „e;ž}ÄJ«B:„Z±b,uغ¼%3æaÚCß“¸V͆Ld±àöx£Ð¾Ì^2ïÕ` ¶ ³IÈ[+µ^ЮQ[Ú¬úˆU°½˜Æ-6¬‚T‘IÙ1Ù•@ ºìJ”Ïspjg:µÌ¨As\«–#ŒÊc^õeÖ+.¶ž­é\}[aˆîøêÃ'57²ÑR9[„ÿAsM‹âª­ :•ZÅÍä_¿{&³TÆwª®E!ô¯J,óâ Îõ†7<ˆ“ê\™x8[Ä#7±‘fY>î'W:µ<çƒ`Þ_s)K\,ÇŸ>0L¢_|q«3 e„qÚ°q´ÍöúÓ*2äòàAþ~éÜ*=‡‡-I䢖&$… ²ÍZAGÂñrÈÔìeüAª°ÈçÉY‘€zLù¢eÎpb[òç31±ør–fÍÒןâê0/*л·jÕˆ– ›z ¥e3öõ·ÿ2Ù.QúvEBy–ûªÃ‚s4ù,à Ч¤½~—k§zÇ­W®œŠÎÝuÍkðà5º :\‹bEó_Á|òtÚAl#3¡;³žšò>.SÑòñOÐCgE" È~¡—c`LŠÊŽv¾ÎVC:`ÖǪæz‹Û|éAο=˜pÅ÷üÌ?Ñåã<çoTÒšš´"´­-5‘[ä{ž4Hâ·¢›%Ù”…ûY• a œÂR>q‡C«ÓE¹ÅüÒµËÒý‚1ZƒÐRïÚ;A?‰àsq´hŽ"·÷ç`¸§Ïø™¶¶ŠÎ`@5UÆœç"“'ü/pг.•Þ2 0H£JÈjAB¬Éh|ÕéN‹ÑÙÙÖZ¼ö”W¨?Z_[ó·<ŸŽÁc[´Xq¤lÂÖšBŠÙO<\ôÌÉ? ?悽fÉ4ÕÆ Êå 5€i´UÒ¢ÇIiöÌ {P÷hªôdä`Åt…QLwh«YV¶/«“‘Ìšþux{œµ;êƒéBØ«Å<)ÚÞv•*qŽ5X ‘¨ðî‚;ö¤£ -Öe*u&'½^ÄzÖÌ»Eî _é 4ÕIG_ð\òîV™>ts£íc@´£BÝEšZF™º'ß@Õ …Ýi#”B Þ9o1[¿Ž(¦ƒ±ƒàTUˆà\DÕÎ23uX4{S+Æ–>ƒ³Ò&@ó¾Ï<\<Šy ¾¡X{Ò}ô¡Â3Á¯áq.{k¯Wßg.‹ÓümÑ©"kÎ ŸYS2eø šOƒ&X© {ö,µ!_MÒƒ+Ftl­ÊB3oUÈÕîOõñ8iƇ\ˆk,ì<ÊŒ%UH"˜g¼ø¥V(6쫌ƒå1Õ™ùUï¶üXMËçHzªM ¾'¼ü„=? '—DçØÆŽ%f]7œè±Óõ$Ά]gÓh›„Ý•2N x^³çMgˆÈŠ: bjÍš©B¿“[Q–û,#^d„qˆ~­Ylöñ/þês ¹¦V°a‰¼ŠøƒÍ-oà FÏ<­¢™\^+5BB\Än äDÁŒ^>‚5'3˜A<ÙJÒŸßíl–q44캮s8PGÞ{óg>šNJ “`ôÍÁžö¡‹ÄŒšÍ`Û#Ó ]¢Êz]5“Ì_-¶xó½å¡ÙÜö~ØXP…å³0¨¡À`ñ ¼#séBûT­&Ÿübô[蟗„õÞíI·bª¿õÎp„~ ™»t=X0õˆ©•](”v|K€Œéíhf×*u‘Vâ—n›9GÆD½¤Wf&¼ð\™&eïM±ö2å9xݪê%â¿’sñæ¹ð) U¢ðH]—¾ÄgøŒ“ÚÖrxÛzðeüä¬&|T'!´äfjæ]TùÙ×6ŸI»Ruse-ç(üÊeöñ(ïœg§œ¢Æ0=Ÿõõ½žN½Bâ˜D‰æ2Ýš™KÉ]6¨¶;šÍø¯Ý Ô‰ãv$óß`³¹ydçB]?‹`¼±æ³æZ_Ð1ƒZõ¼\j•Ƭª3—5Û„cZ[Û„òn—üÜê¢(!~ÖeQÚëÒ…!¹v’…IñèMzëAŽïe^?O”«§ê½¥Ì/Ìøº¶r3”+ø^y8³…¯9ýÍ=ÒÐ,˜™ë)g€ëœE:iàLÇbî'cc8· ÍS0·§[û@ GYMæ£Ã3ÞlÇ~dЋ„{ñ–è)qxyÖŒgÓbý Nš| s<ŽèýY]û g.˜Þ~æ‘);À?½]2$ÿé—OÈ}¿:€‹y®ï&ñ8}ƒ]Ô?Ý °}¤É=—^'ݪ×ïÊžÝLvAí%¢Õðu‹}¢ŒUÁ»kBaC¯€7¡«V9Pû«\ݾ Œá¾Æð˜0Á L‰Ûؽ¦®72Ã|+¸MuªÂ"í÷ ëäWŸQÀÚË©oñ“#ÌøàZ'Ä6#jR›lüi®º‰\¨~aøŒè%°0ŽãÕqR\¬#Æ@ØD´v:êdѶaÊÉ0_a–°tªCFàG¹ý-0YÉÁ`½­Oÿ+ù˜`‰ÃÏðƒYÂp]êèPxÖçÛ`ž`Äÿ†M†Hk ç%Tn m{SÉ·÷ã9†7ÈVwÝè9$HîÌ“YBv·tQ0z3¤‰Q£'‰d¼¡I½6Å[÷­åë?&>»‘Å£±‡žƒM ¡ÖC€u³éPr?vNçÖ±ÈëLF1\­ÑÁdØvË)WØöÔ}‡­é~ \½KN?1?³Ð1F;œý£«‹Ç¹–`/ÏA1™Ø\%æãi!&ކˆ¡@¾&5£¾æ¸Ã˜k™È2=cf#àK|ò;z)¸Koß"U×UÊŒjÂ׫, ¬ú&£­Ï8³7 È=WPªbóp$šz¾…ý¾œ9¿zK]óQ² ¢Âß8uöÅSWþ!|r¯¦:“Œ³3ô¢…uà9Ò=ñQL°Ë –Á©¶EŸVU@x¼V@xòpwçÑÞC- 7þøÑ&²Dy€ð2ýO–“ä_zü­½cÍçÛI¾šv€^¨ï‹‹ä«á@¾~}¢Øý8Ÿ&Mà\³a4àôm‡.ŽøËž¦("À95‘9“‹"~Í^‚°0=¥xÆmÙä—)¶=ì\±w«‹`òG€ Mfù\eoÉ1(ð‘Û~˜.Ræ˜L< ¤.^[éU-2€®숇]€aüÏÇx7®­KS^g8ûf™@Cù„°à͆ü¸âj’¥Iâ‹Qs.–œ½‚-TE‡r#aWB:6<)3[‰ƒk\¡y zï²9¸gW=›jªMA É”€øM’C£Ä¾f¯æçŒ|8ÏFœ 3Øà¬‚/Èe2n-§¢z²y¬¼üä )©É(³¼M̱å'æh#“Èi9†&)Œ—ç¨&ÁÑ–Í<„Ae²lhB•ü:õÇ⥶Í|I®ô•@ð¥9iNdZ\¸¼ÊĤ[Þ¬˜ˆ{ÙðÚΊÎ0†ƒÊ8×áhYÊà·ˆ½Ï?‡Km. ÂJ8ÔòˆR“F¦|¥)ˆI,%#Je3·íO¡3”Û¤£Ç’˘„ËÈ•¡.òD+ë&AEÌ¥F|æ¬\å(7„ú'{iòÑTë´g°¨•¦ÄY˜€' W!Ö_2Î=cªså0mrFñîƒÙ,%Ì_„À•í-¿0·BCŒÍ#‰W®™Óñæ¬ÜÂJ%÷ã+Ϝǖ–zÔÐ]IÌŒ|HÍ>7ÍBéÿ&˜æÉVgóNCÍð‚žJb¿,sé"å%?±| IÏüÔ†tWèÍ‚ߤµ$|A‡rñ–PÍ YãÙ4¢'Ê*A0š’XO UúÉÇd—­m…KØË@¼YÇdä?„o^ûDÝ_‚ºÚëÁï$Æ_‚˜³E)¹RÊ~DÆØ6þö”y[ÊmëÖÉÜxâÃ~ Ê<® MÉ™ÙL`Ñ‚EY…Ãë-«3én®·âŒòS—Ùïp8"ë]Upý’϶6 FáÊhõû÷H#³á·~‰hÄ‹-¥¼ tùÛ¬j‘ì&ÉoƒD&›ä¥ ²œ´“²t98Ïï³y$Ý Ëbø¨ùÈÿ8Áyj£¤Qš'—s%ÃX¤C>0𻀘ú·©tœßK$ ·¦”à·A*•H`7¤UŠ+$é¸+,ÿ¸<+ª&§dõ#„!m² &HýŊ5!SÿóQ¦úºmX·¹M¸ŠÂí©1¨(>Â0ülr–MÉÏvÈWO^b0lOèL6*…-œŠÒŽ‚ÁrœÎù˜4E8I%^—«I™ì]“ÿŽÑ±¥ÂWJ€7½‘`€E¬1JÖ_â†]Úd‰2­ ÙQœï^Z1ö hqØ”ilKŽËæ¨C£ Á~ÁÓ¿Z"ƒóÍžs5(—“檎´ØúѤ”JRÿ‘tÀµ‰=¿±ÓÀ©-ÜD˜VÒö)Žþp—3)5?´¯ÈOè#½¯FÂê˜õFÞP: T,ºp>ÉZ5£áGë÷¦Ù¥ÉtJwþI}­*ÉŒÁÝÅlM>møpŽíFº*j9ižÍ z+N&[ƒõò®ÊþÔ!½ÉBê6ñJ•èµÚÔÕí@RFY­¾bC©×Ètûái¼í,;¨¬ÐIÔÊÓëÕ¯Lu8/²¤^HBà 0¬CeBlÙD%¿•§Fjä`ˆ'ù€Ý ¨6çUwñ,_^G 9H7&%q’óÌ‘}2 ¡ÐFî1û›!|l)ˆný©—F‡ û\à_)àA…£Þô8fýÏx”ˆËBäzº[Kœ€6Å ”¶ £©ÕFEj6¾Z—¼º—ܲV¶ñ«Aç§1?…KY£Õ®H!"9pcë°9é3íÂÁÀv÷NÕƒ¹Îg¤kU©¶{UUU£Ï‚¹vš`æ«ö_|÷ý›Ãw|ù± ñÝá[ÌÕ}Ⱦ£\àÏ¿£_¯‹Ë ¶+|{¾‰$ÇTñÙx˜Í;/³tH‰æŸ}·ÿ¿¼ÛÿöeC\í¿&Î*zj€“Ì©ÂÑ?¤íf@ º0CzÖàô†ò<è Ϲ1£Kq›8K—°°éô³˜Ÿ”ùÙ$íAº{—}|ë”ï°SµšFkA¢Âæ³e©c5[# a#ºzº÷¥ëH4°5 4 ] $cn.É®pžM¤ eÖ᪳yGµ2áîjmšMƒ^k(ušÖHDg&Ôιµµzˆäë"ï´ÝIÛEFÄ ,*˃Ou0®_&ŒàÛv‚É|ÍšMºÜW¼¤W;ËÜ2Ã+— ’—˜µ–UeŹ2@U`?æ ¥W6å³K‚@¶ÅâúЬI°~/-a§‚SwaCó¤²á{‘€e5W˜(À—s4±€#5½Hó1Ê‹]¯f°×:É!`ç߯!È—´¹«Ñ:hì‚T溗|GFcº’ª@ˆ×<䚇àE.6ªVéë÷³î^@éfÒk\°A>»º®ž*Ô>PDhNŒŸ—dChÌß⥉rBùoæÅÕ0›vž³Åà¨ãwÅx8Jç§·çétZ¸ˆÃÊ ø[’®,.äùš“I›¥[-‹/@ÌH§öxÁ‚ë×ýš7ºz¤jWfú™q®y‘ŸÎSLon‹ö‹—¿Gm¡S†Xó€ÈÉÃWŒ?|äËjm+ˆfê‹ôœ\xþïy:¾OEî?½îFÀL³ö€á]ãþ2æ˜Ãn ï{G1WÙ»Ùm̼4ƒœ|%ÌjÝ®mÁþ^L$y^B1kXQ!0tö8íUŒzÄÓc Ü&Y$óôl ly> ×8•År1C†B®÷‚-þG´#œK)BéWß¡@¸å«0«XrׂáUX]ð¼Ý ¬ KN17“Ї%õê¿øø ¹AD;}m³1œ.sXf}ÞÇtšU c’·âÄåû+b]-}ì1UZZÊõ{’À>±%˜m©sPÌ;i ÆÏݲMš1ÏLrØ`s ¾ÊV?ÆwÊÚáWDö<¶l(La—¦b³l®$£óp S ĈɟOìÔÜ`:bS" 7lûùÙ’bNk¦ϪbYªüBÉr: Öƒy3?C´¦f¨ù¶p‰e¸UŠjw,ù»©dƒ`…­ø°øÓ!·'h‘Ù¿À#“”àÈ.¤#ôÁ8øx‡s.Öã"¨ ðx­½ N›À ”³¬rŽ‹³Ž³K† "ÈìQëõMf-Ò Ý|ä‚ÝÚn£-ò`Õ3ÚNTÛ0} ê”:·®ñE1Î0pnY5‚«dr]U r¯ÈPG Ĭ¸¤\WÒ„‹Ô•*WŒŸìC:@©!%|2á×€a6Cw%#îQ+@#€t±Ë^:†½L†Å€0žG4ҩͽáLGEc 0sÁÇq/ÙÍ:¨ÒÁwmüñˆ~0ßW•Êq#C¡‡T(Ü·xD|lÚ~‘â¬6¡±ÇÜTã°- ´²"hó·n’úIcpæëjÙÇ™}ð,…À|“§œzŸ‘〡3>»0¢×|-i6Ì–Eeï÷vþ´8Шh¼AK¾Zƒb95ÅÖÁû@¬ƒ×~ù Goψ²S pã¼ 6̇èL#þ†?L¡.0C@È`¸ä@ÎÏ’;sTÆÁGÊôTÚï]Ø<ټيcA_&=þŸÅxVœ8IôYú¨U ú¢õ>ÄÆjë0£sz_Éé÷êì“zuö{…xÙÉÛƒÃ×ÿïÉþ‹ïÞÞ¼´‰>±‡•ZAoŽlWÆ,R)ÆÅlº&,çÝ|ÔTï¶Fý9@«ä ÿï/a7¡]êG^5¨w 2«ÀoÑ2¦ëT¬b@¦KòШ–Š“êÄ^Ä–ä 6TüI$=ówïr‹+sUU¼ ˜Šq³_¾°¿EÂVø‰¶=xDôD-}::ƒ-nµÓ3Ö‘ô¬š:FWc¬u ¥Œ ïDH™1AýFÏ×vlmm¡F~œ§œË·2[ëî Óa¥­-¼‰Ák¦Ù&o´¶¬†¼ºIšwqþÕJx!…j`‡unÓÈ^DçÄÈÎå±²µ .e9Ž^›Ù³w Éã¢C(c£ïÔ#úJŽÊ*‚ºòØì¦¬éß0«Î§FJñ°ÄúÓ 3¦‹¥,À´#w«xI{‘&|k*uñcÊéU“ÛJ83ÞT:lmU¡·”Μlxˆx: ³FðwvýQsáx.3¼b›Ï—^6Mr:1-°-úœâëpJÔa6EVöFÔ’¹øöê*N ¨kXDäƒ:uUà k{eH2yJ¥¿u·èÀ@SÚfÄi]'‹%^yJ~£I2)>y)Ðù—l^SÉ£|(ËJåˆAŽ{+LÙ ‹RÆâÛL Êþ^Âi‹/µWtûX;¥“™Iès»c}£Ó{–ìܦ*‘VÕì'~E÷ìÆXàg¼j[vƒÐ"zª…ÝÛ@Ÿ¢³àéá?k¢öë=\üíðàK¡5•ÅOpí€J7ÛìuìŠ)¼¦gì`êE?šBaÚ2/Nø{\ä+{îϾÐÿœƲ¨>—À|ߤƒ÷5ÁMšqž£Âņ m8Ä1ÞÔ 7ï ÆzŒ†L<@¶™ GæÙmÃqŸ-5ʼnb¾†‰Ñ‰êù8ña“m!üFøÞÐk¯2²ˆ)p¢)¾è%Mœ4S¨` ¿ßWSBÙä)%&Æ“HÍÅCÛ³ñeGŽ_"a¦ÝdI=k)ñ¡PËL¯©æ$ô+ŘŒØÏ÷ÒyWúw²¢K¸g}@×-Ú>˜ óâó.Õ+KZøºióvóÃÞë¡ËöaÇü»ÔJÏäõš³Ù8UùÚiƒ¯‘“;8­ƒƒ|ƒ“;º =0kOîª÷šÌt_$x‹¨‡±áOXÏ÷QðQïF̺çPVƒëUðq~ÒO€îF´†7«æM/7dLLªêi¡¦9)wõ ä…9Áç‘}'§9\%+çhŽa}/ɯUìx áÝM²¤À&Y›£aUü t>2>èÔ¢õ@§Pƒ¼Ç6‘ëQ`:È]4;ëCçûl=Âu´ùžrçpC^°˜9´è8ËðTJ”m¯ô7Oä“‚üŒŽ‘P¹÷²â6CûSÙLÐfzìðý =ÅÕB3Œ‹Ä o.kLPe­éàãrZfÙÔší+›a­’"¦aˆQ¸|G­€©Yq%Ç;[4·«iÇ{žú«Hc¸L96,¢ÿ»Ô[ÑPçÕ²2¤½Æçå Ç$¦±;`%¢óÒ{Øqû£Ú9Ü‘¼SæKº¨÷"h ‹ŒæOГHtòÊg ÔÉJP(”&2ð~Ÿð¬«Q$_¦Weq$áñ¤¬`ÈÍWêËÐ F"ÛYEÐXVÅA+ U†q[Ç¢ŒäÍÄ\g…³ãZ¦ÚÜa‰(‹¿1%×`u€•ÕÙ$wò y¡ˆ‚%„(ýFÊ×Öèã)H Fv!T¦$ØÅWñÇÉöJa0HÄ¥f– xä]2ÁȨ̀™6wÃ`Qg¹‘:³Óí¢GFSzØò UGd×C¯ŽÂN÷óáuÄ´Îgö>BmNkÒã¬&ðàÚ0>Gùð˜öòîï=C)86¼ßYmU`IhqgbÏr1Eqå£ü~7Qÿ{} »RAÄºÎøÔás{X­”z®ËzÜf«…¹ëñ…ó3ËÞj­»½|)(,ênƒXd64æ`)+â7ã§±6ZÕ½²ä6»^éù»¹]lí8Å$''€f¶dßlN ‰â*x›¶ºeI>͇ÐZæ°ãÒàý¡`‰+漑 Ž™°û+»Î:ÚÞÊžØ'Mr* EÖ!ú{XQé¿äÔeMC~OÏàF)É 97kóº¸oîñ"ù½R–î0 ´ø©NKÏÓÃ^Ñ•4NÔ· 2D–A&D ÜxFd>ɳéM÷v“ ‘’NJÜXÜÝN§@»j‹¢O*TБÊ>»HdÌ$ˆÆ¹ôjâ?!'ývò':ÒÍÞ¢•úmÉ+G`|õ<ÂQ5C³ÌÆÄœ›M¯üQèq;Ù¸rÿÞ…6Šî%Þ£å?ºïçù-Ž˜AþÆ?2Ù+VáZuÛÉ‘%½D^!“€«Ôíû4ÏZ*ãHÉÀVÓHþPeãÐì¹™n+ŠÃñißÛdÞÍç„Ê÷0N7Q;‚"2ì3bÆ1ãE§Ã³ŠZlV«Ë #c²iæVO¼…G!®&¢ #Ú@ûÃa„y÷RkTWÅ,˜ïü¾ÕôÇ>ªCãPdm»jµN'žÚ©ŽˆHм$ÿ!î%üßèŽ ädbŒŒ² µ ·k$J‚ùBq£^w>½8 ¹1ØÝ_pØœ,b³[©Œ4°³{|×=уIùtTÀ1PÝqBmï(G#8„äl8wÉWIµ>È(9ìGZ¡pþì‹úðNÙïß™û±2Ø7ÜÍí8Ž»Ôñ êZ>·šÅÑæË„Šš¦Øªµs)ÓÔïSÕžQ0 _ùQ!µ‘^ó‹U}63ôY*Þ¸Çwè–—kS쯺¼Þ€ñ›Ô]çÃQ¯W?×íæ•jé¸|3P¬§—7q»×FêèïùÓß½[p²¿Ûu5{S°Ž••íJ%oa|µBÿôZ|­´.Ÿ•¶~ =g•¤¸Šä:´ÑØkï þ½¯ÒƒÉÐxUw_è—ÙÓj¹è ¤bhût> ¶ë< UÌž©ÿ´¡Öv}¢“]’«¹,éþ\S|ð²F íÏi8¨¿²ª¯AJ½g;>ûjS[äA8«׮:àpͧ4i‹%í@êí^»‡È1ªâ(*´¥‡þãóÔé•KëÄÝÆó6ß²¦¡±Tåƒ/Iªzë¸ÖÈ1'¯ï³Íq©£Œk”NzàâÄaj$ ä]8uª«› ‡žD‚­E;C½ U•¥°ZS„%”ýŒ#°åTÄWªNv‰™ù[¾’!°•Di¯æª›‡•LäµQí{Ó>¿WA—>MÿDlΑëk¥•_…=kZ´àpÓ¸ë¯Uµx_eÚD©¡û]Eë8”øêM¦ñ”k<5ª«ƒuúŽƒJëFŒÁŒƒÐH±<;ÿ¥ Õ¥ISMˆ „xС~´’è¸ß8g˜bà|׫RWš nºÉ=`Ÿ6ÔÎÕaó%Ž:@Ç&ØDÆ•¹Z¥ŸZ?ÀØ0¶ ?„¾¾Ï0U_ÜXq=‹ôéG_müƒõF,áåµÊ©U½jVšµš*®ìF‡‡?J¯/›Dz¯|*ͧ‰N7§•þý¤yJW†ã”“~ým# ¦–/8¬¹Ëò¯ C ÇI?¢­›ænzíy(U×*j."f×QîUòûè¨Ê|õwg+áWìwÖݽV&r¦Õ7¯nºê)ò-[aóÙ Ôjd²+p©Oᜫ* ‰TØðÀ‰àç—Wį#n‚à1%Ž•°ü«+u&“ùò:¬Ï1DÛâ2Ǹeaò©9¯<Ò‘bpS %ÀþåJ1,v„ƆIùÀUš.¾vÙ@µNyetO¼gBõÓ'4ìeŸ©kUÑTŸ˜®ÐÄĉéÆZ˜:BZK˜*«°©®Å0µ™½¿ „Ö m£G¤È Îz¿æ8È€øžRý¢g©[)»ñþ¼(ð 1íÜm™¾p¥ذ×lI4¢~çñæ¡I#h­:5>Úo·6Nã€b⿣߈|XÉ‘[ ÙŒìAhýa¯³ê­Þe­ÿ kV;¡¬u@ñ€ÆƒÃ ±s½VFG#(Ÿ8:‹D[y7SM\Wޝ6fôÜØÍ†ç*æks#?Fñª³ÍæŽ6 Y+Þ6¿zÚüêió«§Í÷´ !ýêaóŃÙd§¡yv†Vœ¡¦úW_¦_}™s~õeúÕ—éïèËäIíÿ²>LõÃX5Ë]À»W¾tÅ]grÍ—6í ©ïÑqÖtWÛAeŸ¯džG§Ÿž±{‰¤8j1‡ín¾§d‘¥é‰$‡Œ—Ûowg§¥Ê γÁ{ÊÓt@¿øwÓKFÿl{’ˆSDÓ\xü²²Ü§|ä¥Úñý‚”*[ù#FRl î™+*™œ/ÒX“yÓÉ@°¢yÇx*ñ–Íw­E‹YñÖ`¯EÀ®¤™uϺm=‚V×H®0eùôÂdå”ɺ¡;¹¥ÝTu„I©˜7f@ã,ÓD#‡Ù#ÌkŠM5QEŠ “ƒí©R ŒÒÑÆTeàTŠîÎ}Ëšø+Žçàyq {`z%ì2õØqlË‘ÜõwÂÉÈð>ð}æÙIøÒ[v›ÒÈN&â&KšÓ«ˆM "„—µÙã®,§ö-³pÓådvÕÊfØ™~§u´sìmo–ù˜Å&§~»Hç²»9-qÇ™X™FÍJh»:)Ýɧ#4ȸrµTg=w'I{ #àÞc×KºtûÂoÃDf¡»Ö9À=ì^[ˆ¯_=g ¡Ó¸­£¢j ToâÞ¥ï™Â츲l‡î0 TðÂö ^îU*ú—qÙ æ|‹¤¦¤ˆ¼éàê÷.Ç:¡˜X7 ÎñB_@ ÏìC@<œrzòržñJ³…î$ȃ&Ãüx|RŒ‡ú‚ èG†¾ÑyáóJa½Ôwþ‹ðáÎîÃî£?è>‘â]`ÂùiCƒI÷“$Wµu •ORq¨`/t¢ö ˜I´!t.­8–ÌÇ43ŒOÁËêüÇçWŸ]‘±†´Ð6#t0i¦ëEú; ¥š;­nõê&BÐÃ3e¯ƒDc«#×46xë·N´6(鳡YBu˜Èùà•uÇš¢ò°ÙŠ…Q5£‡E':¾Vr—ž:Í0'¦Áê æÆÄ\ˆ,Ÿ>~ëŽÇTÊß\ô·Y³E«tŒî «{Hîÿ”]é|xˆ §ùr¶èÅI³==ÊE1›!μ—š¬Ǫ 8à¥+§“"¸Dh×Õu4‰iã*2¸‚7`ø<ÏkÁ/12e©Ó–çqDV»àòØ¢~S^–g+ØqÌÃY\n¨ ÿf8§õ«SúÉ…®I·èù¿Q¥; î™bPu¢aKÝðTÁ¸C"¹–&~Ù9˜ QGîÑ46ÒôN@Ë›1 ŽªvEï* 2ø£ÑܱÎ-F7žµ†jc3wGuëð…ÕX‡hþâï«ÃÊÿõtXõÓª”WÜ•OÐ_9&J4Y7Ö_iÝ‹¿ê¯Öé¯VLX«Ïòzx›ú–Ñ<û t®yÓ3*¹_·ŸPÍ \«™–µ"ÓêÜM–À11µé<Çü`%¼MçóôŠÞNÆeFæßÆŠ¢&½˜¾Jt¦kš6”Ö³ª6æyDl(]’NÈ唫9ÚZ†x™ê p×fÙý‚J‰º"»™ÃÝÄô;1Žv®êP5ç ”˜j|šPATêÀñ8Ï?pSÜU-eГÍ×`øæ¤¤_Ͳ˜Ãä6©ry¸>}ížœœe ¼!;9Ñ  éˆÒ®Ï0NšsužÎ²¦4ÐNH$¿ZÊÐnûo¨zC¤µš·.­výCéf>ÿ†ª¤*UŸ"ý¿ªÀþmT`¡BÁœ´úÀ‹-ÒÒ9?Ú]ixÈWÙ墘6Œ»¯cÚúxáGÕyó Õ9RÕ™‰H¤9ì¢Î™5Z0£ñ“.ÃFZ²¶¨XZ‘?¸OÓ1ük}bŸ_5AüaM×þ¿®Z(†Ÿ§)ZÁðù²¨NŽ\£ä<÷|Þ Ó bÙtt4'&8‡ðÂ-×^BYGò`·®ëpÃ;$óË0‡¶õØÏKgÂZªû*טHü%¶’D8|?c,ðkCò7]¸4˜EK¥~%Ø–Cè:ž.E®µ†šÜ¢²²µ2•+O샵RDãØˆ«ùÂú„Ûvµ¼×÷ùtY,azÑ€>£ƒî+´½ÆÙÒ´í.:ÏØÎ,eKÂp\fz„ôëA;yÄõ±¿OÄXíà#cÛµ8|ÍÚªQ||ÐÛê½]ð¤·gåz®€¦§" “¯ìÎÑö _ÏÑú-]]ûׯS ˆ€2%6[a;y íS¹éÈr]âŸKд$¦ƒ¥¾ < x.•FöBYPÌ7}•âÉ*•ªU:õ$û#´™à“é#"Å›mÒÈÛ«´‡Jœe‰Äu:"ßãŽ:¾¨Ô$º¢4FNýMy:Új F™¶ÙAÐ/³‘ãÝÑ’š{!Kf|þ7Ááá[zEÀ™Jé\Å^3{° çAvMYŒ/$úLö—%íõ4kÌÄQî;%}ùùö‡—G˜¸þ8iÖj™GeK´Ä­änuJ}Š!G0<Ãq¬höaÖ¤aÐû¤B¹¹rËëÂÅáe(—àM±0nlj*»Pi÷<(±V¯QHý"5¿ò*C¯í9ƒUj7—:†êîHâ¿DÍNÀþâv§uÇYWuT™©»Ž…ìÞ~—â~šœ-‘ùȃUû»| äFe ¤™ñ¯t8†0z*­‚]³N&½‚Z|¾µî7;ðu¿n‰º-Obð¤ò/qâvÙž¾ï]æ›(úi©{ ð{Ð¥Î!îŸã×6¿´ut彚ÊͰv m#ŸZÔ [¯ÀïúN³Ÿl9û¹áÞC'%¦5±…fè׊þYg~‘#]%ÓLTU€Ï§¨yìH@I±”·d°ëG²7ÊU<~ñ¤oÓ7¢Ò6,™"Ö×ÖFÕàÆWÅêEjö&mÝ1²b­kþ@\`0ø¨¡ƒÛž?Ññ“üJ•I!:À{fW¯òlŒ|]RægSXã3ôKUlJ!ÆøÒd‚c6bÎTÎ#àâ„ùøL.ãÓd1Qò ,çsÔ…‚€xI©ÒîîðZà è0\Sf]tPGÏhÎq(hÁ k¦Ô˜FwÉ8òòc~ ä xjœ?æíUŽnF˜e“ç5’ÂÒ£«ôãQ½Lf•¼/¼±je¨¦ñÛÕÍVÕDCBMsx ”z§ F Ø1Ï|(P¤ä²˜Ë69™×°íûý89GΆT!æX¨›¨yÑc¬L‰åneBhãÞ"M= ›2Ú¶+´áÂýYNý)\Ä‹E¡#±> ôƒL êPD¬U…¨*ûŽÑAn:¨‚!†<Á\¤òHkŽJw‰gê šø€èÐ%ͱL¸³¸k(b[?;ƒß¥kÞ‹× ÃA&K »9Á`:“lR̯XgÆGÍ•ÈåaŽrãÀh'š(-gØç-y?˜úË6OË"=w¡G )…ãP;¸ Ós¢¤D üfK*™~«²í(Úõó† Í(‡i6W™#l³ÒÓîO»/×5¾«0åÞûãD.ŠÆ:-ñ¶Ÿ6bêe˨Šò~TQj?úùX¶ÄÑûc™·ç0|Qfè­Ð¶8×a·²¼õgçU8;æ/] ™d·Úd‚^í¬™ c§¸JP;&£/®4˜ðèE}冼»fœÏ‘§}.âî;Ú ¯ù3žÈÄz¢Ç ãÔ-Îç˜wS-b¨÷÷®ùɯ&•£ZQÓ+š'¡'+¹g‰£Ä' ¼-ý˜µ±¨K¸ Uz¼1 {~D@6èãÞ1JõέhX‚E²œ)Öx´¶N|œ¹•xN‹óŠ—˜ù¾~åØîIešÏÛÉ+‡Fg0Å+³N‡á 9^9) <(ñ2¿+Kub|¸ÊûÆð!ÊÛ±ÂwïîX޹ê÷“ _úç*K]gÞÖO„eÀž²q= ¦¼3ž¦lp,„U‘_^½÷ ›ëÆ¢~®ƒªŒ Äž4§ÁT× 8hÎÆ˜ŒøÄ¸žnI>WYQÜ(â­å3×phÕ勦“àÿéˆ#=·•¥³"A¾ ¸C².}Ûó KA)7GâB‰÷¼Îš¿f+}´™¼Õ&ú_XÁ0ÉÎÒÉm«¬Þ û¢j¤z uÈi#cѸúq>Èᚪ3«ñ{C—¤KÀÛn\Tˆd÷ ùÍUSdÁ&~±µM¡ËÓ+@¸¡]¾¾ùI-xF„w’†ˆë!3`èÔr™^m ­ ©ýbÚŠÏV4¼¿Lçgs=bÄMi>Òü\£8GÛ |²£»»ýçðc4 |Kežo]D°TTçÆ)Ì&l“q&¦0¼Ö= ¹rîô°0½ºº%SÙ•<þ‚Âñ¶ø:JŒET#“¸sš™{Ø1ûÑÑÄñ‚a;I> Žë×EÓ EóL ¥°ì)ÈMÉlLZMLª‡öNù ƒÚáwϲE³¡_5Ú‰Ræ²L¯A~RzìÄW |ZzìXÂ.İ¿‰’a;ùNïL¥´'¯rÄÝi4S~,yBIú  Nfø»;y_â÷ælžòýn¹N£ÍþYð»{ö‹s_à wøK>ë3£a°ÐÛIãòTÕ¹Ä3Õ?ÁBQ«}ÕÅvòÓÊu1;³o¾´ÝìÛo‘>%îR”$¢ k~ÿVTa‡ßË·6­5 2ú#îd” ׃7›·€*…#l3XW ežÒÕY-Pl'/ÒÉé0Ž@²ç‚rí—.¾yòÀà,|í$ï‚XM7-: ó¶þG—ÏSIM]6Câf 8Buwî©¥«¡úV©hew¹HÏ»Ùpyÿÿž§ãû¿þþ0ivçáà¬s:ª6ª ±˜QÁô‚£ÿ%÷WŠK~èþ úNdyÇ4o¸¦wº{äN£+áª/–Ó,ô'ýJ U>@`Ü2}šyR•f5U‘i ž›CÎÉWèx̲Q Â³Iz‰ÆdÅÄl–Í!ú€nÄT _Š>aª¶òóo”ýÞg3×Y¼è4¶;nE6¹ž¯Ñé%—.Ãb)Á­ÆÂÅÚˆ¸ÜCñŽêÞgj—?þÝc ðú1DeXåÛlœÉ…²G,Oв ¬}q‘tv5ói—tìGŽ\ÙÃËn¹¿rûwJC¤*ÔÜQ­×¤4õb³‹@æÙ©9‘”™Bú‡ OcÄ©m)VE6½z·ÿO.y.Òá­ žÉŠÉ¬ÍUF>iè&ù:A ?J-ⱊö9n“EZœŒ'Ÿé 2©ëÐ3(ÿ ÊWê’æ1FNÌ•·w º4?¬”%Z‘{ŸÓý$öØÕ ‚¥¶jckvã•„5™ÒE|¶?A Áð¦|<ïJ[€PÏ@ä]lÊÖ™âO” 䋌G£Œ´Ï«®f«qkw2œ›²žÅf:cT??µE¡\Õˆþ| s¿3üØôFüþ:N݃]aŠÕÃ.©Õ†MäXŠiçQCÙaF !ƒ…°èس… xÙ8ñóh‰G3<ŸÈìÝu°¸Á"4Sχf¶ý:h£tQŒuç2•[+g†qô$ŸVY¡UÕX÷{Ç{£‚¸þ"ÜjäÞÓßîêE¬fÀªö¢\N&éüJWsY7ÄLŸ óʸŽ–Ñ†S:?­ìXÇïæû|ã K¾Ê†cüòõÙ<b° î a6å¥õ§Pï‡7/zÉWJš§UB{+|‹‘@¦ ‹ Ì­ˆw¦Ì²äÅáÁ³WoŸußýÏ;êé{°\ùEvš^e ò_Aw?À>?ìì<éì>Iöv{;{½½‡ÿ›HÏ’ÿÜÚBmô¾ñš³ÆŸÖÔ3·SdU°VÝÿ«û·¾Z¥Ž "÷Ÿ^›ëmh¬²º[ßd0¼Ì] (£vrU,sÍ"yòYÍ›³k†…ØZœ+œÈMßR¼ø8(€‰ô‹š|-^Sp Œ-²sÚ§OŸÚ-ËbŸñNÒ°Ýn÷þ,]œß_<`¡°XÜßOX7¾“š’v,i´6''ÃbÀkû+id³+ôÆn¤{OÌÜJ÷­‹1?Ëåél^ ²²ÜÚrd€V°4ãÃù;á)Û²úW3v¤[U<.Domäú["`(]Òü.ÙçõÖî2Eœ‡‘š.SÔ½–ðŽn㪡X¨?a_£TƒˆêE@Xdgy”Ý 96@-³qé*ðx]b™Ûó‰.k¸´|…Vfé|À›µ¼BÎZ( ŧÝy6¢]¶Ô$y‚Èúc„ð¢(Þ/g„×¢À±^ÉìGC"·YØzþ•×Ù¸8Âá–žz«¢PÑŬ’-ÑFØnÔ+ËþQãå³ïö_Âû„¿ýñû—ÏÇ®$ãC Kóá Ôh;Ðüåäñï»F ¦a-çã~c=Åkܪ²…'w^=7„úVCñnxÛ+›¥“‰Õ]g°ñ¼½ñ {&SY?9ÊŒÆöBÈ¡ª6Aâh=’ÄTÂ4Xú:´¾Þ ?ÛÙÛó/×Ú… ’u~~¢ÎOMÒɃ·ú8ü¢ ßÝß$—u¾ÈÂ[£4çkD±¬$ýå`Ôò™qiß¼$7~úŽíñ€é‚Ÿ6Bëqë‰ï K4Ö^ ! ]ZÅ\ŠFXߎóQ‹Ô¶›uÕBô²4•”ÑÝ$Œe‹T·€¶ˆ èŸ>¢K¢+¯¦¡ÿ{4Z¹ÌÑäð&0n5ûfÌ”ßB8øèvØs8³k rIä´€r¢Û?†mSøìÚ\{se/Þê¦ÊÙ ÔÌ Æ‹ÞËÔ´T° 2FVnÐŒeQ Ül»p´¯ù<øè”pðž°\¼©ïƯIÒv‚œ5†ÔY=%¦ç—6•—nð]9“Žqf¬}%DIGõ8G;ØlîìyYÜS¬Åh4Þeb"eàˆW±“ªy7vé|hŠ‚'°{õdk”\bf`°¨t“Zƒ#ˆ Š‹)ýj˜‹7Cë,ÝýnåÖ?'Þ±& 6×ÚØ¶6QÉÊQ˜¨«û`¬yŽ6ˆ‚Ù*F⧪["ÃãU™^ý5M¶#ÖO˜¹fmýÌ=ãÃŽ²œ*ä®öü§iC¢ŸEoìÚvšOHß¾†#!ƒ¶>ߘŠ£»3<½Rd…'[-‚ –Ó¢@BŽ™D¤ëx4?1,wšÀȤ8åI•<Õ%æÌø Ž¡épŒFõéšAáî 1­b…sDÓÍY¯3ÂP¢½Ég€ AŽÀÙ´4¦ÅÔZZØL&ð*“¨¢cÊ ØOÊ.Š ³f«[B……¬(–ˆGÝ×Ó‹·M4±{ÖÑøÄ´l•ÜÞU W•£žDcÊ€öú-2¢Ç`$¢ÃôÂ÷™G„˜ñ}ÄK܎섞]¶ÂÜN¯Z3y´žA-éP(<Ät¥ÓÛ…u ¼¡«&5pO{XÞÈιÜOv×R ¾ÂÇ™õJ’UAù~²³IÆêÃQE !&PvÅØ©°dÙ¸€ŠÞX¥Ê¯ŒÅX$¸©s1FÄX÷ÀX³²ºcaX²c*[=/ÍqIðÙ‰ƒ…ñU‰Q±¬ ac…™„«–2zTö:q½BP|›5…ÀA1÷ŽAoR­–¢ZŒˆ~ä'÷p„üvF!(L…îkºÖ€*m¡Ñ}ýòðõ3Հ¿Ù|Ž!äfÝA1™,§­]õΆq6Z áÖ„uÖå§d”ìc`,æ=·L³Ø·4Y«€Ñœ&((°/êûHiû½UQ˜?ÙY«0ß{òðÑîãR˜?–Ç{v>Ø@aWjÍ_aÉä,ªÔåå¿•¾\[OA¡çF»ÖcŒ±ù=RG»g‹#Gåc“"•IÌô:‹Ö,K!è }¾$¨-8e)×k›E¨QjëÊùÚDÕA¼{mпJ*3Òÿ¼vÑx*µ¶"mö×äkXM~]°%P àæ€8:ÿñ4håÈyg@¼ Ûý¡±ôƒ `63Œº˜!§pf¹;êw¯>_=}6š“Ý¿Ú_Ýn—ŸM?c†n2_uS÷&eïgò7¦¨^È]Jì„S.°àçæų-Ʊ4>ÃÅN!ôyµ2¹ *¥òÁ6&Ç (>11-·0n;þÏ4F=7¾ëGØ v†„Aݱ༣‹5_¢H&+T ¨2šHÁ»#åêÑööýOú@Ť†^:L1¾#°Uß©ð–c³ïõÃËãQÚà ¤™lœŠ‘¶¸,=w;lžŸ.I#niYÇáeBûmW)†Q=ó=G6Ȱ3^‚YAu¦ÓBŸ-IzŠR"+OJ#”ÁvwYÒIÖGvð¯tMšˆCiù=6…Ù‹½I§£x:ÀQ"³# ÀÃQ` A®å”b#ªY¢@ÉFYIÏ%½ÆÓeó-–ø—^¹c-Ã0_Ž6aKgJvÖ3‡Öº4Kw• Ó¸MÓDÞ…Q’2¨„€”z!–Sꢯtý=Á@]"ËûcÕ%1”¤\ª÷Ü9"Nmèñ+Lc‚a#â29‰…'˜}êä¤Yfã‘hNpŸ­ÞÂ>QÜ»‚QÜúµ{÷ÛÜһţz$B4KÚ2'54¥T“މ;ý ¢ú£Ò€ˆs «À- T»½Ø÷j|ô;®C^W¶ —ÌM/¶·*C÷ZÖÓà…¸5º|•ŒÂ·/™ús£*ÐÌ`pOžŠƒ&TmÜk ]qZ2ª%ú¬§g»äå i~¸†šé*• '=œ¿#Õ¿c^¢ ׬uO•'¨âÊÜSPSd>«­ˆ¶ÞWšG.½äÄWŒE†ä)%௟z?é}ƒ›®{âcª¼Aép@¨ Åà—¸.Éì–%r€õ´ .R¡‚æªØh¥LAœ&¶@8ÝGÏþÐ’Tª^…­l'€¦æT‚@6K\]9"ˆôž’ÉölR7bP/ÜPÇ:ÚTOU_à4=SmOÝBa†ŽH©j´´ùÉ@ÙÅ.Åb¸+ºBV/J•ÃÍG‰¹ü‘ÛV©¯(÷YMóTÕ;ÑàÅCæEÜ.Áá&Š5ìC¥.:X»!1ðî0L¤Y †a–€W@ËN´ªò?Û:ScJVÓ e@hޝRœIêå×Þ’m° RW®¨(óKuÓw¥T,Ÿ³Ã;R!mÐñÈ ¬äé7í4k\ÙX×ê\ZbrÏ ^âç³N—*íl­<+¢p‚i¿çSÉÒN8®!Žä¼ÓÓ߈ Ò2cfI ˆ[˜F’'ÕÔÖ¹fàÈ&·W’(jÊÇD1Öku7y‰±ŽŠ³²ytÜ ”¾z¯’ìo-CaØ€¦Ào;™[ÜðB<¹-íÆ¨e'“¢\œXå"s×ĻýÝŸŒ£+êoQ8¦B‰3ÆÙEªLXì\˜)@|6Qnõ&ÿzyuiã%6pèú—ˆ…JÙðƒoW6¯Ý4Øö€•L!‘{QþÎÇ«¼OY̹.yxÆîÆÕ;«c m?æf#¯¶k„ãÕ7ª˜ù×,àŠF:D_˜Tdþï¥<Úñ÷ôx×½êì‡ ˜nïTç ŒÞ×R£!+ìÿ:&¾ñmù;¿ëîrn>Óèn¤ÑÚÜÕê³¢Óþ 2’6wöâZßéì>œ'w—I¾?¦Gø}·»ýŠ5¬Ñ6éîQï1†Ûá¿4´–Ú²›ìÖ9ð¹q67¤'¥ ¬ 6Ü!E]¨MÑÉ@<ÅÌs‰{7[ÎM:……¾n{´Vµ2å4JJFi§‚ÅŠh>üœçgç¶Ãã‹ÊuÆ‚HN;Œ‘=ô 9ŸÐ—7å+Â'>ŠT¢XØÕÇ{­ä8º0Û:/OÇXöÓ QI"ƒ5Ó¥®s 1Não“ Estü€)9û9ßg^Ík\ÁnÀr¿Lge¢—)é<ån×hK¸1¥Òl²Ã/ÙéjgÐ|º¢ènWG;'ïñ.íÄ Yµ\Lœ"v^¨jUÞªƒ¢ýšù WüÙZÕrx_é«3âðd*²§(…ÍÚ•7‘º<·^]àJ°nð&’èx©%ÕÞ¨‡,ß©Ñ3×9lB9C͘ÔX¿¥#å|@q`I4P ô­Móù u<ˆ×]>)\‡”èUFy¿~^åð·ùHzÓcj=DýÆ _³¡%ÒZwaìÖ ^•ôŸ½xfø¶ Tw>4Á5t\8ë–ü“˜4?Šö“u3QÝŠ][>„…"ŒÈ/ ÜsøJLg+\]JïmS£ZAt2~qÔ ¸’¬+0ÎC. ¼="Ïi5Ì0ZȪLW†Èc*`tòÕUÛJ¼¤‹w®„-ébf“±å©ò‚vNºùt%­–¡m©Ó&BÝ6pÇG)¹šîÄ1¤²ŽRÙ.éâÇÔÁ8I¼ÆÌGĪuÒ‰ Õ‚þÛìšX¹*Âøk-ª"–uý)Ëf iÕå-kŽ"mðsÊcW‹|8ò¬é‡ ™Œ´Sé·\§Ð‰oJ$…du$ÏBŽÖ€ØÚ´ª»…nCŒôÆE1S`èî¤4–Ä.OOc’—è”Öðq·´W« †§a¥cšE¯Ü „ »œZ–ot¡rÃm› ‹Ss§4Á²5: ‘ƒÜ¶ Çõ~?()ânÝ ÑiÙªÈÍï¯CØWÍOÂqª¨:Þ¡öêaU1—#ihbD:šh{£½P©(–íÁÐô'¯|u§Ç›Šh¥UY3ä­lã:]“£`1bŠ©BžYeÅÍèdrÕJ•’bŸÜÔȽt^{qö™èâ›ý©×å_Æ»^7Õ êu×(‚Äå~37n*JÿTìÓvofŸö@?|¸»‰C70ö"b…Ú›wÏ>בûNˆ?3Ø_eðýk˜¹n6즃îòý-Z¢‘ÕÈ[ùöûžÉ´ðŠ:þ ;~¸É™’¨ÉglD|tÉ€G;|ÓFÕ[3Ŧƒ+ÞÓSÊÏ—MÃp»ç&^ sÖ½ç4'+dXK´qò…¹.§ºþ„«T¿â͉̈óœïX‹jor’r‡0›-o£í˜wËÙ ƒØV.m”YùMŽe¤ërzÀ:àÆÁ´ª§Ë³•ÇÅJmÆ2jËÕÛóI›=páx\æå¹æâý$ê#(oL–?b( Ty’5°T#Úõiæ­ )cd7m¹\ñ|AH8n˜ä&hºâG3Ù“ëÄßLídý¦­úáUn%ò–üátƒ4Æ5M9túãê¬éÄ¥«W··¼ß¼£díÚ Š3¤ŒÃF:þMrÇŒ§åã¥ÝqÌTþ»«“ L³bY&lEžÚDSÿ°¨é6õ*Ät”ãQÓa¹ŽyÈázÒ‰Aû4äH¢Øá­~GLAí€UhÈy+¨‘ÖYŒÃoJ³nÆÝ7‰ Í^ 5Lê„Æ±ÎËáÞã™AePf[ï"ËsÙjÅjÛiŒÔw›¡‚Û–«ZˆÕܰåxí3<ˆ"-_û[· ꈊ‚¶6dO6ÏñÚ® F‘ð»Å•ÚE’9ûÖ¾FªÐMW4Q‰G¥÷–@¡Gk“Å0âþHÚ’ô19éI´È uÉØø¾{2Ì.º&~ï¿Øß& Þ|J[˜¾PÇm¨oÎoaÜ-7Sæ:"~’l7¼‡©Cñp!Í}LõfïF÷0˜€¼=ÙàDß(®î»ýo_~ÊmL¢®cþ»(³Ùyò|žþ’eÉW?èË×0™ã.°ùÒò–#Øø©Wåß)”*GâŽG*¥ àA RZÓT£‡H<°&XWïäÛÃ7+‚i„šø˜„,%*8ZiçbM® ´ÏaUn6!þ5ò¶¢F®NˆñO=Ò Æfñ#5¾qÎ-Zb<æëŽ×¡BÛéLŒ®ÛRYöoƒûPz9ỳóÙçÐÕáI>ìï>ø¯ÝG;{¿¥wøàá“;ÞÊã'vþëIʚ؅7 †üiJA¿ï_÷~Z\NO|K£À èBw,[N´ÎAWü_Éjb´.n` ¢M½hE’¸Ø|W á,ÙkœÂ_µZ^À9/eá"eLø¤ÀrŒ¢_>®œ×7fíÈqÿ&š[,“t!ad•&Ç@ÓáÕZžDαۨ›)­ä m‰aV]f‹Õ±„¶ÿÃIJ§l]<,› ­Ž†e Þ0ÖÆÓïÿÀq°hð‘0XdVálœ>É`*RZ"²KùXÖ°ª&Bu#VAL­ÂÁ#0DÌ„b(7îeýÉ[ƒ&V ‹{æšò¨ù±1Úiôvñ^yWþ>€¿ÀÏ4ö•0žºøžü}(Å¿©-¾§‹?ªA+"}ß4—ºì¬°‘ôÎc!ƒ á°•’#›ƒ“¹¸¦&©_+@qÔG'ÎS9\>úO³fÞÂ@¿A3„k$ÁeXP¡*å=X+å=~ðÄV1RÞˆ~7òp­“òTÀìs¤ÂÒù`pæóv)Ÿ’or¨üUyúõ ,³.P·I6>ŲÝtù4iNò)²lCNAØs»6|f\$m4Ÿb‚ "u©ŠlÓ ¦ëØÚv›YÓ¶oQX@2=Ô–iäHÉFG™üO\ÂÑ­8èé/W/ÓÙmf)øc6Ƶ»ýäÛI:&9›,ɪœC!Ó¤÷0!ìÌ(Mpš­ÆÖWf À@¥€Ù¤’j‰ëK5`ªºlöG. ï°gŸ™gÔ`%ã«?Ð¼ÓøÊ5mM°¡ótÏY¶ßI,ÉýÅG~Íè%–É ~¹"¾¥ƒè@UÙ¾M\KSÀ¼…ªat‘¾p“Åd¾ÈÑ> sLü覌ÞqÀÙÆ:Ð(u ã ªý/t?®[»I [Ç6p•ë… ïDØVó×u¢Yé…¯æ‡': »êÖ;Š=”c:·ZZ\t•»(r–ØAÔ/æìÅŠJásê0wr1ƸU¨c_p„Ã+1šýGÐâŒOyæ:PTE8.a¹ˆ4=¥_'ÀbNWúK>•BZv¦`û°Ë£Ì_J÷§Òc×[ÉÜ”Q–Œ E (Îêà½4k³œ8¾ÂñX¿5æOИ€07%ÀX.î.ûØòÅ À°çZ~>½|&ÑŽÌ=›åb dïa?²2È÷Á±©F°˜Š.ÓéX<Ñ Y°£f¤0ðCâ÷O×Ó±Þè< çÅ*4\l%ÎuD“!ê®ÙÔ¼ag"™ ¥Š¢ðV†t1ðv„‰U¡nãM9ÉÊù*âfE²O·0@ô+Þ_ÛÅ’nE{£çQºæ² håµK¦ åÌ,«`¼ªÒCã`…¶ÌÒ[ëR ,ï·ìÚ¨r/E‡ ¥D›Än+RM*¤yA–…ÌU(Ÿí·a9¹@«@uÐ8 Æ/W<]ƳQ›±› ¹Oÿ¼ ÐnËžRòh·êS+'TÓ6+g ó½žiHP#r±€•.ð:õ•þù5CÓž´.– ØÃ¢¿ƒÀ2&AºG‡*±ûOkΖM™(c·S–|µÏC»¢µ]¦Š±Ž»Â}ت`$l—èZÀó5‹y³­G¬ÊpÖaÚª.šåÆÆÃƒeosÁcÓàV{P¿äø¶c ÆÝ¿õU—¥á~lÅö(É3z°0Çà%Ä ´[ÒoIþïÛMæéÔÇ**zÿË¥ÊgX:—γD œJ Tºoknî’mál: Švvƒ²¨ieÇ]@ŸFz ‹=:;Ï~?žL‹Ù_æåbyqùáêeîàSL‡æ²Å÷ÏÆ´dZér~Y N0€‘®Bp]Ò°<Lð„k»˜/=ïÍ è͸›~žÃ,/–Wš¥¨ÔV+R­þO·:µ“V"öûÁÆàØÉ[EÍå dQ—;UÃó U¯BYë¥ï@õ=ãn1±W»Z'\D©¶Î•ÎJ*}aà“1Gÿh59Œ%ã¬Áb˜‰ôÂñyȶIï»èÐÂ;ì Ù¶pFÙ:H\B`9;o$éÇtmªçdI”²µ÷oÅ£,ñzP­n™eÃæîÞƒ‡·¼ççËÑ$a3b~iLúÍÓ£Þ£ÏòÞ¾Â7½Gðµtô [ÈO1ÓƒˆÃÓöÙ?†M}HW–[c·uô/ÙÕ0#ct T„½ê#l ­÷0q“I l'Vð†¢÷¡F›gF ââ¬pNjMŒ’„Ô HQȘõGãŽ@þŠv#}^7_Âná—ç´™[?M÷ÆÝ‡;[ÑA`Ö¦ùêaÀ.<®È>\‘nÙ‰¬D">Np¿6’„}îÒ8á9×+IHÀÝž¬ÓÈ#SJ,!¡þ3C=ùWÅâЩHãuTˆÓ³Ùoâ=r‚Úä“ò´Ì”yn9 N'ÂáÂ/R,Ú5çR‰×ç& Yôh2~ûF¥]zTèM–鬥?ÆÔÙõÛè»ÉͽYÔk7Qêá>&S˜¼ä]ê**Ó[Î0Ät¿k{ÇQPxû؇¤ø¨wlz5}J¦†¶x«—LÅÅÍ=£ÂÜ|‡qQ;6Y£YèÊ{d4IÞRÍ|½çÌÜg—ÒÄ[¬C'•ÖeíÂlx²ƒ6G«}|ÔC£ÍîïîN}×6WÀ¾·q:¿ø‹¦=ã–¿÷3:UéÓ¿àër§OÿýÎØ/Nÿ+¦eך–=z¸÷àÁîÞCmZö`goooÓ²Ëì}z#¢¡›ýûe=w&¦À4)%¸ù% œ¦`Âà=Le׋f†ñrí÷l2#sPù]”î[w†¶hU'spÛJ€ëc].Ã~R?§iÛØGãhâÃÄÞÂØ7seÁË,ðÙXª0iì»Xž§óŒ®¶»7nR0?Ý’+v¬½¶ðQÓ%æ»åÖPè—¸ä%šŠà45J‡hì ÄäTM¥ŽÓ˜‹¤OÓ”Vã  h»”©˜)ðZH}½<ž*ýÇgÚÿã÷/Ÿ‘žð-›^äsã/S…lW6_4wÚªð‘ƒ‹ÄžËØÅ©‚Tٻ٤ÕMó“Ø&üF±¼€PýœÎ1•suÒVƒ¨¸€-R0[t³Æåi3 â.eãaÛM抣çÅŽe$=LVÑ4íÞª&i…ù„½h›&[ÕþÔ'í‘Ã/è¶]AøÊ$o23£(û ¼(Š÷Ë™ø!ü0E› ®Ð,×°…¥Þ·­dUêøF1§ÀcH‘ v&‚¯¹û"çØTw5_ÎÁr¨z3 {9í3Š¡[¸”þeÔE À&í¹û …f÷e0¬5´u¤—гˆxØ4 Ú0êèÆEIn3l+ ³2nÄwÜ<ùu‘$À¤¼0%у×|ÊÌárYebÌp˜ÊO_Þ{‘}“/×ÁQ£3ÃÈ ;øOG³øÒìúñ¥ºk1¬Zi–ûèXîĽ̜,„'+ˆ¼D$laFTO#ä¶N†“Â0^ÝÉû!~WÅ+8½üˆ~-¾èHÞ¦~H|idÁÃ3È´ñ,1jK:‚ƒ(À1q¡ô ú+äÕºLnm8d…näPq+øÚic7> µg³È.¸u#âž ŸFCÒ(ôG|¼¾ã­ƒ¬¯$âGlùÑ9-<þV•nJÅ Oùè¾~¶q]èNX·µN49êaPÄó’°‡'VÃisVát¨$j_Hÿ¢É‡ ®#*sÖKü=Íè-~ÒW¢z5ÞÅÞMXç%hèŽh2,²rå1¦> êûrF‚Abac`ì|Û°eõ|Lut$˜²÷"¢¼qå×ÞÞOÓ;冺c01\ê×èaL³|¿ô:v`qvWrG&¤å¢‚ç]Ò÷5?M-s)<Ûè!ú€ wŽJH –DØ`(5Ï&ÅEÖ¬!=£€WÄ ¨;§Tÿõª Õ.=wh'ÿýðwƿɡêŸ0ñr;yñÍø$ÇÅËløö/ðFÎßdg˜¿ž—/^¾ƒŠ3Œq:0@ÌïoRÌÉýò‘ü]ŽùF0ÍQ–¶ÃÔ w0R¥·„¡Ÿò ¢ºûNU¦Å$OÇŠ{É©1 ø7<¯¯^Á¿ßO3Ðëté!¼ÞóÎ@x=Ïœ7÷%ó‰¯ ™´å2 nTÙµ9=ËÁ‰v#0e¹o¾y+zYÌaÒß<{-óa1·P`)_A[í„{_lyÌö[`ät»ÏpÇ °ý·/¿§æÙY5sݹ???4þ\À`Õ¢$?æÓiqÙNþ7›oÁO^ì¿}{øüðÙþŠw¾vÓ4œžºÑK6aÏ]Õƒ‡ÝGÑJ ˜£² È©K£éëÜÎI´žõ¥îš©ÒʋɚZ0cºÂ{´‹VAפ.¿Vå@ÂN‹V˜ÃÈ`© ˆÊEóÆe(ïûÂh@»\‹}½.Vfí·“¿,ól†B¾Mv;ù_`°­ý7ÏŸ;-„—DÌ*)°ÌÉs[uú'<üŠy«R²²“”ÉzzQ™n´#²rEJö br(„©a´—ÝÊ>¬2׸Z¬cêÇ«9°þAN<Åmr€ØhÞA¿qïѵÏ×\=ú)î’î¾îA€¼º|£«Ë'>Áö•ÔPê&kt0“†¿bÇH÷ÄÂE¤­Ö3œü8< §WçÆõmÌô®Ó7X]‚D/ ¦G]òN³Ëhâ`Z.¨)ë5U ð#(ŠÆÕ'1A´(Ž/WúÎíèŽ&~´ú9í«c&ƒ ¶Äwj£ õ¼ez2 ¢Èqdþò#QP*þÜ&®U?Ä žœàÉÄv’N—8‰ÆÍÔúÚ߀Þ$ÀQ4¨„ÃóIŽA èF”(Ûæ|tôôÚy½Õ©™Y~u¤iƒ4´eè/ËS®½fel6rY7dšÓ Œ·»Ì„®]bÒ1çè´®^qŽ_H~ª.U3†ÿ½‚J‘Û+ç£cËÄi,—ÐDZ&¯~xùìÍáÌ¿Wÿí»7‡¯¾‹gÉe ¤Ç_‚·ÊÄß}›%b\s ²i°,¼­q¤Ò[0’'&“äÄ(å·LšVÝ_­:'Éç¤Ìˆ?Áû?ÇJö© ®d;,J,ïè"ônÖ(Ò1isÓ§Úà•çÆ…´(>9tà3à}A†k6ü犉@;¼ƒ³”°ÓçMÛJƒÚn$K^#Ÿ€ÖTiÅÈÓ®ë}WVWº^-jïŽh»DsK+Ž)6ö»•o,Ž;šhMå8L†·‡UdÇZQ¢¤ð³™ü‘ø2Ál\Y:¥ÔªÙ<÷»i›0Ùp┣l7Ålòú :˜íäynƒ–ZÚc}óÅ Þ·ûL$W{«¬Š×2Lfiè~öaÒªâ®CO9‘Éö•#ÒK)/ë“=S´B`+hÁšØ5Îܦ9T f8KGÒøÈ™Þ8GUBLa x “Ùf‡rTqÓ¨ÅuM ½YcÑø‹QðL‰k síð–_0ÀG>%§@8´hü$qs°jvé›(ÿ˜Zµ“dƒ¦Nß™#c; ’ÙèO>rhvèëdY‘¾mÚÃ)r@Zñˆz€XÇìõêìà&5o"-ÆP^BúÛþ â#ï“ f…ùpä>8ˆêsxs—̘Óõ©+õ(¤d¦Ó[ã š;lCÈâ+a {ÏŸ(ó ÅŠ[Ðøb@ÛéF+Ý¡@³j­ìºü·2CȬùø¦«y×=9_¿yöbÿÝá÷¯`&Ç-¬}Hœ'\¨¤¹(,itr™‚,Kæ @šßÛì›Þ=KîtìÌ]ì\5Ã>½ ÒWÓ§p„aï”×ÒH£C˜ÒAÝ}Û®+6A >†›q¨äñ.š©ŠÆOô²ß5 ãx ã(BV×ÄUICF—ä‹&^_””Š•:ÈÞZpʇÉц¼ØÇ|¨UšwŽ8­nVêl¾Òq 3ƒ ¥¼ÏêÌñ”6Qœp²t|(©lÃdQJ[ºî<õÙvÀh¤"—SmiM³ït›P.ê˜{íº úÕ—SùÁ=·?Û e8¬†Ÿ±.à’Lß6^ »Oë/I ÈûT0áGa%&‚4n‡av*Œ‹BNÂ5~š~ýíþ»}}ýQ.ʤ܄fp›¸©ï”mÜÀRx²8A¥¦}ÇfÝÑÈõ¦.ïþHeà{ž÷ëˆ]²Þj=‹ p3&,‹]d¬ ë‹|P;!þ£%2€äÈ5¾Nõ‰Éª2©$O«^›idãÊínìvòÑ]'M¾Z¤øûõ ìN–Ýâ Ã{ËAw)-7àÏrœ¹`~© K…’ˆ§27ó¬iý|RvÍP1š¶ àÌœÀ"-ßcía^0®%V<›Ëɉe>ÉÇéP óŒc€WNÞš}˜Áb`,\8ò‹Sœbhi !ÿe™šü—óì/Ëœü„eš¤?ã<_Ñ„UÖëÀ«ˆf3ãlÂiOðºNMÀËt€K ÙŒYÖcàÑp¦å%ñúè]W$ŒQzklPZTóÒ”2ª,1ô*eæ “.cäWì­^-Rÿ¢C'&!©Aµ÷I–·Ù6íäYç¥ûIs˜2ö$)àôl+=;ŠHu‘©²Íïö÷ZÝdŸ…ÔR½+MXºÇ#„ÄÈÎòžá£ÒZò’ˆEr¨P˜V¼eï A8™â}+ìýr–b*ù-´ÑûS8Œ„£f0~½Þë4Â=¿JçE™aÔ×”Ùæ)êM f=)p*·IéÇÏí\²R€QN›”7 ÁÀ×6é=ÇÁEøÅh‹´ÁŠì ‹9FÌ(5LÕf71ˆE>Lól„wž{ºoœ^§<ÏaðѮ°Ì¤6±¬çÀüvãíζ,êÊÞœpö XÛ³ŒÕ‘äÏ?ä“å¤ ²)*âT‹äD[n£ ûe¶2{Ê&H«1M-OƒIçCxÎhbinD½D,…1/-’¬-"UÍÂÒäÊ$¾üøêÚÎ$LtIÖ ™Æua˜å’pjtc‚ã.%Ñ i€¹´ÌÃÖ oÏl|££Q+3'Q9õÖ‡!“viE ?èJ@s?̦Ãyq†ÂQ¥y–9äê3ÎiÅ©.EiÁ; ÌG³°Ï©¡-‡¤YÊ-vœ/?@`zùq#W ðŸWð[.IFäôrzµ5X²ó4Qw×GÊ4ƒî!³`éÈÔ¦ ±I¥‡ï°J§.œC&M&ùRîB'ß;`e±œdpœÔ%£îóò£O Hš³yŽ>TÎÆ¶½…¨Å÷[y² BÃQ3Ï?´Xd=§Å8V¤Çˆù¹„(WþÁÜÔ–V)›M6Ä…vë‡Î@®;˜¸Â˜½‡¤c+N£ò±O¥Ðx3§éŠôŸf@Ñ*4Èb€çͶqë­º£vÒ–ÅŒÛûÉAÜxր͗Íx ñQ+a'r9¦‘–ÎÙÆK®¨âómÑ“‹®äº©4ªû*}ÛòÆáÈØF¬.s h¸½EÙ.¬7]mæèžÄx|EuôOò%üÖa’[ï^¦ Rm¢œèèÿ*Š­ô"^ ]´½²[‚&=amyìÁx)Ik:¦dÏíU<ÿ²$zìŽGS˜å¢ê8`XPÖe©á•]yÖ iˆµÖBùÉØÉÃ[)zFy?ó02€ð’2Ÿ†&þ™ºørŒ˜½e,…'¥&-c´E ÂØý¢‘f0ç³B¬æÛ?Û¢mÂ#‡%†r,›¯›Àkâ?%ö6…çá*¾B˜>G§˜Ñ@dækW¼ ŸÊa^$tµ9¿ Þx²tÇmSÞó¥d`@- PR80ÍžÉí†H?P w<°Ã/Žyùt>4±óOSâS·XM5t€]ñJ:Ǥ §«oCyG#7QìóÅÖ9e–3<»û(‘Í>p„&Ê."`.—V/É®pÌØY¤P¶Nd„Œ–‘’©ò”AR RÂ-V˜˜n÷“#NÃ5j9û££íäÁq;9Úm'{ø÷¡û»s|¬ÔPÛ†11Ä]ñ®¤!¤QÊþÑ'9ñ4òº©Ø·¸ü§—H®­ÚÜkGT7³Ó”–Ú6Ïä°^H)™ÈZã;¶[ÖpÄEŒ'ˆçUϯ ²0KÞ€&˜è5‹<åT‰S\pxe@#\¼$ª°÷m:–‰»oÉö|k•Ƙ˜¶ jÊ_9©+N•`„+ñé§•ÖÊq³ÏüŸÿð,MÁ£l¢ýëO0õÚ ùbóúxäõïg/ÕO­¤i(BäÕøÖò:㪂åÉf©j=áHFñ‚‰Ö«<ÒY¾±¶ÃEœøRz×0ã”s“tE·¨'Y¡¡ê¿bc†ËaîâsXIEÕbìpéHˆØ*™Ëº[»™zœrÁé$j®oö©lYGjÚNßH_ŽërúrÃ61cê¬p,ý¶}Á*Æ€Œßbð]-t1›ã4ð}mKßsJ s0x¦† (Ý…ˆ Ú$ÝÕw†¹w9xñÃÛwÏÞX@vÜ,Ÿ§‹Èè>m4*б^Psû°þúM±šÍT”ä†0Ù.d|€ŸæÒ$ѶÐ’B‘›iç’÷õ1ªdßíºàtµšø(.kÈz³ò9ËÌmš€øI×p›ZpË\cˆã–!0y)AÜèÓPÍNj&p§fvé7ë×C ûH ›ñ  æH‘àúºAvºP·Ü³ûhá; ¯3".ª¶ÚÀX*1qˆn‚ªzaýnŒ=ÒM[¦@p²ßÖ4ÎÁIin½¡¶jº"; Ÿ³ŸØ!T—Pý•£•°V»ô¿0òvoïáƒGë™l²Ž×x¦”/I©ñ ÿ°LÈ?ø‹—Ä>¬)˜ 3¡ØÆfŒQ ¸‹ÏV‚«K>MF@"ʰäFÊ0‚²Z!–¬UˆÍ”b5Šû\ò°ÝT1–Š1ò9Ê1ꄊßš~Œ`8Yò‰:2;”ª6&¹žÌ‚Y¡+KVéÊ æ;Ó߈_DîoÍȨ©­VíĬ¿©»@Ç‹ŠL_žãa7ë<Æ;ñ´ìïtAü7 ŸŒÙîZ0åÅðÄ©LVYXÌ3R¨†;9ÈÞMXësæ®qs»Â×(Òw¥§BÒÙØ¯ÝuFcb+åOdÏ»fâž a¯*´ˆÀ©´*€ŽÊì/ãXë …%ëWºÄ‹Ù•a@iÜVÒŠ!ÝS=Þ0ñЕ&#(Ó”€š­o? +:‘nhDìÙÝ:8G¶‚½Ìí~:ÍFk˜e“¬šFÄC‘E¤“~K¨÷AÝWtvp+ô;øÓê;Y{dÚ±jT75[ÅÌK¥[ÜR¼vö“'Ñw§™{K½âdÕ*‰’i@‹ýJCþ>î)þÓ=•[[NXÜgJ‘¶âã`XÍ0mû·þ¶ÒÛ°'¸JQ‚ãºÉ´CopƒÜX¼ƒÑbQµËª¿€Þò+Z§ÇÃúšñµUõ°¼‹¨ûôTJW¶U©< êɱÞú¼%œîþm&¬x±¸tA[¢Âï ¶â"…ú¼žÈ*Z”Dš@)ÙJž&;:|†‰¯§8ÀLsS®rùú¨'Ë"P)Òƒ)dV_á‚Ó”˜9ÔSª• x¤ ýõöA´¢E18(Ëfu&73xÃŽÉýUËâ0Åõ'†Sá°T½ ݆ûÉO•I×Y±¸jº% 7Y,â— É®ÄNoÔ­¤&–É6°`„8ψexI|bÍ%ç ™\`œ:$-c;ÖqqVÑSX–¬m¸j5K-µ|@á¦'äw¨}yŽžf<ñR¨Ä‹v‰¶Ò°éì~Ÿx]Â1Øa¯övò¬ƒCn;6>9χ° 6*%túü(oÿìÏà¹ÅÚ_²yQ6›zw¶#ˆ×ª¢q õŒ¬¨q ÂÒ?¯\ÿ¸Ç ËÒÂ#ø~Wºnø½&­¼Ù|ÊG­4V7Dî(Ä ÂŽôŒR©É¿*.Ý/e¥XºÑ¤®“ .&€jŒe•‰¼éÔ¨]+ÌU?Mu)³KµÙCÜлY·«§tv˜IØöÛª'0Õ'ç?zK=o›# ~8•\_­3¬‹Ážh`Òîõ BÞHZ&JÎŒ%^“PÛmîB<"• Ö¶QÛ!šSÝ7žˆB{§Žè¾4[©h¦EªP›µ…Ýæì›aÜ÷˜­ÿŸ½7ïoÛ¸†ß¿õ)PëúGЦ¨Å[¢ZIdÙIÜz{-%]d•"! 1E²hIqüÝŸ³Í>IYIÓÞ²E3gÎlgΜ5¨²ÊÌ9ºr"PVãIä â!Å®ÏëÝÝ›Gª›¢…Ï1wúk9kÉ·ÏÿúòÙ¶ROQìïJ§?çòq Ï›[µÚ`þE>ùÖ0çÅã(÷#*æŒs2›R'ä$†°ZvÑچǜĉ£±,á›x\Yô0°˜Á…„ZNÈ:‚h"¼¬¤Ëè˜]üìi8¬ÒõhÿOABªµySÄÕ«Q­X {‹NÖëjJº$OOmŒs-Õº“ÆaU¥ænýÀjzxüÌi”wE‡O[ñp.7|mz%(Ý-Ï2ôB†‘‡šçÀñ¸ä uœ €²üœGÁñÀ-«iªA5D2ä•>ä†è,Ú…GXÕ[6£~9x–ÃC aÞ¹“¬mtÀt¤[ðWœm|‘®‘Ýênxüöà*_ÊÁæ_m€û\¢LòƒóæªHCÜÆ`\¥ÖW©c8¸t1•U”éšùå$=¶P’pݯ²]½ˆÄì^M`t'䥨¼Š¹`ŒòS6´/F'x[¼‚®ݼKHoxèEEQk1 !é`tÎÙì4ORù!IÉ‘!‰öÇR{DÛ»7nÓËáð,¢{5vR;·{½ÄÅÏÍÇø ž†6¬çWjùéÙ1ìÜ]½¨Ç§é$ 1PÄc@‘õ„bŠÖcKÛÄús»üŠc=ê$"¡ø÷l•窠àœY#3ÕŒ¢3R ·¡C°8Öo¢n))¬_²µöÔrÖ¢ýZs¿^zÊ\Êq~dó~ws *l~ÙI^f#öŸ›ì~ÿlVýœ&~¶0Ë5ôƒÚeô€Œýìïhì&†~J(rh›•Ø<’C&fºg+ød%+¥‚c°Wš:u¶{JBdÙñÉ­W51ØnulÛ]^©Ü–¥ÐÒ'œU€¾­¨}Ð7û`Ë ,l7… õýwopOlÛ R€AÒ'4ªÛNüâr¥÷pÃÀ£ Ww¾É£Ð WÚy¸…³ìä Ó2·á¾®Á*±›tŽáU“)̵‡q5k×A+0¬q±äÎT T¶ ÙGó„µÁÙÀø·c/"è܆µüUr»»qrû6§ï¢¢qžÆÊîÈÅàÞÜØ€ÑB•?~Pçy2FCm bäˆç€aøFÐÕEWI¯¼´‡>P ­§hý@"a.»Yh´ÿ…¿[òÊmj{`… ¥' ñZ ÞcR ¾ã+í+Ž")žñ8)ÈÅ Ô› Ç-„}™ÿ ÕÈæ–ímŒ/ìÖ •‚§H¥)HOR¯=ÂÕi+Vn¥¡»òZ×ÇÅ í2££ÀÈÛª9Ü~ˆ+¥Eé *»¥*«/eu…WôŠˆÓ0{Eûä‚͵ݦké˜]ª’EQ[Œ®5¡ºÕÚC6FÝ,â¶j¨ÉRäëEܶ¼Z€°5±9Á˜Á ²6—3È÷úMÌ›7ß -ßç™d}Gn¹»ì–›ì:n¹ÿÆVYñ„{ #ˆŽºƒåŒ·l€…­·¶ÎõUÔûÔáEÒ5Ц›¸7§ˆQ”-åÛq —øÑl t£6ž @TË%­±ö$™r¦=Ve«"–´tq‰:5û*Êɧڿ=Öi7!j5]ü'¢ü`U1 nŒÚû+´ŠÙ$›m_Pv¢‚lVc–+m¤lÔ}Bn\¨q )¬‹©k?óé÷ 1½Ÿ\©NL^ŸˆÒMwe¾Ú·Hî&›d^-ZÉŹ÷Kàž¡Xi´Žý1srX¹M¢~w¾ìÚð“à§£H MüxzìDƒf@}”†Šw’‚ÔÞdñ^‡i<'ÑÑ4Ÿ Ù)W/u޹²f€ƒvB¤`Øv5ƒ²RÍxÀ¼XóU®õâ‚Ôþ6ÛpÞ—AÛ†ïúbàsÖµéLžVتßwݹ  ºoÝŽ{ô±Kë#O¥X‡ïZUyê)C´b…ÝSÎÖÕêàÃgHê6ãb¯y¤^¦÷žÓŒªí®:ËNK)„ÈNË›pnˆM{Æ1ÔªH.ª'º¬ïlá'£ÕdKu@\R­JÁ*†ä]´7"wÃ.Ťßõ]ò¡El.õëõ¶$µé©wÏû.©L©z°€e£!âÊø9ŠÜfã®íx¿wünù& sNŶÀ™¢@à±é/ç%H²KŠ›Õµß˜+Yz ÛW­®Å„á7ÏÝ[m=äø–™²¸ÄÔæ+NÍi´ª“€álÓíÃBE£×^µ²Â°ñ ŠdœX,d´-G“lÚú\ëTswˆÌ¼¬Nd&à¿[ð×Öð÷aÏŠ-£‡c[kØøb¸å#‘'è ”6šPP54ÖLÂým –[{ åݬåb]#°HF;·_™èšî üŽ4^‹ˆ±±ä¯'Äf<´Û •E¸ÜwUI1ñ¬#!¹¿¬Â+®äòİ‘ j°9ú/KŠ9pwf?B}Òv‹§ Õh”ÍZ(~ž†ÉôèÚ’Ç­å$RpóáƒGçK9Ç<ÙãŸ×H/ðï)g”­n9q¢åeA‰âD(dI”åœøyQ œv!ûÜH…²®ÙÉðúÑ Œ±0¹~ÄB›(pÔÂäZQ ޹ÁÕ.tŽ‘ñŸ(ÂŒÚOÆlgsŽ÷#L¬8>:¾:ÍŽ“¾gdLÂX[y::mò‘Ôn_ªWÛ‰ @3;b¥8øå$¿° ò´ã—¦…ÞejĶyõ–â"­Z]ähHu1$Uª † ô’TÙ{â M=6T<ÝY»›LYo˜>ó¢"77kVÓu<Z6¤X×õØN´Ç¡‰4V0Õ ¸@bÈÍ…­ã…=ì–8ß‘›å˜y>¥Â½w÷T8¨ nç.²ßÈ·ð÷âZØèX¨W f’3ÁW²œa0}ø6q¸ê¬Úœ*6ºÍo\ë`y/zÞkr•R;z'qbÅ™°zfµkÄ܊įÀÎj£¿#2auè(t~;êàûjÇZvn{J˜ÉUì›;7©^í¨Bú=æÜÆi“r°ðvð~Ïù÷=Ím—§8qei•ƒ.™Þ¸ˆCŸ÷Bä ÿÂq­¶ådPé9$Ê¡Dµ.gÇL*!›-#¢¤z ôE1‚'… »ÊÀ‡`P¾Í"úU(‚ÕgÝfÐÕpåèÉ»,s‘ò6ETô™$wzÜZ%E}”û#í¡lâéQŒ¨.êä¨.h,¬;E#"4÷á()š¶N¦ù‡b<+9R“w~’£\Ö¾ïô£´Ë‚õ8ÊTÖfft]£sÁ²¾ ç5»Ç‹ä¡°½žvЏÒÍX ë@ÍØŠ¢ XgBi¶J‰@§Š!qµyqú­6b¨£ÎeóÞ>÷hÛ»Ž÷¦SJìEÝ[V~¹p2¯ÒÖ‘„°»ïãj0îc'è¿<Ø–z¬'ñûÔô5)\þ9œÓ@—ÃÕð(Á„­¶”fšÏSÁœ­cª1f¡‚Äœ®á®Ôçê¦þf«­œíû‹Í  .¬Ÿ‹IÚŃt7X9©dg3UlA6`¯tÓÑÈ/Q‹K×k,£bzýuú *ué‡ÑªG•ê–LrŽ®Îukg4-©5]ܳΥÂîEŒÕx¯žÂ¦º<.¡ÊUÉV3ée›T‘élšKä¶®Ä_Jð]ãû6%­»Ä›'¿Wpå%ߊ½®n¬¬¬Þô'ÔTÖºÈ}©=ä5xÈ5k#Ò1n¡îPù˱nñþ}ˆcþsTêA'yÐä<·PÜ{_Xm³ÅUŸå[U*6z·èRK ñçªqÕõÀÄÔciøt6WÈ–ª‚·–€²Λ”Ø·t4ežÊÏžQ­°ØX^güÏÝïJ뻘Ò7ÔúÞ›«õ}ˆ™>zhû›lm=Ø\Àß•‹§ø¡Ò®BÿZ_Œ‡Lr1î§²±æçã˜9œyÆ}[wBæšU…Õ€ÉD¦0CŸÃ +z¬±é(’ÆÉöc°ÇÏ’!ªF«iã”*ç¿$¬¹ƒ9:´æc#h+,yŸô»¶y¶‚«n‘tœ :ÔÍcðŸNÒ¸Œe´&4v,„=nq¶aš`Ú V gÉ?uƒyÙÞ BQýYm×N” Û)´žŽÇÕÜÉŠîÅ/°ì‘Ü0;^¥ï„B:Z„uÃ&_Oe¤Ò=º7½7$wóBIÁ$ïîï=ždhýŒÑùíΦôø Æ×jfóÄEaC!IpÈ1¡íèT«pQqEâ=–ާÄê7Ñ4?«‚(ðVøÂÚ@‰§-{üÓëç¯:É÷/ž¿ús'ùÿÀð¶îb~ó5üç—– P›HÓ< ÛˆÄrœG~g«B³ä±A—r±7ìr÷@ÚzÓñ…Ðe0³*b¤H¬%üŸ]Ì.(Þý(DÇôãqUÁ®FpÊ‘å@¾/ŠAuf˜küEº‘Ëõ#0 ÜßîZ4nx– ñ*ËuÖ-2;uÞ¬IÉ5u*2zp‡…ãèJsV¦*Øs¬åX9j§…1€I)Œ_ÛÑÛFëv‰ÿ£ø9Ôì¬*±,r©~‡l; :)Œ¶¹¼*»e5À}zœhNEœÕ­BbÉòüƒñ…‘™› ‘ŧãËvùJ;ûèÊ%Õ`G#nevâûP„)^øDÌ~Ç©G­°jl˜à$ØNúŠ™fÝ!Z¸÷?$ÿ@r^ñ¢K©˜8zm÷1ï}Ä=£õ Èßô1Ë.U1XÊÑbç 9#–Õ…ºä#G€Ætóð”J 2g’Vªu&óËt”V6Óš Xt'gXܱíHB‘qÙÔA ŦMš°Ò+–CJIu: b·OñëÀiy¼½ylzï¡e#TWý%W³±vë«­w£V¸VþåwøøÅY¡§ºÅš:ébÄ È…¦p´éh²M\rìÀs†V×ouãþ„³²ËIcS: Ü›—9YBÎXи…éï•w ‚ïOŸël=6è°8àö@êsŠC—V"\÷‹Ûñpc˱àº÷àÞ½ ®1û‹à±<ׂË*KüÅnYŽûâ?þ’}5Êä:F]Ãåç\GÏ~üÓˆ¿}SV3´HèÎÊ«A7Ìbö]ʶk)Ó®Õ䃱cœG“ìN®’¿~´ÆãS™¬­­%ãé@œ8üncÊ~ ÍÀí‰ä6+T¦Œñt2žR:óüŠâMÀjÀˆ“:™.ñk|ùþëÇÌ â§RÉìW°UŽÁ¾øÝ•}èCľ7A©Ð¼Âé™tñÊݧA[?)áÑú®{c™îîdp²¢ăü²ÚÞ~µÿ&y“O‡É$ë¿ÇY·’‚ÃÐÓ¼;Ê«•¼nÒ•Á^STšà*`¡ƒò'R—•“)Ëu –ðÐb8|б)#KÚ$d å’.B<^T¥›¼¦™à°˜û*N½^!i…Blþ¤ª( cŽk »2ƒf`RNfCn /sùôx†\õ|E#zÎÄâ —0Ë0™Yþj9>ÏÝ ULŸÐ‚Y¢ÍBÁ¦5ÕE9|Ÿ_íXpŠ«yšSUÛá”\úQ#ãU ÔµMûPÉ02 Måùò)åc^›rD:vÈa1&R¯¡x÷·GG–Ú9;†½Ì# Œ´Ž§myæv:zjóAŽŠAò™$†×ìg¾Jk‚F>°:«‚ÏP¡¹ ¬ðÆžç WúÊô³n[Œ;üÄÓl.¦®5×ö´öZaî» òSÚ¼þDä0ŠË'ƒ¤“äXÝý,é Ô ) AŠˆ)8Ç”Žs*’¥y6L]¢ë/žšr)V”1¡ í$ÇÀE _ ¯¼kG=ëË-Û+,ßtp|ÑÃlÓ;[50¬·š¥qB£*m\C É:=x±v“ä ®Í“l6¬:ºŽ²ç¢DY§³ñ¬†¾ ._3¤†Öû/l®Ïê 0[žô™<$~D]qKäÖ>Yñ\9õ´YÍ–¥ß§ƒŽ ±å ¯ùpX1„À ÇúVϘ•zµI^,nK¾¥ÃÝbÔÇã;PS\la«R¯.½j:zdð¨a!BC?X-éÞX¡d;z8äK@Õ]Þþ´vTOÞPÛRÍ™_ £.ÅçÞØ}e-”Q¯(Ô{ÍG«ŽÙ«†ìY¡tL9nƒ»ôš;¼Ø<²Þ_Áû­À^F[Š vüsÙ¡Êí÷©­)[Äñÿ ik’¨ˆÀ¿^Æ«¤JbéËZlúV7®YˆŠ¸˜´s4Z¤n$îš®u(\1 ·Ù¼r´è0Ègù¹áyH<ÃhÔÀÚ9öc½ê!±ºîIUÌùÑ¿ÜbzH1¸”ú¾ì!³ü¨ëcEÇœse¹ã{»ðQ¢^Tµg Qc¹~]Ü‹œ÷æ5d^ð8ñVy%õÔ\A¼·$D- è=hå6镈"ÙÁªÖ‰¢´0–#‘ É”#È'ã,.b ë54”n*ÑQQJÍ.*IsVFÒãÁ8/%4‚e5ž°ŠŸèV>:å™YòG4°eùÞÅ6YÎ\ Ø=d/º”Nx9<éì†AÔÇœ\Ú½ÈÑΠK³kKÅ~‡øEÁ —°¢Îò@rG°íÛÖ?ÑcòJÂì‰U­ŠÂïEÞÛ’Zë»ÅpÌÄË{ÔzWµ:Éa+Ë.Ú2HhCdX´&:Éæ‹ÚhÉžYÎxðÜ.·“ÛÝû'd±ë®«® iîêØîˆ'0 vLÚI{¦íÚ1¿¾‹t›YW“}ÊõŒfÀÉpœ‰1È89ŸõÏà­g£ßA)f©÷Í(:l´ì¼ `V°*gC.@ލ‚£âÑ;ñi+¶ÿ[É]²Î¦§7ÅÔR´<ÏÑØÓóh:r‘Ób@slyk~Ëh;Í´>)üC“ž*8ܰ3jö-zCÔÝZG¡A߃¥ úî«‚77 úÐàb¾)–*Äøâ[m˜ Û½ý*G6ìIÔïqyüM¿,ó.p³çùðØ2ÚSŸgƒ ´ðyÖ$Éã|0Ä/ßÀ˜ 0Az·_”ÝÙ$°êMsóùIÖ­—<¿ºÿÄë0Ò(µg™ÂIòæ :ÉÆVp4ž³¡¶0À`èqx$ÞÙRíô@\¾2º"þu`È@f²bHNY@^Ùávj£±"h$°¦+6"š•0ŸÛ¼r¿úꫤÎâæÎÊÊ'©D“^šBm£ÇP“­î}Êg’Qˆ¶”º™(¦°s=ê–Üh€ýl˜£†XYÑ„âò|ØÍÉ%V†Á.«È½±èƒÂڪЪ1¦(»"'ÆÐ%ðF¹ )Áë÷Y×ñ·qö!ùn<çÓÓäñüìžÊÏoNtqÖp=stIšx¬ø§¬„/.¦ÕÌÏCËþxüþþëB-µ=²r2êr K´©Èêúîþ›Wë{Rzk¯¿ÍûÅ$_°uoãÞý¯¼Ñ³º¨ÁîiÝø±øÏ*™â?v€æP2&¥{'†¨¸ýNÿÝyjeR¯‚-<&CRÿPÇ—gY™UÕÔ¯ÛÁs³OT½saÉûÁÕ„ïi‹mZá¨!û£¸y[¾}¿ò¥^ºcNIºVùÛñÇÆÆÓ¼B}£ý>¿òú5ÔÐ᪠¹§Ê\S '„wï¼(ñvðHŒ±yY‡±Šœ™ä¸8ˆ§Ha—è#´Cxz¤B}¨K¬×‚ÇÒ«È4Npå“€aC¯„k¡K ¹<¶[Z$Ó·TˆµÒ‰õAÓÕí$¼Áø_ƒöYb ätUû¿Ç½ïd± †?Õò¸¾ºA,v†cxPy>± cðþó±?VÌÖk´[+TàEPh¤jüpìÚ>²qç •–M)oÃÔÜ.Ût5ºbîGv·‚N`{ø×¿‘;~ö8™¢ÿ¸Ž3¸3^á9=Ñs‘°Q‰DÏ@C³èœ€ƒ (c‡Œ~Í ÒC1 ³Îâ8øW+¯àZc²C€:ÚÑ ¦ñ©—wÁv$Éh-ÑrSäÔ†µá¦£+·#$֨Ɇ@ku yÂj.îÜE÷öb14xc±¦¬ÅùcèÏè*vey¸œ’z{ÿþÖfÓ•e„žÀöÌ»¶¼,JteÎF92꺞b«ÅëËM]C˜IÚ'K£§¤mÅ‘¼J¦³A†…y}Ù4‰66:g …dûp¼÷GŒÄ1 Ð…)^°õ'ÉC§»\^Ê¢$ÇŽS°ÐzŠÙvJbC1ÀOÙ%' s ¿pà(y4½ƒ_¦hín•®˜:«J/*E¨/öééÈÒsä ‚$Hi™Õw ì„ÄCådŒ*JƒHEîÞ£ºy·CÉ@]Õ±AWÁm [I`F9&%†1¾^LK´ýl›À1Ù÷ ˈ,›×m]…aÕ×±Þ›J‚l}-»€ÓÖ4‚bA&q<ÝÊ1dƒÚ6Ænõ(ÚA}w GæäúX]y\’3©ŠÔºÉIðü­XÖIöb–A :ç¥GëãíòVD²i|aEh Äá5½ánßNþã‡OdzÓä/ä]E®{»ï‹/¶î)ªb5#+õsˆUQþ™EU`ñæS~šõß+{­Ñ`)7/^–^%ÏqŒæÛ@ v>~ Âð àd*rw²µ‹È ®?R?V(‚A— öùú`µQ†·#&aKõEuIà†’IÞŸ ‹lš”gÙt’ä•ÇÌa,­*y ((¬gSŠ‘LÕx甘bãY¥ÇÚ¬ëçæËHµ¨·~8 g&ð²Ôqœð@ÌVÉ:ÊÅÝÉþ0Ï‚Gg½‰¯ŒCËÕ&OƒFwt¬Ai€7bF|úžÚ²v†á<ŸTW–ô äæpv,¬×6½8é™RjÓu×Dˆ_FøFzK‹ XÚû¼¶Ð^Ï'Y1ÿ$Rs.ë´oxIÍ&è[ )ƒ»>ÀVT%`ª½öC»¦u§»Vbû€[v‹”“MSP©ÑÞ6j<Œ’ú 1»£ƒ‚Ý„?w˜Íºã @VUü£þlê¬;Ží⎆ÜzŒ…jãÒ¯#Á|¤;Æ)µÊ6+è}M„°<+N*1×yK0ÆálÚ¡;Ùùq(g7aA…0P=ß–þá¿v¸#‰ö¼¶©@ìЙ·²šF" ;öHßd‚1`6Þ>¶“ &\16}š¡-?É®1qÔA6;Ô9R¼Á8y™œIúc7Ùìnu’?Í€³Þü¯jQúŒ‚Ú²äܺYö¥¹e0J-Ã|£ƒËËÁjB*ΔCGŠÜ x“v“ïÇè¥yûš‚ Ós‘]qyt:/+´ ÀóœïU,;tÈdÄ¥8)ãÑ):§L¦ùIqÉíÃ]°†áÜk[r‚!ÁÝæsOP.÷SçP‚Yî|(^ä•$¡KYÞ÷ñæk8fœJ{ÎÞЈÙ/%FMŠnmòÅ.ŽâÿHÔÃP$SZ9æ×Î80O†Óßa1_À&ë:T chô$0–ÁØ¡Ìö× ´@®ÛÛdBÁ¿½ã—â1«FÄ|ÑBÅ*xˆ•]ˆqùoTp¥ì5rG¶ZÄLZýõxeÖÍ*«Ñ¥ Ô^À G¨aég#ÚÛÆO¼†a/|à>1)nµ®ŠVž¤*$ZŽ¢ ´þfg@«;ò‚ă#!Z&´m–šeu;´ØìǦ¦~Áy…’ËN¬=‡ÖyL›é‰þŽþM Âf¸¼Ù1nòèZ¸‘MÓ<Z (}Óe[p×µiF f‡[Gv¥üŸštUhªŠü:’>Z`Fù<0¸Ë¨„†2ÿ ÉÁox¬aÜÀ´M3a3ëºãRïvˆ?\~ضÕófý¤!ý”ETsÊëƒ÷8Š™“àÃ<”1%Ì4;es_'ãÙhБĖÔ©‡}h¨¹Å4‹âš‹7<ýN’…6…=îö¨ŠÈ»œ;¬ '*–3‰ ¤ACÁö·‘ßÿÙj²´Ë¶®êS—"oá¡X‚ÕÖ½{Îkl]1¹ó­Õ°8pÆ—¹Ž–ç_eu¶š^^)_¾ì«¢U-¸;¬Uã5”"ü¬®u Ó]íJZu²;tܵ£M ѬÀº´cK³£Í!,—­×IõóÕB`àHk°úÍZ ÔñX¼W˜Ô2Ü–VtlCÞî™àÎä9 ŠL+Œº†ÙM¦ [EÔ[YÙÕ–r{üleÇÿ¬¬¼AOØœ&Ãx:Ÿó>†ôD¹zùa´ÖÑ{zÛC›uŠ\žWØómé†y•S82¿£e¬>˜ £ý/šÿ¢JhÅY°–ye€¬ LÑæd0ÄÈ sf»—sÔ”a¤(ky˜yÿÔYaÉ|l¾tÀåN”i‡™p{¡YQVNdï#žãŸ:&:‚N±v1Ä¿Qí¬˜XLÔ¾ŽÃ’ìj-y~¢ “W¦É]4ÿij‹c2P G߬&Ë€Ç .‡ ªüD«ׇ¤Öò¢ÄÉRXRJrœ“ÝvnaY°ÝÚ-]4ÄÖíelSoeLÈ0]Âî`P¨T¼¤fÕô14$gN á¿‚”4:˜~Vr£WÜ”œ"¤ÒÅIi^—ãcPš¥ ‚Û¸ /O‰4Ù[!$¸}XQû6Î9Ôœõ&²—Ñ# |>ÍÕ‚ÅýI<&ÚÛv |¯åi±‹¤VÙ¶W#íÇ ;µüjUvzªGœÚP¸ƒØp+^«A]½¦½°5 |ý3 hÑæ)“Öź…½ 5 VÙ¹@¨Ý\¹¡ Q(> ÀX{—RUÃ^‚ߎ®[zš]`éÙŽCTÕçú\‘˜žåÊ žØ²;š0f?ù‘§Ð˜'StçØ“°t@ðVö>Šu5Îc|¯ê§;lÂAL¨¨‚.ÅN2zwØzŒzäoáf6á·= Þx…ß¾›Â&Â/šMéA™ø»Ûíé}ü2¯2 Œç1êu™U.•ƒ£ÌÞ¡Hׯ@S,ÈÝ BåÔûáãkà˜ ·¶;ퟡâ} Î1ùßU’¾~±»×^±ù¨¡_ˤ83UŸ0Ç.ã7ÍûãUÎ&ù½"´1ëË3î´˜E1f}˜±g—IÀÈEf‚(˜ÆÎ·oóÓürr O§vÉìTûÔ “[ÒäõàMÇ<»(¢'a~eúèrÙpRŒÆ^eõl´P}(#±ƒc¾È~¾âUð3é€vã÷á†äf—ª¼„õf˜w—K0—ïVúõÞuÛÝ;ïºÕeÕj¯pGÀÑ{€¸K_\pЋÞy6™ M°:wv"cÔ^¡±®o…^C#{p쎧0yƒ¢¥>úýÃlíç£wƒw°m¨*”è—ï´úÈÿb§æá¥ç½½‚ ±ë1S%ê´©È«7H!†õøDPkÆÂ´§úœö¡Ä¯ÔþùLÌ>ëšç85/@³·áC]à€GÃ!Þ!À©"¿ïáiçÀ*ZMaËt¶Ð€¾u’´õêM«Óúÿyó3'( [óÛݪo7iáè¥ØÔ/Ô8Œ&”¦úiëÅë=Däͳ·øçõÛïðÏËçû{­v'ÉGp%QÞiͪ“µ/Z¥GóQzžJ„æþÕ(Qõ Éde ôÐùåÃÅÖ¶)<Á[9¢_%ùZЪÄÂ+vRãh®.¾lO³Ÿ1H[NiåkÐ0eø_àüyœB ¹ìÿbSÅ$Y9ÍáêV4Áç‹ÓZkêÓ]RF£¢<ûå£ÄŸýrš£«,Ž]ïm¶,_×´U^ät¦†÷ýɸoiÅâŠèÁŠ€y Wøy,ÕwCYæÐXUì™ëúFL!²$L8ù–$gùp’ZG}›ìY)¤ÚêY_.š×-9 ¶¼’s™A¹ÈIÝR3 ø@=öìm¤ïoŸí>})+A²Ù)ɹêá©"Ë*Yö“‘Ÿ\À9âñð*ù UΨÝÅíæ;š°Á÷Ø?úÒ°ÇåB›Ð0:0“bÒØ8½ÇÆß<³ûª®ñsx~öï 0çY¿GS`¿Ìú/éûœ-¸±):íµMFê,4c„pÊQƒù‡-`14ñu@UCRlÍ$Ã@”£xK\Ïö:mð(rãEÞCÿ×ü¢h9Å\~ësÐå§¿ÀÚ!üÚ,ç¨Ç^7Ódg[Œ&À€5ñhª}õf¾ÍÙļÎY rh¯L†›_>¼¬ÇßÓvÇ/>4knËqÖf ÈsþQÊ@LpÄQÌX„”@4ÞìÒW¼úù<ñÐ_ÅZ±ɇ2'6îŸ —‹âÝbŸDà²t®¢ÀCÆ­½2ÍgUã‰-Y–I+UøÛ0ŒQdP¡é*oh¶ÂûúÛƒgõ ‘©R™ŸÃÉUˆß¬¿¾|áÃbAÁ:,%’”¯O×lÀ£2ÿÐtž¨8úòµ™ŠÐ3L¡7únŠãŒŒy?[€Áô "âôä@øË†åNÀà/Ê’y}o6Bi=¦Ð2'±ŽaÓYŠ4S2u(/à]Ù°ƒ¥[fQõώǨk€« lýˆß^©Šó¢ÐÒk¨€½šãñðxÜ@ ¥Öæo‘žu臘s4èñ LJ::Ñ©ë*£òö©;¶rŽìï2§j¨GÒÚbÉ0’6ïÖab8°’»(QÖŽ'ÚJq¼ï¬Ã8½+ïÀÐþãÝáÑwGmhã4›”A¡0·w<÷ß÷X–´#‚jÝ „ÕÅiv±@¡TÓãþÁ$Îg $ßÎÙ¨Îêçþ­&Ï„%*óh­Ú)“ãYE—œ[V}e£+Ìõuº- žb €÷ßçÃäÇçÿÿ[˜ª«i1ý»»ÇïÏ:èêó8:”z"2'ãQ¥šß=Ëàé¬|Øí>ܸ÷è@ønë%ô¾¼Ù‡?›¾‡[|ÙûñU'Ùý¹ ;(/»/²jcsó‘îÅ{°^³aUôË›N­’€K÷ªk†T!9x¸ oϳ³YÖÏàÛé4så‡x\ûáàÛ/þGî„(%èÐ%qíò›ÿ£o™kÎ-ÓÜ&×¾§Ûä‹—±;ÍŽ‹>½Éèk 5ˆT²/?4”ý?=ß§7ø¥ÂwOЂJñW·üæùþëµ/¾xðåÚVX¨ï©è)~Ó0~øöÀ¡ ò÷³Ñ)ª$G½—ÙéU6]ûαñ W•6®aä6Pi¯|ȧǣ¼ˆJ¨þ#›wæJ²zˆR`‰£:<Á‘•rȽÊ=ª/¯{EÏhÊ(Ï÷b¦L¡Ôpî.æ^õÇ ¾pÛ{ýÌíÚªèï⇠(Ý ¦Bk>†T œù^ª¨”â“O¦H¥Öõý€WIi]Z%Rž¢hè8Ù-gÇpühë:JÄ×±^«£ËÈZVˆ·Â¼”€'ªkw¨owteU°2‚´±R »ÙEYî+þé¥[.ÒQ«@W&÷_Ñaj²­Nö¶,1éûÚø„Ì|$~Àq¿ËEÙú€x{y@R*¼Œ~±a—1¹óHÉÂc·ì‡"jt)±ý¥XÎo%…r¢ìÆ~B'¤ý@]#ígêâ<3·ç±Ëú;¯ ?î>V1#­‡Äl:˜otEÚ@Âþ­¢8Úì¶hÆ ¨óbó­ÚC…9ŽðK`ãŸ-°åŸ²ñ»·q¯1Ð.×õ|Ä#›‘9›gz÷LÊÞ€ ^òª€7y™ FÙ¨HÎùÛ7³ó"ë—ÝÙùà¦ÍïŒÞ÷ºÚRè%<«Ðµcs‰äZS¡àu^x®<Ã|Ç<’+ƒ ôõ¤ÐžŒu. DI »@å“5>ìZ1ÅÑÊ`V5Ixܶ°ìü¦FÃxS˜uÁÆT×¹Íå£xs¢èX°9)=¿¹“¢¦9Ò§,Ú^ ±i¼1ÖÚ,ؘVñÌ["yMcÈÀ.Ú1»óç¬f‰@{U NcRz~sEon¢åæ ¶h*ÌotRÓh9YfK醴¬iŽ5x‹6§õ}sš+?D› N¥­¸åù}}*9–çN¹¹§ÓÔÐýÞr'á}õøÑÆýÛü íÐýhCÃAß5™Â¸rÏ¥ô.-èWÛåJ…ntW¿“5žv *xôœ=Ç@ñ:|‘1”‰xQ)Q¸VÊO6³>ú٬㠂Á̯º~dü°¶ÏŠD)lfS§y²Á¢ÊHóÃóáL3ßürZAÉñ(Z¥4們(­¤Èiç^…i~:f(wœ¨ŒãùÔM¬Šœ ¥GËŽËñpV1}2ôÄQ)ôU.ñÜÍj4“ èYdV¨äé„a6P° «ZÛ„ˆœ¬¨ŒA«TýVã£áöÖ…3ô¹Y#{UõO€õS»‘›ŽIM&¦É¼ ;jÏrv-yòqWFþÓ¶ ZÑx*v“3…‹¢ÇþþÝ(p*Øià«ôŠÁQÐR\·¬NÊb&Á¤ ~¢}@1ïUØV¸o[¶T\Á¢Â&¹jå ¹Ä§ÖÔ_U!' ±?L¸#¼04Ñ${Ó)y„©ªTE8}H—¢v³Ø2kuª»ŒƒccèªÔ‹AO&ò#\dÆö>2òŸT´f'Èp8SLá`$ ¤˜\@Ǭ™yó.;o>ëçoï#,0{accJ«%ä4F»ñ3o%…- •‰rHɵ¨ §bD. ÅÏÄ.NÃU)Ž3ŠÒ½y½pK}#)$m¥£Â àà¤;ê:Åht£ç-^ÆèÔ2‡$^öYJqæUb ë踚¥b˜Sæ—çaò2œSß.­—´•¢ÎÏŤ½þ5|mÿÏ/-f$Üþ@œ1J]¡<‘çÝÓéx6ñ»¸HéH¬KÙÞuÒ EÒ ¸è*u;‘UZ6œ¬:~ÂR{SàéVŠ5Åü½¡ ­?ÿöÄÏFT¦#ÙMÜ•cøC>¹"¸”WÖU|cܾ¹’ZÙxýÑtJvOóÊ›öÚýèÖ´î#ƒV‡ŸÚMÏ"7}´çƒ+÷Uª«uÐöwÇî]ñôÅŸ²uÙ éƒÈ@ØçI|Þ€ ³¶ú1yªµõxDöód£ûe÷½ýÆhòÓ[?”ZÅ ÷6)¥aCÝ2ÙIë²®Ä jF`óÚÐF3y‡X¨´Óîè[i£ÍÊÎ=Ýc=k"Ùƒp½÷csSÍ)+‡¸µ_ÁÃyq‰ù}¨ª:²B»…Y«]£‚B¶&V·D»Ü= WîVd Ib`ªûÂR§m­2æ,=þÞUd‹`JÂÆ7q­Èp ’p ÇE mëÅÁœÚ*{–«Ç¶|µ¦Ä0õ+%÷øuIr^»öYÊI´ä´kÜ©ÿ)­ÔFrLMˆÂOd!]Såµt†˜|´c0&c2ÜŸTAoèüÌ+ë?¡oÛÙgwÔØ[œ¶ :ŒG¥¯ÄÜC…wÒ+Këëùy8ß_À$”5çÛs(Y BCÈ£Œ“q­zŸ_¡’¶_È ƒÌÊÐÒQ…®ÙÁ2QQ7Ë[½oPæhŸ´…·«l¶.$Lç°˜Fº3”ÜÂ1Ú“Dôk$ÕôõW¤ªÐš.B9+í]u• ³ã|(;Küìºá˜ÀàâxÁpG7Ù÷k8BÍ•‘ kGz^T˜ 7Çž•œ‰ÍÞîí¥ ‹|(kYÞqÀ öçYKx 2S—+”«2VHÕˆ{¨–WÊrï#¯lOEMÖJÐÝãh®šVPh\Œþº!ÝM ã+÷Ä­¾dOV·iÀ0-–žh¹RëΤ cEqUÉ«¥·©É2w5˜ŠœÆÛ„§VMÒ$,T0r{'›’)Zgr==Å6ún•McŸœënË¢ -JÄG3ËükƇË:uìòÍ¥è®Ú’½:§e[ÖinÑ-aZ£ \ss2%64ªÕÜ`¤ˆjSOFmÃñÉ´ÁGAM•XÙÈM§&3{Úz¦N|ÿ(²ÏdaZ¡XÔþ¨ H˜ç¢¡é¢šÕ^yiÍÒ#øáºðmb“Õ Ûܲ˜_fý .öìp¾mw¯³hç:n×x€%@Ž ž ›i™e® Å,å·M»‚BΕÝÙŽV2gO@&zËÞBŠKMR-³§¡ŽÖf‹‚t34¦à Ù`JÙŽ†ku6£ÙÙÁÁØ6Ýú]¾UE4¢ËOùÞ „¸ötiìùünB³hlµPŒk²ê‰òݧª;ô‡ñ&ã•Èàè*ÀU©GS:IdÒDt‹ËDÌËÜ;i|°ÂÍõ|”øç2b·ËmøoÞ¾RŸ–NÒKZEÉ‚µú"£P;ÏñQ¨éÑüé§ýŒ0Ë-ÙñÚ`°‹ @· ìúCõŽKªîš–­;h¨ÀZFe-ÆÄ¶^·¤7æö5vuz)îB•'˜diµ’/Cü¢š~MTÓL6ª”ðdŒæÃ©CÀgk꫺e0½shfypDÃ3pÄæ5Ze{».5yjæùJfº"S•Å'ój)kfÕZD34›äÓ´FÞÃÆ mOü…_dZk˜Ãùk­v½5¬©CÓv£"ä†[÷a‹¬ï9Zg7$Ÿ[ý˜(S-=»!Ð+«”ìãžVèˆR‰ýÖI†¢c^¨ãYõµÈCßÑÔþá G¨X¯¦Ë÷Â~PÓÈ9=7¤›ê+m8½P`OG(&r"&•–HÌHôh°tæÃq? Gôô£`$™S¸zÛâ;ö¾IÈs”,, ˆ6!<°¯ìajI)³Ê(é©ñn*F“Y%ê2þº2JMeD4z:ÎE¨*;½$6È5‚¿0MÑR yR­SW<š"î:ìß΄ھR0dz K<„ëÙU«Žç¯e‰üÃq`ÌÚPÐhò¯RÃİ;¼Hä¿S€ElŽ‚•æûD™{ÅtܺWŽS\c÷Ä Rk]q„­Ú<Úuh±:õ :²t·#°-14­c¡*UÐL¼6yÒƒH¹ìÞ:Öï1Ë(;7J(ð•Eç`=Y¶ÈjP-GÑ(;-ªåßÙ¸]syü»­ ö›ø ×6øï¼.êÌYK ¾Ø­Y¬5#¹,_sŒV×>ãÃ>&Oè¨W1¼3Î^A5ºœœ§Ñ­©–l'9´§¶vZ­ÌÛñ¦¥‰Q„ìZý… AŸÞ¾r¯ &¥âÑf¡ÕDMó D 'dxªÚm_»—D?8¨¯1zÌ+.3|«Ÿ’g#Rð¸køó·†ÚË•/è]`·ÂaQ* ‚iÒwÏÝ ÿ–$R™¼t-›bµxLIF+çDØQiÙÄlšSðZ*ôâ#…ÕeC2¾Îw}`têr(\ ?M†;›Ô<券ç…7Ó!C%ƒß]×ëOl¤«’ËFLf?_q B³¿½1ï6Aðu/µfð‡ÞÌåä‰E(&=`sû9a‚OòóFãöo˜m{ÆÇ(ow|^©Qç¸ÅA› cጅˆåÕùñxh{Õ‡ž»–)Êvéþ¾# ½J0îT†cÆ;HÑB jvJ¯1vÊEÑÈ)œ3Ë-¹ª ÎeCLòs\­¿$CX!øjp¨Q}NgôÆ©n]°m1nsGdŸ/’S-|e²Æ’—A#§q&öïKíEü-Ù–ì°ß]/—%ôˆ¿Ý¯¹µ<±2~8àwÍY!’Ÿ”SyïcŠõ;˜¶õÓœí¨C®×oÊþ#Hø 1z¶£™éÝ ¯`—z=ê? *íÈ·)2ÑD˜¶•ed[þhdúZî^S/0YîÍÜùý‹_»¹Ó_sÏ;øk¥^ighÔ„ú'ÑÚëîiìÂBÚ„»= âví¦ÿÞhŸ±ixËãw²i~uì×Î4œœq<~Ý­Õ´™œóR;›fçl$s„º#üßsôÿÆ9Ê›îwzŒþÚÈ™»qÈDwt´ý’ü¿j°ˆïöïó!J‚t6/åKGkÉqÖZ[ í*4f¤à±MOTÞ+!hfMùë”»äzÖ°O0Üë‡Wʨ4XßöXë\<º,#.Gj@-l÷xNvJ3ÜuÐCr±àuL"Ûô¼ß­mîlíE¶k9;.«¢š‘/2nXмaÇøqQJŒ»´“…ÃÍÒ,xiÉÄX¬§ »1y ›õKѭ·%¹ùŒF]Ê`nÓVw}½lyz^*ë `¢@”B~—½b”J#K]ª²£*wQ𔑠rX nê+@nÝJVa5]’ c9Ë“{÷}ƒfÍ(®žBhÔhS]«°B=¹[g­½ˆÖ#ʤµ±|¦º¡NÄNš kX$ÌKÃÅJ±¦bž£ª›M0¾vðpökP—®§Á¾ì_µ2ºY…©[£ÖQ‡Zó¨‰EÈp1Yã#Ûµ"#Ã'Þßb Ïå}5‚<]a{poéæCvÁ…¶‚b¹Ñ~Ô†Pþþ$ÛγbH[À8Š1zº | ÃŽI.Zímo¨¨˜w–…8R)…%—öº¦EÛzà­ÀyÆ3Û‘Zv’lvŠ´„ƒobþÈJòkbB®Ùù'+€G_ݬG+BFʽ£Om½ø×—/ÚsåÆÐ¢¨ƒ|Ã|xÃÿêÇØ_j,º!3š%Ñâfü ŠDÏ8&ƒe«IG)™…’0zVH–RhÀ‰ÖÐ3uî_ã¼ÿÌcö¿Ç«ËÍâh –0±ÈîvﬗNØ~½žÂ¿í¯Óþ/í–»,{rX–¶»¯6‰àXƒ~ ëdÚ±‡&(—StL#ª_9ËØ_ýj úÆ*©!9Io••ÕUrÆÚ¯m2½ô´.ž1*u„ŒFpŒª¼øž ýOKâže£Á0ï1|T8Æ|T+^´¨²SÔsYù(aÐFy…?7¶ÔŠ1XŽKîý–õª22ÀŒ”ªVYÖ²Æ çƒ¢r ?£B}ËIi½Ú¯²ŠÙp ƒŠ~Úeq 7ÅêÊ\œA‘Y£ÓGzfQ€ÕdŸ#4”˜™U%Uüƒ~O}F ¾áçèé~u^­Ó·–cüÀU¶E§.cÖz7jqÜ!çè=¤÷Ĭ)V‰UŸT ƒ-°#û0¦‘l@‰ ¸8I%AJÏNV\ DK*âÂf€6b< ˆq1©$ˆé•P‡“ŠÈ0@1µ†jÐÂ×øÅEŒ*m«=É+סƒ˜Á÷ ÷°©IõFø§É‘˘©"êµcz-Kj7¯g-I` l›w|¼˜Ï EŠ’ œSÔÇDzäÇrá•Õ8ØøÅo ÝÖdèLC?GïBùöóÆ“yZSì|C+ÌÁ†ÆÑý³lºScM®ŠÌÉ—;­Ù¨À„Ž'€Ï1{¹›à*„°tÆ+;‰Õ©â7_+sÔ´&Îv\5³G‰ÂoYC¸˜eH±3à,+ 9šMZÆ!P?hk‡6îûS¢”$yúza¡äL ã$Þ¥ÐJÕGÑÀ¸œ…K¡b¯Sj[*œÜ$’Cn^X@¬5ÉýEŒGù¯J2+o•¨ ÀᡸsN–3hÇïß üYñ޽k =w°‰íŸ^IgH)@‹õ;r¹\>QÀ|÷LÍCyÈ쨌j­ÄEÞ{ž%ÎöÇ#ʽ…-áØÄ%Ùк.ãºD,Ö@°êÜvVá$Gì)’¦™*É0?©x)z ª1Í#áÇûßw¼AxðjgÚúr·Õ}’7e£+5Žä¼®ÅZžØÉ™>‡bQƒ V·œ§­ôëóö?n—Ý;­Ûð(/ûÙD…u¦k®©_«…¡ß×k#‰rŽßÓö$Žbó‰/Ü•KÃX„¾ÑIÈ=²Q¯Ï’.³ÚUv'åhí +8„ï#-cü¸óIu¥¢XjRQ΀oqø:Š«æ{C ˆp1¶²ý,?'N–÷(—ÈG¾æ ¯ ú–{¸¹½¶ydµô6Ÿ ñŠF*yªy±$ýC»“¤ø‡Vfú‡„$3ó8ÛÓ[À£tǸ4ìVún3y·‰ß­»issx ‹%V’FÕÃú&cl‘æ¹ll¶ø^á–e†‡FzÑgÄ´ê\P 4¹ i‘`dƒÙôÐiÏ£‚Ac Oêkd½!Ièâyc a¬wžL„ÈHy=V·²Ý!àýÿ;Îòo %õÛµ׋¶öŸàxÝiØü¦ Ư< æ`oy˹„•l®æˆuX8|VmŽfQëÝ4gª"Óv#ó¶ù[ÌÛüÿ,ÇúkMÆæoGËæøtþ¾Ü2ñ³85‹ùh^w:~+š&ÓñŸAÚ>ËǶv.od2B'—ñ ¥£¶ÑK÷/ühšÛS¤f&¡©qåê4MIZ89îÏ8bäÃDüLR.èÀ°*‰ãDSmJ>*ƒ|R­Áh¸XÐíÕ!ÑUäc1oU™å#aŸP\sdaCœ¿ÑJúW“éò,ÃÌ0º4€’Žu¿-qá‰l\~Ö’s×ÀËÏ]r¾¢¨l7.Y7tHüv«áßú”h^LzE¨Â¢‹)–Þ?K.¦P.¦›:¥~ýÕ´H,~Fû0.ð³CÎÈ5Ù\EåÝ4ñ0Ô‚Rƒ¢Idͧc‡Óxöޝ8A8xú=.õŽÚ[¤YZµ€„Ò¿xoêœÜ­¥o)(ñw|€ß:†BÎﺲˆ½]ûÂDQ-ì¨1òÞ;Æþæ·_ÖMñ24¼ÒÁÐb>jÿ™%&œ»žYµ8]L(lB'­ÄãÇËèEâ P¿nOµ¢(U׸YhÉ¢3¬©‚R…àY²MóÎrÔ: þe Ä7»È®´ók7¨$¾Cöº«KXfá8±ÕˆãѾ ¸6c"+p*£ªË§IEcw0p¾«qò¿8 ÿm=²»âí#ã3ªÂ¹‰»ÁèºND€X)ç¡éˆ>¬©'´lÿ·ÆìÓÙè!*TWõ‹,âÆÃ•ù°’ƒøªJ(†xG3PðCCïÕš"ó XúÖ>ÁVt,Ÿ©Ñ윔©WP†ÀÎåÅÁÂrø±W³Ì€‡! Œ84Dæ°8" 7|Ï«Å|£´g÷¦ú8§PF5l»š>(˜@ )Á5L 6=º¿±° Äù “‡Î3ø.‘þ¯oq“¦ ȳîeÓQ~ZäÉË8Ú¢4ÏF}Ôû=Õi¿“C5݇G+'Ô:üÛí—]xƒÈ­O¸3ëå$ÏûgëXz}ÅtóË/¿ZûaTòݱ¿Å¬|ßÊÛ¬û& qÏ!¶:%3ØØÓ>åzgÖÝ7`™Ãp§äsŒÉFÐë@T /ØLúÀªi6*ûÓb"êðǰ€‰ñ?Ϧï%¦ž–Àߥ›;“iqN©Ï·vÊÐÐ௹JžuO»Û+¯v~x»û"ÙL^%»Ï6“½ï“go7’Ýï7’+4Þ&•ºIn¾¹õhãá—°÷+ÉWþZnÀCo~ ë[ G¦n»,NGÀÁdhž'‘ÛËNòð‹{ªüE6»{ÁãðŽ/­b+p”ñ6¤PnnÀè%Zèë›1"r’•ÕÏuò!›´¬¼9ƒ[ÜyÎî2S. -Mäq¯=åÙøÍàÅTCÕJž]f5û§hÈÂnø4¼^‘´ ‰÷—¾Ã§áõÊî.Sš1BøßO-úµûŒÿf•þ}°²û=ÿ8›ñÓᅦÙMt×r†kž~C¿ðÃ>0 øùs¢žp¿ Èb+Kž®<‘÷Ç9ÿ}’<ÿ›UmO0éŸå8'øÞÿ}Euas½§nµ§R f2—ßÏÿ¶òLž>Èßï¡xöVõzZ Šðè`åÙßÔq[ðûÀªö­ü=T¾Å¦¾“‡§°àpv¿KÞ"†¯äñ÷j€sý*=—§…ÌÅóï†Ts™+ø}°ò'©s*­ÿé{g$þ,ßçWòÚy!‡Rç…;x/åïy®~C…ùûWzåÖy%¦D¾ðyƒÈ¿ún嵬±à ¿í.½Õ˜Ñ;€'+oäåDÐ{ã6%³D×þýÒ¾<.óŒþöeœJò}q…K%•ÜJfUÿ¦Éßý~å5ƒã1a3øƒµœ~~#abÀ?üeåGyùA:ö£ÛšÚ.¹ú ªWE>Ćþ†u^è†þ.Ç:W}Í‹Ÿg@šh(þŽ«Ù±ÅS&xÆ*óFÀ¹2Ôy L~ «”m$÷|4È/õûaÂËðÌg<‡L¥~†ôZ¡™œß°°/ 8x\!˜Ó5åd"/`PÙgfÙ4" ’¢ò‰õ»e–-6DLÊpçÚc Kÿ “œyãbðØÂp¨$ÙÊ?Ö¶ÆEuHÁ9€q€!¼@{Fp¨¶ éÂØÀçÍ£aÜPRŒq#ép˜Q3ªýUø6PIÚ ÔI‘÷ ;¬ªœÃ =”›“H}’”‹n)Rч[ÛGíÄ Õ¨¿@¼°¹œxA?Ø|´¹¹°xa<çúW¼~õâÅÿï ZY{cèðZY] E¦ªŽ r˜PôÎøMx\›üÂ@RÓl²<×àa3r/²Ÿ¯^f“}Ù£“4ñ¥t9®Á ڕ†ÏòB±Ç Gªd}d)¿ì'¬)sÚk®ú³ÌQð)æÐ–é¿òD0ÁEK¡¬ÇTUv í§€Ïó§Óbð© ðɇG&Jr%Ö!ÐŽÉ7 ŒYÚ°Y;@¨|<œ‚Ò¬°ËFÊ©ŒüãE+Nî\SÂt@¯‚B ó24C2¼è¬ q;8€j0KŸh\)9˜D$_û”ŸH*u`Äü¼IJÔ9×M ž6ºM.#O¾EÈ9¨^d¢W&ø|¨TãÁx›äéålB Œ#Äeäu¬»†ÞJ<ƒâä$ŸÚŠ5œ TC³lÈŸIT(uíߌ˲8’3 'Ç_z\¶Íý×8;Ðå‹ñlhtp¬J'uF4¥Jæ•åêÛ JËÆ™Ö4]=ÍO…;m³Ù¤›Þ±-'Çãê uøìdTvºy;Øìø iÛéô“íš‘^{úzoÿ`÷íÁÚ Í¦ˆ)ú Á•õ§l¾ât4¶xÅ–Äñ¬B†à”ëh2Ìë.рà öì‡<1ºMåGfNaê½ôX{™—èúÒ0_°þ¹{Švç.뛄Lÿ ÿ1†…jÑ”¶ú¶¢¸‘LlY*þæõ¾J– c)ãS­O”앇Tùàí³gX©«¡²² ¢J{ßÿðêÏ”¹Ï©ù Óy¹æ+jl”+L£51Äï9”¢R÷óý·/°b96ô±ÌÏ3¨ÔO¦cdâ²ã|ȵŸ÷êõ[j™×c+НüÆuWž©ÕÉ$<àê¶µovŒRí åYCB|*c\¼&÷^¿øáå«ÞÁßÞ<éKiÂ;8‘šp†«ƒ]ïHÚ¿ÊJvÉùM.äE¢„éÓ!"¡%Ô£·Ê½}<é¡sò…@€%Ð{äÛÆylB°dzhºìá€SRòDÍ .p{Ä­ì„ Lƒ:Ef4 Ã?š’žî-ö*XDˆÁHìEË×9ܹl8ZN=Š®¬ÄiÓ*…×!ë§‘ŸXº|]Òúå—ϳ †»Â«`Úï"»(:lðdÔ¾Öèµ}ãk’ÑŒÄüòÊ©¥f6ê몡D>v±u7¸Èc¿!½,°)ýC—º¾#àmê§È+ìrVÖ—| ÞTŒ°Ö”ñß\ÆWÛ/Æ bŠ©ÄÌ!(ñƒR*—' *ršW=É} Œ<ŠIÜÓ^΃m)´Gâ:n@u¨HpÎ]k¬l–—ÏÛÅ=j®Ù¯†^Ìóy àŒ\ ]ùF܆s|Ø4^o^—ð‡aÑqì©‹<ö±r´~6A†Ð‡^(ã~6Ä+©3³¶ ¯êŒÁ +€vƒkjžýÿçm‘y‹ÏRÍžÁºzü plømÖñºÃ¬º3ú6«¢Gßå_ž&} õpìm`‹½Òé~ãÈ[5çÒ*dÊI6ZžˆÂg©á/„‰b!›þÅláç†_j1ÈW§;7µ,Ü>ÝÐÂÖ. É6e/§vÓ¶”q!Þv¡¹ÄÙÎ*e¡¬2K©)ÆÇ×öÒs=ð×á<^þú‰òC±ÊG†©q<òã‰p„e®˜T(œgïsÌ bܰZb„²÷{ñ Õ“ì*Îß‹hÏ®{^,Ázéñ¾Á3a^×?Î̡㗿Üdº#ú3—u37ïܼ±Ûçw(L~Ë2ø_G¦Ä}h\6«É¨{Üݦ¨«$ÌC)=§¼eÙ¶ Y>HH1¾Ï¯(¶“A ’Ò[?b2$èaj-À·…ç'ã©ňð)¸(KëvI=‹V-—µåŒ‰ÔšÉ40¿²=‘nöz~kÃ:gšÜÐÙ‡}€Šæ¨à®LK¹ ±£C,ͦ8©PmÒý£™ëÃèü =  f+ä ;Ž«?RMÑ âzTÌ Uš C>_HäéAQö³é ›¤‡——G ª²1Nõð*)*Ô…’XŸ¢"ª ûR—iQýÆ®nÚÂ:Î’Ãj‡N²Ñ>"{£ä‰$®äC*x硸w–c@o•–€Æ²ÐŠZ'štô4‚QÅ¢aÓhS‘Ë0¾hc¨Nü.˜Ô„CeªÏG¦uLéxœO-êö»Ñm+#_ãç6O};X.Ú@ƒx6‹S‰–þ‘¡8ÚܨjjU°YÂ|b¥ZAmŹ£ôpS´-ÀcÕÓiÞ=ív€†Š#õ ØîZÄBË¥„L0ÇëŸu¿¤)Ö^Ó‡²žRä bù¹°ÔŠÒÞ…  ï¨ícês»¿4¯_Ÿõ†aGcâ®PTòÄŠI–øFyEÊâ¼fS¶i ‡ÊûÙ^„;K."UïI¨F]™®.Z€4+h`•±[ì»–ÚjœŸíHênËzTzÐ1¸ÐyŽëH8FÕG«HÙõ[ê¯cÙÍ0#¥=‰ ì$-—ºÆ]êÒ zÛ(ž²SuÚ®i6àx-*:[Y}Y5G­ xAtÒ9Ï*òMÌÈ"®Í¯ïKO †Ì¥ÙÔ![z¶^Ï*]3ÜqÏB!KTø4&y´îYò$ôe@éyËGä;¼*×6º¸ðxâãVkO*?ß–pýä5ÉG¾ÍÁ:ÞzòšQÁC˜h#‚[ÑÖAšŒ}NLZß–T,œí‡CÔ÷ÿId­A=¦#ÐEÚfthOÄ1¢ñ’ƒZC¨);§œñï5I„Ý>-º²mmquÎQ#^Ú‚=›ZÚ2œíß½#T—@&)ý…nK¥@m5Ò5Ýf=]3h…tsJB+ik[%˜l­½xûd­U[´m}+*mkÀúwl»BÛ«¶bÒ¢NB&²mŽ»ÀTdòŽŸX‚Œiwøo—ÜJSh§ åZ°–áè¼JÞçù÷² É<ÜLnõþ»]¶%ñ£à¡ç‚§†Ñ²fÒËOâˆCn½ËùI¤Á §Kj'u!úî§ái„Ýô÷%nÎÁ¡«PŒ,[¹kZ{ÓÅL˜fÇ,&fð®ü.Óš40q/z©Us <ÒÐ ž }ÉÌEÓqö ßÙÙ¤'µ£âÃ2åèëkÐÀ ÊNöVmΊ#’B’Ž»dÐ×Ò]±Œ­r`ÏQ¦¦J[`´Að8á@ ´îÑ b A‚ÅßÃ6È]ïá„MqgXm#=E& ¸ÅØÑ‚«ˆ%Êü©0—t[¾ •5JáH«¼[x#,m†i4Æ©Ar htH×%‰{ªÈ®ó©Zw0¨¦:VVEÓXraüdNѰámþ)Õ É÷ãº@Õ ÀPi´©“5ü¦ÆHF߸Ñ(’ ˆÁŽÂŸ6-ßQ"Ü›*¡[¾ž²‰sN°%»öªŽèS=ÉÔoOäöuÉÔ¯Dù,¬ñÎw”Ü!Êb€[3­§J“ÓP¨çòì }êΈ³…5@w[í¬©©BÚVìz5yš÷1Àë/4¥b:+‘ü>¥ë0pAШ–Ì^µ¦>˜’ ›Åw•€}>¥ÌÉÈ xJWÕƒð  lh˜ÅÒ@ÿÛ¡ÀW£XXÛ&ÎûG·]§–<ɨ²ÂP±9:‰ú(žØ[û±ŽÔáç.î´ã¯´h$¡WcÒ‰ð &°¤‘`K`=ž¨_­LE§üå™rÀ¼p9.«BRæP@awEC¢¨ñºaé÷÷ù]ø~-³WG7ÛIîØ¦Ë7l .Äi‘•q`é¢ãœUÊ•)ƒ{M²€£Åxt}³rªù J]Š> Y=©­MVùGsè^F"R’‚¦8:´‹ù'½Òä`rÁ›] žâ7ÔDè猆Òìl®\´ÈñÜÖ>У05æ­®kG¸wºñI—Ê­FcC×Ù\Uq=¬],¤•¨ñž zîA±hôv×yª¼ŠŠž«ðˆ.‡êtaÐŒ`…Bj7TLó]œä´Ê4m2(Èw”ÝÇtŠÎLgTB×/å³û!Ÿ»n†« iêÑ-QÀÄuyOWîðCø}n~M Iu=FøpƆzg0¶òÆù¼£ÜŽ6aGnÞ7^ÄòîD™ ôJN ¤¸è›ŒÐ,,–ç“qéz& !"‘>Ìè bE«É1¥¦ÃaøØ+¶ 9>©Îgê˜ =Œ.Ђ^­ù9s‡Òø “¶(®*àlÛ]ŽÊIýÝšk@òK5Tƒ˃XïmÌæ Ï#bídÞƒ·Á@·JÓžêY§•ì}ÔØâA<ÎÉÌ/m8öL”VÝØàÒRcƒß½2ÆUÊ÷ŽrË™%Q¿öö>¦!Vm|¼ãgbCƒV'‰j™«´ûfnòZË1œ€5{“O¨æ“Thd³§îŸ:¼  1k“6hQÍ:ÑÙžvÓ!ÛXGŸ"£oob+šÖÝ"§IœÂºqà(báE«*³–Géw,"õ Ú‚S¯§,ÇpìœeçkMzõÞ°t…-¹-(U ÑuLáè(b§n·5íEWŽÓôjò-nD GQ_Nél ŽaÈaí…Ö^ÁÅa¡ËªÁãîŽs祆¸ˆº¨‡‚2KvntôÐHÎ1 ­É"¾°”ÖÝþƒèZe 9bK½'¬Bë±ÏÈH`u†iæ°ÛåW¤†rz“ºd®á#ÚÆ)“É´¡´èða=¤Ján^r6¬›nÙŒøÙ€ê"»"H«ÛÓöö»a/Mt<úl‰öB-hÑQg€­Òˆ5†W–¸wY)2ñoÞ®W"àÆTgÔl¯@†©!ôõ‡‡xE…”Ú*Ð’ªp”ŠRwœöñcX ´”{‹ÊÝÅIô—ÎÐ]S§{R`Bø.¬ëaƒÕJŽpÛµâ–PA'€­+²!ÝyGp=Ãg‹$E'5Y‰ñ×G•[¹]Ǔđä..s¼ÏbŒ°R¸I­)„Ï©RË’j ;iÛç9cž„ì$ á°àC²aßd;Æ7(B%™µÀ1ÛÏ…+Œ ‹6¿œ¢Ä|PòMwKJ-– ·²ôÔã?ì˜î/$µÁJÊèç­¶}’èÖšotö6?&KY]S–ì.rï|ß % ‡oÞZËÍ9±L[éíR¬]`G^Å5 #Ö6ÛÊ]²ê©·Í-}.BéðìBƒ€FZÄÔÛ‘Šxka°1Mÿá`ɧ•ÍpÝG“%Fø¤ÞÂõÆ œWÉ7 ÙöéV(žm?œB‚ ïPSš‹û܉ „Öàê—_bA|ci! ] Èα¶?Xï–@×ßSDH,pÖÔãÖqKµƒ­¤M†âTQ©ÈvBª.ÆBÞòY„‡ñ>ÖIP×½µÍ£ØîpËyå„D9©_Ћe Jˆ€‡B¯¡c“É^À™4ŲÆz¦­r]éà‘ì`=N€"k%x¾*8”ƒŒ™hvT/‰µeÔq¤¬à5Aѭ墈Ê߇6ï/Etã5¸šJô©.ùùñDÿœO2à §Y9fÉã÷ð³[ÊÏoò³Y7/í¨¢Ïߣ*2ÜTžo5.ó ð̦ߜ+8ÿ†ÝþøÜ®xƒFMO®H$N3*ßa ";¨( ¬˜ËDö4€í·[;»Œì?êîb»c^Ä.\Ù&W×ÎAv| ~¤±Kš›Ûkçm~š_NL<\;Éi6)9,À"Yävbî‰~Oy_#L”CíðŸÕë~–ä´LNJu•ôb¿ÿkt‚ŽBö|^9f!¤7Õ3&È}­7jûÈ‹²@ìU;¸|Îù³@LþÅÑ]<ôÒ2Xs¼›DZ#¼€_ÿ5Æ×BøÆ0]<úÓõ†wœ?ý9nÁ ÙY~¸‰òÝæÞHzÇ'ÑÓ© ކ ñãe~úÑžò_Ngf+xmòå­«ƒñ{ZÎ0“l‘O3J[T?£/vf˨rÍŒ ³«Ëcšhv¥@&*Ϲñ š“ˆÚà1ä¢õÓ+¹LºÑù¡Ë”j ednÞO ºgÍ(¿¬´„²ë¬jÔE6ÄÀ€ø¾ïÿ*"^L’ñ +€´ï=åB›vÄÆ²tœÒ ¦k]*Y¢ÂAÜ?Ô:òˆ…•×òP+¬ÂGÝ¡—… ôwšõùÆ«-™éÒYÉÄ>¥´Q4I7íEàbF!ν8­wUËeÀ­ e i¥îÁ÷Pk<½S)±P‘ Òm›¾l¹?ëb¬áÍymlª6î/ÒÆbö˜?ŒtnG;à3‘2ÑÜPD¡YPªGOþ%1($sÂù¬¯’ ÒªLÚUèç'yUݧõcyÇ)w8|ß²ÁIøÉá—¼“èQyè¶­”Cõ€dP n“÷–»Mê·î-œ“¢È Ÿs|þìÙÛ›ÏHÑ/˼;çùð“Kt³ßM^н é.†ç¦^òŒ‰”Ø$Ï0f÷ N|ŠÚ.x¼§âõ¿z¾€ù0¿¬°«ý õäløau©¨†Ýª“NÇÖ‹l°þÅ—÷»›ël•¦r &4Ÿö¾ü’ÿížU瘇ҘÈZbœøyïõ.¬×·) —L8°)Ñûð°møwW€ h’~óàÊ¥LCÞ¬±j1=íeãcl_·#ðD Y)+.V¡cìou˜^Á±2NW—Aâ9k ¯&5Ä*6Œ1Ñ¥ñâñ‡i¯ÅÉ5Cc:Ë1½å@@¨ÚÑöþÏ'˜Œtíáú£¦ùõhãQ³fýúª½üåP•|lýz´öÕZÖ“§­`þ8ù‰„·`’€2! q¨Qüu+—¨›‘ŠÙÂlÃ:›cè¹ÿ+¶ÌQÕKM"(>_I÷§Ú‰ ÃdQ:íÓúž¶²ØÆn܆ßE‘cŽ+8ö)¡êÏiz‰yÖE/’q÷‚ÃÊåolÏÆÌÚ¢íÑPRQû«Ùµ¿ Ô¶gk¿L¯¢_òñet˜QâÒõÇ€m¢Uz8«# ›P§å:72ãvÄýªimÕÌ›Ž¸«Ôyšô-9îßϤ”É)è_nßÕÄn†¹D†„ïÈóVéœ`“HO0…¿äO8.]‹#Û¡.ÜÔ²íø·ôJfÈ7ïµê ¨ïn”P_­Á‡tÔ¯*¸G¶Õ¢kù¢^èûæYNZ¡VŽ®Ø5J‹ˆ«ðíuðƒ>GíõcrñÜŒáûÆqãAÜjµâÒàëVT¶ÝTˆI‰7mŠîÃÆzÌvF¦EC³FlQ7§'Ê"ÝE“%Ù— ÿ™AÚQZ»•riñz¬Øyš-UÏþ–CµP%ðü±0fv¶ŒŒ´€ŽáðÕ³WH¯çH7ŸN$ `:7jŽäNSä©TäÈ’2‹±Ã% "S1|1.]¶Îƒ!)^ W¦òV…kÉŒ•K2Q`yËF‘L-‰ãûHtT(,lÛÊ©²ôìÕÎO/v_.U¥֚Ā…Õ¢öVV_^†Ò ì€N%Sśșj"œ3ÇÊü^tp™"‰; Ní¬Š<»H0e¥ÈÌŽ‚œ:F0’æQé›È/iкû¥feIë–ÎKÑÞg½ Êq­æ(J#P+J|äC¿*gíí¹Úû!ûÔO;yx+w9n¯:…«¼wýËl fz§}€/6êç| ôóÑnl."8ã 8qU`}¸JJG·SêlÈë§ë6Vê {Y2œ«Ö¡RæÅþ«ƒ•CV¶ÿßH?:µPÜ•ÔfÛÑÓA2FÇ“YwœùÙPT¤('6µgn§S¬C¦ù0œAV”'tGi$¢•è[è·ŸÀ<ìF?^@Ãç£é˜Þü€Øô"`™˜¾ó"ãêßæƒÓäšž¦ýéùÃó®aþX¡TªšöC§Ø :UÑfìvæŒýE™ûçvÕf„¡‘bhsŽ¢oMSv@ãìue#¤Ûß$Û`:Ì30f‹s _60ß7窼‰qAG À\Sз’y82_v/„Xr~2€C˜À“³éŸ£ˆ/½œÁþT´…@~Ú9p®ýCP|š‰¹½IÀ’ÜÌkì» ¸ê\3Rœe ñùî?O÷q;þx¡¸·â%ÂB%‡üŸïá?-°þ p!ó-jæÙÀ(õ޶†)/pÏ<&´-|ˆÌm¬[*öÙ»ê­í]ÂÓöZ›­Û˜+ÚÑ\[¥Ä›¾°»í\®l ¹V1(ÒQ'>KåGj§»8¶{®FdÐæ¨~ÕœQ§>iHŠØ«æ$`¤²{0vsjÞµÁ‚CPùöñÉ·×>|º°|;ÊF È·û{ÑþöË;°}χ Fßçýi÷"¿*.³hó(:ýf ¦Wd¼0êߥtzOIKD!0 Â’¿0, FnwÔåÝ JÎm[ ¯ß¦Ç•MX‚›-œÕjFýŠ]€™­=ËÄS8jB|áòòåV²Ö*? p•x¾¡{m‚þP ¦ÂôxùimB¹£ Š€.`þ†ê''Ð&ÔÒ?K¥zy×*D¿JeyoÚ·ÛRjlºNþ6Å%,¢ýk Àa´ÕÙh=†Çaœ³.åM)-tÙæ—òîV3bT3•sbŠø³bÞÌœSl‘™¹g—´ð$øþ,òÔ®QúŸc¬IŽ»ý%+k”I æ%`¥l"¬I8€u*‹û’—ɨ¿šWiädK1I²~Á9ô¤®ê©ËtEµw©JWæ½]DÑ9*SNœ…ÜÒy>ÎT_ò)<ƒjœõz”EvÚ×StLˆdÃäö`šê‰´Œ¼–Š“¢Þè~ê–îs*—ëÌ# Ù aâ&)[B£çFë¼eqËܤ"é´ÆI‘XÑwýr(:=…­óº˜\w^+kÂC²Ñ£ -¦#ܦT:í›Æò7¼O ¶'½H³±pQèmŒº9™çé+Æ©¥)ÍbV”,§ý>Y¥}œgoÁK®¬‹2+(¹^:AÓÚ N\!ºÔˆŒsQ~m—0ZQ&ƒÓ Ž>$XzdÂѽó"€Õ‹M³(¾4"ÂWÝ©!Àü‚ƒâá™,J^Y€WÅè"ïçç&æL†IÿúWœ- £‹¥h‰t‚á÷8+Àdö¾TÉ¢Þ `¬ÅÈìT6 ÔqÊa@Èîâ­çä*Å ®WlœBÂNуBÐ `TKÜ+,‰5N–W±ú“ÁwN'Ãü„ê2Í,0ÖP=^BÛ¹äÅè߬ƒˆ±‰[ÐI´±=D&‘IeRÑSr˜¯Pq`UÈ-ÀaJ±që+WnNöŸÓbűN («’¶YÚßýå QÖ2íg´ËŒ¯)¥ ™!.4È„šŒt˜à–§ƒq„„fóësQîàz¢Ó q‹–<¨f¨Ã_ÀW%²4º™’ß^ã@kÍèÝ aè® ú3P&œ/ÿ5Í'%¤~™ F%·„b¡E¸XFùPçìâmBÆ[˜›#¨‘Ù?PøYŠlsÚœ±YH'‘hæ‚dŒUÖjh]|•$í8ÿ9^¿ ; 27Ã\Ê­ôaÍ[ÆÊËòßBÇA'¾€ÏÐ ZûÐvIòsØíIŸ×Ÿ|@–)EоJ£90bÇ6‰y‹ß0†Â†Ëâc>9hbâý‰t{LL8Â7±"W ç2½>Lǃ¸a%@R½‹ÆEBÛlÒ,È/‹*VÕ,¥÷’6JÏß”†aÚw¬ ]­AIù¨v$µ;S¦ì$7@é‘̋߻<±µÕzA¶MãFÕ C^x°2 8»•=WU¶EfÈ•B4ÔèH}?®hN3£55\hL¾Vµe†< 8ƒOÿ:á½B£ÚÿNN¯ëz£1„n[VezÛ¹m»²Oh2[À^kFkj™Ú+.|K[C§¼I¨Pã–p¯—{õõZ@ºc9y[eõ8@ã7[~&•îÁîËÓ¯ží’vZŽo_U¢>t’ÊÇgèþöëíßc ÕWô2ÐÏ úÜ+ßÁ\:tFó™Æx·ä8Ÿ1Þ1%Þ!Ô$”J9’,œ™ä ?|¿uæ¸mÞDÀ$¨Z ä ´ÇЯ0«w¶ÈÈ-ÐnÐÓ‹¢«cæ<•d?‹ uê µª9qè 3惦ø^Ÿá¦,\Òüž…ç×*HF®(ܧmÉ™jÝÝ2Vá Ùaù/XQ[Ô‰Öô³*ûq,ËÑ@/Ã>ìá5ò"3iªd®ªmFkÚ ¡-µÍp¸m¯Rãâ­¸©jøu°i®z$åPü=@Òƒ¶4ïl˜Žà)òabû,ðXᙳåJGôº­¬‡ƒ¿Xú+¹d(t„®·í Ž ³‘áeZðŒ‘)Ü<¹flxšzåð”[³BDÛÚt¯´ó..fŠÚÛ«$OKÐXÚg¯†õ¦TËÓãz5½·emÕd¸nEÅ€†Î«(¡¡·É©Q¦0ÚŠÖæÉýVÔ%[²æJ÷èÒl;OŠRFM æøÛBë‚Ê2Ÿ}Ãi>¹ˆœ‹ ޽>GÑPºcˆKp¡a[îM*†íaÈO«Aßje‘a8]4íÆo1¾öTv())³'‰À¿lGîP®K[׋ºÜ3‰' "ñÀÔéÜt¹C)·¡&Kþ6mÔu¬ïªþm‰÷»º†S¼ªlHÛxV®Ø¨…î¯g Q+?¿zýŒ%}ÀA q U#WD¤ÕlÄ®öÜìÅdDûvg-¸//fýešû_iÔ·å ®'žè„Þ£ÜP1Ë´5/ËEq‘_…võàYÁþC´ESÕ곦$ΙÉñÚøˆ£ˆídou1‹úqÇ‹ÝÆí϶ýøƒf1SA‹ŽKÂr‚~ªIYέx÷©}[ƒœ§³ Ü‹®R`Á¯£‹ämÊ:peqû»+ú|§ÿÔˆ6a©‡b!8EšV{Ù. ¹ê“Ëc üø8œC³y¢ ¼TÈXa:¯ÄÄ“zd*vw=üFôö¹ ®1(9þRŸÎR±T™"ÆÝ‰pªœrxkfã£`ã#ÀÍ"-MòËPueºé,ÂD]ƒk^HQnE„{še»‰ºk™4Ô (¡?¥âÈÇÀIt¢¥r:Gg -€U Ì\kÊfŒ{ÖSFÒÌUcw΂¬ sø†Ám`‹À”£id”;0gJu!R©.m†¼ ]xãpÒ¥”G !gº•-Bç¯ì´†³™EðsçÒŸ’`°½a"‰‡ÈRHŸÂì=i¯<-O°KàÄ CšÄ?FÅĦ ¨_ZŠ-Í™…àÓ/ã…ÖW?}W±¸´•¹Í/IÌ@Qð›NÜÀP3¾¦@}˜W!|PUJˇ›&²+O¢vä<_åQxe°B¤×#ÕG´Ð2gOÕ d|]¬Ú?͇™ 4²7T¬‰ä2+ÂC³qþ<Ì_UjQÕ‘j$â¬h²®0öY¬~³‰°o¡V«[ô[ÃÆ¢Öa±ºµèùîÚ:º‚G›×ÒìuA "г¥»‹I‰[ò«°Åm“J6ÅHˆGXÅm»Â™xèLÒ~ß:ÞO§gg®ûNýÑÚWOLŇbA;ت[j¢…nZ¶µ¡×ܼåW€ÌPè"|[yê†Ô:ÊÂ*¦¿<å¶=à,+ƒÒÄbš³’sÍ“Û9×<çšGÐÈ¢Î5CŒ%0¼œç]ó2|‹N÷é]S´£ýdÚ¾M{É8ƒƒ~s?[§òó›s$"²½ ~¯°â 3N«ƒ(PÈ[y‹)ƒ¬W©ýn·Ÿ¢ ”Š,ä³¼ˆ—ޝð’§qÑø á§B ^þèdzΑ¡°È>`™ZÀPaR)<0Ëé8%ƒk+g,¬ÍimQ©~qÁ_CÏÓ¡§Ö øi²Ì^å$6m¥Y÷±¢û ÅiÂN(6€ü6•l5˜ƒ˜¤î¿¿?CÓ1•Ù—®odÄ=u“ÝJ¢lÊ¿L9vS»Vâà1NÅ ç4¸12fž5ÝQW7Æ2LáØŸÄ:3LÿE´óž“ß`Iù\B|ÓÔym%×·\úgoa5á ü$‡žð~yVK†vúäìX ñnq[èp’丸x¿î”á–WÐE\µG!yÄ¡¡rPQŒFµSÐχçL2é;àcûʯenê  Mö3%hº:q(¯$â¾À÷bÛÀ·ë|yòÉ´*Ä%—d²“ÊU©‰œ\!d„H`¶ÙE%'u¿D•Õe…ðô*Êȸ.*TuÔó¬ïeçµ–ÔŠöëã´_ߦhT¿óÁºi”º7ø4AHǰ3`Ž?˲³dÔGÝ®Ïgy"°ÃÉ8é‚ìSb|à`ìg^DðI®“õJ±Q%ÕºÕDTrl–]‡ì>ÎꉴØ`˜å ¿²€Œê™ÿ*0ì2>m*jGåÈÅz&u hWmäØ÷óˆÝ!ÂØ•ÑAÀi`ˆÙ«‹„}) ©¼ÿ–÷2:ŽF9…ý1;™b‚´M : … µ“¦OØ/ VÊ»e˜sçÛ!$t+3¬K­®:J '–9vˆ€.fsØUÉ!T Äùieœ8ž(ôùNGzCÒÎqôÀ‚b^ÜÏMžÃ à°=çÓ1·¤IýKœ'úe'…@å‡Àáù›÷ÞE©þQjEAíÈw¯LyCáòÃ`-ÁsÇYw‘œ‰<'UÈjµsšŒMdD5+e“Ýð¿]¤DN.ì7¢,Q6¼Ý%]Œú‰(’ÔºÖó÷ÂVª„ËÀœ"?/!¹,â„}³VTÁ2ÔŠP­Ž­•CÜ‹ežÖ$GB­ÂËí"·êŸZR¿ê…1Õ»2ý:#y]ýî,žUK@¼qH[a9n`.Ý¡Òg´HX>0ò7sN«÷ 2Œîs3ÎΣ‹£:PsZ왃DzÑÈwkŽ#ºÏ¶g°:{Ðçìy°hŸHÃÖi‹Ù5#Ôôß_<·÷JÈlä÷€-ÀÑü5*r¼åx}2@Ý,ú¬”§’\Ý’›[&þ¯1ÉTèiðžÙh´Î¡*Ñž§ KM“~ÄŸz¬WÒª”ñÕf™¦µV2ÁpGqÖ‹)+¯AZYÓ&ëLŠÌЕ§òµkO BNÖS³§±n@²ø${ÝÐñ~w[ú„ÓþOmåhr˜ó¨®Ò„h†s½µÆ^¤Ïß]mV¼üÎÛ•‚¢«Û=—ªt˜ô’Mö0éEëkkèí–D˜¡å¹•QJyd\b©”5Œf¶2U`Jké±²@ƒÄÖš•ŒdéerV>y!°VûàŒ¡núÐ{ï•j-R£^œkq€çDqwÔ«ÍS=Ö™ë’ù¬Ò“(¿ T-ë¬ÕôYQÂ2ë}8‰WH¢vÂ&åLïlºÙ,«GBé^-+|’àð‹½°ÒqháVë¬]…QF…®½Ì5‚ÉJgçÏ´¡€IhF›•~Ž**XKÏßónÌ:ÛÃKL•o\8ýѯé8oÙû– I5îçóxH)çÁb Õ(R®ÌJ†gaO£$Š~6-J\„ã)"ØtŒàºkH%J *fК ±™(' àÉIùT.œ1—Ç­/~ä«?r]DB5<­ŠzyjÕ¯Àôfn ·aäÔÛSËîbÛHFÈ—ì³JåÆŽ/B=ª[3™Ñ‡f68£elðU0:«l¯‡æÀKhª©T¯uŒH·ÉúIþ:û©œíP®9à/°M_mïG Gtf`Ôr¤l\-Ë=s¨~[—Ö­3õ‹`¥4êFÑó>Þ~ýýšÖ¬Ã+‡/öã¥N¤±öræ2ðb˪cÝÂ<¢¥ÿpÚ ÞÉØš¾€j0-•Q‹º×9´oFc¼?›\E)—)ô÷kÅF›{ŠÊl˜éóù{Åõ©®ÑfàÎJ½Þºá‘+Ï/£é¨â–Iñæ¶1y xæûug7j–v›I«H%-ÖãMïpFŸ>té íä÷‹­£^·NǦsR5sÂaO€tËà )Áè"ëÿ&?Óüæ3¹cb1^˜¦;Œµª=åôò ª×ÝÝý·0ˆHÓ̼̓$_Îkæ‡{åq?À Ùt «±ÎŽ\€­x—%…;ö±oÈòÏw¼nF}£+ÊðNºO7¦”•:^;I»‡JÏW-~”+ÉD8€ìä÷hÔ®yÿ/mv¼À•‘Q:LUÅHð¼.¿ f_«-ÝîqŸ¡Ã–)©e âKÚÛ4§ÓAi¾Y߸7£õ†S\±­eîTdsi³½}·lBÓL¡åW$¯jÜ pöd¨+`vzPZ…¨^ >·ÜЇ…;`2 R˜Ã6xaßèé Ø•¬o+4RÚ»ŸÅeè†'íËH]a_íßÛêâÖ¿·%òdf÷Ú¶%!»™I6™JQj%±öÿVDÛ»Ýõ ECDÞªà¤ìˆƒˆDñ„œ ><†]@)0ÖÓS*€úiÁÍJ•x$—Î1 ¿È­œ l«ÜÌÏܶb¢1ãi7ªŒ™Äšˆ—0±ÿšÒŠÓ"Û ?;¡a„àÛ¹‰`.|8Ic¬ Œ M‡dZö&ŽÑ“ 'ïø2Ó ÌÚiZ°:Éèœg€à”NNhÊ1ÆqÇ¢ÞÉ0‡Î‰mr‚½MÎ —èªiγuõ,Ý<”×ÝV«#ø cà[¸*†‚¿Tˆ»¸csAR†’ËåÖs#@hî%ªôö²á‡é‘à‘ÕdSe× ®£¢¬hš!((IhY"ÆÌ7h¾•±²¨¤¥ Ú"ûP«º5>ÚUH(í•â …ã.1û†iULfB\f\bø”z¼¼óÃöÞËe8kŽF-霺 ŽúØß‰ýyùãÑ–góƒGÞÒJóÓЊÖ?­ì?ß;ü`Z™K&j„±h_¤h,½ìýlµZR,ÌÞŸ-–…+ùäW­|¦×\‰â–à—d6'«ì³4éK4u>ÉêïÞ-__/ÿú«Å™úÒZ¼W l[%aÃáÜJûþQ%³˜ö±8gH)ñ”õ|‡Kã`) ܼ¾nÎpóƒ\ÚŒîrÀ wˆ’]_[¶f µ]=T$7B‹)~Vñ¹Zf £ÅÆÊY" ¯c´{ó{ýxešÑ¶m5š^WÖ:Y\S½ÒÀý^ã]t#½ÎéÆêˆÍ-Û3*ÐÔâ“¿T¸Ù® ¹†§…ã ¡ÿmiÔ»Ì?iÇJ0–‘Nåã¯gœÿ '¶Çõå %XÐGêŠÕðè¹=« ÿ–:‘5Ÿï\ÂB¬éÃo£¼àóÈŠ°¤ž±O|¥Û .B­, ±Ž×€Ã:ÕÄbÌwU±*eÐÅ6QÄ”À–G¯ e‰-ïPn öGˆ`Ù‹24xfæ¼›ìÞhLÂ7zE]­¬»÷À=¯A×TWn³pÇ»]±e Ù²¨½V© ì¤)ê5ñ'uhÖÂŽÒª¬•†jßC©‹ £Â}=÷O£êšn‚"²ÝŽ1þ®€šÛRþâªð‘ÕÐñmr/ÚRœª¸`ìÓÝR!‡\oÓ~~N9Z…‡¨@‰{¢E,‘HØìÌnÇaÜ*cñÑ^Á^¿‚¶M"öî‡ø¾Ö$Á©÷ÑRv[|z;·EUðñú£G »-ŽŠ`8'sý÷¢ܺ>:-ØÆ-Þ1éSmF’0óÆv)|žüz½Cfæé/äg$ƒYóç1®Åñ‚ÉžéÉ»AS£—}÷p*Å¿¿xî¹)VI9^îÄÀ Yrvfäøõï¼îæä—0}x±Žn•SQð‚aç¥ÖZ6Ó}˜5‡=&'sm|RòÕ}ì‘þ yËU}læHˆ¿åH¼ D\¿ýxî,#ÜΟ™tu‚Yáqq-:bÝŸœLrŽÛ<3yðmÆ©^W w^g£ ÖnŠl.€ìWqïÎ0œ(Ôv]¯Ë?üðÍ#º3/žw¾´%ëˆÓôÝå}ŸÝ«Czt'Ç–NÕö €TЃôéž'FWaš³É}¡&%*¹pZà6~Hêccq€÷ÑÁZ ã 0‰žkÀý 2.áÞÂ.åˆ]øj6E¡U(¹È°æ&bw€œ›ÈvÞ¬3ÕZ-—|߬w³ôd!ŠÜ³3þ Þ‘9¾sGGyŠÄÑú¡tVTo¦ÖKÀa¹ûpíƒòÞnÀë…yÜQý«'ïæq¸ÂËîgùdý˜£ñ¯×—Ñæ¨õÏÖ¥ü²bpÜ= 2¶ág-VñŠ*¿8+5f±§&—xí#xS¬a:Öú×í"GÊC×ÍheD¸á %T–´i°‹¡vñ_µÆ'#» âÀaǬãÍ´ýcëx¹ñ~­¹~³Uo-ÝØ\aümŒ WªQTÔ(°FípûûïwŸad¸RÅúÑU÷دÜÚø šÔ¼Ú¡Ê^ÝÚá/û»~·‘J.A™í—Û‘_$&V‰Úáîß÷Jà“ïnÖ³ ê@¹‡»{‹DÉýb^ŽöÚø‘üì~:vý]÷­²a9¢I¡ÝŸ§ÃÎZSŠÂ¬³ý‹/ô^éän×Ϲ8¾àoðF¿œÒ½T !¶Káo(u/úyûõ˽—ß#á—ÙHÂÑëÈé®ãËÀa‚¾€žÎÓÖ(ôŽ1žj_ð$Tnýâ åÍäù‰q£n±a¦~âsÖàño ¡ép‚±4™€b¼Dq߬â+ àFô†¦ 6ªªª3ÇÙŒBþQøJiß#ŠÅÊå¿8…B—ø~ó:±Ü ‚»Ô‘ûÞ ã&FðªiÊÁe´4ôa­lÒÎsQê’ÎwæBíÇXxâY¢:õ¤ô\D¡ýà]ce_râzyÛѱN_ x¤\æ§´ió(jCÕ㺕Ð*´„£Í§“ÑtÂzZ’+^Æ ¸¸‰zã' …Ä©U• *á>ª+©Ö:¤ž,FÁ‚¬-ÜueÕ*ŠÅS»0&ΰ=Ñ($¹Ù“Ëí4…-M#ÀvPÌRßTpí(7‰Å¸£Xµ‰Ï©ŠiR H›ª€ð8ü²&Û©§¬ä: ™X-Â0çÄpBÎ]ÅŒ{ 'B«H“q÷‚‚8¶Þú:vH¡þÜk ËDÅõñЩj@â#8’@EûÄ0¹1°$CÛ¯jÏãçMÿäGèk_È6‡QÔŸ>]«O—e¼ôu¦Bb^n´q÷ɯۑ¿õóó#§ü±FŒ_š ÆáíÅÕ2'bú⋊ñúåUæ&ëü¡÷Ò1nz2xç±Ütª*g¸>$™m/ëNê¨Xo˜ónã¬â½¿¹¹Æ¼²ÜU¸*IÞÝN†uŽÈ·BLk¯V|ŽH&‡h¬z˜åBÌþGl•*ËVJ;»©1åçõY¾ÇÚªÖ#áعñ>e½ˆb®‡ i¼ò#Ì»(¡­¸cÙ@h¯—3ݸºfgTôÒ~6ÈøöTö,ŠôÛëé9óÇÞPdm2š§¨3ÕGòúXm2ºAk¾ä¡.¤Æ¤z•€D~ž+;÷d‰«âƒ™–‚ž Ѩ¼ƒcƒëé>¤kO,Ïâ¥4]$“c!䪴¨•9ej7>„U‹ÌϽ¼”Z^$³5É×c½ÄÈ d:¨×-r°ãâʺm‘Î$N›n¸7ª×¡nóBåŽÎŽ¡h]ù© M7¢/1¦ŸW™z$(¬~×xµ–À.‡VL\¡ÑfrP=m·šM8`ŽÎT̈"íê,+ªw0 Àû2B ÍS,ÂDé@DP+ùÛt<ÎzjØÏß›ƒëF~¼MÆErÒóH_LAØÖÂ&%ª‚•û‚s»Nä8É»s¥ÃˆX蟿ÒÙ %ÒU‰Ñ“>Ñ4å9±zSm¨V=”JðD«ƒRCz Eº :$” !‘âN4Át0"ç>ª°Õ£zÈ´“ÌL-½Ä[h´û<§…6ºŒ q®Ž¦ÃËÉj §hV\´FY÷OÒpÒ›„ÍvNx2:bÈ+ifPäERãì(òGå?P€Yâå`‡:3“žÝ ÅFÂ%6q‚´ÛYv08'ƒ£ÉK¿¼µµÅ:Q¼Újµ)‹(7\kµVãry†­S±ÙñôÇ­å7­É;ÛAùØÄ@BZ ¤åQŒËpf{.=Hê+ABÆLv8M? ‘ oÅïŒvÔR/.ýVö2r÷uõ™Ñp‰R©l„$“¥bîôt|6§uÜ;Ñp +ná-dº¨ÊâDã ]Â0˜^i}Á¸²t 'õÌ}6/Í p,ùt gXÉd½2‚I°R‡ÃfV~•oÆh-ÚŽvÞÛ›*'á…YoUçS¦]Ñø˜Åcsý=î³*D õâÐ3ò5#×k š T©r ²åû[ùÀ}œüÞHÀ»Œªµªw¸Œœþí³dà$)„²¤U$l©Dgö*tÛàÐÒ3]½´U[aðPû¢K3%–—¹‰b9qÎ]‹/ˆï~…´®Ð»ÆÉïüX²•êOµ: 8kAê5¬Öz ‘-Æ©³j÷UpEΆzËXÍÿQËÙp.ç;ÄÉï…£ÀðX‡ªäC¬*+Åûµ² m¬5Ú*ÑÆšdw$%Ò­_†²H¤U–õ’ïM£ä n˜`oXf¿­Ne cLn&Úk¦$=ÕKíè¶Ô ŽæŒŠü7fO}!).BÛ…i‰¼6ŽK8)´¶ÄÉLNhS¿#œP[Êîû÷Æ Sæ5~i7ʬ°µ®µþBï¾1Úðúf­h¡¼Ü@Ö•ÕPÐZ2á/IFdôRÆ9YÛÑJ¹žVE^zh1tcQ-vÌé*ª˜VÝŠ §W5pOZã8{ü£Q‡£×€”PPö×CxïFÝ*XÎ+ Þ< íÈÁ&Ka¶-¤«­Ä+‹«‹С”taçZ§6,“qTFô0-<0=_ÚÚ™iæ¦>/û©ÁØ\†d4΂)#ƒUIœ¢¢‘ëúÀḬ̂ÊQþÞ4#þ=HF7œŠ#ô7 óuê=;E‹Ü"„çǾÑ56ç¹*mq TF $ÓOv"8Ña‚EmÂJÁÛ9YÍ$À '—5åÎé=ÇuÆÛ`ŒNMy}ÐlVù«Øk¥Ú=BµŠy–1 6ƒÎ.7ºPWÖÞ ÛN1óY8[5x;;¯¡ðü`sÞÑO'é\£³G.l¸(LÜàGBµ° Û¢Pqƒ ÕÂvP‹BÅ ~ Tµ{H¸ÖNtšâ]%×_«{ð|<™žOÓ"½ÍÙ5ë$ºÕ)ð‘rAuY*Ìh¯Ë‹#=n}·ø±Û#tw:ÎaÆúêªð埖…jä–¦Ô̳ÊÃ\èr_Ø jÉ܉ »yÆ×tèöîgG5CyéRla¢èÞ•Ú+†Ý·éøšb‘D” H1ÃâdšQyAž¾ë¦£ ¯Hf%: –×â2JÚ¶2çÝ”ÎQºT€'š³k:÷h¤11Úˆâ"Ÿö{Ñ?§€Y6ÅÐI‘çW,H§V6¤2œ¡°¡˜WØ”f&vVÎ`óFŽÓݘ;<è‹ú¤ûE«¯CZº˜b‚. c5‡õF¬üΈ8x )JslN³Ðœ·"»£(æ½Æ4íMW˜1±CàyÉG;dk€Êßâá}D|6Åpg¸]¥éPClX"œ?C–ö‚h™Xò(¥@9%1'õªþÿP’ÿp¢ u„V à@/ÄC½JlÿC‰ì>–·õ`ô:+Ÿäw¨ V½ü›ª‡çè i«…\HIÚGÕœ£\ôo ¬Ï\NÎfÜHÌx™OöŒÓ¯È¸Ÿ:縱¢d¬àe:"³D{CÞ律@/«Í^¯qÈvãÃÂ?yôh}aCZ®DçÚÑîGÛT¬ùþ¸¶´wiKçtJâúß§*"=àiÿbŒsZÂL«VC2 "MÒôbJoà Ðw=ÃÉ[m5Ep©v ´eO¶¦Ãuätg|²%º—Z ǘ²Å4ñhã«'k´kÁÖCÈÏ&×h¿7¾S[Q;«=zòôц”̆0;“|L[Í ë v‘¢—zX¦CCXÁA Ž%õ¿‘zÁÓ^b¢„VÚ°êÛ—®ê/÷ `¨— ƒ]Ê­`àÃò6Œ¨˜¯YöÛŒØ~?¿RP0/t@ œv„ÓuÚæI­—¾…?íˆ÷Œ·)ИFŒß4i²EÙöQÒ™\ ì‚­–l {µˆs’`ƒÅ´gíi6‘ͼÍ[»²…õ£6íÅt2‚Ù³² ïôMÇ4&á^ƤKh›N¢ÿɧhV ôø:™ Ø./²f´ « 8ÕúúW_=j´¢íèEò.LÑ.F]G/àÀéS‹Á×J†ÐÈ §=Êb-ù¢¶_ïoG?LÉÐÚJÒîÅ0ïçç×µàûåÌØGÖbu–óêÿ&æê(¡€wÑŸ|õ¨5BoÊÅÄØC²ÉN§~¶ úFœ9Fॸ¼5–©ÌÙÕIá[µ Cõ}}¥€ÍÁq]5yíSîÆÿØhZËÐÏ­$!Â(DŸó‚i‡Út_ðÞÒá~ÜWØ'§s•ëlH ÷•µCtœýÉ uN>kCt-Ó¹?fdЂ“¾!\ô%.ý1AF/ ð³Q›q®qÕ´°Ó4èÚG+Œ4}4¡i_ÔEáCŽ»>bÛ¼ò8Ÿê(âaúó—Ÿþón £OÈš[ LCñ †LíþQV4W*áÃÖ™þejT%ÄWÉ¡l­/Ó>¥)í8q‰„­aŨ®Ôõû€êEñiÀtvÇå÷v·Î²˜øâÙ\AÛ{ËBÑÖ4¶X}ÿ̯݋dbæ%ôB÷Ö·¼|¾wSÐ~€öðÉã[hÀaB²¹”ûÌþsòÉ”¹/NÙÅoÝ8Ü©ýNBúS‘Ûqe‚íEæ’·ûoq+"„hýÎôœø$H© ’ ‹j¡(JNóéÄËÆ®.š#¾2žŠÊXY”&¯™?P{´\SQH§nƤÒ#ŒqÚÖ*tÝÇŠî#óIÂ.hPl+é-ª«P‘«½¢þþþ QhÂfH²º‘ÃâÇMÎÎàõ“SØ–,o»Ôî‡SÂV ‰Àwr¡Ÿ2ÜòꆯœQ¨ˆv(ïÝq6Ònjv4ú¨˜‚~"‘Œûó6Ø-‰ûl24_8”‘_à{¹çäè»|2Ÿ¼C:ååšv6y©¼ˆWÞÇú¥©¡qeLàRB²PŠR³‚w¤þú8í³''ÐÁÎ{„ë¦Qö‹Ó]À5Î^4꣸§¹-Ív8'ÃðÍ‘ÜPÂúâh„NW¢³©ðÐw`« äÐiÐKÖ gõDÙ‡º·W‰èª£:§¼ »ŒO›ŒÚQY«£gRgjt ë ô8-Fùý‰óÒaìÊ% üó‡ÉøšU6t!O=JjJÎ9«÷2}V©û^´wf‘…½d?iu3À”kåÝ·ØlbòxoÝŠ—ÑÁªé Ž{'V}4f7A]̰FNU¤ãz¨Ô=eà´ï2¶s=Ìž¤÷œãèÆ<˸8ZнN»¸EçÓ1·¤‰ýKš)úiÀ”ë)„ÔüÕ+á¢UÿðJéqˆ®¥´T^Åì$é= Ö\wœ5r °ò‚KÌ Qã0Ñúe (iƒÝ#©JÓªÒÙÉØÅl}m-d æA)|«”·:»‡]8¨ÈÕ€)+'Ÿ7•Ä 6׬d#ÃX/³$‰>­„µÚ½;ùÔî½×¶H ¸q®a›Có^4`*îª?O«§º¬3Ó%SZ¥.1·>ÃÉp:hªäãMŽŒ9ÖÇcÝÙð¬ÏÁ›š†­h7‘fYEb˜ËñKH •/Ä^Ù ©9´|«Õ Öv‰ }ÉØÒ6˜A€\'Ã7m ïNÓ1C“™6IO‹éù{‚å½²7ì¥ïÈè‚R.±Ã¯é8oÙ— NG¯fć‡óÛ»°XC5ÚT+³üv,ìi”DÑϦE6În¼2A°éüÀu#8V“fÃnŠƉŠ÷–£Õ~”F“k™œ”ãbƘ™äTæ¾±=âí¡åº¥™.©pu‘K W µc(í¼Ïó~/¾ÁŠ‹!¨QÒûçt’ñÝÖ)Wè¸Néxª*ìD] M’¡¼ƒzþ^+Y«Úˆ6Cj{õ~ëÆvëçùe4UèÙGR=F³°a˜æ‡3‘%æ©JÐùT­N«=KP¬FF5d†ª;¦¼ —‘?‘óÛgõÕ Ò2Ö†2*¥Ú-BK(¸tªáÕ{!5ݹê' lÅñÍ_„åàT¢ÝC×úšU ÊfÏ ­Cw…(e¿#öÅ…Qd²H8gWØÖ¥ë$;O°Ì©®ì…¶ýúû5}ý?^¬¾Øo”Æ•ÆJv¤ËÀ¤.«Ž-ÊH=jð¨`†vÈAj9ΧI iOÓ-ͱwée`[ÈØgÓ?£17Ÿ½ô…¶ôûÅVµÝŽc¤¡ÏlûÀpØGLˆX†qƒ€àþŸ’¡-ØÉB¯µ¥7½³éï2Ö 7Þ^+µ™Y<"ɰc±10Tq£éÊjÞ²!a˜³ôð½¸çL€ICaÜÅŠ ß­íO í-¢â R[sÄ!Q?#å«òkd‹ðv#V¼`3¶€S~Ø1󥥯‘=¿¬|É©<O®ëz˜Mt¡êØûÜ¡Ëu…o!ÔjòV€ãñë,t¯&]p³ðš¼M²>A…޳R“‹x_}{7“ex‚T§¬KìÉÁ€Û\ÎàèiÛ 5Z–À¿)éZŒ*§Ð[IGbsQ8š’g:^¢RÕñ°ÁKá®ÀgX„z%Ón@ó¹wë nà S“Gí'¶F¹ÒãˆÝYÈï„ =i#F ±ö$]è ­=SgVÆéœÕ€àž6¢ÿ æwäíѨo4LÞj÷éʵ‰½'Ãkç*pmQéC­e—r%zÞd'‚°?1‚×6/O†æ…žVUŒh’‰‘(îóúµ‘ðµ:ì÷y:œ˜uÈYÌ^™§ÝÞ¬ƒ-fI±&f6×`Τ ˜ü£µã£öÊ£ãŠ>ôв)O3XžK¼BB¤<°Î|9»<îŽÏ•"v…S‘z¥¥SQaDõê1pÓ }þ¸<›Úk’ÃÎza_ */j_ŠŸ+ Ï=Š[€-¯Ï­_!Ï7|Ń`ÂÕ;˜Ðr‘\ºG&zgÖòÚ6oáÙdJ³+Üab9\ã½×ùêNÑ­$¼P“Ð׸óð)wÁ'Ö° HOuxnò•à!¯TJ…vn ñPDk¾#´ÁmÚ9@ ›ÛXL„hl¼íV•‰•åJJÿšÒ."O¶q~v‚ B¸sLø\q¦Æ"ë0/È: —›š›=íó]”!åÓAdZáPì 3Q‰Îy¦A&íä„&þäò%M†é8tm‘ŒqýÈ)ä_5íÅî–âÛáyx¯{íVÇ&¾Äs‹Ðü ©3qÏæ‚¤ž$—)ê XóŽ¡1šèl0ä»=˜¤4# Ï úö#X‡—T@4Ct‘]2^Ž[ÿ„ê13îßaa aµ6S23‚™?mǨYÝs*Á,”viKñ°Âó—˜—}Ã6+67!>7.‰J=^Þùa{ïårŒF$-霺 û¸´3ûSó$0Ï( ÏÈ»¤—æ'¢³z9ؾwøáô2ŸTÔ3Ò¾HÑôzÙûÙjµ¥˜ã˜½Y[lWòI°Z=N¯¹¼å/ ÈŽV ¥IŸíNå`«¿{·|}½üë¯ã%©6¥JÁq«$Ò¸ì^é8ªæ1ÓþÌØ}¡iû#—¹¶.÷ q†Ü¼¾nÎróÇ\Ú”îtÈ xˆ2d_›9¶f ¶]=XY7BK-4£«8`-V`ÆF Št"J¯£k±ù½~¼€®OíX÷‹¶­âÓ«ËZ-…5™qÒû ¤Üï5nÑGw0ÒËî‹ìx'­Ï˜ÈQ¦‘½ü³€J²ŠòžZï®:ü·¥±ï:B¤®+AÀO#¸õŒÝ†pr{Ü@âU« ×±áñs{Vþ-u"k:>ß……X÷ˆßFyÁg“å[¦žñ2Ü*…uó²¼Ð³>Ò‰îc±bæëžX•2èbÃ-bQ`ë£×ÚÙ_Þ¡@ìÿŽÀ²é3t&hðl:ÌÉ·æ­t‘t©ìÞhLR7zE]­¬»/À=¯A×lUWnãá‘87t@t±E'ö^~·÷rïpïo»xë–±fçûÝ×?½|†ÎùÁþöëý½ýçTjÄ©"=xú8D¯,.øÓáO¯©à™jîàÐjhÿõîÁîKz2 µ´—ËlȰ¿ûú»Ý·ò«ï¡þŒ ír'Û/Ÿx%NC]ìo%JçÃö^C¥Ýׯ^bù‡¡òË3Q/mï(ä%zpª«Q *=yùê%X‘7FæAá=é~'ˆSþšRøûˆ çïo2üê‹$âÛPöf FÑ@O˜~Å ÚL~ɽ¢˜m§_èwAˆZ Ôp>0€=°y`õûÀt³8c¸Y^&®HL5Nþ¶ý|ïÙ6Ñö8muóÁcã£ì|ôöÍÊñÑÙhˆFù)þyˆÿ$#ø÷¿ãEî]ÖÅcÑfy•’â¦VR\ªàŠuÚò‰ýìçkÃn×)zì­A2é^@í Þe—"¬ëñ6›'NX;ÖrŠÀõèVÞŸªÇkOž.êà=ž¤ó|»_9|´[w´{• £û)È}›)|ÿðÕJ{­¤Ûš^Þu쬰c4zd³_qnåÁhF»hàÒgïAh€Ž’*Ñ´Øçê ÃfN’KôGRÆÏPm½‰ÿn­|yH¶¤èˆKOÔÕfQ£êCˆ¶¢Zí;xƒ¦il»£L)qË‚¹Y>é¥oÑ¿€¢Í Ÿ´è¦¿OÑÉ©*È !a±Å8eä¢[Ûåx€zÌÆÂXÇ脯Æ×#JŠ ÄÓQ’N=ê0V5qý¥\%8þ6^Ølb%:KëKVŸ¥_v– FqÙYÚÛ]Bîì|rÑY*€P&KÑVƒÍÉYw&ÐÃURpPJPxý2½ÌªŸþ;ÚxÚzfý>.¼Iò޾³ñËô<—Ãôl’ž'¿R+9Ðl6„)¾Ž ÄÒ6óUë‘i%'lL&Ðóå0¿â 7C 5úm’]&ý3hdXœs`äøò[GCâqtšOq]ˆÛÄëiQd’^÷ÆtZÄð÷Y_·ù:/ʨµ¹:ÙÒ'âæÅV 3è† (¥Ôº€Z›«ˆð­ZmoÃì±À%®ÝÑy??¥QO‡Ù¿¦i´÷¬0¦hX¯)¡`Ùb:Ik;ï»j-ÜDÐí)ÆUMz=Tž<¤Å~4Úyo&y… ‹b=b5ö¯œp’Ë·¸‡¬š ¼;zÈ„õýÞ3$%c;V†1^E᪉±Äœ®i°”•-® ` ä= ()¢ À@O`Qõòna…e¨áÙ…Š¨:|âœMJÞ|IŠ*•f@ö~*O×nCÚ@¦#Ð ±À3øºaš×È'Œ!*•R´£­ªÖÄ­‚OYvQ¶¡b/(Šo ï‰C¦lÀäY¹j–4ÄM¡Í»×’• Ñõ`nôÀé TêeÄ%½_:|ýÓîRæ4¬îYúnûùX«*€»ÂŒú/_ae@ÎQéþȆ½5 n\G¥‹€ü}ª8ƒìålÀùAô¬xè)tnêÔÞ3Ùö ´1Ï·|AV»ž`3•ÙPŽ$S(Æ8“ÏEÌùÑïpL§xGÃ┹ôcÕ÷hGæÆeàůה˜eÁ¶Í_1òÐ ¹@V¥qW篲­Ï¸ »n£Q¡méd5o£:FolÚÇ-ÏÝÆßbÖk‡ŒÒ5É[EqxŽë1¬ý!βº( ÔðacVgañ‡â~Cy `û㹃⹫âú÷zmE € òÞ´O „ýIN12¿ÚM´°.L°ÄŽÂ-%ަNg.ãs™jãn2}®8Z^$#­tA¦»ÌŒ!¢[nûzìÓ"-䀅s7ÃÌ·õ7*wû,/f Ðî%ÊÌ–«ú"³E!5FÂÐÖq¶*÷/›2 ª­Ìéz4…S²hýù{ØMDÈd|íRž½SCÚ Z?­Æi3~L¯IñQÝPÈðýH«ûŒ†þVÖsà~”‡œ=Køb f ?EdM¡bA™FK ¶¡.'yÇOIcõøƒBnÀ‹…CRÌ38Îæ©­¤Ș­½‚Eþ%¶übmN’.üý¦[´ÓVþk+qBÆû±å»E‘¶@è¤ýSŒYˆÅ£:l˜š,Ÿ"r¼Þ§Jx©æ+ümû¹þZ#{=®í"ËÀjú.w3Ü‘"È•ZžeE28ÍΧbWþj|žPn”s¶wž¯ì}ÿ|÷ïn7Ùy?}'Ô0Àw2æ ‡ðg?…)HÍ#þúÍtÀÁ›ÑOÃŒ$„É5®—Ùp˜ù$iÖ¬æ{-)¿ú¿ª…UJÜt1ôkÏüðÜ& ·èùˆ<9ÄB!«锸Jûýœ Þ¦[¬\S TÕeüä@~œ)fé"÷–šÑÝvÃÊÅïè±ÄâR‘Žß¦K5Rª‘ç,Á™p¬?K¬áâ˜ë£«‰QTNyt‚ÑÄó¢ãÕˆEò.¨þðz'¦ä!òô'³#a~‚(æj^žõ¶kMmsÐTøjJ Å-›²¹/$î¢r•ÚÞ%‘rdíè=Y‰;‡÷òmÑËœz\8‚ÈÑÈ¥«ªˆäÒ½f°gÇ#gŒ9j:à7mŒ˜Hã Ây¡LË1¯ª³¢xѲœHÙ¿_hë?›XM×0øñuØz(í‡\…à¤cA7À?÷£úPÜ!–禆vØrŒePg xazŽqJ5ÌÚ=ª_µx\éz°RâêL@ Ò+“º\?.÷€ø0ŽM‰Þ¯Ð>Ì›,þ!AµÍÚ³ãjß*Š62°³Ò8Ë{7"¶é÷³‡Òž„ÀÐyöà+.ðƒƒR&¡}‘v­p@¤êCÌÚ¼8ê¤&Ü”öÒo ˆ_É»ˆ›¦¨ýg¸ðP=­{çÞ ¯¼ ©+dí8ºgÄm£7»•ÇÍ®"›]’º I/3ƒ/¡Eq/Àø’š®»¯ï¬®Ò˜Â©˜V gC¾S ¬î¤ÅNH`¨©ú…¶šFceÝkNoË2Ê#ÓKÈzU…•âìOΦ˜ uÔ)xñ0QV Ê cù¡þeÝq9Xuqy‹LÈêš›·$¼7Î{@ªÉX<å¼Iù:W«/¡­ZŒröVÞ ŸÞ<­ÉlâR¤É¸ -á‚íÔ—Žþ±tü`é·7ñÑ?ÞÄÇÞĸIý—wsÁˆã~ ã¶èÆt˜GÜr«Õ*3cš6-ºÛ­¯7ŽÖ۾ɧB‚4ÚUaÔ(0ªŽ¡Bõ©6ÒsÚPGÿ Ÿyc -íuSª]I3CM‰·¡–Y<ÀmÜ]„¸àj¤ßæ½ëy€Z­Ì284–‰½¾ÄÐôq²: ) ž¨ðuñ›¡øj¹°•gÍ®Œè;$ëúY¹8qlnTH”ø˜)UQí3L_ì­\üÒÊ·MØýrrªëxRÃI’Zø»°Åþ™‡–ÀçèAI÷Næ¿™™¢’õ6›_ƒÈO;p2,®Òq`fh=p¥»ê¢ŠX·y?оÙƒNТ¿tMÜhP}Û%á9rÏááÂb\À`dS/˜‡¡Håô—y ²û€ròYïå_+/¼¨lBù­’†“ìÆ–v/‹êÒ²©hÊq­ÉÝX!V'W¬Àyí¤¨á‘µ‘Œ4¬Ó›F§¶š¶šÀ“ªhuINCÕâ‹m^וF8®j|XK„:qfFWÛ½›=HÛlÜαg«û1³j±º8~”êL­¬Rõj(y 9jBº:›AFë¦'gV-5¥ªºèl×<è¬?Z âù›ÃBÀ;ÏÂn#¢>BÚÄ ÃÒÒ·;϶·q…\‘Rbég+¼ÈÌ.6Á¹=_d&½3:¡‰Ž;žæ·JL1HÆ—vÈûSÑñ,œ²R¼½ócÜ4àÍÛYªÑ¿yu¶EgL€…ˆY”¸øñ¤óÃÐȽ!L‡z8/쟩OP_ô/)c™…˜¥gf)–Øáhtoð¿ËÞqpådœåd¤[ Õ$)úõĨH§½|…îìÊ ˆü/#ý[Qg+ú_üþz%œ ?ÓÓú8Þ¬ýï›7¶`¶Çñ›õ˜¾D{€Qe›/ƒµëoŠ7_ÒiàKïÿúf#ÐŽ‚ÎožCéøM¼ô&TÛ,¢„”N¯GcQ?—z½¥Õ­0T›GÿØ<^¦b:Hu[Çô—Þl@½@?À·¤ÙùÏ5Ó%t½ùòlœý­7Åòæèè[ÇËØVüÝëíƒç^£ãt€v›_îE­­¨ª¹=ÓHhðð87ßÃàn`l7³§ øf«mŽðŸÕQESõo~[ýz$XªÈ 2 ›_š¯/ ønõË7W¢7­™­þ׳W;‡¿ìï‚”mq†pcª FXe“GÙ±@w´:‚¯é¤[ÿ£7«_cƒÞT´‰Æí¬Ä¿@Û ×*ÜÎéUm˜ÇlóË a³Ñ_«ûñתEFÖý :¶û¥˜Îä”!-ü‡¬#§Ãx¢|ð’.Øß—õ¯ÿ Váoç“ßú“ß؉~û×4Ÿ4BÃC"‹“Ó.ÐGgé,ÏqmDÅäºOa|‹h˜Æ+ÜÄqCã[P|s¶õ`çGÑ›Éñ2æ&¬‘ã_7ø6Kñxyéë-ÀÀ6?ŽMO°L·Þ¬S?È~°—h1vh¡¥½¤›‰ß,Å[o–¨™%»åɈ7o%C†'·2dxúP^?züxmaC†l˜uŠâ¸HŠÅ*nö}¸5ƒo¢Ðïu?]{l±Å8 בšº¹çÝ>ÞweYÑb¼­ÄäjuçǽýÕtx.Wò«]“A­¶¾Ö¬+c)¸¯hZF¯œ¼ Èd‰Ââ·IêžòBs‚Ð9Œv.pûÁ8Y®ƒ¸6ùÆ 3IjV"ô~¸ƒq¥¾'`È0¤.¬žÔ·étøÌ,hÇ2Â8CÊ𭸓”ø´bs†š&Ü@~„uÚO{ð‹’¢x•àäÓQ“âÝg±· ¯t³”Šjµ½I0¡9¢³ŠJ#`HS‰žŽˆ×Ù¿ÉSþº|0ÀŒvÛbqŒ¸‰£‰IÒ_9¸€-n»ŸÁ–ûœ‰¬¦¬GºÒJ—!‚B,VO¯W†Ý•"YÝh=^=Æ”ó©dyÇèqß­ü’Áì¦@¨ûÙÊw :IÖŒ~L/Vþ'K†Cy‹¨Ü¹˜®¼†5òÃËa¦ú¯d²ƒaßÐ~Qd^¼[8ÂÑ”çS0 f]r« uÄ™™jF€eEyº²¾öÀ} $e0Ý!ùoÿßE’¯¼@/­ï ÿ#¶·òË”J´p›Xkùð·£g)ú4ÀŒ¡Á]–4kÛ& Ñ÷Ó¬—ÒñÌ-¾®ÐÁtœ%•šÆ“ö(þ4 gcö˜Ñ–貸È@·‹"Щ{<Ö,<fpÖˆ”_¦4Úè‡"K/¢: îQC­e=:vÇ“ZÛ}GÑàhëòÀÞûŸ—Ï÷WÖhF?Y[yü䱸…¬iÔWÜ&ïÖÎeïÙîËýïöv_û¹ÿ¸÷æàÁ `·½¿¿ûòÙÞßýõ¯7;o{­etÎ=ÜþþçW¯Ÿù…Úp"·ë߀…´¾Bù`aUÀ-ª®6‰”®ÃkØþÞͰV~ízjòNé½Þ)klgÞüQðîY·^RÀ ˜Ø äŽðM©œBsE)£ŒNJêf#Ö(]ëÜÂrè*C'*ðbV3ð/PI?;»Æ%;6;ÐêWÍ ›gÔ'Í+2yzÐÔ÷×éÀ•™ËÍ‹8/NÐW Ò‰JÙYŸÌVDX šy˜–K¦'Ö; /x"ÎÄoy˜evïé²{ë6f÷è>ã—ÿy>¾žËî±!ýŽÿxÖ¹\Ÿúü^¸µéªdZ“l74 `g‘`‘LM%ÝA«†]‰OrÓ°~ÿš¦*žÛ•$v­Õ^þô¢\Nôˆ9g E V†x–b#¼UÀPÃèëÚóW;í.Z þŒâX7Ÿá;Ô@ö±€rM˜2X5ùx˜‘«Å×µ°ý´èBœ²¾‡ý4‡WÏvvÚHÅÔ˜D)@¿_3Tèæg<ìzY/ú!¹J²,:Eþ'åHJðúkÇÖóžfO£ý}`²’®!§9Â<…îEGïÞGœ‘ÊþÅÊ= ç CŽ1 8v¿²‚Cäh¿A1»F}L@ e¼ß51gÜT¢G“³{t)vr«§­óVS[À’å,òO÷Ø[s ¶±=²šW 'G‘„Z;‹Ø 3Ãj#9äzÈuâ(þ<Ù¤µ—ÁÜTÎáÊ~{zíè»,…iÔ‚àÏO›«‘c¬‡Te> tèa¢;ÓçU>ÛœòaéX‘°ÎIÆÁü év©ç4Ô–-ÖŸ¶« ÛUÛ´f·Šç\8/_0'Ÿb!Ù¨œl68À¥·ž0~c–ÿ£1ÀòGks§O×?•OTÕ§AZ”ѹÊ&݋Әˆ¹LŽ)úñ ÎïÅ·Xr å"‘œ«æ~ƒ¦ô–Î 7‡0›u<J±ûé, à=9&8Ï— AÛ ¬H ÏST'“•*¯PŠn ¢=Ë⦦$¦Œœ¾’¥X×þ+‡¶¹bGíA«rç=@•\b˜zR¥¼Ïz7MjM‚‘ô8Ÿ=ÔHYßMM`%¼Æ´=÷±¾î^´0ÚÿvÉ~Á›Ðš '’,‘Í ˜<à×€A™†L©½p ÁYäÅÄ?©‚wJmÜø›ûš¼Û1j¬òžãš¬lþxO¼æÉc¬ãÊ=k­7ùØm„«G&÷*_z^Kuê{Ʀã7â–׎Î÷‹ÖýÁX>ÓÏ‚¥¥/¯„y¬ÜÉw{Ïw𠈙wÛý{”¾ x‰Ÿ/q¶J‰pˆÃ|¸"Ü&N`o_¤4 —6²IŒHùйg¹MWpšçóS¼ÉüÖÑC„Uá´H&]…†[f\§nù¬;ámÝ‚Iäðß 4»÷J†ß4ìvÖ~/Ьޫ ÔûãïŸé{þ~O K TÁZU¤ÚK/04‰ÍëÖiEu<¤Y’– uÉéPL〳)+"³X}VÖ§M•‡Ó!“—€À{â)¥ fEk2¼4yµÅl»¢Ú«ªØìo†q¯©â]¥æ ŒþñÐERÕÒê­Âí•T]®…ÔK3ò]Í›ÏPå|^WÓA}Ñ‘5A j”‡÷}.‚lÌU•Ñ2_Áóä¬rlÒ¥„*©LìÒ³dÚŸèwゲ՞^GÝi—$æ…R ˆ7E‚ñÇ œý’¶“ÓÙŠåF޶9?ó•ÂXrår|îûù{5¡-=“˜!ûÛkê8`O6iø’¨10žIÂìQÀBlÁ—'É[séÑÖy ©Ð:.áuúº_7èëCüú0”MTøÛéD!@÷Eyàxfá€ê»xØ:3匕nR¤+CÙ”æ—"krÉ:ï– ÌýôÀ"#ÈØpw‚>ÆF¯r­j¯évÄTA^ª'£‚¹¶@ ˆ çôî„ø·@Eç(ð>t >KÝ®­Š$¯»I[{†¡ùÒÛ—QKåìñ/áYåå^:a^7‰Ù<-Œo瀾µµÅãzCQ‰¢ÈòR«µ—Ë3lÐÞ÷0ßYÜš¼›Ä£òsOvÇ·_@ÝÄq¼ç%—z÷A©›Æ©AûWºDÏ;Ñ{æÕ ‘*Em,?¾‹»$sm„áùgÝ™i:±ßò¿åe’Q›Ý öTjÖYžxÐŽpê$ŠWq=8ÍûEÃ:Ý d{ö׻|gñËOñ¸qþ,\Y/´fpmݾ!{iݶvi-ÞÍ~UéºóÅ!Ý=­9 j›kÜyÍÝR“#ËóýS¯LªnÊ‚ÇR¢Ìz‰JŸL¿RièóQTŠ@}b*iÉeaZþ.ÆK~K¼E[2µû*69Ý–’Tý{‘5ýû'UÛŸ¢êÒ-™Kܾ/+â?Žà.Ê® V i2Ûc9•wÞ“º«‰šÄ›9|*>›ð_£’U›mObB«9T‹+ÝC‡Û …&¾ÛÕdŸÿW‰ò?éòªâp>Áò ³=³ÎgýÈs½ôçxÌú ÿgØÿi&쳯¯*ÞìS¯¯ÏÀ°Í^·bܬÅùçêü?ËK~úÅÉ×<;Æù%tãcÞÚÏF¹½’É©¥žy_TºgSk³—±(eäPŽ<©q¬Æm"ãÄ–-èüDVËÉøµ¼|y…ß*6£=¨’™tm][ÅÞŠ n™Q††<5;,|€ùD¢2ÜàUþ†y¿×aúÐß4ä‚©`k\Ývüü}ÅDÙ4ºÆfEa­œÞÂ*RŸ§ƒa–ì4EyÆÃ”ËÙ}‡µès`–y“éÒÕfôT1ÓŽ¥~‘÷ߦî‰Õ´È¬Ò»DÇðÄI´ÈÒz5Ïâ€ot«xé¬jR3˜ÑÍMU»PWv–`n¼n iÆq 5m-xµ߯SEôõÀä`[oÌΘìÃóš cö\-$@ÜÚG‚T-X|HÜÚG‚TÍ‹}HÜÚG‚4WÉáA6m¬×é郠öú. báÿèA|5ÌÄÂ4òуøú) "`¿2?4zØ Fô·PŽïŽÅ¨ßd‹ö&¶é ÛRPÝ·YõÑÈ›Âp†±¦ G3E§×5¹Ääc®ˆEæ )1E×ÐÓ„ø&Ïè‚OÐ÷> nÄ£‡­2@$ÁHÙýëˆb¡PÚª:Ço¢úP_¥C•Ók27B‹•qјËNñhO8q¯ºåV.¹ÍˆÌöNN¯‰˜Ì¥~è@¢ù†Íh.§^fÆ2­° 5ù.ÛV‹ò[~(äü®*‹ðØeñ·Wö?ñÞ_Õ\,½@p:e; ÃŒÌådŠ4 Õ05ë M]’h’ Ò–ÍvªÐÓ^*B´¬å¬2ÚꥎF¥8àd6‡“â4éN“ G©:'™TX»k×Jù-D_¦gc#U¾šªb¿/ñõ­uãyXàe/×ÙÝ^-Øj`‰‡Æyѱèx.êS*•ê¦ï&áºe(°|¹%¢?Y$^)W’¾ÝJrnS ŒÜ0LÓgdâII/äðÍ8¼i!‡ï‹¤ûVîEîhP8"´áób{çäÅ«×û?¼R–VQD ÒÈGZlnáTãã“M€,“Û¦cÆ'±X Yv•~Ü,5þŸ¶vŸÞŠ>šÑB·Ÿ´â†mÊj[S#"„É™ÝSޝ< úœõ‘fbŒÛg1M<óG v‚•gë¯ 3q[K×*q¡“r–¬`í¼É‚óÄ‹ãèYs>‡é®´ãœgÂÙZF‡˜fi;v6éN·|ér_±ÖäÙZ†y'–,C*ÈöˆŽ3†á$»Hüsòh²ì2°1Ïe`ýÑ“§OŸ>¤"Oäñ£'-ì2€æz ì½Ø;\Ôa`íi•ÃÀIúÏÒ±ã‚_äÚ²LIÜ‚‹ Êè™ p¶Ù OA ›”k É:ç°L6j²{#Ä.Š*˜:YÕBw= æ“5óH((fýÊšêó1%´£—{‡ÑÁ€#Q˜ÎŸîE2¦ý&trùïž“¼aæOAr+$ºÔþÎ ½±ÔjµœÿZÅH©uÜg¥Åæ–¡ˆEjŸa‚zÄÄDyÄxîm]Xççrfž»G냧$8jÜY&zØ ‘Z/ÑõÔ¹}”™eHT ãÜE ›)`?@1®’Ð[’‹ÆŽÎ ã9€xsÖn×ôíwo¼¾rövp±¶Z¼ÛX{böç•èd>ý1ƒÿáÿñË-žþØ”f†ÓÁ©%™‚XÌ.ˆ÷€Q=iE6£¬ÍÔ݌޵ÓwÀ"Ù[Û€ˆZḠ±÷Ì.c pOï¢ú ë×6¯X«M§4”wW0}«õ—bSw瑈©bÛ™*ó.8#üȬx`k¯ó©8ú`Ê! P¨«ÒÏösÇ©CfŸec€A¥[ãKÞ ]SäHƒ]e°ÖÏÓ‰¡,h±e€A?ì‡"fTÐ –)F—ã¬×ĉãàTG¹Ö®µ»)¬žÄºYíâ€qÙ°£ÀˆÌ,ø)d›¡‚:¢‰èe´˜“ñu½Á#ÙÇób’u­WÚn[ŠÝ4î ’–žúÙ¿†“”˜T« ÚØ¹X±r(ñ%·†Æ g å@°l‹0c§ý””c±é«Ô‘EIr¡Å.]¨Ãö{†¡ÅÈßË.ˆXL0fχ²WÁ†˜¶ô&­‘„ÀMò˜}¨NˆãÅÕ*-“ö¯® ×ïçW…¡m  õ1ƒÁ’ÀØ™ @]]vža_>… eŒ+n×®×ÜyJAë/dEç®èÞ¸@iA·Ñ (…3 ÆÕŒ6ÚC` `/–Udù]„Î…µ à[¬ùÖŽžPÃ×ùø2Â†åæž¶¯ÒbBeÿÒNƃk q?Á|åoñD£¼¥L0|Ÿ|äPãòðõËödÌ—Íèðà°=6Cy¿˜ÒùLUmPüh ”M€.("¢¦:äð ¿>Í\@!–YE€Wõq‘" +$ÝÔ{Mê?ÿpؾB·´fôíóÛ§ý¤ŒhûÅë6â8ëRî5˜ÅÀì¿l£d˜+ªp3z8B]8¬ú~ó믿nO‡—ÃüJ™ ‡ëà $peøâ~Tÿá } GE÷8u€æ H eô<§ŒoÚ§°ÄÒ~>Ž )Õ¿=@Õô·Ûfôâ¨)Àzý‚^¿Ø¸Ðíÿð¬ <3ˆå¦×¨¾ñ¬ù?Ïš/žAƒ¥±aônÒž[_ßãMµK%iß½×áŠÆ©µ [|Þi¿Ñºf;’ž6?;+Ò‰pm~O¶GÆ|HJê`tœ‘’ASZ ¼¥.À3É™¨BmÃÞ™€ƒT  ¶‘aó&÷xÑ=ò.È6’ºx)¡ÔÉ Ô2†ÇÄÖ( >A›(äÿLÏ[*%¸ÄŽŠy Ú» Ç«­ Á#9µ>)~Sës¡Øîta,Ó©‚ÜÀÝ`Y‹%ÈìSÒ`鮳†êœ^GÌÓ-äòØ-¤‚Ô ŒJŽ´.‚ÎDÚ$¶ƒÅ”¥óR"Ù#ÃZ8/PqI#V㤨j ºLð-kÕ&°ª/Í©]™X:Š˺1™ ÜÛ±I®¥E»-:;„-¦*Œòyý0NGýäšÊ*Þ ~[Ö‡¢ZÄ幸ãëÁ(?¦LFÜ+éLô¬é »D (®‹`Ìxùs9B}šþ Ü8‚¸¡Ê; ­¾XØ€¦<:ÁÒ'Ť×ÏNo}ðGù‘|½ETG¯åáµèä¡N$l!5AÖ^0K˜\‡¢þ´é1SrÑ '%A;~¬8e{ÐÝìñ¸@RÁ¦ÄîRޱžK`1Ñ9¹í1æèÚÃÀË ‹î8MXÁ"uSÍu‰%´å)_­,½îõõz’lRvwö†€÷]ÅôÔ&â™v4„Dг«3R ÖÔ¸öø^‰6õÕ¬·%£°"½ºº…ªjWã^ÛŠÞpûFö>e\\/Aò¶mi dQiü‰Ò?]#ðP沫+ðß*ü÷¦U@~$üíý£j4~‹£%†g+šyƒ(ûMQÁË¿úÜVNè_G‘Ú„©,N8‚£3Dt)lP쀌=šÁUÞ,­º€À^¢«’¥biª˜ºöä·ÁtÆÅt,Ú†+Œ–Á´ÜË1ïj &búgët˜Q|S܈»>ËÁÛ˜.ØÞÁ+ºÙÄK$ª¸ÉÎaͨΪz}ƒHÙ>7`@åí-0=Á‰uqŽ~~XH ¹ñê×íº·d© a´(HµWsLÒgÜ4/©M²¦hMh?5;¬mãË1.àHw”šº›z(‹;4ÙID÷,Þ劈ƒ[à÷Vªw`ÿOHœ˜Ô§–ê ëpm>&Ö݉1ÁB©xÖÚ{ÍWE†ö-ŸõÁÊ+VÆlÓQ55I*(ÚÈ„é1tþ £÷êõ ±È@p'ïæ¡ƒÙðž@â¿`§®†Ånw»é×/ënp/D/ ØÌȘ¶^ú ÛñKíð.y{C1€vÞÒI‚«Þ*ˆ A…ƒ˜1ÒukOmSë46„í7Þf¨ø ¥_°e4‚Åô–âÝN M4ª 긢wÌ«FWôeó4—ͧÄ3£†ãºÑ,q@ MEáMÿY¢ÛR6‡é;õô¯UtÔÕ°Z:l+&åç¹Ýb£Ñ•ý[½šˆÞÔd:×l—ŒÍºô='¾Ž5€M÷ ònPÚ2;‘Z˜•‘ó­SMºö–(E"ììòõ¸¢>L„W5™h†”¾«¬Ãúu¯=¤šô­ºC™ÏR·òœ;—³Z)ƒ þGüÍ5 Mq§´âŽÚN— ‘•ùî1:5KÙ&$XGÇø44ˆðoÿÑ1o î¬É¬||óí¯¸ýòÜš‰ûø^Öו=Wyúezï —µvp0²uZ¢†ÞÐyäD?êpÅdÉ’°i¨T±*cOÒ9³\mHb£ôÕÁNN\yÆü޸ܟ‘ÕÃ0Õ‡ &rÏï#`*jÔŒ’¡J¯Ž âêWMá23S— Ëì:|¤|O‡0å«jk”êke¢! *§¼¶­Ç1åƒ(—epêâ(Z›…±ì}¬b° EA¿‡73ˆhgÈܯ.rt‰» «K™0´¹.V2ëîTYët:kÚ`gÖ…Ç•„¨®Y]ÿ Ee)ìïW)ƒar1ðU¦÷ò%´v™0Ê fÐ&3ÝÓN±éq£ß10\¬Nï^cìqƒ·NÜ–@æ6#ï:n{äûjóÕˆŒÔðš­oò1ãJò&ÌãÌ UŒÓM³±§l¸Gyº‰t#vsæ-o¨¯Ð¸öjU¬­²¼µ]OºÉºY‹ÙOëPAðFDsAE[›°&gxÍûãùyœMøÎJøð%ûˆíŽ•ºóÛ‡3sBµŠúUë\o”zÔòŸr™n?/üAXsH&ÝKcèe°§)œ­|¼6hzÈØ+™Nr4«á0§htÚOIç„—™Wʬåáš^ú§ˆÚÏœE{1ʻՎƒ±•Æ“ºµQ ‚?·"/èαÀ8TèÑ#{AW®aB`'š±Á¨¨åºh¾ÏòFéðÀ†Ž¬R4¾å¶Ú«Ìrû¬0 ½˜ t°ü£1ÄÖõ­¹½5à†.lmoE=¶†c†Å¢!m£ä{~cæEö7öͧº c{t‚]ß‹^PÒ¼h½å…rmLßš~'ãkïA8¼pÒ·îòóËŽ ”aUM£¿ÁIYܩΓº]aû»‡'ëONžï†ëˆC_Q_¯hs”¦½:eÝ  ]OrGê-ÁŸNšÌ¯I¶ÄôZ@D{¯èÂj—‘3"D[[Åu²%,1(V_ê&xÂ$]:’h:õäd˜‘f(9P²L%ú¤è§)0ø­µõO³‡-#ɆÀɘŠÞ|ƒ)ÚÓ³×¹ …™u;@—È8ƒÈq„$Ëï£gÚÌ™“3Gk­¯ZOéí7ƺ¾„ŸÅ"J>  ά%8‹ì™ÂæZÁ¸{ÆìeVÎýZ¨[sf|C{$lÚ„,Bì;C±ÖPׇ³F[ºQ Ø)Tºt¬†Ð¹øºgn«fÒÃz^¯aÂl«ÕÒà¶Œ.Mû =¨) ¢ Aâ*GHI'Ši¢•¢dþ>ï Ý"ºn‚ZiY9“ñÏgÐÏ­RÑ.ê?ÆÍÏó ÃünÐ÷Ã5_¦× ŒŽYoÀ¯c´tŽ¡Þ¢d®u¨>;q›”\À‡•®þ:÷~:¼'êÇì$¡$2«wÑP[|YN¸—9ÃG¬º‰$OH8g3Ù¶‚ØðÉ€ùðT\¥D@öU«1­RukŒ9¨¿§| Fãü49­êÖúɯYÿÚPìÛ Ο¡@Dxàb ’â—®4ÄÆEüæˆJºé½´_ñ¶¬qã¶âþ»Çbg6Æ7À‹gß™ê<„òíë kW?: 3-—ÐlÏ­Œ˜›@[,è¸,MJ×ê ©®z ܤ…£ÕÈá[rÔ”6@ßNxi˜Gý|xŽQ¾¦£¹má δÔÝR•,R #âå ¦]}@ ±a ~ Ï"‘\k4èt+…Ž&ÝZ†Ì,úàœÁäÉ ¦r89‰Õ‡•Êìߣ¹ìßS(òxƒK¸öôᓵ'‹²xèÏãýœÑOPxX~ÀÎ ý§>»½+ôÐ~žc¬·Í´×Ç/ߜ͂5Úêfŧ Xb¼,/ÿS SMµ§?n“Š_êîgÝË>6Ñ·Z@O§5š\‚x. ¤fÔ²AÇh†HÚ.z‹W3ìG½ËÁ/а û9µÒŒðòõ€TÙÎãÿ—ðõ¬¢±ƒ4½Ä?1ÏÄq²E25Š|:î¦=`RªòCV,sE«ŠÍmoŸBͤ;yžüúÿ³÷öýmÜÆ¢ðó·?ÅÆª/ɘZKr›ô*–SÇvRÝÊ/?Ki{ެ²+r%mMrY.)Yu}?ûƒy/KÑŽÒæÜsØÆ"wÁ ƒy¹>,ÿ¾,Q錿–§÷à)òórJáîlÜÊçŽáø¼È!ì÷mAMJ2¶x7Õé(q¢ýw(¦ û¬ÓÅ2'«qõ®ÄXmïÃ0’'«$'€)=,öͯ@ìèsÔ+| dQ.†y–ý¾¾*ñ§X¿8 —β6š8oB fÚ¡\ Á¶¢` d^Ï)ÙÄpüù5„9¯Á¥ö] a3ôÀUÞÓéÑûˆqøšlqU»l@Ô_o ªx‹°÷!‡[&"åü<ø ÁÇÞ7‚Ãp¸u›@éIöçúÑGŠ­cCÌ£Á¼¢.ÿ£œ×ÐʤÆ(•8‚t¨Ì$R®³Àðq—šC(fØ®)¬ué…¡ô3ˆìºˆˆZ^¬uCø<Ͻ=ߣ€ÖŸ]çhÄ3!ˆì¶Ëxf¥6†}ßÚù¹Ð>¯@ oL¯9î©ÌQφùÀʦ̰†å&‡Ìély**8§BO hd&# \ÔW˜üƒáaQ¯¨ÍR~=â#;4T¶#aV#X-˜fV|ÁÅAœ¬QÐèóý!ô&»0}K<Œ2¤©Ã J&üª7h B+w ¹0xÏãÃV;¤4ä›o•¤Ò;m$Y+§µœC YƒÆ9[š™4 h^qx@ ghÓÉ~lÖW€á¤¸†q%ä (ijinj•#³‘Ü`^ðÖîwËøÔp(ŒDh³ÿÁ”ùÈbKcÄ¿ F‘2‡Ýç_ƒ»½f uJXrHURyO<Èrè‘ß0H˜W4Ð12G¶A;8¢Ã!ìÚ`ÎôV(ÚYŒ¾øP}$/íxÊGÓ2ÁF8¸Š÷( };7›û¼Y7Ì¿˜/JŠBi˜.'qƒ·ë »z;ü2×Q¼Ío0$UÌÇ8y† ÐÌçï`¶¨{ˆ¾2@âo;ijÊ÷Ĩþ[X#(;v*ØßÕA(»%17A&¢°œDÙíé…ä`,§(V ;v¬©ëj·i)0åP,¬ˆ`9J)öÆd^bÄÞÅ5Y}–ȧE²¿#ô* 1ÓwwxJ ó?ØGF#—»Žñ€¥rtL(žU„bÊ™…Ú$É‘9ö>L ð7JÖC«¨<;«†æ1¼N¬XÎÿ§e`0 ŒÈHa‘ñ˜FŠ1DÚ Yr‡!Œ~FVb„*ì‚Äyü`—â¢úäOy,#_ëukú3ã4«vÚ¹1ˆc:<¾†™T¢€œEšŒcyøÖw¸Øò.^ŸÊy1?…CâЬJ qîÔ @Løªj.`äÐá\KbLKæëØÆ]ÿNÐ@Ëž˜ÂÇX=ijâl"ÀIá´Tk›zÑwK‘JbËù}ž—›0´:JM”9‰B®]¶äuŠ"Þ4‡“zD‚Öh9çujA«³×WòúÂ[¢çè!¿J9‘W4ÁÒVHIcXØØI6ø-VÄ9µ™•…݆”')}“¬Ãl<ýÀ²šcö>òF—µ:¨ LP”Xð(Çcà“q“.ÍÙÇæÝdKJY.—¶‘¢|x¸Ø<",8Ëéù²O”ÙܘÏ®mˆF&BƼº,æ™']’mž•‡—œ^:ƒ>+úјZ{Yµhi…Ru[Ä­Ÿ¶NDIÚÍ a¬Ê#Mó$Ð";‡rä’z¤Ÿ~à¶Ž«\횃v\(pyÁ={ [¥á9õy •­sqðaÀŒɬ¢ –)±ä´\\•(aê]Ãñš·É6ÞˆqmCæoÏ?ç E-¹a^p &÷ñõ†ŽÚùYGŽÈ(š¾[zÉÖ'Z½Óh@fuí²¬‰ œ30 ƒ$LXhèh¿Ó÷8®€Ñ器×à v/±ÓëZZ epÒ“C9#£ƒ‹¹fa67¦`’‹O/!âAç ím%¬{‚\M¤\¡Em–›õýŒWHÒ­}ì{¾ƒ8×Èž~Ðø~Ä”Z¬4Feœ¤¸ÔáKù$T¥O›«F´àžWÄuÄŒrH4LvüÒÕ3ÈÆ-Aå\Yá¡û´ôõS^}_Qc'“»°Çp!à$* ]\¡VÉ  LÌv²ÔYq(ëµ(К} <Ó6N¡2 Q’`ãýP<£„;V£² rŠÛ –±î4V%!yZ§e½ – íÃP¡– ÙPO7Í`pnºU^æ}ÈÍ“½Äd Õ'£‚ào†õàCB?î,Á€àf½ãꀤAX’IÌs4òE@øèÅiMމÕÜ‹?Ýg† _½ÊÀJÌB©F—˜"å^ò׿RÇþúW;h8[œþ‚ê$¹Ù¹aAÕ¦EòKÐ`€Mø¢ÐýSÉÁ]aSFŒIÖκáÞƒ½;DÜ–hŸ ¼Ñ2’¾„‘¼’®Êž¯nT¼h¢fÊpá›]¶Â¸Ï.äf>ä¨íJèàŒR#«È:5‡Õ³Ênr¾Üše™$Ö%Ö‡²Z6‡sê°š›]€L H› WÐI˜( 71Ùqæ¼´¬-#ŒøÜÆÓ¢øo1ÆÔÕUc¼‘}_qJ^LJ ±Ê‘+ˆ¶è–Q_ߘ&ÓØ"@):0XA׉è¬CG¨¤Q©“bá!Ó3 ­Ì·ÏÏñª Í“›â›þ8EÊ2£Ú2&èÔro÷tèô‹núã ßÞ•õ’š‡¬”ƒ·uÄð¾ÞÔ7’ÚÈúÌWF†‘ÚÙ˺›Ûý þ£,XÊòDCór¾¸îŽ‹Éé¨Àª»™?ãf§ÚÓä%"“s'ô„:}sŠgN”âÔ®OÑcI€Ñö…5Ó’2¶\+)´ýŽ>ÆE¨-©¦3³¥³Ž$SˆH07©áΈɺª"uÁFÀ›:]0éHŒ)R (J2— ^z1®\_Ö‹}IÜ[Ž˜nåRœ´::5© AÐ2Rèýn§Ž7Á0tŸŽIÏ¢ø‘–Ô…«g”û&¸´Æ1aQ¨ …0å` ¯h¢5&Ð.êc× ‚». 6|7¹ ƒ2JQŽvþË•WØÄ渪eéF‡ègíè„ü2h¿ ¤?Ok ªv,–»ºHß«è‘Ôö+èñ)*ý*Aº#ÜBó½l°Wõ|ŽF (ÝÏ—STëÁwV¡Xt ¡JÆfQ-Ï%FâÃç®(øÑª…±œ™ƒDpé¡´Gªœ²È=µ0D5d¹©½¹°k¯ÿ’×ê¬]S„á(õ ¯E‘3s« MŠ#yÄn9¶å1]éÅMnˆ½ ]éö2ÔÇ»J¨ ./Ë DÒ†Þ |ð(³Õï¬ý®×˜> ©ro7iZl‘ÁªËçå®›œï—ßsÅ»*¶R Y‡›ZÔà¯íØ,Ä £O/J¸C…l[§(…¡ ¢}PìÅ=-$è³£ÈÅ äÍ…Ø<òknŸÄ’›7:Tlç„ÎŒ›ì øûIØ›ƒäÙÈ’iÓï ¿l߬ `‡pëÜÈ~+ÞHÜçtÄJïH Îe[»æÏý=¦ƒô~!Ú´µz·Ãb8Fl;/£.?Û RŸ7gUËýˆ´nÁ€ìl_(ŒTÁ$%ÝtsÞÊ«z9ãÒ¡Í\î‹Kèz£~‡ú`Ø.‡¢“Ö)l? ¨l6ÅYiï3 o¢+f³‡ïçƒÕñ.øßYm ð€ˆôÜ‹MuˆËˈÊg°3­‘©÷Œró˜}ÊìÛ• Ü0ß!ë Ö(@Øô¡ï&(æS°š²ñÉÍâa–Êvê4ôÌÞe Y©æ¬µʼnÂÝñ¯øý¯tQ¢ €ŽˆÕX<]W%Š[Nf°ehkΪ{5ë])P'_UèêÅ/yè`pUv cp÷yÞE™Â§¾¥W T"Cw?:ôÈ ÝðÝcÀkmÜ,§ÞÊÑf 8 ”™ÔhÒ—%•fNQB8çåÔNfpdÔ|l(D––„H?ëâ%c×ý¤µ{’qE\§ãÙ܃HÌJx³¡Óúœ.1ó”N'@?7u30| /ÖIûEÊ«AHâžÁJj,t¥ÇBŸÖS°ÔYNКb-²m³³,ÊL(bïÞ¨—êx·­ç}Á£§BÂÇÈ3tš&œR”º6ïËÒ,˶,Ũ¹èêB "Óõ£øÄ©—i|l$¶hD4Óóv1AÖPÜVÚ;19Ïÿ3}Ô†¿ÕÓjøEŽWE¼(Ô´ë­ÆQ*Ó‘ð»Lªfá#ò*l‘Zɵ{¾·j7JQ½|:`eÅí-|c3¸)B³ Ò8õ!Ä6Aç£y¼-„˜;Š[¯-%EÙvCw¸ØˆtJkÀ?vÊH:5¾©]>úÚŒ)ù¾]‚?”Sg#û³R‡Ñ¯¼AñGE :(J5Fº{i ÛÅ ÅJ0Ä[âýë"  —#§Îz2ê$[BáxR¼ïn)Qj“¥–õ%Zž S»X19TÒ“ŒIÏÍ#n~Ò4ž² V¼2u˜•ÙÞKO·Û¸"Z*SäJ$0u«Áy3 îqݨðˆxúQ;ù§:i’[²Ü²¡œ^‹¨™stÓ~O EþŠ´4Åhd54xh´  ôˆù'ÀÀפ!õ`L–c bX/§‹Õhœd_r1ÇçaÇ»ôþ$Ç;Zس\[È<‘ÍÜÑ€k´8­5Òσ™Ò“^œ!ÈFp¨Çñ¼<“ãÁ‚øDÈóNØpq6SèKFV`DdŠ<äJiªŸmG¡YžbÁL'ëô­†ÚGsÕM¹X$û¦^ËÖŠ™xŠé*‰1Ö®É:‰!pBÑðÔs3O¡¥ê®Ïp¸ã ²âI³{ ðŒ|Ïfhƒãt¼[œôVuÆLiòÂ?¼µJp\1«8Û{öùŽ6²‘“¢J•Φ°òÆÞbÑ…“Í(»Z{íÎÁ<(=`ëî) D/<Í‘úªLd³Ó!Ãg %µLIBKí»§àS•n¸£Á‰Åy²Ú"êY6žÙ‹OÕ‚am>b¶jE,q{j‘àHm!=ºi*-ªŒˆ8‰»gË2»¢Iyá²ÌÎk`?ä#H €îûÎÃmUý)·ú µDxÄÉœ[m5b͇|Âɵ©‚`3ÃNÿP²c†ª—í™5 #`+mÔMo=r‰$7š¢^ÂèÄ niAhq!Ù¢8 㟃‚•§òþ6\ñÒn“žù½§¢Åe$jd>>A¿¨ÁóÄúâ~'1L–¦}m*.YÚÒGõ° 6eµ»½7ôG2Bâ‹n1ô§kÁJ-1¶'oBˆx?£‘9\Ëú»—õfÑײKŒrí<]ĤýôÚO,ß8Óvó~.Žˆì¼WS.ø)n÷‚®óuô7wfÏ8,0¡Û‘,¯d K+m@l+Ä#ÞËÊ÷³ XU 6è2öEGùׂ)Œè5Ê·¤~tOôµÄB²b83Vrc‹Z_d>`CyÕ€“7´Õ@lB¶ËÕ¡€NbÉp^Ž–Ã²+6@Eÿt·¸ÚCËȉÔiÃNÝd7Êb7-K¦¸è¯QV@Œ½Míõ¡8H]SÀ¼Ór`2®÷Icòw~òu63xR,Fbi”±•m€ ž¹‚žz3÷yñB _³KQd¿7¯ÔuH w4 77qtS^1ˆp¸À 3{¬ÖÃ÷6!*× ;EÓ Óo Ëã“ «S8êz? zWóëÊ‹œcYI¿ ‘úûÉcÜìy5ø{·c9m ›BnP…ì ÙZÌÃdÒ .&“÷²–ÛÊ3° ýa3ë™3´æHC½_ !f÷æK“&ñg ƒÝ¦ØG£L¨¹¹å2_-Þ4C‰Nµ|tŠå<-ÒyÚs.ïX (!-,€Ölhìß•½6ØáVÑ€êݾÂ-–¶ïå)-'³p÷E³›’U!h=g¨šÆX_'²—À{o¶v:+!h„\ˆ=ÒÔØÁìjÞ)g㬩HB&~ipà-fvE3 —à$:: PFÌh®³ E½o$”‹c ÔãÂõˆï|)¦;YP|@C⼘CÜ8ã§ •Ëieæ ˆöÂxsX7³N¾(f]êÈ€ß7¢èyµCbâÔ>Ð>ˆ‘²W6¼4M2É+šŒ*~wðêé‡ûÿùÜÔÛÞÚ‡¯ß¼:zõôÕy$g Ni”gPOçÃ½ï ³'|¢sYæ¢õàtm†kÖK+öðÑg<†FEÁ9wÉAœSùxì¨Ã"áܲkþ9èCØÎnÓH›º‘l®ø×ê§-™fyrpÕ´žµ2/Ë(®gœ-Tèá¦=Ø_0,CI–7bãºIÌ¢¤ùÌóWßs@6_il7Dª©É O¬ûЃjfòé=ˆf ç a&Y„úJÓI¸ˆ¨Ga² ¨¡½dÇaÃ] ûáŒtÈßv̾¶H1EÔ²«Z›„ŒàÇúîxôæÃ¬&ßǼ|o6†Æ÷]HN…*sÃé.}³æÖˆ¨ÝW‡œ>†óÈ„J ‚2€øšƒ9å—ÅÜœ^6H £Ø©ÖóN“ý­>ÍÌ¡¦*ç¿÷bKÄù†c`w6ì\½\Ì– ÄºÕæQ•ñc”ú WJLš½Š+mÑÌñ&¤¦"lB•ÝP1(§‡+èdš\ØjoE¯ñJ{°¨È1Û 45¹|mòȆ¤OÆ0%:ZràG@Ë‚}™/l  ô´ŒW˜\<×(×Lä’ŸOáãx5ècÍ Ý›§í'1ôÈ®ðläM)Z0è{“wÙqÖíäf†púð4êkò³Â…‡S`ˆFrC¥V@褶§/WÃ>ù$¦l[Û8B”Z*VkàÖ¡"Æ«3Zd9M>P™êÐÛ<|‡WÈä q{Ò¿Ý],<Ù›õ®Ì¦<¼{ñΖá§lƒµ³…ñòšPë$@rÌ '[p",`f¼vU8Ú §ÿ ãª1Ö+Ñ…"Ÿ…©E£îÅ1Cý<Ýõ°ð:Ä"2œSúíÎÛig%ò§ãbú®­˜)„²~Ðrþ hodÏ]`ŽH›ƒ½ ¥„Æöï¸ñï¼T4JbÜ0KÆô!‡nJMÒ  ÍIRò5›eP:r9B;ø \=ìÀÎËssôFUøó’…̽èÂÏÙo/ŽÊ%‹a~‡W–ΛŸ(ŸoZ(MÚþ˜³¬J(|ôàþ%ä €áVý Â7ÚlØUcº±[kõqR‚Ĥb¼à*܈å„6¾{#;DŒÓw×ÜjÕ‡Fö¥†?ÿ|ÚM-¹c0HS±f¨€}e¢äX̱ðX¶QV®Q®é±CÇ»ìÉ›ÓzìZžÈ¶Í’•­+x®”SdJ` üXCåO‘f †(Ç|j['˜üˆTCp+Ø@ôÿNÜi8` ÖÙ!‚v\½´…œ¥W0Y ªªé²Œ^®ò9R2‘áFÚDº†eÍc'†çu`!(Šiíð)?©?u‰¸nEv'Ndý}9†Xè"î²Fºà³ÏúN¿,Qù¢›ÄàFºl@óʧ8Ú‚°»€µs–Œ§YçËî„ÖÚæ$GÚE ĹmÝ[•9²~DÄX‡{ÙÖµ'À#2 Õà3‰XÂaÄ ¡{x&œ$fº@!à$EÆËvO»T<…[ým¼D öž8`^”@ÂþAk’ŸÏ륙îì ³FºÁª˜ì„½9~ÛtO¢!Ñ®0Ù‰‰–Ær²cQˆ,µÂ8±NjTÂÒ±F:\øñpíìú#DƒçÆ)ÆËHÅ ·¼çÑõ”Þ ™ÕÔ4šF 3[²?jÉÛiÙÛƒI±í%imJ‘ÍC‹ò^ÊKáÐÜ´lTð‰]„þåSF€äÌM;ä®ù-Æ©Õs‹·  ÕotÓµùnõ²(…-@$Gcw^×ÀùhÏZÏ‘aŸ=`P¡tà*æ£ë™Œr¢•]¶F…(ALG´àÈÅ v~Õ”œ—îÕŒ.{P×Óæ'àoo8:§Zú¾/8ÎèJ¢’«Ï™@ZçA‡S\¡Y䨚ãÕ2ä•1ˆú§YÓ~•Í^ ¸ ß TË1•Go÷1^ùÁ:†7Œ}.¸§VÏÔ€dÞ5È«}”òFp»^+vô”"T´—P&ÎìÝ`zJÊ ÙkŸ ®ë]4ùªá”8~‡QÈÀ3É¥°N,jpûœJTÕ˜\7+Î!ÈBiViœ¡40þQ<}š•±ðW t1IØ4}ÝÆžf x'DG®Þã­ )÷:÷špÈ›9¯¢Ô#· ¦/Í,áhˆý˜ ßçP…˜À”¾º¹k9H§fÔƒÖ #âEå¥QsR»œjŠ$ÜNnJ`Îa¯]ù"¦T¢·š|4TžSÏÚÇÔr¡®Õò“ÝæÈÚF—mšXô’q§­¶oލÕxä8™X¸Ð'êo“ð®äzCWê‰ lŠÂ-y±KÕ4‚6í@âOKhBT!`Þr7£n%B;ö=«ƒR0à,"Íë ÞæõÚþÏçÅìÂ%!"âS¸íC[Ó[ݱÅùy9,¸´‹9ðÂ`$3A8çÙ›·u{jA{·&vQvöðsßœÜÕI^)TÃÞ~Úãî*5 ãOm—dò÷)ùáÜxÏŠƒ*W­úEœø77fÞùÚâÔ¿_=¤¿¿~øëu3_–óÓi¹¸) 𩘟 øsRÿ›3ûê´½ïW†u¢Z8*€%3•ør½¬´ @gtåšÅ¬ ž¼ŸŒÁÀ=e‹v^]Ž+úçúwoW¹DIðj¡ÏÙº1=¤>$[ö‚ÝX @í,ï¿’´À{h?ƒ`jßȼ8@+´¢m”¢,ñ&“ìïh(öä »èKb+ o´K,î'/JΘM`6CЈb>Lðbh?‚„­g(hWx7ƒy©t X2$Õ°ÂV±UÂúó°%‡4ƒ« ýáãzØEÍh¤ü†t§";þwå5gF«Ռ۵)Ä:º)ð  º þg€ç'ÇŸ²q.Ü®“a'‹ ÒÇ àŠNº©²J—€+B3Kf³iÿ}YA´ 71çŠÞ½Ä ™rÉËEd36¢V׬f®ÀÅúênEÁ˜/§M¶³ùp ¸$;wÝ¡è4¢J¼zùÃþ³Á›ç¡þ§{ü—·›oó“/{›Ýã·£ûùÛÍ“û½_uìÜ›òÜý3u™é¬^0.d¹("¤¾ìe4ˆµøûWoŽ’ØF?¡Mž² ý—Ïžÿ9Ñ£Ï_|÷ü ¼öî¾ýö[Ó绦“w³«éÞ]üñeïîñ_ŸÜü϶ 4üX|yøãwOžfûϸ¶õàÛÇ+ѿփ21©àT¯Ý¹Úwœ€ƒ‰¡‹jè%Ôñ Ÿ’ЯHåÕhU– 7¢©Õ‹Åq<>«ƒ6¦ÀìUx;`û™N(I¹Åh5&fS­6ò\:}ÅáŸdR,=žÐU‡ºTÌÖã÷ïO$Î ò³“—£Òrp:5|æ[¯âåTø1¶Ì?»ŒqÚæ„åçå¢ÛâêP´ z¬R&Bå¡ûn‡¨óðýý‚\æñÕè§Np¼Y¤&Ú1h™pxñ³Or¸'þ¦ƒ™è©¾švú޵Ý]cÎ×›wò[µ“Ï8ÉÔãRàd`–.ø7mÏüãshcï·JdÙâCWbGl1)C‡é=#Ø  9NòžÝõ7í'ç´ïɳ6DäH^»•OCC\719¥ØÆFÐã‚ ÂGÂÎËMˆMú /Ù´­›çvyÊ]>©u5ÔZ¾EñĦ{õ†®uíƒîõø=}¼ãGL¬s=dœO¢cÀ¼×&7'½ì±v#Ç¥Ý0"¦×`­†‚ÐVçl×6§0X¹ÅgY'ÀÐOŽ z¶(*²7 qºÃþYOESöåéÕu-W3Ш¨¨ŽÂ[9j_‰Dj¥ Àyt»Èc[`µ¤Á:Ÿøõ·]!WâÆûÏxÛµŒ¿… ‹Û¼eÀ"È=?|à¤:­ß÷ÔöNªì^–‘K§´wè ž¤“«yôTG 3Ïš>ÙSv­¦O¨my&»Ù“©wž¤5…A{0<˜Ï7]ªA±µ„„Œ”g£uY·YÂõ„vöìÌ–‹Íÿoç¦=8ŠÖ}hS›*ãÉàÓ@Põ4KÛÀ{a—,®” ´Á=„U·üåN®uaXݘ>¼ëí G»`Ò(xô9/Ûÿ’2­Ô©%^ntTK5©:u# øÀÕN4ɲǺYiÌß4;pã[¨IOö°†ãñåµLÃÐÏÓWÝ+É|ž‘?ðxEúÞÝG/fWéZš¸JT®Ýpˆ/)PÉÖ~`js–™œC^Ž_ÜYÅÞ Ð0²^Cäð×F¦tÛM7ÜÄð˜ý Ì“O’Iå¢0 (ñð@øQ Yµ“òâÑk6mÁ`­rO×£­²jõÙGó»T·® Ú Oh»%޸ؼÞÉ[Ü'ìÅR|`)¹B°˜|ÌÝø=³pV;oåÿ;ÿßþÎiÇ»w)à+N½PXOôiw{õ­A{z6œ$Âî£Ó/ÝÒíws‘8Í¢ ûtKÍØáà…¿*Tƒ¯ÞÄòæ(pð!?ºÀZæ](R*ixÐÌG8¶‚kªs¾º¤ ¥v07#¯Òh‚~“<†J²0º¶òmÈ— iÅ!`Ìß—à8O:ÕÇÛ[ôJ\Êl†ñuÜš•´y…¤àÒq™’Cå[ ¬ØÕéåàm(»áŽæ…s¥+ôElµ¥½9óLL•h5‹c§ðù×nCÚ8\ê\ó¤5=n¨7'æÀe-õ >÷†ÑjLbÁ<ÔŒ¤†68ÕΏ1)D|1|ÚÑß(ÔìdJµÓÜ1>ÿLÛ› ŠuÏ!7M’¾ø¯·ôÁÍŒQF5û¶àAgFž0Xò£¥®GY9%˜5É¿*Þh%yb ðÀ°;º“‰ …ÍMìDµ [JÜ´‹–Âű¢€ÉhVò i]v6úÀ<_ÍàZ#ÓÓ]4ÈôÛÈ÷xÎÇ0&ÓhXÁHß­ÍØ?a6±»ùkð¶>(Ys â]ŸÁ¸:?ÉH·&®GrCä,wñ9…ñ 5D©P²hÀÛ A-}M(n ¾à5#íœÌÖ_ÍÀt‰‘qÅí“vîå7üI\L>(§rS;k÷1œz  eòåã¨Í5¹}æK½Çp¾mÒ^·BAÔçuN]àã7µÖ¶di9„;n.ĸöv„;edHw(3 ¾Öü\B$Kü¹Œ†ÎßR~`¿›0åyÖyøu¾½il'ÅL_pcx#hÊÔ ’¦[geÓlr ‰ÕÀà1OüÎ],/†àû _/šÍª mD#öv\<Ò£ÂGN ô³vTûýöXÅŸƒ½6Z^–-ÿ²V˜ õË\d¦;£p•1q½xòòhÿéáƒ×ožG¥ €™¿Þ2@˜æ!¦õ4 ¼'o~8|`þ %7Ÿ :÷aµ]¬¯öÜ*z±²Ê‰dfÕ5_²%c—OLÔXÏÏkKœÂ[ì`ÿÕ§9ØKÁ¯¿úz]{¸®Å@;7xØÿ©·x0¢üÉ>ö‡‹hö»Ê@|Ôœþn<úNõòùw9ã¯çWïJIN)„¡þåášþ÷ìià £ì¹Ú~öz°Y+L#øÖMKH™Fqø3´ŸÜì«ói=/ƒ ïâè»´Y6z}#Eñ¼¸b;‡F;(Àã•+g+–­*ìÒz­Ö“_Ÿtuð&þEFÒÉéÙTC¬4ÈlEí±ÉR“V¥¿Åw‘M) úä4ºÖª4Wf¥Ø"ºíˆëRç^Vf9ꃆ/1Pˆi£³ëÝÂB«A¼¿ ƒôk†A½øªî´ ¨{s´×ƽ–XÑoÂ"êy‹%m„ÞÆGŸuÒâ9¼2+¤¹ðÖNjAq+€Ç[M¨zSûáµ éN ×§]|±ÇŒµ?kÓØàgÊ*XÖ.Yeg¶MJw7Fg ê ‡ôÕ¬û¥Ћ·¯oÜv¾zøõ×[_}ý•Šë²müfûS¶5âºÀì½,?e—)°‰<¢ß9ÿþݰ×f#©Ã='Üœ†MSæËi5)ǧP6/~[ïˆåbÇà¶h 3€vϪ€ ‘´Œd×OFE ñÊ iyöú™ùgpøä¾ü±Ÿ½|õãË~öÇço¾ƒ«”äÿþ™c 7øç²sÇl@w^¿:ìBäøcW Á!¨Dü $1 z³‘t0F¼¤ø‹ZBzPÄ <™M€ ð†|`ŽÆû/84 u‘™£@BÜa ±]à¼v¯¹«žÖ£kõhmšïuñݨ¦¼É^¿Ž_Þk0¹,ýx2.k¦áÁËz9Mn/Â(¹©—¶­¶—ð½Ë•/#°ö-ºLÔ–fëézhÝ\ŠZ¼¡9WYãMEñ;Н‡$âzxr¿olÜ'›Nú}ü´Ód]| ¹tzi‚v­šQ4Ë|ÿåþÑþŸ¯,¹º®Æ§O~öQª‚vð$ªnÎË1žûJ¨Ï1¿ò€A¿Â¬¢]J.ªcæ © 2ÇF¸$¸ã7~¦‹k#LN¯'Mèlã™Ý2^ÝÎï´ýˆœŸ ¤ÒPLåzýºÿ· OáµO& ƒ #¬¯bc¢A€Q#òiPÏ’‰}"€Y¢'“rþ îµôä¡Ä=ùD^Oèú`¹(׬½§+ƒ»cVšõj©kcðº5+>öp7µ9t¬[÷/º.eöÍY«ö¯¼I#u°¨×¬ý¿tíÁà¢h.âœè\^’Žb «zåßmÌKL|ØÒ.E2Þ£2øK™–ëùœ5]Ÿ?*LË8.ßWÃbŒê˜kRèƒ~íÜÈÍs…wŽà ƒF1Xµ“òæ¨jŠÉiu¾Dö 욣’<Åïb!©=ï>‚"óG³º1ÿN—°0ÍäÆïÊ• fþ$L©ˆÉ˜YÄC¨ÁàdçsmÓcÒù ëB+P•B1„ÑÎÃv!e-ÇÐÂöLSÁ;ŸÑCH¸Dvêå霸OiÌe(QÇ]ª¼¬1I¸Ùü¹×°; œ!…aUgg%E熤^ûÐu¹ÓãE>Í·v€BñõmuîUßðlå=UuÓ±¹!ý·xîëðäÒœº1ć›Á"yn"™ÔSì=>”@ Ø=2š:.Î?p8\A3ಟ­³(̉f8€ìxFÖÛDMN!çÊïeëL`ÑØ•e¡¹ŒÈàðËךlw9ͳÃ2ð®Å’h׿ £^Ô¨~z0)¦®¦Õt¶\ä¿ùÓËüb1o4Åm—ñi)ã^6htnfP;梶÷f„B&¦Ã•i YPÞIC„ãq}…™è¨_>ƒVö‹ÔEs˜Š(è½J",˜R§†Q23œ“SP†I´7·¬?À ã2 Pð†Q³w$—‘¦á)Œd{.þ°RþŠ…5)T'˸§€DäI<øØate‚ÊÞcOj îø+oTΫKTA™¸–ílL† ¨Ÿæ÷r%»3þRû-aävPB¥œÃµàa%fôà4(ޝ3Ì 0 Õ;EªÚÓ¼pÐâ¬ÊéMHËÙçuOI56¯f·‚7ä1ßÀ…+dj·ô]c½Ö²½4~]Î8€{«÷"â={QÊH§6)ûuOÑSy7÷ê=_­ïÞ°˜Þï¸ì:Ñ— zò»òÚ¼Â̘ŠÊw Ê@Qó\Ë'` G‹åÌÔ«z“AðôõäœADF7”lîÞk@tÎAz†LªjZÄ5"#âÏõt@ÌFáb¸o§‚\¨)×0þÙ”B/ÄìØíš’.u ')zAí\%ŽÌ‘|ÄÅGŒÊ™³6Çvì¢8QáfQ2I„‹PYþÎæ”·øš 3¯X|Ô·fÉ€þH’ªUu `¼ž$þ…–ÄWñ·õÀÝ÷Na–‰­Wùí[¨-÷œ4÷mRµ{©œ“{IŠ ߤÀëË0Îàô ¾ÿ^ ëpo×…ñãN2Ê–HôgÕ¼áH­v¦üJ¹ñ1¤ÖÕ²KÁí±›¨º¬ ƒ7‹¢$Ÿ4ž¼,(<FU|‘(Õ„¥Ô'Ñv\ºr‘ìÐ×—”¾á,ŽËËrlYN©mVrmfO¼¸À$,’¦¬q ^c À›’*[$+”ï ¾ä7ÆíåK²&§i¥føGx©îE)üd0ï$|ðfÇ QñÙ¶ÆÜ2%…Cñ­ì$MÑ­rô¦*õ9’4×ÌE†½þ/!J[¬ÿߟoE:ÆOF¶©¾%“f9£´2IJa“•!O¿7Ävkç/A%hÒnMe{sð.Ð2‚£š9Sx $°@ $¸GU‘¼e¹U*ÙÚ»‰ƒØç–¶ãƺ§‰[:ldOàX~&½v\øy•DÜ‚AP•x8ü”8)Q“óbù¸Â¾Ar¸ÿÜfµŽ_ñA%~±êèÀ{DË‹q£¶·p¾£·Ì btá^ù­_Xm)ôìÞ‘jæ½ê•iÃçèNiêæXDä —V[ÇiZUÞR›¿òõ´wQÏ0Z¹-lY°µ1aø©kÌ.`š‡ œâÙEç³\Ôf½²å™+JØà¯•„Ööï eÍ‚@W’!›ËaÆÒ)¶žŠ—WNób4êªq,|éÆ`OÃÏÝ8‚“ÖÛøïã'îÓÒY¾!»§×p:Æ µØ ¯ìG{ (JSÕ‚°FVêÔÞ;`d}ª¤Xܶ³^€H06Ú,'t# ìÚT·ûÂEU΋¹9³Jpx=WÃ]4ÔóÉ\ Ú®²žsè1%ÙWÔçTI̬ ÔËE” {NÕsãÕ ?(ÕDby°°ÄƯíŠ÷ƒQ9[\¬ŠwhmBé*Ó‹ðWOÏKµ)ã$Ÿ*õœ5Í$~aÎQ¯)'ãŸew«»n3× °Zá(ÒÇG7R7Oæ ¢& ,fQÎë4ǵ¶ Næw÷"wcßsóÜNá(uõZMÒb¬¤Ÿa­_Þ$ fŸ6‰ªNb5…I´ƒÿ)“(•tÞšºYÎK§pëgXbos»U­´˜S¶äêdü´œËl9•t¼™«Ó­•/ªY™›xúwÓ÷øñã,´ƒUɉdÄ™«©WcTC"ø«)kÓºó®¶¶•3'”3ƒzÑbr:*²f·ÉSÓ ‘SP¹'Úv&Ô}hä¼jRŒ¹M9t;Có|ZÂãáL^·ÖšO«Ëz^Ê›¨b=?7u›ITu6.†°ïŒ[«Ž+8²ÐÚ(ª>)&“u¯.À‘Ÿ{cŸ©pQžÎpߎ/*#⎚å?ZUovqÝÀAcÚ—ÅuÀ{|’ä>-6ÔD~"?»ÚHz'<á^çd<¯~´râüµ,ê°tÿÅžöãUU.æá׿Ó+#R.[êº*Çb-Jaúè”dl¿‹ˆ »MÏ1ñóî“J4+ ÎnŽ’Ž¥µYVhAš¥yQ3ÍQ¬&>tU\û›B¼ë8}j<¼¦ñ øy’VÉ(ÎÊÐK=1¬§S¸pf•%Ÿ ³ƒDˆ©&Üç$üÀï×MÌ厤åô;+¶ ½³A¶&ÇàÁ¸d+ðœà$ô©&pƒMVï{ ÆR¦VÏ8:ªlC"`­W×^„E†ñêE[Õ×¾â£ÅŽFú÷c”_l è Å[Oree± èÞÞÓ'"Ám¿ÝuáÞ“W0¿' ¨¬ó”œXRËI«ñ<­Á0UkÃà 1Õ£$P$àն⸿ØQÇ™jË…•%rÏÛ]ýì¦Füú¦²—ú<Œ50 +ÆÙ?4¹^‘g+(`!ˆ_Y©Z”xí´,ü•*z4šHm9ç캒è"ŽsÃPQ7t­•#쨖Õ.jéJAÅ·n£ ¤Lˆ];œÔ –3Bhªt€¦(&¯>LÁAª‰dðt«Š\Žcìp»÷ íí¹¦¢¿Îk4æ$±y*:¿yÉï½­[ÙÌíýá³-l7wg´Ùâioêdŧn7ÏÍ~ ðUD„®\ äù‡¢o³Ídqd)½›=ý`¦;âÈî½Æ:ëÒ¥(b=:/½¾Û TÝ "¡ÀÕÖ¼:¯¦¦C¶“¬ˆn/)±ô•Y£Cðâ%¶„¬k¡@è‹…íºb\;êÀœeŠˆ,EW­¶|Kœ¡^mÇÍôTp…Pªø”ó³O½®Õî%GÀòíþvÄŠm¡ b5ðY d e κO¡Ö%×q5}'´¿¸ªíªîVŽ„A¹]¾GŸà qâ„®A.Ú"±°ËH5OL HóÒœGpsm ¢Ã) @éÁƒ$.fÉÂFñfÊ]Jö§s@ L}%;†aKÕd9Éܪ`#78zƒkÂrOë¨sA¡Oj~½l)*¢© Iojó•m­^ïkJ`GÖ¢D–e8©W•™“Ó’âd·ˆh£°yK<@ÁÉÁˆ¯¿‡Â<Š428~X‘È¥]Ë…C"æåÉ`K…ÔÔ/—4YáÖ½ž?ÔºëùÅA“Å?|LÀÙ±òÁz€v@Ùè—ÐŒWŠE$jÂåâÜlûSCÁðEh|o2 㬠†“%OB쎖%oi¦äi‰7¸Ëñ¢š50:^©­¯GüÁB¶ËÆÅv/·e0ùÃUu9÷hsÒ‚Z“ÒwÝTõÕ<ôú™{³£ÞìôNâó]÷]yÝÏ0 B'`(h^ŽÒZHŸ²GÙèØ=IÃYÆ÷ø *DÅÚ¯`‚Šjð,µœ†¥ŸÓÉè„ÓõmªMK-Pb稠Î3¥ì´“)wÄÁøm.;'œV6~*½ã•ÞI–æð{bXº“¯iy¥×¸Cå˜!œ@0=Û$?ÝI§¬Î¦ñÈð_`麑G~™vù† ó »­rp¥tÔFü\.ÐwUp‘_ˆbQ gÒ:ýòJqVh {ݪ}>4§7ê¦ÕÙ¢­%\‘PãìMµ-•R.ûDa‹zºä€p\«žÞ8$/[¬MMl øzbþ¤­Eaï•hSÎ{µêá½b-úv¿Ð ýzˆ[›>}ý ÆÕ¬ûEožz¿üªùXIÁ¸§)!͘V“}Òòç<Œ40)h¾ˆ³X¢G¾§í8˜f`¤Cx[ƒ)@zÓ0A;m/¯£ÈD˜Ln[Ú™!PôÀwa>´]$ù³FìÞëSS­åê¦ÜÃTZ-L÷5l¨Ï„í» )®´‹ÖhX2X›MkÜŠ/ê+Á UÀ¢ÉhÏ”Ln܉ƽé\CÛ»²z¶’_Õl™Õ§?0_¦õõDYâïëi=¹¦#!XÙÚF@=/Ížh†ÐÌ ëfð] žè a´àA1…ë9\@&B0?ƒŒƒ§p±º,)Ç$ÈÈÇ%ØI1uh°AVÓ!¥î\«2«V¢n,G®'Ü¥cÙv±0%¶:D ÊËœÏb?“ÌÆõ¡ ‘gÛþI‡2§Æp†ÞZµÁb¹=‚‹`RqâSλ£q>ÂMYàϦñgŒÓ±¸­-Žx–Õ­ìAfõ.F†Tš—ÖtÔÊîáOc$FÚ¯‡ï²§†?ÕsÃ~1¼¤[`êÑKs”b} ¨nQeBfO™Š[¤ X=.çÄn´µæL¥ÄjNE²9®Ï»³;ædHºÀ™õíðÕSdpȤ„ áÈPýlõž£¯§€Ýº•Å»¿z¹òí{t¡»‡}¦ËB%Â4”ÓÇJN”š,½NBÅšÛ°æG*Sœ´¥óÔôdiÇ| êI7îàDV™íI¤-:Õ§s”—ÃǺQ4`éë;ÇäéòFxðñT¶½t‡=[¯_@{¯’ÊÖÜRgˆCS|‹EhR!"÷Bëµ~,Nþ%<|¢æÀ1|>Œ}Çð÷/Ùnå“ûÕrö“ûŸ–›¯‹ñÄ,ÓŸŸ£Ó )⋃–3/DÝSÍaûÁèÌ \²à—§Í0ï¢E¸uôð´ß=í»&|2:û{ tOxü¨Î¦Ÿƒ¬_צCç ܳfdK*vz½.Gp‰9í4æëÜYÚæÇ¦âºáYq_dßO1‰7Y›4¦˜á¢g%D-oRn;¡ÙÛ>°È3ø*?Å…½–°ã5* ûCõ®祸Ab%¬kÊl›×38úoŽ!pVèëdµ(©6µô(Ïþ„Û¦¨yA.UpzvÞY0*¨‡ï“‰¶…óHN+lõ¹ÚLYnÌÁ„ÃŽÒ„õ°8E 羃9·†Sp÷â)$ñæv9ŸÕ(TÁ›‘.I¦ø—oôšÛçÏÆ5ø_þ<»ý?Êy-—Q¡~W„úÔm 3¾ ÃM?qe•ÞyfÖ¢¢ÍÀƒù·RA¢0ùp'L§BÍmíº& 0ÛRÊ,âÒ7@M*ohœu ‘«¶Ã&ÂÔT¨*š3èx‰2 Ì;7K˜+´™£Þ‚²—YH’ Ã)Iq„°Q˜q9)X|i–„¾W·é&„pi8ŠøÜ0É&fj8cqŽarÝ:7p!ÏHÞl–žÈ!eXÎßÜà>¡Ýß3õ£ì³Žp˜ñjÃ3d&ª  kC¿DÞXj FËþæñ¹²çPP¸" ®¤Óvö¦ðö ²‰ŒKOWr·k×"Jß–(8Fô3€$˜}+É{¸Þß Î`#zÊõ=A#‚•´«XÙ´J<Õp=©çMÙL«wÿ‘'¾¸»ÿ´ç\Å`Ó&Ð8ý•/ØüÛ6j­‚aÜdE Á+Xìªóœi…½âñÔnÏF29ø`¶£Ñ v{{·»_å­ŠO.I°PYLprC¨(@Á˜¿Y±/Ûʺ+ìå1ÜuN gG¶i~c|d|Òk;0·nwúæÈט0ku@ßBÒŽ-±˜[NòÊø?U1=ß4¤:þõ_z„çÄŠ,™ ’š ÍŠùPל¿ ÉmàLûO»Í6lJðe§g6ÀùÌ3%½ÿY™Ÿ¼2?ݶ Þ"òå¾²‚7‚Ÿ¶ '»)ËÄV î|^°¡bü7hÌ`î2‚x¿øß{}k”ëžÌQˆ¸¦íå–îí@"¥P¢Ñ!l“P›èPóÛŒW¥­TEÿl(Þ«¯ÔÊÕOç8ædû?|&â3Š¡$8Îÿð˜Ï:­&®o[f9–JãäÙªÃئ ÷×qâ}2Å4(Ð_ !\g3ë¬ÂlÍ0Ÿ™ ´¡\Xæ6j Œ[àƒSF®1“t€„d,…¶¨ ¾/_ü^b *èó“¯Tô jÒŸ¤b;n¼µ˜6­¾u¢+ þTŒeë± kˆ8Ì¢°ÈtˆŠâ#àð‹¼èÙ׊å{.EŠÊΊÙX„ßtý.àT„AL4Lb‚ßû 6xñg‹™Ìˆò1IÅ;5VÁD9“W&2}pãÁOø\aC „@·/—0¿QŠË*Ú4-s’Ñ] eÎ"“ωâ;¹9Å+_ _{ÉÚCÊ«Ab’Ü£Àr”_KrÍ9\âù‘[{OäH±ñiÍß-½ù5ºt_÷ß i„EPïjNPŠÉ "J)I)ÃÛ‰•7‡‹z¶>˜Ë4„i˜×Pz„ëL¡Ð"©ÎÍÆKpø>Ÿ& ŠYHÀJ¸Ÿ>è+ˆØ<]¿Óþ”¾wƒÅ6 7EPqJš<8›Àƒ©îàû7O^<½ÙùÃᱚ&&›¸Ùô>FýÓAw±­lRSÊ Ö¢.޵/Ûëe.ÒxäÐ¥?¼Ll–Ráߺ‡íI9ÖÕœ'Gçs p†kˆšÁ{7l‰74’Þøá³Aʩ…—A³›§¨'Ûf#>èÍ©a§i²shæàþì› Äì¦Xixƒ……¶ƒ‹²nDæ4©;T²€™‹L×?®=_- VHxo Â*Ò¼/šoÂY©üå@ …0×:”õ<%Š3ɯ XÈM-”÷Þ½—E5ßp[û.¨Ôñ¢jÒ :ø\”b—éM¥ÔîöŽ·N„ò¶bRÃê¼];PéÝß“&ïÞÖÎF\U@ºÆµI0ý½‚­¦:ýt*hå-qì†ì$)=;Dýûh…~ßa×äü5¦.3°Ò¹wïÞhƃÿiv9V|¾ÁÂi=ÅÔÞtÖx@Ä?®¬¶Hš²IgFwÓeÂ=Ñã§”mp¸ÁPòÝáï¶twê^ųdÈéêOÔñ^¸ ÷ÌŸàÿ[;f䱬õx·q¯ú¬ƒÊK ÆJ%ì7u›—¬’"ÈÝ>%ª£µ¸6hEæY°%”o¬-úŠsw8¦¨GÌEx{ƒˆˆ¬ØÍ¦†Õ¢«£ô]‚° Z!)&Ü ×ðªk'_Ïý;ß@ÐIïõ«ÃÁÁþáQDHž›¡»O˂ڒ«a|úàK—žg`ö¿Ùŵ½ŠMÃs7Ø×cqB÷)ýìø¤w˜ýÜL6Ò'-³±%Ûuž±ÊJªÑ÷ÆÉjh´}µŸŒMißpJõ %a§f_Ùâo ŒÆpVÁZO)3ÚÛp wa8%YlM¾Î=®=Zs‹Õ‹Î ‹Ï.:Lù“\r«Hž‡UºíËg@”Ý´ñ|<éõ™ ÄyU%60èù³éóÍôLÒñ¸$ùVÆùK`—7°«AÊmÜbhLÙKVÌ™ª©®<á#~r­wgŽÃk°/õ|k›)MA8` o6o 0êÌ_±~ß6²WbÚg¯½A@ÁÙ\øÏ+¯pÎËÍeSP,€Y]-¦è1 —b)§8ˆÙ—Xz˜«²3—‹6Úîâ5ô9T„žÒW÷M6©Î/œSpQ¸„ÐÆEÀòà} Ï2Pm·öŠ‚Û{êµ.㡲Vˆn•½ø›5]Š2"ªãw˜³IûÆ0ò±*]w f&)Õí×Õò¡A­#Õxã\˜ fWM3+†e*ƒªá6u{+ºZ“²‘¦l ÄheM+XùÍYÓ€¶z«•„Ó+¸=¼2'ᄎžÆE¿FIÀ޼WV«†ÒÝq÷ú­•7²„5ÉE¥ƒ²Ì}BtI€ÁS>êˆ8&‰š£3³ò;úp±„©«À9lVÑÏ:+<¨6€Õµà'#O#.;Ý%ÃþÜçúšÕw9÷f1]5®ÁÆ n1njš45wí„r3{LXŸµÀg{ x;FQf_£ÅœÍµ.ÌR@FÆ]ÁÒ&5X“͆0¨¡zl åK éÖÐ/ªfx‹e ñfBŸz’²;]%H|6ºP±æ3¨ãÓ—òà¢ìpUb\ˆPŒa^[Ü”gû ^ž¼rÐñ5z 1I'‰-Ž ³+´Ø¾rgðu\F}§ËòcÂÆÎ|š€kA°óì ]oŽ7µ±ëIbËÉóé”G´D·åËN{d9O´Gà‚ú„\P* Fõp0°Y!)pC~•ÏÆ:„–DÚ/ãœ~Cæ€ÏF9„–DÙ/£œ~CèßåU‘´Cˆ „º_0F=ýþ†Ø¬? õb+ê~Áõôû‚<þ$ÔCˆ­¨û4ž|‹ûjênœ9ŽŠؾãñòÕ¸Äá¢,Ç¥‘g×3ÈNë iàAئä°ßÑŸ¬€RMu:Fƒÿ’”¾Ë,óƒµè¯úà9‹¾p¨å ƒ®D›²¿…˃à:ôžB›árÞ˜&Œ¬Œö‘3<ƒs©‚Fp³ëbÀõuœV•*‘תFrDÛŸ—jôjšó@CçFí½Õ¹­ìðbi¶ÒÒ+OÏZ«Å|tU/ýJüô¬½Úi1¬¼æÁ²i-aÎëá;³›§Ožý±×Vò´®¹Ä.S-à‹›k™³ˆÁh=Ý;Á±:¤‰[kò) 1]“ãÑ𢨦Ý©ÚEgO'xÂxñY™JGöTòvqõ|ÍÕB]¾ÁïU‰â˜#t|³‡‹¡[5ãjXv¥ ƒÂÀú]¬ÐKø01§Ä÷úVvt@/^½yýûW¯~Øúä`pøãw‡GûG?í¿z Û>ØÊŸî¸KÁõ M«ùJÜúuI¿Îè×{úõ>‘ƳÛù½üŅЇÀ`ùws‘¬<)1`Û¤˜RñŠJ_wzê––¢èù躂ð‹~”úG²¹r¤Ë¸é¹zÿ<$0‚ŸÁ¡œ»Ze³P?æ þåø£qòQ¹¤»çB€|ìoUvzz®_ü@ùè¦.‰9YmdïU¡í<{2›~;_‚=J–uPÌ™)®ÍF~m6àë‡ý¬\¨8ÙŽÍÄM¡9À,@àÌxªýÄ7²‡9ÛërÒÌi¡Ý-¬„we9ï¶ñ5`JHc¿®—´VÊ2:õ\² Ü°€cØy Ñ&³³åk¹•"‘[Ýqˆ ·»,O›EµXzV¬¤@yŽWПvHh–*wUðîxw–=õ ø´¼Jjô!‚l-e õÑ·îMt_Ú”–./§#òq@Düî˜óDKÅf[úÓ,Ç‹ØY#8Ù›dh-ÿŽ4uûÓªˆ½ùÞoOWªf±yvwµæˆÆÀZM™º«–6˜q\”Ƀ êmg+§2/$ FŠÖû‰ œ#ü–-EÍ+ ˆ¡@W‰yßOfؘ¡ ½Dí¬Çc².’C;$–ñ-êZ»úÁ8B£éEëñãÉAãww~2c‰AX™…G× ‰U‘º÷ÝM”lëéêÞ:‡Sô]#qŸôaĆ©WÓkÌ;øŽOnï,ö”\*Raåq“$9ÿµŒ6Ô;A6’n ·tì>€Hýøl/ÃÓ®9 OêGÁ<ÚηÚ"a7š¶ùú‚‚ÂY |ý s#…ý§Çö_¿E1ù¥ØÑ…T!¸”ý&“F×ÇÊBìZaB€äØZæ”0ŽÐi)½ÏQùÌÿæK3¤ç|àŽ”öHËQ°.ÃË g¤ U—à€O€³¼@åFDÄü²hà æIaaf.#È€v¢çp§±•?4Ÿl6f+0}yu*dê$q†çŽñàѲç½px,%íf¿¯¯²ÉÒÌÑ#ÔÒ¹`bõo—ÃÉ@ ¼`j´ƒ˜HA͇Î* G7áIbf̘—‡<ójç‡]ÿ å&]2é „ ‚WÓa÷êJ§•úáÐðÜçü³ØáñlFÑ­\h]쬟ñ¼"‹qõRëÏ­Ä-aŒGªw ÷ê±™ö­ˆYg“A´QJâB˜C5yÊ\ìÏ{Fx‡/#QGš\Ý‹ öw’ó£Ù‹àâPq:={0˜¬mlÃj© ‰ðIGGÝÈ€†(ø„¬ébRCæH»V)øpÉ=ÊÒ¥î_]D(‚|—æ=† ô@Mö.¸×sd–üWa]–c¤É&Ofi±UÀÄP!¬š™Ò..,¥Ýßã‹*mp–D;±*Åa+ o¥ Ê•ÓðgÉäJô/Èî°ÿôsó;ˆF×*Dl\„ãT©øñœ@‘ÇÍ1ç¥Ë{]¬”Ï–SÜC̾ÊÉÜÑÿ£<;«† dž-¶ò¯Îªá»1ß~oDæ[Fä„ô¥/ŽþÐiÔÍîH8äµeÖ†—Ð Çéž»hd¤á²µ¢ÝqR°Ìæ Cþ ›Õ¥ËÕF¥-íZp§w]²b“€ ’eÅl‰JÆ…ºò-´lÆ¡/• âÌß–¶ùªF§—u BE[Ÿ#R8n0 ¥*9ÁЄƒß;‰ˆÇwQ'»CÀâŒU4> •˜©K¤‘¢· ²¢/GþVÃAAÀ1÷âÝj¸y:¯¯¦¹¡£»JúiN(†ÆãÕ"h¨R0˜7”‚…k¤ÑÕ¡É™†Â0õgP“öV ïwÕ Çæ‚8T|ìn«R´‡lEŒXÏWšŠŽ|¼ë]êÃ'Øî¿\2,D›Õ,êÍW/)GÐ[”"qrñxt÷Í«WGwS¦„‡ à-˜°o ÀßZ|1 î|‘\¤º­|?Çæ¤Òãq ÚæÖ6¡ìèÕ³W»¸9³¾¥&=„;.¦tÆ…ôÎÂÆ¦Iý†X<[48K›~‚pH<˜”E³œ›V.“ñ;Ic“µ¬>ÙÄäóÍKî¤lK>É-J>ךäNÊ”äs0¼Ñ€äsGî¤,GÖ¶^XÓ^Äbù¹v"wRF"Ÿ‹e›iˆÅòsMBî¤ìA>Ë6+G“Ÿiýq‡"¾›ÁéµÄ_‹$eç󉆃·@ZÐ!¥•4é(‰\ÙàøÀ7 +~KÚh°®íµC)  ì"µèååò œA÷ãç¨%qÍOg¯NV…S\ÍIÚbAŒ…0™0q¨h¢äí÷xíÛÎc(;+¡˜·‡Ri”#bÚ‘«æÂAkÐ`Vl),š¾ƒ§‡¹7¥vÄU¾!ˆŒ>14„ì×Hç’}“cÄJ hd;'LT*Gî{@v ²r‚Ee¿ûØ&•m¶wïöt90=JžRÐ œR¯è3ÎÂŽEdo­(PzN¾ì`¶#-ð|—ÒÀÈ»¤³:qP‰°˜œŽÌQh7ktºK,:Žm¹nÄ‚ ?;-/y„øý6Z› ¹aÐK8ä)%(Gþ]9àvDG0àB<½Çd~Ôxˆ#®r#èjŸ|ÊTï$§úi1.Ç.>ž$^m]Éâ#³ÌPs³¨l¨%KåV£ÉkžÁ7%3‡éQ5¢;=_®?…f°¨Øþž«`‰—Žv1ûüvÓtæ"×ç|G—=à†@ £žŽ­šuQGmz ZÕžÓŒ§x dm ,VîÛüÇ8´3í¤ÎeG²gñné)&y&Áº ~‚†­›v¢Nn"`‡Å/ãâÐ…ÄÑ”ßýqjŽ‚·(£÷Ô%PÆÝ`Î]Ð,˜ñÞmê$Ÿ•“úÖ´0#P®$ÙXì÷ï¸5Þs~¾ ¿‚K*G:Zÿ£/;Ø(u;pyRÏ‹R›­²6Æîâ0XM_Msåz@O(î…®lh7{}„«k‚Nü.ÄÍa·s^ç—ùÎvG·¯ qL+ýÃ#騪€ oÒRˆn) ±­©›ÿ(fæãŽù–Oó­íΉ}qi_\ °§ï;˜\{ù®´/&Õp^_™ý’žèÀû.ÒŽì=›.öå‹ôÂ5^Å…¼²cðšÂ—=×—Uå.]9m±èú-Ü÷!õ4¥x%ƒç¶†®p÷eqYSä¸Ý»ê…¡£Å܌䘆²ÔÅvÖ+ö0(¦Êteræ0³¦¤YjÕeAž?`©‡ørÄvˆ½–ºÅ¢žTÿøi՛Ϯn¾Ü\ÕQ}NtßË'%X .js¶NŒ£* |`„Š:HSðt^ïÎŒ$jНœÀIYŒ]™d£OÌ¡œr!¹£´B„ŒFˆš'*¤q=+†ËñâÚ”ßq# z@ŸUãZºW@wpÀà À.ç ”üZJ¶Â¼23)C«xì]àój•Mƒ>€ÔáhaÊçôÕT+¦‹ðx’€YeÜn;—~y“fZϱ´)döô¢CVëዲ¸¼Î BYGtô‹Ál†‹¹¤>«€y¥ü¾<Ÿž«æ æü¦¡œ,“}1¯šz*KNÖíôؘž []ò̬U TÉU@ܹf1ˆÎ9ÞŠ]±êÃk> ÈbaëµU .ÞÖ¬\†µXìÁ“TÚ|èÑÇrRñ§“ùÀt䈉òO5ÌÍé¿£ .:í] ®Ð‘4 »)'ë€WH)à9aPÏŠäƒÁÄì`À’9Iw¶¾ÆÿÉgû7[Û[[_}õë­ð³Í¶î|ýëßlE?¿zH~ýë¯ó‹ÉìLÁ&2Öñ¸œãÏd¾ó€ ·¼ŸŒGõ°Ég×ÿßFö²0’M1ΊéùUÕõø]µØÍþüâ 4@3â÷Ózv=G;ºîÓ^¶c0ÚÜ1È¡]Töz^c¶ìÉrqaN–à] îbßUóQö¨9ýݰ1\i9­&åø.ÊóbùN½ov³G|›~uu•#!Õóóðö{LGÏþ|tçhlžFVjL…|geú¾Df0ðÚ·.†ÚÝÄ»hú3Ê:æ}”-Åe]à°w6Ë8 <È€g Ó5€2+†ïÌö‰;|’Ö£rØàÁžã® A“ÐÀÊœ°ž‰Ù r4/˾»Q‚½´•³ß½‡{:šÆòr1‡ÐL܈ 9õóŽ¤ØÃ’tÜW>Ê]íŽ+CY¨©ÈaY¾ƒmÓ¡g‡¦z1aêp5õ»r ¦Î\ xÅkÐgñ ]‘IÈzJÊ™9KƒHÔ”þ4+÷¹ÆŠ- i–óbVIuÍZË/Á5†+|)‘†à×µ…ü|ƒÚ/UDç2{ú!hø#åo2€GuI·A‹âhš©ýåÔL€!æ…slˆ…C¨Kš¡LÊyÁ³Ók«)‡eŽÈÁ¯IS‚{xžaä/y õHÛc#hû—œ7YtÚ638@j‹b4÷œ+üØþ°¥>ÁTªXÔÌø‹¯+– #ld/`láRPâí@~ªáb|^רûB¶3¤9-˜Áq†^¨„ÁPvìuÎÑz%q¥à—p¾¶!ð¼’Ú ›mÈ«FDft×Ç tî Ý%QsdÖE?oæ®a‹Ì€9sÇ”¦ŠàxÑÞHZ sÒ]BUÍ*ܔ蜩_a|6ê~qÚ€$#ñùÈȰ׃À²0}]Ýæþ™ËÓH¤ã%×±q™ø=÷aÞÑ5ý~ÙˆEš{–ô=JXŽUz6#6MQqêDʳ LII“?¶¸$…eqiìjõ¢²I2»)Bš,ºÀ2ôY•}Œ‚FNŒç Óòì zGK–AŒÙ.ꑘö @%F“£fákóY¹oQ<*Œ¢ð@e§çXÙQîŠàžê}„ŠO}Ó\/¶M/’t)̹md šïÅ›¢5 ià¨@ŒòD-_²Ó±31“©\ŒMŸ”¿ å^V&`|_c#T „ú¨yüÞØ­µ Y†– A[€o0ÔQš÷*ªQqåqÙÅr4HÃ>x•šˆ>²©ÆçS Ê1= Q%NÛ=V¡={¸}wIùv†^Ý\YÌÿ?dϬôLnXf[ùoñÝïœ\ѽ ÂXýïN>§•8øù]gRí±QýÂìŽ8B2¡Xì¡pŒ¾A߀ïìz)3¡AÓk©ðü*€YP%Ò°wN‹‘@¹7‡‹?úAU6>FòÍ«òªK²ÛwFh-Gîq›ÏÇ¥yG¬§A«W…cQÌ7Ù»ÓðÓB ¨nÈæõe…"; ä›c`N(žCŠ [ ‡eÓð•é„8qD¦]-*²eaÞ7¼#ŽD©jÓ‡P å露–ÖP£Yžâà”hÜ×gIüç¹ܯqÔƒB·;þá1%W ²¹}ˆ Ž?à´`Æ¡@};ZÍ…ø¦p¨Ik °¿¡L™pdŽ&ÓÍ)îˆ=¾ÅAâ~àœšS#¡rÓã1pu 'ÓŒ•¡)pÝ;@ûȼ<_šc.Ø*Ìa^à9³6¦ó1ÔôÙâ°€Çlk£7‹®y‹÷pxS€;ܘ¹è<èxCæ¬ͰóÎ)û³P!¡´ÜXã¡MC#³ºî|Ü5Ä»¨g›ä&í“xŽQSÔ”ü˜Uœs¬>m«e |Ä©¡ç¦¥ yˆýþô–ó/Þ­ C¥>{üRˆçkv×öTvúl¡d3i küÓ`ßãf®Ú›¾H=u¢BÜpºAüçˆ)‹'’’Ä[¹›Tâ&扷,.«ÂR'É+fiR”ÒÙ§»½†¾»'—~Æ!^±äœå-1L”±,šMS¨[åeÞg§*-ù‘ÙIïä`ûcÞs’ ÷y52rrŸcņ–wÀ€ÔôˆÎsÅaË"Œ>Ï¡ý ¤ ¼â6È ÝÏàÈ=ªšÙ¸€Nž.Ïϱ‘傃_4‹ð_tøæˆãät\ß±ËÝàÙóï~üA"¸¹Ù`ï&ë’>¼( ‡%˜f`4„‘-*HÙ~Mƒëùþ1ôï^=ýÃàpÿ?ŸCä­_·ú÷Éa¤SÃbÀ*ÒÑ*Ù™c/ ˆæº69M¡6!9Ê*‘µõ¨MûÇmDâsÛîx«Ýín77ð!Û)¬ÝPf*ûúIí=ÑÖc÷cù¬¹™uöö·ý¤ |Æ.ä Ä´#¹Vï„Uô¶¾¸ƒµê¼˜6·KÆËÉkC,äb%®«ÜCʰÀàƒ, ;>P0ó¸EEñ¸ŠZ<Å„’È´ñŸœ'$4ÅlI{§í»ih7H´MÓ`KÀ9×®³^¶ù8ð 2>xãtŒEòÚW« e〛Ê0Ñ-ÜïAl_~xÞyûŸJ`‡ÞEäê‚0ª5ž{‡>Û™?áéêÃÖnfF_ƒ¢O'ìn‹» ç=h8åw`^¨‡—›‘sLBof×PI}Ø0hÇžLõÜò5Ì8 S…”Èã-ÊŠ}XÓÓEvíG?…¸V{ä…e3rƒ”ÕÃÅ@žGGâ´¬ªÑ„I ˜=ù¢ÒžDMéªþQ(¡«zmäÂ×”z"PVq !SŠ5C© q,B¨¦Ò>ôͩ봓¬ J#dóùw¯^ ~<ú~û«ÁwÏÓáÓ:ËÅÙæöW›§eg}`7¯ ìáÎjÌî|fØJÌ °õ1ûí @¿u@‚Øo›/}kÄÅ·§ù—oO-©ÝíÿåîÉýÞ]sðÇ{ æÄ×Ýí^ ð»-À;xÇïÜý$àÀ­5 ö Z6ÜNy×Äøç´ü6Ôa„ca—‘Ú=h!×fvÒ& ÕÓKHf[¸ãYI5ŸÏÌáz6¯@Vr;îçxÓT¹-Lm{?NÒ(é$'Ö‚LÄ0òE[Ozr‚‘–Å›ÊÏ®ƒaiÎÇ”fÑž¨NnÀa¨‰¢’yb’ÚO#q ‹¹í˜µVúÒð–£D¨+œ–69̾~"‘0ÙÊe|wä v˜.ÈÜ™5fà/‚BdFÙlø$ìÇL†;Ã…)ûtÌ"ÄÜ×ü÷ÎG¯:7)Á”äBGZ8 þã£4õÐ9Øž»Âî°|nµ‘*ˆhrjæCh¤Ž× "TÍoÀÄê _8Eé{^œ“ð‹¯Fà²0ÌxVN¯\¿Iû/NŸzþøä`ÿÙÀÀ¼yîK=sÝñ_|iuU?»ÝG_lnæ_~»¹ù¸—µ}þIéì'¡èh*?}öäèɱpr’ðOJ{{åvæ$€óìÕÓ£ÿxýümsÿø/oO¾ì¾=6_NN¾<é}kX©A]vˆëÀ0ÿ±áø/OL±öNàgE C’¯÷UuŸž¡·ÿiF¯¯&7öêèÉÁÁ?Í7°]~uø¼·zžå,€™MØ1M„/‰f‡»<í ¨rгÛ1Ôà&ˆ¾(¢â¢áY‡ù>zòÃàå“Á¬w™±zf¶™·ÍcØhÖÄ”bB®@k“2Ø„ø[Á‘žËÉbf1#Ïß©;ïrÜÓ’i–ÞÓŽËiÅŒn*pô•.lfÃtÛ€E}'ŸbX’ è×fcüi½‰ V—³¡×ûÏŸ®ZRê S ¤0âähJdâ6¿î·¯=}õâÅó—Gå™[9íôÖû§‹*4œÄë}¿Ø£·ßšÊo¿]D‚ךêÈM«Ï±[WC?1VƦ¥´)žÌ‚öBVøøüÅë£ÿ­W³Þ†Óîh•’tµI5e‹`ÝÚ,”`—È £ Eζsˆ¬Æ‚<å]½û¡ÖN©E©mðÁ;Ö9(…(@÷¾v#Ô6ÖÊàL aT7xˆ’Þ£¢¾¹q^Ø’cVyžšã#0Öw¸Á›­ó¨ƒ¼{äTÅSrG$ÒÛ·LT†‰¶íc L a8#ç\u¢¤ê¯2HJxs#›åxtÄOdÕÚ² ÑÂÐT6rœ îÇøÚ£¸½=Ô3„¨–A¸¿ç x%ÜžÕÖD¥0߯ãMÐz•d‚&о GŽÐ’ Ÿ™¤Fb°$|3—ˆ"&jDl7È:bóLK¸$:-5’^f-SJ¨nçøÑãCI!>m¡½Lçq'F01‘Ù¦'i¹ÆvЛ«š…»ó^|³ߺþ8-Å æî㻦¿fq+£ –~2³çS<Àž pK‰ÙXÕ~éàî’Ȭ —z'B‚Ró%ƒ™Â {ù²þubŽ‚cŽI÷(ÏSäó†:söo5εà4a8DPÝJ¥>\à'Pç N§käÅ6'‹âG^8rqµÇÉZk,ƒãÝØIK´ÊÛàGòÑúFáQíqݹ4 ⪠øôØpOÃXŸPã²Ùõ×MÜ$I‚ ¸Gj`CÂT‰=éÊÛCäæNRÍÕžf‚ ˜ó£P׳\ ( $O0°ªr¶ìXó^ wÜ­EYü1Œ·¾`oo¸q×*§»âéÓŒÂÇ~˜K3Ôg6¶{›*Ž=êâCR¥pù€uöÖªÇ)ñ=¥¾®ãßBp=ýp/¼?ðÕ^4C`xƒ§0Ã*@º!‡9@CØ03˹U¬8Çjþ1ßMt%€Yê½ý@1r0ò˜/´Ê^l¯!Qpÿ í膨2>na˜lá[Ïþ¬ Â`èh¹' éÌ„‚#©R$~xu0¬†®S8 ñWš;KK€ÑÞÞñ ža-*­æeÿ2QJËã–¶’:Éè+vË(å´ì–#É ‚Eß·¨,rĵ·ØxK}=¯Áf‰–à#C‘µaDZ^DçùÊì.ƒˆ=A£÷ vu³`÷4¼èo ÖL®Â÷v~Ó°hÒí<èä««iW4ÎÇ›;[»'}Bˆ%¯„”j_W騣b§e·C]ÌžêïÿOÞÛ6´‘$ Â÷™_Q×SÂÐÝîYºé ¸Í³Øø žîìe ©€:$•F%3^æçÝïzâ-3#³²„p3{··ši#UeF¾EFFDƋѩÈAìw ®“÷çýßf˜ý2¯NËU -nˆ„äNÁž4ÁÝ—KlÜ•ÖðcbQ*tIƒŽ+øS@õ\fƒyøÖ>¢¶Bs¸ühÄsH¿ëZ†áIo£J‘].·ˆõÿ9‹]e# ŒÁRn#Of„tïà?±ÎÇ1‰ÆÉk«q§4# 6…ÃûjË4‡Á~G“Ðb·Û5-Ï]6ïÓRÍw¸Ãw¡5á3Ó3w™´*Û3“,úøøÓ< öy×ò‡¶•MÆ Ãæ%ŠŸVf''-5¶o’y‰ºÝ¦ Oã»G@q|¢èÒ\²4.Ç­Å7žÖêýgl¿I{OžÂÞIÛìZû^À•Ô|æ´>ŸF†œÇü~HFVÉ»ùú/•¬åÕMn¤pä}#P†ÁƒÆJý2Wú\RäHã¹À¦‹ï4½AžÖÍŽdŒéÇÜ /"ôȧ“›H}«7ë]Ö}Jø™ixYKÇo °²EOêúì4˺ØÎï,“¬^¤Ì´÷0ž(v­Å6þ¢œNQc\&çÙ„ÈQÅØ1Z¥+¥vœ#Y„{ÃppIºòõ³'iËNZ;Nþ/ÔLXDz„ǯ:àÚ'9šÓrÓ|87ïDþ…N‡ „ÞqßÝô,ØÐTÍaÆÇUÿTfl‹f»†(­ú±‰m°´‘B} “vWðëâϨ…uÐjYù­ÖÑìŽfËÊkj–¤ð #s΢¢˜ŽÊ\·Ü´º¨~îüdsõ›Î7œ±®¸ç¶ü<ôsªÚa«ª¨b±-ÇBh¥Ê>9›ÀeèÆ21ñËu9‰³Êö“V³³~[pZRhS¶ ä;š®ž:åÿŒÓû­“ôqEËš<ÆnÒð/ôÁöúCò¤©y®^Vs«·ïÙÖẠ·¼Uä1ØGêÂÌ>›ï#^t ¼> c‚㈠͛ë‡gžl@ȪÄP)WØF˜wZ0oçn˜Æ{§ñþ+ôj÷‘J¹_»ÉÒÕïro÷õüs\ÝC?öÚ5Í¹Ë ®œí›¬L@ßVú‚{` èQ)Êr c¤`æªI·¦,o3© }ÃÄ*¡päö[ Š!ØÛϦ!…PfG3:Ù‰½BK*‹Æ¡Ž,}´ÍÆ(Åvß”¯Ë¡‘é£Ò(¤°÷N$º+F‚…5ô®=q”aÎ’,DYš SbhÓ¤M/1F.mXfŠtͫаØÀ{ŒM(d€\À•sƒÙLDCŒ^ncÒû%&¿RÊ´ž`6UÄXûK—×? ã„¢.RßÕYyüÈʱ5—Áà|â2§NPïdõ›ÍX±ÅäÂ06sôHµ“zîq¬CEÎuÞJOÓÅL2üŽmù?UWr 4\·¯4r‰¼¦ÅtÀ>‹t‡¶¤¦Ï+í•Y$ÈQ·üú Êé6ý"oÑ»iVIDZd>¸,‡P þïÁù³ÖÁþã»Ìl¼,‡^|xu@Êôê¼O«ä’šû(+ ?ªü 7+FÔú§|ò±˜LgÓâ ¶l¹Å´(¼ù§n¹ñ5×É?A×ó µ‘OѽØñí LT s¡jËÁ ”~mÓQÁš÷㛉.8»˜US*Eߊ‘zy–÷aløö9|ÅV_bS.,›Qáï\Î&½K]s4™ ¤ýçù(§è, 6¼énž ŠÞ•Ýsü1ÀË“ú<Ÿ•ùôÒt¿Ã`Þ§¡*%‹(ôèÍe1(«r|y£jÞL.§ç¦'ô#ŸL/q,¯(Û¸+ÚË Ÿ»Xp'Q¼»ód¯¦»í°Ü¯Ñr¿z妀Pë5ìØÁç0X;Îd?¬´±H%]ëŽÀrƒpœpV$ëë÷}XêñåˆòE†aÅΓWùôï`Qz—„OãK Lå¯\9Ú¶¸ ˜P®a2b,èÖ!};(¾Ó\t'(ºÛ\t7(ºg·w´øž*>›d‚F;ð5y“!:ÃiXùev"%à”+§Ór¤V±º2ïó—ªTe àÀ:±j=i®·ýR°èë~± ,¶v¸Q/)–úÚ¬+ö5[;üº^2R0,õÁ0Wì†÷*™f=ìËrTN„èlϦ³†uŸõ à0ŠÙ0Y÷«lܳʸ¸ÈGµ]²‡‰`.f¤¥øÈN¥6Ÿ/'©÷ðgYÝhŠ— 14%Ða…m^Y™<g#T‰«ŸÊþ¬zƒ•öèkÒz£fã‚:Aè$ýA õ~’_ôwÌÄþ ¿€ºâHvGÑÄÊ¿4 /ÿR•‡‹a-¡ì›*ŸõËÕí1löUâ/éÌ«+ Ì4œ»£™ Α'DøûÀgpN‘æçMI¹ =ªÈu]¨ž¦’ÿ Cd¹9ÿÿè§};Èz£™îþ]dú5Z…òf8À¤uýrèÕ¾^ï,9È®aÔ;#àW÷½"Ñ"~™o³éÄ+ô¿áøL€‡þ³WîYs9`vnºMŶ…ó~PæC ñkžÏ)ð{h–cu¯H fýôÏù$?÷æìNânùI5¿¯7€m8½œÉ@^ÓáRÃI^ÂÊB«3¯Âõ`€'jö4 ȆÉ~Jë‘å%ìVS”~ôÔÛ öõy¡²Ìξ¿u®ŸÏ«q£XÓzÞkýÅÌÕ5ÿ¢kÂgÓ¦Jˆüô^•ǦŽçUÀIë¸Vú5›×?Wó×̯ ›¯Îw¡Œ·¨VñˆÕRÕ†Ùä"Û1½ä=ü ŸŒå“.*y+uì—šÒñ¯§7“ˆÚM'ñœnÔ %ìÙ”ò‹aþ9W~U–±…cß+z#…ˆõÝ»¸«×£¼T§ÆküåÞ¡Õí6ë²ç” à5Yâñ¬ØnEw‹î„Ew‹î†E÷‹î©¢%‡ÅLHÎ!ýP´¥Ä4ñ‚/ó1ZõÌ4Ï?2ƒÜkñÞ êÑ¡<—÷5êH8BÂ1yªÅmâù°ZŒŸPõkìÖA鯚W«QÄÚ »»ôÓÊaéˆJümæ C›PkÅÚ¹þ …-´Îìç Ó3½” À÷i}iîe9¸IÞ»U4ÜuÅ+ÌåT‘øpDiŽyŽÙÎT;”£iw1@ù*›®Ë1EJLŒÖQFº™ý°ìFSAUršÇem9@Æ>Æ’±·G£ÂÕø˜O@ÜÂòÎ'½ެˆôÉ…ö¢¥üÄ%Jè’¨Óî¨ÆŸñ7üm=/ûƒü&ùú›¯ýÒUF ðg ×GðQ6øˆù'4J^W°Çe~í*²ï|]p†Éê\ž;¼­DoÜ+MÙ·òøé·ß~·q§A4š>Þ•œÌKLf“f¿°I³¿À–y¯O!øJ´Bø!ïðËŸ.&Y£hu{EÕ…4aÚ²‡4i~ \Æ>Ã4ýýÆÌÌAÉ)Û¸²ÃÕ)’—Žßþvú—ý7/ööN_ì¿=:¶±ÆEUŠ]âq‡Zf=L31ž”Ÿn$²K„f⤑BšÌŠÈîØRip–Ÿ{gÿŒI•1G“g´ø²'‘ÅG ‚'ûfc.„±àûùy>Ð*«ªìt!G¶h&9ôÔD4¹ÀØo‹Í“0èT7ÁoЕ؆ïâ¤6øäqX§0Ã¥ ‰R8‘0…×Ù Åp£¸;ØQ·ñ3ͨHujfs† ¶Ÿr,F0Zg7ɰìç7dÌŒzëQ²óùô”–÷ô”ó à¤FôÛ¦òlË`öÏõÂ`°74f8§1ñÓY?Ü`qDƒ/KÐ[•òÙÑN’ùÔ3&$¬,$*)Åy1BkOéì$WàÆ!^©Òr|Ì aÏÃrÂ$^MèD©–´(ZŸÃºï|vY/1ñãc”cü‚s6(9̵øª`k4 4pCÞCjÂ,¼QÕC$ãøî´Œæ$Þ|I«˽ 6þY¹ºÆoê‚á¾éôL=ñ2**+¶¥Ûón@Ô]ÞyŸzN©6?fb“Š«¢&Ý ™a1À?á+0¼ä/:=nš1/Ü‹‚oì]p»‰± ¾qÖb/Æf$uòïkVÚ­µîJû'¼¤š¤ï7ºPtíý:¦6Qcön®jD4H%…‰½'´!°g[ISÂU6í1Ýô-“$%äAY^ÍÆœ²÷Û—5fÝ ‡õ€ ÄÖIb™µpBÇü5̡Ш\›Ùã¡§ðÉ!à×V×[|«¶¢°÷®‡ÑAŽCn‹N¹BëL†ÙÈG¿¦ú NKó)£5&ž ²¯€(Ü÷H?Úx!é7tžÈ¾=]ÃòQRžGG/ƒP[æ½y–3­¨‚òHïè‹|Šg%„ø£–f’÷};œ6 éGS1‘œ¦â@ilvcªbð²Ñ s³‰£ç‚b§¢³Ôæ-©™HGu7õ8Ð3KQ7UHxL7'˜j*<º’·)`EÄ+D#—%¹h²Ùí®tOŸx›ªÆî}'»ÿÝ{Iâç› ~ŠÉ‰Ÿ5³û|bÎçó gþ_‘³ÇSÚŠ$.Þ/fEº@½ýõ3ñ€J=±a¿EcLØ>YeØÈÎôt8Ì&¹UÙÇœ²©†‰Y <úÖÅi i‰SwÂà*ÛÆ23/›²³ÞÚ—¢;ý4½]NÊÉ‘™°³PQdñµiyÓí_Ü.KÒ’s¼^¾–6°ƒÓÕ«Ø%k¿BÙKV€JÑ ÷?Ët»¼™‰Í}e úm~-Î&¶óÙ”fJò¼œ¢#r}ùr&;«ÊÞ1PN2ëÍ&Ø¡¦e@ûŸþôvmnÈS€R QæÎül‰wÖcƒa—ò½ÜѰ—¦MšçùÕ¦$t”åV=ÖšQ-OKŒ?G´4 oe€¡1}QN^Z¢ƒ…Γâ˜UŒdÅñ’Ørf¤€<ļ»ù_"J GÊl»d€¸+Ü!ñƒoYÙƒ$[òöIGÌú‘HÓv=øÌäù–öVeíþM-ަõÑëgʹ€,Àˆ|9'Û;€íòKþ1GP½r\äuXKT¹úD'Ó^«ÕMåX;ðM©y„Öü¾Î³«I~n~ÞdÃA]ØžMƒâlÃü¶„RÂ9Ɖ›¿€—†wÿ¾¿ Éz»·½‹$å翜â×NòËÛýã=yBß—–,CFpþ 89=úíõÎ鋃wG/±}‘ÌâU^ì¿Þ×Å|Óí7hçA ËÇô̓(…ä m$¿ƒ½#r˜Ø?´¹Éåw½c%}eÆéJ%G¼1Û̓)`ˆ¶PÐAÊrd=Ô–vŒ_MÐM†tZæ#ºÄáĽêHD0Øb2TNP»FYæTÐ3[mw«@®Ö‰×œö…Á…[©É\ 3ˆ‚YL1F$m¹i3bœ³àLΪ(a´$~½©‰Aë­¼–ÌÐH*ù\§ØÁ6'R¢8”“UG„^mòD?AWç {8},& ~cˆÉç÷cÈ“%b»KîÝ-ÙéU>&²þ‡%øWvóOc /8þVúµ”¼ZñËfb$„—·HÛ–ˆ¢úÛv§_€¹€9ÞÄVuÇp0"w¢3ø¤p<§í°ÍIº³ùÞ5‚ënø`Ï{àu•<Ž©E dÅ'˜ÕÝ»ŠÍHï¬Îÿö›7fö¡çïÉÓy‘)z÷zÿ×äÉáQòëæ}ôg"]›U“µêX–µp¸üލþüÅÙܺþûHwà$’_«z0ÊÊ\•º«A‡;Bö‹3o ?[Âõp¥4ÉcéX'µÙwq¥UÉA½ó­76QótÈj²«‘IÛ›RÈ]9øŒ§ÝÑM5͇ª¿·Ž ®Œ×˜æ4Œ:UñHƒJçi~¯ìbCá“U·ÂÇV­% Àïhdæüî4÷æFÒzr©QWþQò3I!Õ`Eu¨­$5³Ê‰ßU¾ƒ€ÃB ô£)~‰€ïûåömÖP½ð^º'R•ìŠæD4ÄT:È—‰@¸>ÅÒõÄ[/!u’†äŒõ2Q³p4íÅš7ï‹í}wÓÀyùs1„rydvÉ„ÓèÉáAÐlÆ&Ç™M˜†kŠ)AÿëMV²SÆ´>ž0Š“˜ãUÏI"™ 0M·Q‚Kb*â¨íŒEðNfÕX‰”pʵYj´0¨]Þ샅Ӣ+’-½¥¬^#ƒõ¶ˆS0ÙTbU>ÎMš¯0µWÒÚù¼vÛF ý<æ lçÞæ"®…¢}‹XH-„àå$Eÿ÷!»ú(m©ï<½~…§—ލHòÍÞÓÝFor©¸äuç’®N ™‡« ¶›ÓâŒÕJãÙ3µaÔo¹šq¼j ‹IY}i,6´¾î!1'å÷¦üÏ*¾ß».'W˜s´š Zy÷Ò•Á’ßæD/Ù a<Ÿ éŽ2d"xKY££Ò Ϲ1ÂÉ]$ÿr¸7Uø¡î¤\\‰àÙÌ:tŽÇ1YdÃBcø·EýÖ)ÆñÏ®jcsûBº¼|ÉÊ0ÂOdøô°)þv~3âØÉ=òȧSI,ÅqÁÄ1Ñïqš/åCÓ6šåbšâ•«åø{Jóô`UŠÆƒ§³1/{ÚxX—ÓS™pÓ)ºÄœÞ´Ùð¬ŸQ7U¿ÉyuKÏ/’8oï<Ê¡¶:`B„¹ØreMg(c«^E•D»!¾ª6'¦¥{Yƒc¼þ¨ÀG¼”/)êTu¿)Õp@æzÛ¢f&V$.K“éZÚ®õ9NšC¾Bˆy;?‡Ð7 ¨–ùR;4v>›W·Žvsú*‚ÁyµIMˆª9Öð±¢îzRLQKÇ4CÛxœg’(ɱ…©'±( ¤¥ ¤©’¿õe«x(Lؤ‚µF¬>ŸZ«¬Êêsʬ7̇ö:xÉì ›–:Î ôicx¦ î|çæV ]Îg"ªÆœdM@ÂîQÝϺ2Qõ¨¬¢ÔJôÆçêpØL_Î)Äy4›¦—CJÀùtȤ7„±]Ä$U€¦ÉJ²±²²ñTãA-_4Þ‰#ø•¯f>)—çÖ¿vê××TF#"¶ š@Ùô°™Ì.ŒŽL´Æl•›±©ve8u«Á¦ð'ÌiùdsÀº’ñ$°8x…~¬ÎˆŒ+ÓIÊþÍèüêMNzÍéÔÒë³4è·Ñ”„VÍø¦DŸTl~^b•ÐFfš£_1íu¯°;þ•ä„ °Ì†Ya 0µ(Gß'ëv[Å9^=@crP Ž ­j3oþ¯t±»Ö@Еà£(„kgÕÄFfÁ—çHüË9/–/29 !@;Ÿ ÕØÕ57‡Ta3͆g0ù‹D= ™jôgœAXˆž7M0è7Œ–7¸ýivD,<!²¹cÆrCDꌑ kèð‘1yñ³Þqî–tïNÞéö-{(ˆ˜f ´#€GŸÜ çìÚö××p®P¸Äf†«ë­!ωÊêWÇe;"~ߦƒyÄÂ3Äâ<‰È2Å[Kj–N­­-{#[PµÄàGŠ4ŒL˜Å Ÿ4Å‚fÕ¥,Ç 8;%–‚®k†<2­\£]_Ù¨ œYwÀìP\a’H©?F&uŽîyo­Z4ºuñÓ”Ý?”ëÚA½¬›¡I?¹f<ʘéSmª¼¦£i±lUÞ&ÜŠ@–Õ–tûÆ«›þ…¸P¦]&½?q#Èj­®71Š|ÊbQÃ)‘šÄP:#ä3ö§®BäCÕlx“æ‚°p±@7œ¸´µàÑÅ»dAijY$ˆ::šü°Cà& Ó¼\µ­/$I„=³Œu‘2ú´&ú*»_]ŸnVn㿜’‘4ë;â‹W:ìdT.Ób£/Ÿ%ùùyÑCã²ÁÍÝÙX GM’^HuòÏT—Ȫ×/ÀZóyåðœ›2‚U/~U°ºöº1¹ÙJÓ{«@鯺”pW1Ò-ÌH)=m÷”¼:GG*Å5)IˆMÔI½µ å‘5F;UQIí´4Æ$5]ÚJ‘G»ù8 Á‡V¨¤5jì† ë€wœ ) •¤%ƒ~ŠþÜ5ËO­­}Úú÷ÿXk¯=aûõTÖÐ p¾#=em+…7ý-S3D'ûp³Np ¡‘gKZÐå´‘þƒö¿Åãˆ+›lB™ òOã¼ë@+²´‰ÀÀ^¬£{ûWêÆ_IˆWv.$²ÅaŒ$·)›ÏØÅSiáoTðä 4"e;-¹î¡¦”ç 3¸‰Ñ š»Ô´æsÂ^]¨’°“Žã¡¯dH3Ò,¼MI™ÇHšEF)7Ô#ƈ9ýŸ—0J%(¦Ü:žØ)¸bø‹tçñ¤¾MùŽ! Åÿ˜Ôu2:ÏE(=¸ýiìý÷f31‘¢—®Ê¼ËKb÷;ŸMÿþbôšâÊrj¼¶d6Œ¶‰»¸Ð%­™Œh^ç©L¼ëäÈ£ïêåø±CºU†lžnÐ; P_sÿ‹·üv ±ô[-±=Ë|áso– ÷`÷âïa|滘 ™2£©•,½!^*ÆñŸxymñ /rü¹6^OwׂXìfÇ `ú¸Ï¡úB…°#*Øijõ3ÿz'õñ¤“ÈýNÌA¤NY"¸÷pfjÛL”³öÃ@л%ž°3J2Ž–Õ”b ÚÈpÎò^6«¬Áj {YŶPÞWdÛ=回IîÜ{Œrìz†Ç©¹¿?%óqŒ5x˶¹®MKû¸ˆµfEí õN®±Ó8Ëæ®ø,7~=CöjF/e`ÎØx•Q|ÔoÙ¨š†¶YGFßrliÈ:™‡4)gœMÃcTF}s,8'‚š€—"1 \-™Ä¾uŸ©Z>tŒ/Ð]åVwkxT^ qò/ .“(XÕ6× ˜À8ãèòzDslÌÒ`×ÐíjòS×÷)ˆ¿ŠßùìÍö­f-¬aûƒãÐëÓIø6…†8N| ƒÆ vÁ­¬ó½±YR{(gch‡D¼™ h›L˜ŠÓs ŠŽÊCÊbwèÂ|™Un jÚÝézúPð©ËÜXÈaïBÞÅMr`.SL ´°‘©E³,¶É!e=åÉM f„L­GõÂ9ŠD‰éqÊ1@¤ûŸaZ„ЪÐÒÃSò÷£á^/äŸÈUªÃÆ€‘kj×WèÈ0ûVAžGôÎgã¡Õƒ¾þñéȰS»ãÁ-ß*ØËP_Ð.!P?kÀÔšýÂÎé8^GY½â‰Np·ÖKB>Œ™k0.˜¨á¬šÞRj›Ðgç3H n×8ó¶DsÞÁ ³dOòk:¶ÊqøôŒõoÞöT}¢æ „Ü¡úxZw+·ÅÞêAòö—UñiµšÞ œÕ!/•±žxj¡¬òÙ¨TЏ´ÖR“i!äW‰é¡Â¯ëì¦KÄÌZ"º9vsÛ­Š88³i‰~”½ øc>óU »âY°ªudc,íš¹qWòvÂÍAõH$õâ<‚&’é°rDÅm‹!ë8æsÚꮼG”n¯ý_Ûÿò¨ðÎG-èöÇ0mCÎ&]éìÛ¦W “ÈlâF‰2+þ85oñÇfHlmÖucW……í`\Êuå(RTøªea³èlj–À„<=û¶7²—LtvÌGœÛ­F ÷w1šå0·>œ:ÆéùãµøêFLÒ‚3”PC®¥Í1«ß˜y|V©‘®xCšWEÖÁ6h0#ï|ñJ‚ˆ לä1I$%|x! qÕÆÜ™3GùÃÌÙ]Èé¶eÃl|!^>˜D´Þ4tÆÚž¢¼ dF£AÒòGIv\'Q¼ÙÔ俱ðg†¡˜Ë‚Aacéáã’Lb#ŽSÊ{ŽÇc[‰Rß`ˆJ•Í•ÛÁŠ˜à5&ˆ @‘ŧâ'›Å‡''ò¾2©ùðÄ<ÙüÐn¤:t:{ðïŒ>Bj;³T»ÕUv Œ/œ*–²ÌxR\g´ˆÄò(‡Õæ©O[kNìMí!ýxâI8cßà^nãñ˜°kÅ<ùœAVZžY%»éÈfòã?J@©ÖjwAæ¶µ|‰­Óîöõæ Ðf+È”Œ©Êÿ6Øøú¹s]ô§—[Ïžµí`ŸÀ ¾%âxJ|Øfš`ØœV)ÌC·LG]’€éçÇÌ"Í0á"!"K´¥`¾]l5ñœ=n=øŠa-§¹X´=gf±í–z·õý:ÌÆcòÌD£p"P.A2)½Í‡ÊYA1vÌŽ½E%'Î[FUË2©Öúp ‹öVÀp´ p7xD.m¿;>®ä¾#Ù}Z2…Ç#èýÈžËß[-Þ2]¾uÅ›]>Qõ¢:›ÝîGTF3M0èy¸aØ÷¦íÊï»ÄbD-b9ð2ïßßÞ IMðH¶U‰àଛihc|ßFÆ_ÒÊù}[9ŸßJÌ×=úñýca¼ãÿlm9¢²üŒÔj³~¢i‰D²|¨nQ¯ëÒKØK’tì£SQÜ]I‘Ñx”7Xú°“F_ÁצBáUÓ­ôÝÏÔ‘©‡<ÓW©=÷‘EøÛ,G·fá9ØnÀïÕ™’šFÀ–œèQ}З9þi„|·x Šä5x”ü‚½ÀËBÉþ0ï&t»±\ÇÁUè>z•€Xu$I¨gcÔ³ÁÌM‡ù°>™¢²¤€áJ}OºñÄ/¢R¨³4¤,ñ횈§¯0É«^6ηÒGÒ@ÃÁ¾Ð˜9$3š¹Ð..0§*Z´QeŽC޲0º¡ŒD®ûï"†òÄÆfÞ¼™ä°™°‹&˜庇YÌϘ(;hbÅÑ,C°1çè¼pC•)¹x¯r>š3HodG“_"šz·iÞªí,&|žÞQýû¿@W±bÌx›/±:,ïÍV€ƒoóa‰Ù1j•­VÀõcnË’Ty®n4ð޹È޲wĨ¹Õr€î…­5Ê/ãñAP ŸcvšÂ:˘ ƒïÅgk)#eB¹–[ô=«¼XßtºYÆÍt#±@Å,öwÖ3f6¸ó –ÅbZ_q‹CññQ™ŠÎrºr0qȾÖèÈv>G&ò6A'JYQг—¹KÉT$3¿3Ô[EiΟ^)áÝ®™´è66'&ƒ.dz«èæ]¼ñ#»!“ “#€aèV¿¨Ð˵g@Šˆ™X©ð nXñXf·2p+}›ãа2gžf&0)G)˜¼kŠç*ˆBȆ³Ûò Ö,aÑFRYUÁ€‚Ùð®Æ{>öbÌ*3/V D9y;Ȱñù}òDÀâ1‹šSo"ü©1B÷fH7Œa¸E½}ß& c-Uö”ø¯Ôõ»\¢cTXÇ»µ(ú€É˜{{"þí ;¸KÊê>fæùÒ7d\?|Î (ö†Þ5‡‘eQÑz³ÙºÛ}BYf&ãkeE?CLGÕÕ}¸mOB5¥Del!ãB|‚ ý7-w>OóÁ•éž,«È»ãë¨à¸c“¬bû ?c­éN’Ž%+g“ò*­ø KWLãlWYÙ„ÌÐ Ïõu†Y̦ÅGãWwð™[{žaÞ$Î~ûç"¿6íôD­ŒéÈGv3àõ±¡³xoÏd‘l ¦Vj’‰_µñ~%*ýƒ)˜6Y¶âžOŒMƱÎ)õLÔP^•2ÞÐ)œV¹ÉU\÷BVg²o=ÚÁ~‡gnËæœi+] ¦ÓÆ ‡ äPùÇàîÞów?Fc¯ä‘2K U,?É0k8¡²†ì§p6fY"Çh:¢Ãî#ÕVŠG\oª ÉÁn¼6žò.ê+rUgùE1r’é  ]Z°§‘¬DJlx µMdÄÇ~n§É¡ÊÆÒزƒ ÁiçT[œ5•6…1=`4ñL]hîÑ!šHì} ;#n:6eBÐâûß°1Ë]êÎL£3»’c~™„Óœ•®*YHÙq;O猂Qž’É©áHvA«üL†ækUfz¦ìIaWíyÏQ¼.auô5MuëÛ*˜)î= ŒÝ Lhg9)³>Xz„,ãMîÅr¢RÜ×¾¢ ÄĺÅaEšér+áòpÒ˜¤_ŠßÔd(ÑùTá+QûvóR ÚÚŽÛcæôÈM¶(ŽŒuÄý’Rt˜vÔ¬Ô†Ž;µ2ïIÐMÐÙãá•×´™@éžÄþóacP-‹®ÈúwDwB¾År¹à‚gTlæ†òÆ2„½‹£c»D*|®µ‹ Oq  š«õÑP¬Ÿ£/g¼3±kÃgW@8êÇ4ªqƒŸT¼•©Èp>bÚ/Ш‰’kÈ­Ãú¾ge#Œ€!pUHÜŒ(®%²÷€uÄ#à”Σ½Èʆxm aq¤ÿ*Éꪇ‹Aû£ÝIdq¼ÙȦѭb„f‡x®ú“mº Ò5b<æ FÆÀLil}n=o“÷¾°, ¿¬h{Ζ ûÙ¸ÇZ ÝŸÁÆc}Q0g4•|ŽàEkÜ<¸åº §ÑG/5£ñêщô)ˆÂÍ&2ºKÚ1Ô;+‡6öõŸ´j§'Ð{³3ÙSc˜M®8í¼µWÌM«ÄœÅ¸`œiTÖÁ©ž>Zû½¹ AFÜpÔ 9¦5ˆÅsöóA_fc°ð-ws˜ãm½uC‘3Ÿ¤Žßq¬¥Š€$7=³…õÏ|íSÇ>&õù§b8Ær`ÃÆqÕÍz­ÒfA¹yq®iu˪*΂`£&‡ô0‡Ølm³åqr]df^Sè–:#DÑHÆÖZÜê‹iRèùÑê­€ ð5Y¦Æ9(ÜNžpjæ3nó5²PyÀ-xˆF|À=SÚ¿ ÊX<+¦1,sØå_ÿiXæMÓ(¿¦~ܧ*˜‘Àq¤koï8ªî«"‰íšÞÒ|V¿EÙš*ˆL[ö“U·€NÈk‡Ûì4BoÍä*škgð:A—œ§ï1§;ðõ%*Æ1XäÊéfžP6}?J‰ºóyŽ¥Ê‹‘SL¿n†/'l×Y]%¿T\è]$Œ«÷Äóz0X[ÆV¨a=Î7ª¬£(NDØYƒàUdÿhxùENž~¨½¯# ]ßl(9a°v,íUŠÐ"ßíþ«Ç¾i–S òÕªÔ:ᆚƒ­­¦IxŠK—S¸ÆÉ°åÊÙÔ/ª× Úbõ‹s¿©¯¶B›˜¨ñPˆ,š-OpQÃçrªp³tIÜâ~.ªuät­X‚jF÷œÅÇÀcJ‰ÉÿQ;ï¤s–ÚþüñéÓ§õ®Ø+[ÉF¡)¬±1•âc.§Ä•ö*'ëÇJG\ÁOµ!Ö…,*ëGý;i L|2Ф0‚EYY,yËÚôW«8!ßf²ÏæšÝ°„2)¡ÏÙݘÈ*2Wcà Uq6DGæc^oZÝX¡³J$•´Ê %ŒÊNq(q©î‹æœÖJød3z´s¼1¾•;š–ãý©¨vùøe€L>}“UÕª ‡+rjòP¼È^h¥»äþü å²¸\4»/MÄʳ®a¢[¬¨b’KHš»›ä;ó»‚¥GñPšâlR 4ʼnæbMˆšjL‹å¥ñ ˆ-qPÑ µ0–•†txiËÀ+þS´"tUžêÚÐòQÞçqòý9/ã=®|©f—2*ÖÅKÁF4²·^æêÑ–Lõ’ßG_ýԤáŒRŽ‘f¤~á 4`õ–#Žßv…$yúˆò\•µô}¾<ŠñŒå´µ±í†šÝ—;5bƒQ®`FWy(=‹*•’‹’•?vÙˆÜá™0=€€7h¸è À~$ÝåMbòçö=NÔ«‰¸N‚GŠpquˆ¦ÈjÆŸxÐIÖëÚn;>× Ðè¯sN9Ã[{œš+"™G|“O¿G׆1Ç{ö'(21¢Þ•©±§6sd%囌Å>Å/£6 îX2vQÄ„[Å ’Åfî[øÃ@˜³A×´¡ç¤¶ý9÷/˜-zA¡0Ãó·vI§8:x¤ÆnÆÞ]ê¨5褧zàYÇ[&]“GÞèJôPæÐ5ÆûöÏVÒB«×øjlŠaê›xò&®Öím€aRÌ5kXM9û?ËuQ€Š´h&¡¥º¾ÒÀv¯Å»Új¸:³ýk«9®‘±†a†Éûâ|d­UÅPªèï ®þ‘2¤­m.²ÈäÿÕc‘†|êKàÖ£d¿Æ+}û´]» úsnû½Òâ ÂjWwÓF™™¿Ö^®{“ahqYß|8—-ÞvÌ.™É>hR^+nkËþž';ªíÅÉÍÙ7½ æñê£ÄYþŸi¥súf:sÔàæâÁÂhº€XÀ‚ÇÂl,ºop`u޶cíÒ—«kµf±Q¨a¶¢±õ_Vz½k¯Š1† ™ÆÀ:ìñWptÖïÉÉÒ ‡9©ñÆ oWŒ`D¬Þ™‹ Ü…~ ÍLºµGn¼zk”¯æe(Ÿ[‘/ÑMQ¯˜Rǰ†óìͼŽé TeõTxË‘;”‡ãpP¶Ž"”ªOÁd¡¹FÀ64óÒHcPd’8R™ƒ“V(3ºsÒ' Ô©¸×}±Ðzr‰ÎLÔ?‚ RPé¬Ñ¨ ïò˜fU`¿ ¨zKhש¬t¿}k¬õw>³Ùµ¾ê03ˆN(»tixt^„×5›<Ò«å“Ü \?~z4i³j±´v)˜–X½Z¿(_ÚåPZ¢jgt’ìð*–ô\n„b”=å*c¬.!d~"08Ÿ¤¸ÚLÉP,Ä–hÞqh¹K¶(lgnEfãdÇø¡£žêD,Ò{%<ÆH‹½Úyн”0Å´>²Ññ\­¶ìò4 a-Ò7k5FÚôé¤WØîä«ãII)yM(!©Á>UùtÊ”€¬îW†ÙÍŠ‰r=Ò­ë£kJ­ÞŒœÞPÝÓ燯N·ŸìÙ˜÷øIgÓó?¦›ÉIKܺ°Ø»ãìðÙ÷¡ã])»þìô`¯#ïAŽM£Á땞»JgP©Ö…ãoh,Þ¹³†Ïj|½)ÿõ†Î׋*=w•"Ãùz#:i,Þ¹èp¤¥°Æ­w«b|"mÚ#¨6R’¼¬ï™hžºì>éI²úA²úxî•&ˆQ”À$V(…xˆq*Œ#™fgšU®žb6qËsXdå ¹£^¤Øó¦^ôZÝmóóâS‡õÿ˜—’Ô“" @­*vÓcÆžEŒ_üZ½>rw8)&j]HÉeü ° þvÇ"-T‡x5—Åf.>T3$-(‰eŽÆŒöfÍgV¿Guè¬@Ñ&äÄØàô4 ((ÐIŠÁ…TÖBü;ˆ/ž‹¦Óä}L½ ˜6¦ƒÄºs^z@0L–7‘Ö©CÆ¿êÀÍøcòLI|­ÂúáO€ EÂ(ÜgDs®ïÓKO¿£ÿ™Ïú·ß¬?}úÝwß> ?ëòg}ý›§ëß­óõþ4åÖŸml|Ó]›ÇkÖlUr‚çú¹†§ %¦­ºã›ÿÜÔ®y"©†=}UÀøáëQ1Ä´/°í~ò£Ê<ùÓ ¦­ìÓ—0]_™Õ*b®^<ÀA†#‘c-s“O†•ᇞí&E/Á ”™ IíîÒåt:Þ\[»¾¾îŽ/o@ö鴛÷gkÿ0Zc¿ûµ~Ù£Ö|…}9––ö•N‚"ÖQ‚Èie®ÓÙÓ–S«ËâïÌbÙIêbB{Ø›ãUÒ>T³ëû”¨$¥:ƒ¬¶Î²ªè%ȳ•™”ðf]ìŠ$ü#òÉ&¸ž´‡ /ñ³F`6ô O-…Þ¤ùd2U&9GÑeN Ê¿CŽáM<&¼¢_)¯üñǤºAÚKL,¶tZMz)F±HNíIøaŠM]‹Ë·‹¯i²­¹ÜI–‘æ`ÎÉqN?æ#©[ °Ò„:Ž9ÛÕMµ´d9u¼¸Žû5œ`*x-i»ÈB åd‹5(¦´ÑÂÕqÚH—t(©4F’²›òÜcÑU6ìkM?ëø¾‰Lc[Êf“ N5z…iB£ 4¢Wš¹T} ²cW¦–ÈWE$”­€utäB· ÜÒ±wÉ#£à£<±–ï‹Ôì9fp2R³\§)åû2h¥ÊN¢²/ÛòËÜ4^™M0A•MƒÕ¾u(pˆÐ…e!«[vÚÕfbAµ¼mLÓk:ÂL ‘`‹˜Öå kY[ò¥ÝÖHS#?DÏ.ùú–4ì…âáOÏ3@ïÉ GL?íçj‚8:žQZÔ;I½’ÑIŸˆ„¨'ó¦-€x/ÖnõU° ³+ò“æxjC,ùë_ t>ùë_åZßÍfn¿9VAÅvæ¥WÍ RÐØqÈ&%v€ã™SÖa"JØØ™Æ»†;jì÷.É=m„¡çŠE6°Ð äãõÙ¢éTÜ{§­~1¡±üEjªSQÁ0 æù[9ýͩ“˜>†3©zÖl ŸS’ºþàóÔdù½ýáTsúA«îw¦§k»·åO¯¹%Tºµ{Lû™ñ Ï&c´S³ÅÁN4(º#˜NUržc¦k‡ŠXÔA±t¼ÊÈÎRD}¹ñÞ®E’3ºq¿)f"^¹mnŠþ‰lö©–ÝeJ–póøãpÙ¸Éî¹±º-]ÕI/M{ÔÖgèaKn–˜ÕoÓ‰ò÷Y·Û5îÓéMë<Ö/,ƒ¸—w`œyœò–[ÖÆ+kºë÷Õö‚›ó«¦ü0õe›?ñS¯³ç-#}ÉS[ú\ì LQн³YZ¢%îçg³‹ DT!dîÖ…Z0Ô™-¦"ÜÁ-YÖZ ã€Ó0@ùdלFs‡!Ä`ÑPQ˜|ŒDËq•­Œ{:½œ® ÍF­é £ oÝ £2²Z¨9&¶Òº\—ž‚8÷¶¡¶’™’mCƒÜP¦-¿WÊ⨠áÚÔ&Rž/-)ú‰“ÀúÕ\Ót“Ì¡èñáIâËRe¬eá®—mÔròä™DG³±mhþ À¦dëû¾èàûŽ?WE*žw”¸ÑÚŠœ,!bÇ"O— ä:L ûD̬ž.‹¸Ì›¨ŸþÞ|éâ?Àæ¶æfì öö~Þ>Ø;Ú£Ù¹#\2÷p‰j¾Í­jžB  ÖÊ願¥grÛ—¡cSϪú’‚ó¯bR‰XôžÞK‚°K•Bï€C´ÖŠö£`c£!šž€ØÚŸqœ¼°3¿¿/Ð*©1Pm #4nÀ¬ c , ¦Ü·,××cé&KüðøåþQrtøâø—í·{ |óöðÏû»{»Éóßàå^²søæ··û?¿|{Ä€–· ú2½Ý~ý[²÷ë›·{GGÉáÛdÿÕ›ƒ}€M¼Ý~}¼¿wÔIö_ï¼ÛÝýs'(ÉëÃcs°ÿjÿÊv¨ùzÝäðEòjïíÎKø¹ý|ÿ`ÿø7jôÅþñklðÅá[µ¼Ù~{¼¿óî`ûmòæÝÛ7‡€‰8ÊÝý£ƒíýW{» œ¼†¶“½?ï½>NŽ^nøƒfHfä0=ðäùô—ì QõîþÛ½cž|c0Z˜SèïA'9z³·³_ö~݃Ám¿ý­#€öþç;(/“ÝíWÛ?ÃX[nž’L–7O°d;ïÞî½Â1Àä½{~t¼üîx/ùùðp—º}´÷öÏû;{Gß'‡G²bPôÝÑ^Ú:Þ¦.˜D(ߟ¿;Ú§éÜ}¼÷öí»7Çû‡¯Û0¿À\Ao·¡*.ô®ÀzMǹ;|ûBÆy¡µé$¿¼Üƒçoqªiö¶q‚Ž`wŽu1³l0³ÇjÜÉ뽟öÞ{½³‡ý:DP¿ìíµa)÷°À>·ýË64üŽfÊ0,è$ÿVHÞ¡ÕNö_$Û»ÞÇQpðãh_Цqç%CáÅèÖïA¿]úôÙ³ošïA¿Þøî›oן=Õ÷ ß~ýÝú³9÷ åõˆ’dMðôQòyàä²ÑÅ ó€—åઘn&Ï<ùCòŠÔ5»¶â&QqWŸ­v=X_Ý€žñmã›I‰ ,Ûžw<ÙLöúäz@±¥~Èû˜¡iò§‹IÖ/Ðn¹W .ðŠóG¨÷îíÁfòƒº¥Ühåäb ß¾ ÷¾7EÕàì+¼ò`" {÷ø×ã%ºÆ<6i»z<"$‡r ¤fC¬šÌ‹üœbLU˜O%,Ñ]6°È”Æ#ŠšÍ†ƒ@Ôœþ¢ª¯G"¡K:k˜5]tPˆœb?»KKf²‘V¿‘K[‘ÏÒ¾RTºË´NrðÙô«ÕN~p+Ø5¼]²)9€INw¾9Ä0=òʶÎ~ÉŽžB0ÂQð3Z¶=¾bwrA?fI=b‹–-.Œ/)K»jL|ÒÐPaô½N=‡’I€¥kh[cÉÔ0‡TïÂû˜’m†Ê3ùÒ»ü¤uí‰Hˆ'᎚9oŽHö“ Íf¦­5ùÎq ´>šb¨Î‰¹î^2¯‚ é®$…Óå¢iNãú‘VKn"ó @VeÔ_8µÙYoÁY…’<¡‹@@ÕrQÀTØÀ6ŠŽ;Û¸.'ýjÁ&¨¬ ƒ–ìŠuËŠôüÆÈB·˜ÎÁœD¤¦Î b+3”{§ùpõósô-ÜHrÏ…Z×jv~^ôð¨ÜÕ!§&w †‹,Õ£Õù,IÀñ§$¯'Éþýzé |8Á2¼K'@âgÃQ’|°ßàóð½œié%æÉqÉF(=`ÑRòTà &d%9âS˜ö•-‹‡ò*Ž^aÞÞÿ ™¤ÊnäU÷nè¡ü‘2©€êÛýì– ô~LÆ}‹Q6“½×çs¶ú>I±þlœ²/˜å»&QP`&Œß¡ZL´d#-ݼ@Ìa v‹*SÀ¨\ä¬PW3wœõ/Õ(©Ù¡}¤)…Ž1F^ª÷Å·xFZ—² hRŒÝ1‡3€ ]³½/®µìoŽ82ŠŽ5Î˱IvÄN·–”€$4<3ca‡!É‚¨l’–@WiUvJ¼ïxóŒqÃõÁÂ=Ó“IŒ‘ És`ÕoX,ÆØ4,¼ç’5Òd$Ó’s_á#«„`™À€2¾KjñL0*Æ6p_2–(°±!Ð èÖ/&L åÙ”•îsŒî¥×Aî›rE"þ2’ØÔ0Œa’Ò€’'Šé”õb´Œ)ã{ ›i“ƒ_è»8ÜjJí±ÔQF‡ˆEÀLcŽ Ì¥Ø@G+ÅÓÀfŒtÙE(øÞBÀè\¢*¨“£Þ‹OÁºÖu<ãê*vÏu,!±”йË9b(H…µe¿58”àX-ªK“®rth`\ö#`n•1ËE>ŸÏm"7J‚1ÙÎLLj4b‰ÁÓ" ·ÈZ;ÅÓ «hô„a´ÈÇÅ–#å:—U$¾šÎ/ 8æ|@ð·~p ›®Æ|Å@o±Ü* ;FGù´GI40TÄï µÏz†,ƒŠÍr-éÆ3˜0äð¡UÜ&lqÞ!¾ì©HzR¤î8Ë Y[eNŽü³É”W¹©ì 6¯9p×Ü­!#û$“²$H[^ôÊ®/ »ŸEs=r¿0é56&åÀfVÐÜþ˜Ò\çØ SÔªé gß8Hĵ?$v¡ŠT²@@Z³Ic¢@L% N!8 šG…"•LéWü½Nˆ­dáö_%Î6ô­6=¶bÓ/.U="LKáV[¶„DÀg°l8?U‚r¦Ñ`ÙÅ3›ÎªMýY ¤ÝÁÄ›ÔOÂXV{L™-éR,£XdÌ:U3ŽËƒÉ°zS—Ñ\ò¹Ú7`œMä¼7Ýî&¢Ë$…û*Vø|›Z:¥ÈÈ×:ƒùÈ™7g°öîëàfU÷ú§Ÿ®êü5Ë1ÉW-ãf#ŠU•9Ì)G¢á4âÆßž8=?p¢n(¾Ê*­ ßIù€ó¦ñnàE#³;—ôY%r*[;Ò~_Ò±¸Q³‡©ð!=Ü.[•f3iåKÓòfÕÔE¼:+Ë+û  ˜=jv …Œ«ÜÛêfw3=ÓšmzàîÕ#‰{@Ît ±ß †H7'ÕY>°š‚­R,T?QÅIÎÞ3(@‹ÏÎÿ?--)²Æl¯ž~õ¶›0ïdMg¤äÒƒªŒ0OýªÐ÷”¹ø?†|ÃKf!e’Sò'ªR_k*ÁV€€˜tÈQ«²Á•ŒÞ$5€Œºg]s&¢M6R™ÈžÃV9SIÔÆ@¥§îlèP¦Ù¬Ê—hp‚œ ­3á"»dï€ê‰MÞx¹·œò!)W®»§²OÈo–ÀÊJjr ê*m*PÅK@¡0æ:)/‘ Ö;"ÿ–ýCë$¿K•»ÌªËAqfJ ûߊ›‘*õ{ꪅrüot5')Z]%«ÃçŠ+îÍôêªM.òçå§ÈkV7L²ë.[fJ‰cü)eÇŒÚ1LŒ×}×9J4ÁÏz6ÝÜRe—–Ôy`vÓúÃ#³4Xhi6À´a4*ªCc&µêr¡SwZMûP¨•JaLHý`ù£nÏJ´c[°¤eÃ1óIzµTkÂÚœ[Z.žšòH÷ƒŽröO³;´¹‰*§‚kƒÅ>gJatú.¶Ó¯¯´rϤ]õnj¨®§{ƒöÈ1ÙÞGèÔœBQ˜É¢“†£ÚpseöX2YÌìõœŸÍ{ÌÁD`l U«Õ«œ®Û·Ì‹¤-.ú‰Í‹$ ³C[iñ²á³:öŒ?uÏêå9mòlh@Nòª˜«˜óf+}7¼И"HNõ¨^QN.]¸¹µh”p$ò2°~%KôC ¿œrÆÎFÅßf‘k7Ôß 6'·”üì9©Á§Bz}¨¼Œ½¿h˜¨îà§²‡®Edp€•$À&ùìu/ºdçsjî±0y-þ )½Ó7@9èüë•Ôµ›i­„Ó=L9L$Á¥DmòDáRYU©LKŒžªv¨i°ú¯q_šòžÕÚœ 4p-«¸ø•aWl77B=ár\Ïé•Ùl”ˆ¿†Ýxµ»ú­}Ù8è`:Õ¦EtR?Dõ±-/÷µQ¼ zoùÌ-eb¦pF˜yQ Ì©n ¢¢Ï3ð€˜ n`ç€Wí—¥ÚRWÌô¢X}¾ðwtÇ0×[†XéîÔmJ"Ó8É_¼%áWåù4-¶€/ìb´ N#„É‚`òVº–¶OV×?´OÖ?ø=@¼fês(M‘«‹þ݆T-–¶=¸Ùí¨gÐ P dÈ6Ø*’Êϊʽ<+ú·¤×0\Ülr§€¾Â:°/ævš n#ÓΜö²úƒ–Ýÿm¼ÍOŸ¦d¾¤T?dÅ¥_ôØ¿¨®èýIùMÒš¡'Aj_aõý=J$æݲ€ä¼BœÑ$Ì‘asü`w6&p­´.Bɽ豳e%kÙ²ÿi<ÏMõ»›!çC,ª‚’é–æ†y&ÕªY8~peÞvyp:Â)šªÖóCIíô©Ÿ<®~L“Çæ(7ܦÓßátR(‰»öSü(j,èâì'Ñ3òÙÏE˜¼Þe1èOò‘Çé={ãfà9œ…›aÑ‘Ç_}ÐÆ §÷೺ våà³±o»•x›ÍÌ´s핽Jªg)w=ÒmÏk'!_› [N3RPøÙD’úõÊú Pà„¾SÌ«t’Ÿ§ª›"k…„ã¡+i ÇÛ0m …"ð–Û;_J|Ôýy ©ùQ"úJ*‡»éE÷ËÝ‘öjôrÛèy‡Ò!±LIL΃ÏTT Ý¢ §EìáÃYr|BÌ ”u ›¹G¥ËͧéE­_®C»NcÁæ\y9±1(G5Bni÷ªêj´²ƒ±¥^KÇ_@íêòwõüœ@üçwæ\¨ÕCMxÈŒFº)EL˜²e^xûåÝ‹ÏêÃõfÏôçwLŸg"÷àó÷;:h'ðŸÕCšA²ýÓ7³Ö¥?y_Ú7;sÿœÎ½—Ðä"›BYÀ8u‹ÕNJ‚ÙxuZ®âÙÿ k:øÝ‘{µ<§%l'€7…¿¿s¤¾¿çzêhÔ™ ¾»'sL†|x½kqJªw’=7`¢š9‹1w÷ÄÔ™Û>¨%߬r^Îå¦hŸåݼ"£2ˆ2§zxŸ@[bPæM›Ôµ+h`mÙWÁr<ÄLÏv‹EÖÓu–®1#FÎa— I}0§ËÚÚÚt[?C`õÓÉĹ=b£hÂU±Ä~ È5ž°IxærFkŒè­¹¶›ÏNÜŇf6prr¾M‰çÛä‹Ãôσ%+Ù)GçÅÅL’S?h‚³ÿzwï×ÓãýW{èç·•<{ºòì)´¸ÛFÂb 1̆’—–®s“ì/DyÖÈœƒ¨ö½èO8=« Ú’–ÁfÄ•¦P/% ŒNJ³+ïM]eUÁùÝ ïî½Ø~wp|Šü­$ýRøTÔ¸hÓõ_Ì$Ù›~fº7%ß1¬,ÈÄ–ñ2zdBƒ¶ô|âŽr›Wg'bTïdé‡KÂÌ"à’ñéƒå½\::Þ>8ØÛÅ¥°·7vn­€fó2X$iœìVJ<Û2•‚ÀùÒ –8®ÀâÄëÃãS¯Öçá{âÁµÍcç=¥Id†’?`£Æ6 U뎱­“gö6í±ˆºGÛ^ƒÎ¢ª>ó­¢›w; « ²ØÓ‘Èuz궇wÊjDF®réðPè9By³ŸÒV?M¢¿ÔÇf˜Ô‰5V|ÑVÇ•“´c–l…·šê4Ù2éCS@´ä@óöQ4Yt¿IV’˜ÃæÓñ}wßüàtœb!×Å c„)ÌúÄlp8G€C.“\’‰¨üÀÍvŠÎVãéä…0®Šñ©=¹ˆ:'ia+y*1¯8½–w¯blýN£T?y+}GªzK?=Ï¥¤õ¸jã]Aô‰§a3w+]im<{b“¢â©ä—’‰ÖaY…iøAßSœjKûµÂÓûd+ùzÎü²Õ÷–aw¬e¥»ãþꢤ«Ó­ä䃷É4mª’&Jvj^ª j¦žB­hUõ^Õ¦K%.|ÃFº¦bt-W›8“¦F·—‹)%‘i5ÌÕúœ¹2 “kt¯(Q»Ù21ÈDÁ/mµ[íHáAm¢üEq”]E­"N›Ô•ÊŽÉ'÷o«mÒed1šÕ3EÕÑI²i1Ášýί%ç̦œhMÎdƒg¥ßëÍtÅäåᤫ1A7sºÑô©ëŒ›°§ÒÛîé‡0¹ 7æÖh<3hA³+ß%OZ†®½'ÇE?š@)òùî[2Þ¬0æhJlÊhºµñÝ t¨}²ñÝf½7†$'«Éã þ¸Ú2©¤Ý:¼¶6žv’´›¶u”;ý±LÉ¡ˆ1z?JÉ2fRãVX½&œb˜?&O눌Œ³pÊ12·’IvÍß[Ë/£Ú#cñiiq3YŽç’ÇÄ•’ÉÍ„û«Å)þ0¹R£ì9R'öɰ AiëdååOU–º–MI="…Ûa“êÁ¬¦ß''«¦f\õ#»ÃôÆr^;®3šî-yc >IÛi ïéÐ×´?½Üú,7= /ËåáèŽtµjF@¾ôÓeO¢–.ºÁ/}2¾bþ±Uô;ð½mÓ{»Ñh jE #Ô°&·0ÃÆ*öƒëûšüŸíÚWñ®©õþòeÑŒ»¿2ƪª€2¯r@ZwZ 0¹¢ÝƒFûWdŽ‹ž% H«Û‚õ+©æ Ëd&'Ÿ>}`÷ûþŒ$5}ïZIPZeçyJÊö3`~J’ÃùåásCêcL¥ÈPVðÖŠ­ÙWÃ\‹?à ')©k‰Æª5SFÀÌñÃg9Oyçͱ"FÎÐÏÉ!‚}øÙW/ÿ„»l/­m¥ƒ†:fìÿ€Bº±œD‡Cöú)‡tœ4¢aKȇ¯Œ:¶”À.•ñé¿æl¨Æ³_ê@ Øö’'ŒG~*p1+HóL¡"® v*צì”BY:d§„º¥2W[ôôlfj¯€bôòšÀcR¦Z—2\HÁ? BDìɸ-Ý WeóÝ]œ?Œí±CY{»¢_ɘìŸp8¢†ÝÝB8Îìoâjª G”m/IümZ%Y ƒÎ“ #ô 6h“.rêeC÷MU­†Îv&+”ÑkY¥Pϸµ(y§p„Áªó¼{tFÊ”ýä ¹áˆyλX\oó@Ê”©QSBAx¼Ðpó¹£ìþÚ±4XaÝö\\ð·-±ÓΜð÷`?m2*ãŽÕçØœ&-àEò‹ŒC˜e&‡ 5Øv“U?—šÑ]p,"K.2:wcîjM$OÔaó) /;fl–MOÖýAèÌ›XÚéjKN뺶«»ÆÓ´ ©²›NÝqê& eúR…4…w'¾9)>Dw¨¼‹æÏ^lºé÷ö©ŒÛ¬V¾¾Óv=jÊK1ÉW«^fâYÝ·†œ¶¢^crRŠÉ[|ZvÉ™):Í–J¬ýÇS’;¶J )¤Lµ? óQ ¶É4àëÝ5Ý1Ò »LÉs˜Œi«Ždífw êÒb»)° ª.:¡­NC^[ƃЮÇÎîìŸ5¶Y¡±Ö‘£9Õv°Ëk«‡J¯§O®W"²3 •q<Ð]³£hx`&êðø$»Ð&Î5§'boDQZ´pA¬6w1D«`¾mZØìð ï›æ¢iï®Ie5DxØÙ¦­^'<»NºÊ¿ …–£»UÎ""™œŽ&"Œ¾Ýx„B ï¿ÙHy<+ZJ/BC¾u¼ "4“#ºÇ&hŒè€ÞÑåÏ2^©ø“5›Jy*dp8ëë³Ô/úˆUv¦Eèü`vMíäφK$…H¹c`^çË"¡Dj¸‡INQÂzõ»=üsj æ%,Uý—¤Ý›ˆ3G|6¢èe“,ÿÚ¨ºk²eë$›yܦAVÄh÷:rÁ¹•ê€êi&v0=-Gl4Ó䄎EW*ª/•¬*"ç]JK_|D †§#Ô9ªË|0¨íVq\O(zíËáõÏ<ÜÕcÈ §ÊA”£˜¥”)å»-IôÒÞ¾ÊnθŸ§ª£rÅ&]ðs‡bð5cZ=“À?…;5T‹â)EC¾‡q~ŒØ_ïo+F–ýÛï9:`•8._MõÉß89ÃͦãÙ4Péæl?Ѫ æml¥!ÅÅOìʰBä/ðöÀÜ2ˆ'i±Ëîú}£åIš¤+ßÌÙ¹ø¹[Yw?½í#> ·LU+Ïõ‘Þ@.iª‘ÄÕüvØþNŽÃ3ÂN5´p–Ì͈èB£ÕFã›ñEí5¤IÜ5D››“¤ŸÂÏó¶J7ÑI“ÑÚÍZþa¹Pï²,z¹gCÐîâuÞ8r©ªú(õ…NÒ´ø-ýÐÜ]50Ϩ¥N°n áÚÞ‡fMÕìÑ/ã­y'À¯lÕŸ;‘?ù ˜FJ¾w×4z°#[q$>É7g;ÞbÔÅ€Çc˜ñj‘}¿A˜3‡´ëSBE]è<ž ¿ŽH£RèËšð#Phü¿þcNjdÞQ\>ZˆÖ+^ÃEÄœñÝÙŽêö–|9Ù\ý¦nÄd>Öä–‰Ë|ôñ§×¿.{,¹+ΗSÉ}–¢YKömäçE ×ÿ‹zÕ€æ&<Îâ8EƒˆŽõ^(n˜øÇd¶õ¸êv»wt.ílÇ×!Úù@yzgÇ]H¡¸“ôWwv=ì9ìÍ4ÞáGMÓ=¸£»šú7 kêò£/ès3†hëÎivi¡ Ù3=0âÿ¼~à§aP»¾Åµ`ü}¾ÁRaΨÞGåÿYz2©NIŽˆ%H="Mx«Úö•×Kºik)øŸÑ¼½ðPöjƒ<›x·6ÔUØ$®YƒÄ¹s ÀZ+wŒ\TØ;ŠÚRÉ@ï7SÚå-ÏZF~ºŸ=È—!— b(E¢y-j€ç̈Y(Ì- ùÆâQC1©”­:ßóG¹ût£Ýo²º‡eÐ"f*JaQ7þ˜g¿7Çøc|uqj/ðN4Ú¢M%´ÊþLW·ÿ¼´×½>—b8vLE9 ¸N'ô&á­§ßÒvè÷'Uê[5¿3Œ—(Ÿºé»_-BêË]pçƒõ*Àc|—®¯WG,ën2‡Nü®›Fü„Ûæ«Ø¹Ë¹ãÔM›àº»C¼{nŒÊ(;ZG¨_ƒÊ¢VîD }hìÚýÅÓú'>:üx¦ÊÑŽ)+ Õ5me¹î_ƒ6Ü 7ã©Â1¾Ø.\€` ‹"‰ÔQQi<ðC–BH:ÌØ†ßZõ©+¦Ã#Ò–l.°m 2åhÇ_q¸cwIت! ú_tñŸV{5âÇÈ`£Ç^pÐdEãDØØCæÊ”ó¨¡9 ãìÚô6P‰ç­ñ žt>_  §¤•‘Ⱥ¿LÐßyÒòTºš')—o ¹‹ÉæZJÝÖcCM·ž®ç”•16~…ŽfjK*›á“7F $‘†Ç"!Y™Ã–rÄ( Ø@\3¡æÛA|{m§ŒÝkµÆ¤ùÒͶäóÆõIðÃÆœ8‰Ô®'ÑŒ×4>OP¥¨¯Òï´GÕM«Rmßâ“ݾ\p‘J2ö¦³làÂ4{0¬3„o!¯ƒ•Ìs/¬;&êœÌ ´éw¤Í¸ý´®î/\Ô‰Ö&û¦ò‰[¿¿ˆ6U^Ü£]U_5­ìšDú‹añü\2»ê—°@ñ°EøùÛ,ŸåÁÛùP[[È›JßeüN¥çèv¼¦»”ô¡Ï•‚Ô*4ßÝøë,‚.}÷ŠÇf{Ë‘9ßoVj0«.µžïNËi£Åt*[Ë0ÌW1úI-£‹,ö1+”ã@²^ó'¼Äç¶JgÏ–^àAÇaW$†Ó.s!ÏäÂrcjFÎK«3ö'EÍ… ”ÅŠN•‹RNL¢O8a,y,Ü_QS×HNÜóýD“‘ ªÓ¤ÉAÛP3q7Eg mS“⌾ߴs §ÔbÓÊ(v^s¨&náySñ¤ÜÜyÀׄ sD©Š~JRFÑoåBízM\ð(®õG‘‰E»¤¢¶ÿÎ…}XQYà?ðE›Í.øð’Q•O‘uBQhÁh w‰nIHr4·xGæÎÄSÂ{½©Áö/.0a8–räÈ<µqCV½fìAfÏÂ_'š êPºE_Áµæ á’l@;½i ²áY?#(›VÇÎ V½-½ãîÿP›¸Æ¢•-vèÜ/r N‰dò!‰£ öV :<)ú}²QUÉå+ [ÙQ )¹²*_=»YÅ¿ÐÁ„w¦š;ŸõÈn“lr1C‰âÉ:N^,ÐléVû¶ëôÃQò LayÍîè.øW=Ø´’Êw>ï~óÛñËÃ×/_íÝ®$Fto;–.(p‹b¯FaURÄ`ßpz÷¾.@ØÙ|ÿžm|{ÛöûœYs*àó§1.ì¿ê6%Œž 1£…áîDÁ¸7µ:p7çÎçµY5Y«.³ µ¬Ìdµ,,ÿ¤ œ­¼7cŠ^]›'¥þ „ÅY¤•dN3 ÈüÆŠ³ßшYÞ¹#ù}M‘†”NÒVú‡ جî5D£= ÞR)ÅÈïCµÅãÜœÒQ0´!ŸŸ½›ªŠdÓii =ñäÊÌ%ŸOÙ8û¦6bõã6âHq)‡î£G’øFFÊçÚiÜ ÔìR³M“^;ÃܽթY[±Yg.%<±"hjÿxûÍ›Ýíãmg¨SÐx»fcR¢ëbôõ‡ñN¥B*ñ|ô±˜”#¿¸FÖCFJœØš4Öj!È“nþz×¥'¯U™â1ôµÒ¬¥5¼­c„÷‘‰ ™žå²à'~æõ1$wÙ“¼8 `²lÙäêöGJÞ%†t¸“¤LÚ8—Ú‰f ˜áEÜ`C¡XCÝ`£ý wÑoiS±¸ÉrmÌqä à +tʬ3¼P&U<̪2g9. /!Yr]8œ(ÓÐ1>1êüžÃ;4ð Ž»zÀ{ ç¢s„.:ωGU.ðQr,¹4~~·»rRM¿ç›Êº‘Wyªœ\uƒf.†ãæÈç{I’özPbºxÿÐîhwüÑ ,CÓ¹@7ÉÞçÁ¢Éh“Ùh®IÙ¼Zµ\Ä˳Ë9T†ö:í í®}ï«ENyš½E4 Ø•rÌš_T"H®¦+ß}ëZ÷ ‘Ö³?/·» £d%2-5`ÚküJ~®®·1Ve¿¦É„Ê$Õ”wØp Í}÷lMs/χh‘2Vúx5}‚Yp®ÛOÒ*m¯„]€Béc÷P¼¨ÊcÓóùï«Fp1ƒ= 597‹]^×,:TÑú3j€Ø°N5”Lûm ý*íä˜üÚkKBüqÙN^æƒ1źl'ÿsVLƒ³·!Ц3¡Ÿü˜¤q—¹¤s6yôßÇ#Äb@ä[¶’zHN‰(Ú>yê뇞V~Sp}³f9a¬DX?j§?ñýªkCÑ/ÿ“ÐÔMN›ÞGG®ÆuM²‚ž^FzÚˆ xýR¯`ï>{„)ó@R°Ô¿!}ŠùFb§ÖzÔ¿Oìá‚k¿ˆ9›ì¸iðñ$™Py1,þž÷QûåÐÇ«üˆN!2ö‘Àòj3àµUA²)ú‚Ú^|Æ©ï,ʸ6—®Ã4\Ï ŸÔ¯Âà) Hµ—ø!«±ãT:s,1©¿+Ɣ̳ÛÓõÕh¢mnBÖÎÞw{l`Òlá¦ü>ù´ÕÃËŸAû§ºƒSK’}{gùclÀÞzB´µ%øypBŸXtÂÀöu b>a¸iÿÝöþ‰ífü4DC Æëž‹'ù(B²›]'°–.ãç#­íÜ}‘W\‘¹û•hoì´Ï~¡DÕf„åQÇvcJ"ïcð:(9òÐôÏÜ þ1 @° .òk€ðªìcÐP/#ZªéÇ×§Cc‘¹PéébӱРéÍ -à€Vƒsá•~Ü·.m*éw²m®.#~N.A¿R^sUKGw ÚÀ„‹>8ØÐŠ ·áŒÞÂÂñ ®.y"z1ÊãÓã{2IPÍú^ñ3 ºÕj<ðƒµ“ÓX-®ëzU¯Sôûò°U¤ÕòZ±«3x’Oa7"qû‘õÅBæî ë Û0@u^AC 2¸Ü¨fn ¡ ùJ ¸™ª™›‰Î~âÙÈòëÓþ@´2Þaö¶§½h»³×ÒsŽˆ?sq÷¾ÁÞÁ Þ¸p}U?ri4¾Ù°k©)ÚA eÑvÍVŒ4Óh•!ÛÕNˆwóùô³b²qÖBÜåmúÿ³÷öýmI‚ðý­O‘6Ð&iQ±“œfÆ‘ÄÏ8¶Ïv’Ù•u<„$DÁ@KGßýézéîêFƒ¤levï~Ë™X$Ð]ýV]]]¯¢÷—mÙ²l¤žk¬™ÂáÛ¬Z6ÞéRµÍ!ßÞ¸Ñ>æSfóuÎóØÖ°;bSú>? Öqxª¹®<è&ûm0Ê©7-Üu#ÿæ³òÛC‹[k„? ­ñe`‰Khn@ºLúC™.7›ÎH¤u Vt§^H/k™¥ÍcL&L„!ídþˆL³ååüÌ~zôòùÏ?½x`ˆÏ–!‡_/Ô¥ þ¾Q×4ü‹ë&ÁgOálQÉÆû£b±*ó³s”~‘|}žO²y…°/ëó¢ Ô}óË‹èuö>vã„! vB‰Oôô?6AÇa¥àNX±á§šÆJ«ª-/‹ùö%…Ž×튄®T‹$Ý ‘HKm˜Më³i^kÃb]ýùm+4ª‹ƒH:7Ñeº¢[ubsª ?ÐûÆ&\£ýúôÙ?¾……ùLJѾ^Ãè‘^zÊk¢~ÜØYv¯dpr=(Ž®„YïÀw‚s³ É9*Î(0?€ŒrfFêóè*ƒöi~€P `©a;ÔékT†MdåsUWC°é÷¨.\¸ŒHù Ëû¹lÂö> ÚÌ®ÕÕe’׳.}¦8ÕåAs>Ÿ=yû£žÎ‹é‡vz>j¹ê9@Yßña[-½<ƒƒ­Öõ¦Úö¾Y¨®@ÒÉy Œ[VN[ ¦}Y7K‚˜,›žu2:5§œ¸;Ÿ¡×jZ?ß×£kvGö†²­–YxñŒ7Å·³t¢ Ï^<ÖÑG¯ÙÏ*«}`ï ³38MŒ6w±D>z¥Î Ðú¦Ñ©b#O]”u³\Té<¯YpUÑ&¿µ:†‹Mk³Íöô*O$…Gu:ì>Ê%;Å€aSJ­fi®æ‹2cîÃH€YYËwoÂpj·(ïMy{;?ÿTþ¸ãoôÝ㣿=yýò ìåkPÄ{ûûû€/{“É$ ÙÑë—¿šb"Yµˆ µÓÓkïµ󇉚&‹šªwÊõ¾Ú¢çlƒZ)×Úß\ËK–†mžr›_Åž’Ÿ¿þ[ûtÐô¾ýòÅÛÑÛÇßÉ’“ ÛßÿŠ'n'Ø/÷ÕWÔé‡u¹0<ݽ‡ôBÁ¥<_|iôû£W¯_þðúé›78”×åÔ÷ªiä’³Ùû¬Vü`Ô|‹ç˾k”6VpwŠÊku‘¨QæÊ幕vÒ¾ÕIÙ£9öòlµ¨3ôˆÜ<­v$xÈUÀÁì¹z ._^ñËêld\|?S Sm.gn#ã%ã¯Qz ÿ‚•ÀXqu•? |OÉ­B€x¬ÚD³âÌ·ûPF:ÂS¨{ðžB+:¹ÌËÄW]Rh¸\ºÚé† +juuƒ-“ð_/Út–¾¾•¯õϲâ¢ø%ñýGûêÿ±ûºV'wÖèWHŸlY3xVÂõHcq‰ïñàÄé>– J±>†|íèw^\u)ó'gŸÝ³ R ô¨3º>TKâoŽˆßïýã[7r ùŠUg]éëöÒŒFô8›6‘Șý«³}¤NsÏñFŒòEäŸf¶Êhºª-­©³¬®_¢ìÎ!?Gdüæ¿B!: 4Þ`P%{mñ^#¸>ò²3üdœ«,R]ÅŒcÈT^Q6°9ž15±˜Ía’Î Ò ¡ó³uaR˜ÈÞðª=!ÐfÍF΢Y û‡¨£"% "(·¦ÉE‹É9YD>”r¢Ü2nF Aÿîù[›ÚÿC¬ŒÀøñî VE‚Ö¨­Ô›eïÕBž–N•N;ð~6åO°eàm¢ÉØ»Îòìt£!XLŒñj><èF‹tz=ü ÿ®†"_ÆéEùI•O‡±¢*‘#+ê` ãqQŸÇN¥3E³Geqei“j„n˜ÃA³$m[x?T˜†s:#¾ûªƒµª=T•T‹ª÷j¶*ÕkqÄ*D¦©(ÆË0ÕX¥–‹Õ0žgW2S2îµ@ùuìA+Øñ²®Õ4´ÂïC€?o¼åì= ̲ö™·!p>µ?_ú£É•Û'‹çˆÌŸ32¾‰ˆlsh[6;#ÃP—±Þ×±GØU¿ŽcÚnñI“éÀ× ±™ÁT¨#wr1WÍpÑ*Ž«&½‘H.ìB9HBAÊÚmu±†Oü¦rEyáÌjþâ®ÏCAóî™Ë>Êê…ëžìõͨÏÒq‚ðçð7Ñ{±‹‘µ‡ê'Dú×¼4ú2Ñi‚«¡v?’$¿?è|vй7ØUãÓí;ÜW½=Þø¤V]p«Ùá«´.V ¶¯–Œ]ÀŒ 9Í•2 •ìFƒÎše³òãŸÇ"ƒÀ²Qrä[,•ê:µÖï·óC÷çúªM‡¢©qq=bJ0øb=2[”üc(†4ÒÕ<`àtQ–9›ÞÞÉ÷O€æ@ié»Ëøf’=üæ‰ó›¤sÃÈY\”9k‹¯Cb¿Ìó”ëlÔøoxp{l>»e•Ób²lcQaÛ€ü—.«€C,–êUOl¥íø7ïÒØÞ j»Iiía­ÖÖ"w<ÁºnÓÔsEElHaâƒP;€¿¶Î\Ѱuôm(µõ`<#u#]6g[®"œrDJòAm -iOæ“5L–S èQ/ËJuÉÃþ ꈖ=‰egâ®5l3šjÆBq¼A|rŽñN@øä"[u‰ävø Óñ’"B¢wKÒ9c¦†„d@rËÁdžU-Å0z %H<±âI±,IR®oEqŠ÷íøLó c*T<ž©¡Å!Zm«‰€Ó'Áhöµm;`¬Ñ*}ïk…ey@²‡LÖUMÕ‡èÝ`q:!`r‚Í ¾Šý”IÚõ Õy•äÚ‚î‹bÖÍrÐËø“oëÃþƒç7ÌA]^wý‹&wÐ2eÍýj°ÿÏÑkuÔfÕ9óðÛ¶½WKJͺFCâÀ¸k6XÜÕÇC¢>¾j•úÔå—/i·î!¼ÑA†×Ú?·=Ù=Òu‡¤NÃ¥{_ƪÙw—çt˜zÝã-Ï®á]öO¶ƒs›Kˆ;5J5’£tþ>­ü¡Ð¬ìï›KÖ&Öª­;®´_ug+0šîUËùE6·‡?}ˆ5˜ñ.„áÂá{Aǹ' qRÀ3šÙŸô[ ß H”ÒE¸àOZ§§Ž…ËöÓ)øì³yÕtgå,ŸgÃ6e·þ´mót2Q·uô%l: òqK7ªl‘bÁd»~AÖª,’Ü€Óå/·ì2ÍÙέúÒd,>j[{äð1m]»í<¢±çóâÌéj›IDx½Qº<+Îî¨GO¯óÚéÍà6½a)°‡{Gu9ëI)oj?­&é4ÓíÁrÝ…ÃâCÝé•O±ˆŽv¢øg6+Ò†l D×Ý´”ÌL„‰Ÿ÷ÇýCJË(}N°YKFõUa€%À;¡Â÷D08‘Ìð}ž]mMšwvçnGÒ’ãƒC/YÐ{$KÏæõ/i)“,¢MºßZhLü¤x{Lß³|Ÿºv©ù©ßñ†ñ‚ªÒWÌ}âI-ôü€íìŒ ú>ì ‚6Õ¾‡‰ÜîFéíGç²N‚'@'õÂ8YjÚáµx‰©ÿРÍÚ½P˜üš£4ë÷ù|2[:ìk ZeS„_A¸öÕ,¤zsǘ7ð1O·$k0òñŠ My![K¯Žö´ŸðÂoLºåèÈ@=^%uEUk—¡ú2–îNwCDqoÝ€ždhc;ŒÿØù{MKLôŸ5Ó,8‘kö LMpŸè±Š}¾CÛ¢¨.šªÇcuÁûxv)…êÛµôŒ IX„þ‘ B Þñøý`»£‘Ý–›Ó«{ÝT3è+ë÷ö² …½˜vN –""`îŒÅÃQY{œ2Êc4áV$º…W¯³ µo`Ï\íÖPý8 !ÐL¸¹^"®­Ý…(3p¥P‡PÇùŒµ‹*áM'ÒYôM4Ø?xxïÞS§?8þöÚ•¨B HÿÞ½O;<Ÿ7`üÔ€q ?~­µ>o‘GNOûä•l›Ö%ðb¬çERìöˆ‘àèžGCKÜÔïCßã¹%º£ñ[ò}–ÖÐ@vc2nÉ~ÐsРbOr^z“Õ5²]®¨f"kaYZáÔº³±}*ƒÐ 7=7UcÆölÊûÈñë8 ºš©ï•ÇìþD»éýŹR`€+¢/\u£?«f{Ô$™Ç˜•ì&ß<µ¶‰¥µfáÖLµ˜]óÔ;;Ù! ÞcpUÄêß\ïfKí'ã­ÈüæƒË”êOÕ±[£IQ¬hQCXˆG# ŸNŽ÷Oúj6ÕÕJËž†+9ò¡¶ÚšÝåˆí aC+)uL£ÌcÔnožØl­,Ù0ˆÖÑ·ÑþÎKžï|,ä½ÁI« ¨ÕÙÁJ;J­VýWŽò›(±î`üá”n9îû5n]¢ÁÛPwÇyÝ`©øŽY™ã”q³mÓÍ0dpgÚb{ö YQqÅ%Û<”í®Ûd¯Ô -]F°/_µðÓ~BKÉŠ Ûѵw©Ç¤áu$g  X NÚD=Ú À¸ØzX—dhµ•ÚÔ+»:{&b?¸I§, nöKm>„G,Gȃëì´#Ÿ”Ï´_By"MÉæ¾&U’µhèý<÷š­ -›DjýÂë´Uß\—žëšÃñ$‰“\WARûI9WynhÛÜž%]p*{䌫ÎÕÔu"›¤±2ª&e7"HÔ a1V9E®ªˆEQåè%ˆêEÈ÷,¾ÄË!™ß³!‹eб”ÚÈ%”Z±wgç}u-×]R_¾‰þ]=ˆú ÿ¨Cî¥êp9«Ï£âT@P;?]¡;£bóL¶ 5+°©ëB½Ç® è”N!±ŸÙ}!•–à|]õ£ÃÞƒ¦èÝk’Á£}Ç Jñzz¦@ŠßŠ.÷œ:—³qòñFª$ÛÒçûwÞ’=h8ô<Ý_EŽK=P˜3R|ІôˆîÁ>ËüÆ^ÚÑiÕ°¹Î1u}d½í.¯½f¡üºÕÖ°£öB¨9ÜLÀ±ÞѧŒM`€5>C ÕÂq+“8h} à¢¡^;±H©‹Â[-”:Ì76ÑÍ$÷Ú¬gÈøÒž'fμù   ¯ðšÕ Tr¥ÔÐþ Êxbç„âèT[ñ*\„Çl¤ „MGn*z㱫ÍìæàO'5ŸOJ‹$ãý„^¯õ“3'ãÄi¼ËƒÐ<ä“ç£'OŸ?þ÷á`¿‰µPÛ»Œ|*™t rYÉ[,”ëÃ] )kx£(Î3½sæNð^ƒ×…#‹‚Õ…¶kˆ¨xÚVã¢è“UCëð’¬ÍRj…b0DZœ$ ü&°³ÄòC6vÕJâ_mª¦IŒ¾@eÑËAc!¢SkÐ5&V?‘#W°EgÈP­¯x¼¬…@-a€Š=ÈQª°2§m‹‰>O!jõeöŸ¶Á ;DçöÏ1«îtÁ¢O©uÒX3dc$„¬† ’GÌ8t_$Õ @låûÃh°EÏØõcÛnéi·} Mi°¹Ÿo‹'j“­kÊd÷ANʦÐüÓºÖöÚF7 ´µ×ÖX±¬{Åi²Z–˜ªz¹¨o=Êïóy^ë)[7V*)¼»a~?+û·n×#©¬kŽB­`•ßD3ûš±mÄe{7ŒßAûþÖxOŠ;8’5å× jý6µZžÖ3‚ è#Ò÷OP'À†–“Ó;«7Ÿ-¾t)”él3kÕ¸Y¯gßÚÒ~î}ÎuOä!ˆ}—¼²ÇùëP'(¨H'œ ₹îþGs¬p¥ŸžËD1 0¯šÙ÷2@„Ó3 ¢AÆþªèÕ¹ZΠÕ?MŒñ®M´¸õ#8œ6) PO7£’¼ø ) Ÿ¹]ÒóJAy%‘OkÇA c¥Äkoë)´Þok[( 醀‚’ QvoÿÙæv—ÑSí4Ô&¿M?ý樵¿[õŠmÖz`…¾ý•ÉÖøvèk¼é3EA÷B—µõ3g¯QzúdÉ6$úbB$KÝ®ëÔøVYe™ ¯7Š€ïæ1¥°G¥–`' ,ˆßµG%ÝFª60°²ù€X–Ö ’ÏÛÞÈd5?`þ@¥Ù$ÍJM@ÒW¹USæWW» ™|—ÍçžÀa°å¨HL#{AúÇÅšàQ¦A홉ª–­Ý3!«Î"‡¡6Ê‹@”ÿâÊD¬ð=ÁföÄò2þÝ:`jòÌK Žt·˜/£‚•ÝFˆœÖ5H D“&-EI鯓8‚ciˆ@.Ú?Ëjív($‡zJóyÆ UéŽcs<½¦Ø¤‚Þ|³…ØÚ=Dê¬á€ŽÁÒ00¼ŸÓðÇoÀIWìÝ-Z†C­·^L›À\¨ÑŠðƒ³Óe,:¢2 ×kõ}{ á¡¶3cçLpú,)['WäÞc*›V*-³ ¦å.tÌþƒ!M'F(­ VâÕ×¢vaÓÀ©o+ - ¦`v}D̶¯×Q ê{—]®ÐÜã¾åàWËù¹Ú7+ßPNúZNƒ&‘c$ÃPî]\yY4Ìš~Ù;Þ‡?>}þêNw„fê8»*SØ>@ZuB¼Ã÷¨`©‹b¦³£.+Šüj°/• Y=‹ˆFúU„‚q#%Œ£ 'k?ŠžÒ©U.–°fÍÁVm¼¬YjYI"µÏ²H]«Ññ¦](üšƒÝ~½ÛÕ’L3ûn7Šþ½Xš8M¦Ç&,rnâðÏV I‹VT\-³‡°D2Bqj3°Ódþ ˜ 0Ää˜C:·ÁbÁtsª5ïvuiê¥I€ý¼>Ñk49/*Ø-ºárIcåYîʯÁ{ Á–KgUÁ !¤6ÌOO3̽ÙÒ—[& l'ð³2jPöé[ÚrW·Á2º“š‘%¥šÅÄÃÞÒü-[‹´œ‚¤¬'˺:ÔÛø˜Ž’“wµaóô›åž¾¡i  ¼PHê¦ñ>†>Šb 6÷‹€wd’Uó£Bԣ߻“M¦†JJ†¯Où˜Ž¬÷™Z•Yv–+úkÌJD*«ø4¿†FZýdÏ™†bˆ3úÃHóÇ@¯å¢ˆÕ€ØèHõÚ…öËGÚ©Ÿ:ã+nÛùG÷CÌ0:2„¦øñw/†x›É®×Þ»ùîýf‹»¿–y]«½9^EO§W€Ï!æ®åÛgoŸ?… ú^4ÇѾ$Ôý‚Ågß×Å7â'Na~–ðgˆQ;¢ ±ô—"Ð@êBw-F¸©KÓ`aîì4žèߥw‰ttV*VIQ‘0óÈ2Š _jï®91WqQȼE€Ú]\•T|J29Žñq|ÒéêT@j¸Ñ´ÇJö»QâU¾wÐy@-»ý÷ØÎIŸLbGàñ’ä÷BUîC@ñÞÁZ¹Q¸fzuy”÷׃¡µ®×Ùûlÿàzâû‚!øWûÑ}µk«$ÿì‹Þç{ƒq+š€ÿOž¡'Ö Å²€œ‰]”_fÃø³ÁǧˆhgŒb̉Eq· GÞìð>êo¢Üé¸ûk³,p‚¦Àà¸aö»öÿ8zš2/rÀþI'ÀËz 6Jdåĵk\ºê2ÁU`¨Í@Q”ÓJŒÉö¹ã½å\òÀ}ôF‚Ï5$$æÁÜâ´S¢Ñ`Ðh°ÝЮ ’;r(=} Ùò÷ QùÆiFS(ÿÞæOö'Ïà&lòíMÜ ­Ô×êïH4úc5!ÁÅ£¹Dß £žš×æõwÒ¿,ÔÉÖ“)¸÷E§÷°FaQKf n«ÆèÀšP¹Ë¸Þ¨À-ÛÔýÜÙ©÷¶›úÎÅ0[¢ «))N*«Xß H½cYwJðEút0WŽO@¸d ŠÌFT(… t&ËÒ¹ž¬±˜jÝ7-’ YÄ7ýØ‹8ôÁ8%ćF¬®+1‚Ô7„"1%VqÚáóßöY·¶ÏQñàúGà#JD¦Ëâ6ŽL]Eº~Z侄”…€ßà1$aO—%‰ä³:GIÖ?ëw)nÞµØ69¸¥r݈äãà cQÕÛK€=c†)óË50½@ë7, O2¶¶F´í±Mº‡±"^½£!—vâÖ"ÃëáKÀRÒ³˜“aõ7èÛzz»j¢ËÊPæI: Åðû¨£ƒãÓHÿl>ˆ¡Öí{+k?Šè²i·À%uC뫨ÃnOŸp'Ò9 µ&”ŠÅnDx«AÁ…ûñ*Q”$Ä“•ÂUÆÉ…ôåÅcp$ïãÑV÷Ù–B;xÐO'ÿXæe–„ŽàÐB=QåµÕ°Rµáõ@(›­6Å=ÃjÉ]1ÒÉ+Ð3¢!RGÎvé€ )ˆÎl1òR[˜¢Úÿ6_sŽ øÓ|étG•r~7‹Óy<¤n;¯ýùé;3:w"qæq®ûÈÇøéëT­XsáÙßÚÙhS¶¬A¨ð”hd‚p÷âÀ°WªÚcj_3HBVÌðxûM$f ¤ÒÖ»ç2V¤½?„ÜŽ´,o6Öo`xñ¶ß•›Ìû[§j+3iŸdæoGÔf꿾—kíœÅtc!m\”thíòz—ÛäG¹ ÀçV®X¡­Ñ°ûÀ×ÒùÔÏ̽g¾Ýzb¶ñ2°=ýOƒõh´ÆÛÀ6{;ƒ cݪE3PŽ–nû3¼Õ¦ö·cÃßaÃJÜÚçÁ¶ºßÆAnIníñ•Üê󬶲-0ž‘öVÈ  ¡˜d)NIÔ¿JIt…FoÑYQL£Eáh€Æûa^ë iÓ\ÁÁ‘z¹ Àø,4JÈ7[EK‘X“.€­óãÍò J<ÛY#ì•üÕm8üø±±†Ôï÷ãf´ ´@s?&2>³i†ï—Ží¸Æ-„ܼnB;ܼCP–ÇÀ=Iat62¾õ~2[Øü¯Pµ;Êlfãvöîä³³qˆšžï—s²Úº;È|öYÖT”!²Ác¾ä‚ÒЯù¬¾èç:·]g- §FçÙõ4W¼gaf M³ÒÙd9C¡¶B4æ’`ézòˆS3//õ&DCr´yS?ÀJê]ËrdtÇ ×í/‚c´,½ì;»T‹ììdì8{ßÌ1ãâbq9–y‡aœ4H.M1ñ"•ˆ:-¬1Kªp(tÃqLS6øâ‚ T’`ÞvxzU »õl³}¢  £Æy2E»j¿A°1Öù é#(‹ódª@ªVy6›Zù¨¾?üE#˜²Â.ƒ ÙÅ‚ƒApÜË öh¯'b“©ÁeTº³Gm1!ŽÆË|6µº0m[]¥«>bÒÊD¯$D„Ðgï³r\T™ÌŽ«óéu éÖIVÌ!·‰á­Í(kâ ¨Ä £Õ°V}G ©SPàÀtQÑ¢öåÚºè „åáH`Ö^‚0½¢&¡JèR˜:íP¤Š=“á é©0ê]‰ÿT1—ý«Rj2U5ªÏûÕb–צ™Ž›JÂB9-«óDì t¹ÿç©Bvñý?òÄjO\-°t¡¬ä°QN˜4ÇüÃÓÖ(Œ§fÍ…@H‘¸I1ÅÛ6—áá?{Ñj[€W bžÆ)PªýûB»à ë‹š—Ó¾þŧ*ñ»ãkD»k@8SŒ D@Ì1ç;èêiÔå~;iæJ´S·f® ÞS3›aÀ H¨q @j$¡H…   Àfõöò"&âsó¸Ù¬mÏä"5;O½ÓN9Jvž%+0‡è6 D ÂLJ#Ž0†êÈsWwLu}6tŸ‡ˆ~U´ÇîQãCÉÖ° Ì C pÀfŠt½(š¢!æôÍlä]{À9nJ¼ƒkÍ 41Ov,jKË3*ƒOéXÌ%npClñD ø|¯Ûo·ÙsÞ¾ãŽ2Ó]qÑŸÌ G¼h‰!2I~o°ÿ`ÆÃÄ Œ¾UO{ƒNã…/Voо/ñ“Ç=Æh—wÈbÔÓè;8d³òîÀ¢! 2ƒÈþŽY=²hÞ9˜wØP·ZŽ'1A)(yb‘ Ï9FI`¿F6ܬa}ÇþõåŒM ˜o[A¬ª±ºt²n G!"œ¡’”ó¬ì…{…˜ ¼ÃDF@02šYÀÅ#×T P/µ­ëô@ëÓˆR5ÐéxžÉë9”¤NºyÇß »^ Šjaœs25¦ÙŒ¢¤ªÉÏ᡾Y4¾KûAÔo|$ˆΖ.-P›}õq¬¶ãaOK,.œ)vQ]â=ÐÅ„:=;ËB¸¸P\dóüŸa¦¸€‘Õâî)¨Ñ«Æ„˜¢´BßkÇVãIÉGC>p¨ãÙ.wÓW}¥{ã`å``^“²]‡ œ0ô‚?²éU½½Á xs'i¦Z ^]ïífïô (y(möp’¸f¸£©ñàæ†·{öÉVn¢KÐ8^¦õäÜl.›_‡¯ŽìbÇ•âÊ,Iß[»8ÍU0=G2¦çÍÖ w]of¹Ó¹1PÎØV]Œ •j+MêÐ Ámhžÿc™¹W”=¸M#±´¿#·ºuÆ-}q6RÃWYá-ÞlN!+« e`bUž†jÌ ¶#>¾%Šh²ù;þ¬z€ÿG›vMòu7ºÞ½Cr®Zœ6ªÔ¦yÏòR¦ÈPƒžÃ`ù) PAƒ¯Äq;b§ïÁ’5ÊêhN1GRyØŽ(ƒW‰–\LÙÝ’ž[L_;½÷g¶ó¸®êÖà)V³»®Ž–™Øz® FÎf[³¸ ±^ É-ö~>¯Já¦*<’¿·i†’Ã:¡±Åj¼Î ¾ŠCh‡—p°†q6AY>ƒ­à!¼ jÂy·Ó1 iI¥B|hîçgO*ÈÆû -ó)~Ô<¿Vä¾€ïÊ TALºçÓó¢¼°àÀÁ§ÿÉRmœ 0LªG‡|™wA‰]¤‚ËFâà1¾Z‚¨ÇWêõ9gÕÅb”Í`Ç?ÅkõÛ2Ëúü=‰A8Nb.®—,XGPK§ô1fgëPƒxéèÝöm¹+jš XÈŠe1L£öµ6ã!- cÂÅY?#æWFJ¬NæÓüzÇó˲]èÞó’eîÜêèóèï?=—+Ñ%ƒ^°×D e£(¶¬”£ "¸Á“F}OsuèCÈtGF©àY6ÇÛ%!³KU8Plä OG<4犼¨çõxSV1çsoWD@Ì,4‚6¥)Ü Z¸~ÁÞvƒÄáˆ'‰ù®æGÍvxŒîc0õ‡æ~G‘5zÁÕ>Ï1[Às7•\,¥—ç>é´BA±ƒç„¯û0·Ð[ñëvýà ÒЀœßëA1¾áÑ ÏöÄ2ĵÑMûŸ§·A¾WY©^\â¥õ¨‘8Ž †P@T6Š_­ n,;9øF¹ž°vÌÖ¹ì >1qL΄°9ðÝôX ;dd)!Ñø¦(Ktÿ4 vM›ôÖ É2œ—y…Ìh”|VEï+Eƒ;¾ŠÞù$v»Ðª%Ãÿ¡EØ8#MÌÛ¸ÈG4aвÚf @_ ˜£(×ÂAuS‘øôî$Tïî»r æZàäÈ,£Îº‰ù¾|VSŒ2Dl4phZ£™OìÜGãˆl£ê¨™"Âo‡1ë{ t¶Ê( õ½CB=ŒâÍP邲¼3©“RŒ¬ö7?ú`%70J0úUßÏo:.¦/mÕeû¯PÚv ¬ðo¤‡o½÷±•ú­ RF²¦¡„ùìÕÓõ²²ô+Ðê'­Ë… ¤z»¼Tü °V˜µèÓ 7)¦l˜} HT ¾¡µ'ÂZ‡-$ÇŸ‚é’$)rEqÂ?«˜Y³“Úµ]ÿL4VÇ9£YZGµ;ÇV½Ã¹(<~QH¼·Â²GI§~`OùôT]:]Ï‹ÙTжýxóh$/™š¯€,)×|~Õeñ„Úãj¯ÒÙ…/͇ZRöLÒ_úm¤ÊV×â?q|Ôc­[•\·éYØ”>,/s§ˆ*Kb)ôTvÝÌÅöS­V,¶šª£æ% ˆ¹Lwn˜9Òƒî©Â\V V+^gÿæ#@þó” ù*6ã f¥Z:ÞÕ©³‚iž¥´I™)b˜“”—Å‘”Õˆ«ñU ¢­åœÂl®dO´/öH †w@㊃„¬ì/kÅ×°y¨1‚’#ê¸=ž«³ÖD…„’¶ |'d¨Î¾;(zÛ­wˆ`rTmKñ¨OîÇp$7OÝv­dšÕ6êwÌÔ¶}ö«¡£ô³òOïæè¼ü$rÅañöó®¬ÝÀÑ Úñ_lœ¢—4ÎF=G(»óçgO CX ¶ºQ3À9'>ÖÀ)Ú¨¸×¼~n6qÐk¸Cù‰ §½‰¡^;eZŽMljÕd³ Æ ói?4UŸÆˆËÏVLùúy߆Aßvîý ³îWdS#)fú+ÄOÐÄRO32ÝQÌ¡¯ì²Çlw&÷ê@¡h }`«*i;Š@¤"ÉÝ©LJóùá]C¬¯’àlÑ=ˆwå&våø€ä`Ѷc /ÑÓRø\wvŒ ì0¥Œ×5²OÆl³:ÏçÜ9 Þà#QµÓW,+ž-s·Ú??s+™à‚T“-ÚTÙ+ýtg'T@úÑ]„F#0±òÅ¢Fò®‰—H´_Á£’9øªF.ß0ݧw˜T»À·ÉnoºÛv{=…W»¨â©‡»ø½e@³¡ |g8%ŒŸ£÷È“g¯wÁ«Nß§åp~¶·ÿjÿË<«Mô¯Yÿán¥àf£º\f­ãèiäšÖå~^åE„Àf«5}8¥>(ò3ÉLô¯O탙« E<§:”†MRº¦gõ,»Îë^1ï¡!™é!xÞŒŠùH?ýÔžB+ØÁy„ £bÔIG°K¨W±dÂÝ™zŒFðPÚÊ õȽkQ¤`«î;ø”ïE&›Se¥*=¤JnvÚ!w¦úµ5ô߯_~’™‡øk gžM5çiÃ`ª|?¤É¥ð†¡:Í#ÍÚº~zfRn1Ÿ0ð­Æ»ÿ%þO¾Ø¸ÿå—ö½Ï@ÿ<<ø|p°ÿˆž|þ¥(ÓP_.À•©§7E‰?LËôê4šúRøâ‹‡~Sº­Áàóƒ/>Rå°È£Ïõ뇭oêö~ï/Vÿc/z‘ÖKPœ>OçgK so‹bv‘ׇ`¼±8Ëâr‚g•bòRÒÂé -“GÅbEùò’£Nt zÐ;P¤P¯ÊÄ!p¢-ës8¬e Áè›l:ƒ/Õ!‘ú“¼R‡E6Ÿ÷³éòÛëõ¦FÒò]®*Sÿ:©ª¬¿œçŠ{CÙ~ŠÅ~ýü0úæ¼®‡\]]õñŽZ”gà-˜ Ìò šÂÃÞ+/)ÌJTeYôüÙÑÓožößþý-ìßžM#1WÑW¿üŸŒ­§Æ8ø<Útøpÿðó‡ÿÁƒ¾ ¢ƒó›Âêp¿=P–cÈÙ%Þ`OMà\¦q61s§öN[¨C.¦æÄnÄé}ø’ìâèaÑÍ!|"ÛÚîÈÁn² ‡î–éé,—i¹êô¥´ëÉé™îË=û4MÇ„å³üÒ>-fE­Eç?9êˆÈŒ¤Í!ûmž`Sev Áž ¶Û(á fPzâ͵kÇõgú 0:rZ'Mžî4ÈÀû·Ú›¿ Ç_>Ú@¨V7mË£ïˆÞçÕ2åÿDôýWlûÞW4Îí·È~‘ƒÆ°Ïd”¥&¤êc mºXªEÃϦYÊB×¹êŸb . ÀÜ^ô¹ÚsÐJ³„‹Öz0Tèf¦N*Ĉž.D2;-M\” ¯„BÏF:6±‹m LÌN絞5v±È¬¡+ÉtiÑx,;¨2º¹’2•ꋺ..)êð›îÇÅ¢O ¹ÖNª>c¢Ú,gêÎ\gÓŽöcÚ/%‰8 [;®L'æ–S×à)Œ·Rööѽe ,ñ¤M\ yVPßôÅ€ÑC_—é™33Ü)!.ü€.-Ý.ĵâɸüK' ³Ãkt6Â>õò¦™ûšÐCøÏA~Y…0Pšÿ¿Pw¡w/^QOÓ9zöÿegç~ïS>÷w~W ÛûÿòJõâ÷%Ó$îöóûÎïǪªðDMâ‹è¤QÿAôn}}ü ’ôÀ}¯`S¿ÖÔÍÇÓâ,öíÇÖWhù‘õáÓÖÿÍõ½õ»ÿqíãD­iV‹µÔñ2µõÿ÷0xªOÐ_p@yõQ *Å3˜gj‘Zúïµ³í§9·Åÿ—:H^Ew![›kúÐëñ&ÔD¡:OÕk0 Hea±¿yw+n­˜ÏV>‡Æ‡pÒ\ ÙxœÍvk=M˜›UÓqñ>s´äÏ\ÃŒC>YzJ"> –è,Ißð¼¥ÁåU àýá™:e°G'­U™³‘ÎÞ#ÐW]×ß«9ýA­/ਇS0˜Â›´bFöäà$ßøÙ/|'K÷væú`¼2ýŠžƒßÔÞ¡èm:O0‹ZþÏlúÜ:=~ýúå¯Ñ0z³º³_óéYV÷ßüûOß½|þæ8ÆõKÕ=àŠc`â?68&ëÓ3OÕ磫/é^ *zÛ‡øÏ:ì®^¯ØÉ0ï)ÆÂZw6Õ§þéèù G1uz>φñ@æo¼~Œgˬ-оù`ÛI|AÎju‘…€»ƒƒn«¾XcIš¸:»Á5Wg{S¿eddë-%BGÕ,[&ñgÕ»Ú˜mÎÎ+ˆàïŒBþök£':.½ªDóçé)ÁrS±†½]aê*+=‚OAlÀÄI±:Wè¢óòâ¤E3¢Ü€iê¡*·,aîcùÇP¶µÊ´`Ó@Í×Y•ÕÌð@‹VÕÌœbm‚¢Èo†Ñ¨L· â˜Ía›u¥gøX]NÒYq¦ç­û·uRA}Æ´œ·ãèƒiä†H¢>C´‡n‰ftv5Èj¦Ö«ÝT&‹Û˜ šú 6 ûX#cQ­(Œ€C0T–öô=˜·(À؆|5Z±Fý¯³³å,ª‹>¨8|äh M_ý“2ö¨½òªÌÀWlSÿ!ÒC¹ÊbkeËsœÂ¦á ³·¼õŒÔ1>z ٩ʬÏÍ&ånò¿ßU÷Þ]ÝWÿvÕü=Ù½€ïïv:¬„áƒFÝ]Uů¡éJ¯^¿|òóÑÛg/_lhu7ò¤A{Àql´ÇÙ µmAíÞFõ%Qù=>~wÜ‹·KßÞíþþîwÚ¹÷o»¶è5wjôöåßž6µûÀ¨>)Hà>¯ –詞!µPO0—yÛË»­ºqÉÉ•¨ ª!îíAr8 u7>ö\ލiêµ6|@¨š)eTǢÔëê_kU·çmSŒ‘†*‰ß€iÚ± ©ÃÁJÝ.“¡óhÇ«bN°¢©M;r¨Ä6U2èK±èÍa1åóƒáƒg¨°Jh~üãŒ!.ÿmB¯ñþp ‘¢™kí+Œé—TùTÝíUË1gƒŠ!)Y¦ ÷ƒÃ‰nÞ´ÁÏk ~½ àô¼žŽæ–ÞûlŧÅdi¦ÏÊbÄÌyQ¿ñ¡IXbËA0ZS×08å` ÷µ+‹ÈQ!õ—çjê’€hSZ"¾‡qç–µ­T«ò*CzÉ8ÜÁ|¹:{´‚œéˆSª×sí|ŠÅ]7¡ßa1w$// 陿q.³¢™ñà ²Tïëô"CP8²fè¡Ö;k4ˆÌÚÖ&òu6Á£޺Íîmå|B~«”Xú*¸‡4ÚK°kQo%5ùôpÒ`\SY&±5v”8Iü œ=e1ëýãÛØì œ¦5uÏê[•×mLÛëìµVº¾eÇnSþi5IÙÇôjrË^­)¿yÞ [¹¸ØÐжe5ìÔ–GB°ü-ŠëJ[·ý†nQ\·pn«ÀvÛÐÀö¥¿øE½½h¹‰àf4¯CÇB˜Í§U>‰ ™lD‡}ô稚¨AÎ ˆ,K¬—M@kÚ…hY“ £OÉöáU`øŠ®J'k2;Ð2u îPºÍæ%ߨ*~ÀL´8p…YM“vY>ˆU7+ê Ó]›—Íô±B#û–Á¸t[4³zŸgWkê7h¶æçVë:µ43éñ˜Á#G Ì3…|9˜'e:rH²ØV)=«´ Zðöµ‚0  ä‹WÅ1H5 º€i)>¥ºtƒ@¬ß¤OŸ¨Ã[ %¨ÉÛ².ŠìbåKL°#†ü·,[D4ñ¥ WA¹? â~£ÿWÔ£°PDr—8t 0ø´Ç ”Ý+ SÑÍå,ƒÈkyÝÈ€¦ŠÎ——˜þL@|Jþ4(42ÐÀ5‹©“$“(Da7#Ï묧ŜØG­«HwY,Ê\Qö¾ùæL¾ŸÆ»†bþ-[½É8í0¤ÄæšÄP­¯,ö6cž6Œ•¦èK?J®ÎW&)E.Ê‹¿üŶ të$™TT76¼§¼”èv1ÂÖ^­í˜{X+zòØTgaLضÐÉ» £ãdÁZÜã)\O:dâo“¡%£½ü$«-µð;½Aw¿Û4•¸²Ôq~r¼®2ÎÃÞTºPCy0'ó3Ÿ/›ŽpáJÐÆÚjû2Tg›’-å°újé0V_¹éb1íÝb;ÎZ¨¥ìFj+o¨î,R_MYç]EqТ޷ ÌVeœ·˜âô¡CÍæâ¾*v÷Û¤ÄW}¸¯:aõA(,Ðâ¢e[Q.ð{Üeu|Ø;8á˜4î[Gñ->V«T¢ÕõÑÛ“LBÅyyô>&ŠÀ1ŽÜR^»Àì°7Õd¢rÒ1‘°Ôӎ׳qÞTvs2@Ì$Øì݆µqP·s»º|®©Žc(¡» AN'ž„§¶Xü®þ=¾ïÂítÅ7*ÇïÅé½Ýl½ÇÓ»ÆEɃè²yé`Ý|<ñ·¿Âë4È¢u¨%E:{ccÊd;ÍíH“ây€Z¨“-¤£ºˆË%fmÌ´‰3P+­àó݇)žª%ìEsôY2öAx´H€’ÂI¶¿ÛÝ=ijm(¬‰-9˜ùéüë1¢YÇ€ Õõ}£æ=8’0_r0Œ¬€)Xà¦UAœeÎ60Fß§˜f§‚æ”:é’e±¤U›R¦…sOã°Fñ}gFÖ0 Û3vÏ ì]6§êÏ uæ‚úlŠfxGðѺ_íÅ0Eá puêlEÂö]ü;NÛk5¼šú½ ìÓ¯l¿nGÍPÜjŒÒúh\ãp6÷ ’µI¹aÐjuƒgc Ú.‰óïè9¾Ú?=ýJÊœ¡a”Çþ± o’T£8kÅß<…yr‹ŠÏaP: c¶éÐî@&J)rÄ-ô›Ï 80¼è07[Â${éRf«Hl_¸b0½& § ÿð™Àž…†‡;Dòú? ùÿÜ—Ðäo¸í8-ZBû>åSß…znÏ7 ±IœÜbÓ86{}Ü8ÊFßgÐ"¶÷u6»Ö&Ýá(…ˆwÏëz0P 4Ýa`îCÕ(âò Ì)À,t6ÑÆ]Ç œà%>|ÁŽ]Ñž˜9=þ~µ'l‚*hôŒ¯4&õŸ½ Íp>ç9sëMnhþ‹1yr/Ç6’ÇRd{ PWì~eœéã’á=÷“ªäx¹Ó†ÒÑLÙšË1RC9ÚÆTs·î¤1ý¶•ìZ¿Ç`¾Èl+%,è ÂÞŽÜâ¤9òùy"\ZhWÛ¬½I?ìhå/Áú3a(cb„ÿ”i¿Diup˜ÛR€¤eGzßÞHCSy^jÑA‹X. ¯ܱ)i×K}ðkù î‰e.F8èØ÷ê&¢–^Üü*^ µå!Zèðî¹sINÖR6íYÃÈ)Àøydµ@ÞRT¬ÂªE‹aŒC£¥„!ÿ¿ð¤K/t© ptóœC0´ªÈkío5Ë1v Æ“Ä ´pz‰ ‚" ’Êx®Çdø'#Áë"¼o+ ²U.jÓ(O5V 6AŒóˆü«ˆ9 A$HAÁDP83ú8„ÐÝËÜ—<†ý Ú6AîTèÊ%¹? zé{gÃ?ûY´lb¯y”lÍ E¡@¨ß¥’=ÔÎ}¤PôÕý굑ª‚j^øÒ1H ¦–ß@*DéÀ0ôð½òå¹ç1ŽOaƒÒô:¼š*ÑE#æ´6œc¤oNàóË'êGh6Ð&µ}z›RôðI õ»K“IªUËÛìë:Å-dõùL€öWºi qwSf†à;_‘ÉshÖ( ³µöý"%ªrÙGWÛè†Z TAl’®˜†…îÿ³ì,CŠKG@¹YâÓü:SÌ&HK³tÚœÎ:óðFÝ,¥Ý-{•¡'ÀattøNƒ£'ÙeѦ[H¼&MS´ÀõùðËGÚ$’úÖ˜¤ÿ¤^uî0rxÕCw¢2kÔq‡‚Yª ‚HuGmYO_hÏõó ø Ú }Àvù^GÌêr‚ÀÌ™dÈÞßħXëTw"ûÆÍƒBFL"i]ð;ÌŒªÊF•TLóN¦ô˜Ïæõ/ÆŠ;ä ¦ŠafÆÁøt*!›æZ£Û×E¶2 PÓ‘e £x4)·ðç`Õh8ñXëì ¾‚—ê¢3^7 ±öRŒä0öäÈ`늀E+úP7ælïÁ­?TÏÚ’§æð1Àìîi..\à’è¯s?bÚî!ÑN¼qBøK³Eq½FnqoàdÅ«çЮ¥é¬C4­Fò¿˜é” –«ó¼ÎšZÂZÿÚ–ªM:°ÝZG`Š™ÎIÚ~B»/ÉtÀÓ’Gá¼A®JŒä“éůÔ§“ ¿ožØhbnv~hÉHÏk>¾¬Ì *ï:1/¦ÙÀFÔ€D¹ÿ°ãÆ€„9ÜXñ ã]h²Ø2ãÌ(#jàˆ±×óÚvÈ:mDüb!¸îç‡l´ÁfsÁ1‡R5œ¡íW‚Ùc'3 –‘LŒh9S3½] y–*®ÐÔë*-«p·ø#Ä€{ÆqLÖ…¢£XoQ„pHu‡ø§Öž(ƒšÔw}wtgPÀœ3º@d’‚…˜³ðÎŽ…°“o²3È5e›Ö3ÎJÞjk›$H[*#ºæí¹ ŠcÑ©•Õ_d?iÔæ…cØ^¼¡fK(^„È¢ ^·ø»¶ŸûÝÁþ~h3Q/`Zж‘ïppã0X4Í'urœÌºò£™ÙöNÄQe:ˆú<8Ê„!3ß¡…Ìx{Lè*,!²)Ä·D„f¼ÙIÞÃH`'Ø™$ÏŸwKµÃ_0ïîyæÞƒ\Uò8F¤Pä*G×FŽ6Õ®;"§óú÷[©*ô2ÃðewGt¨Ù†ñVõÆrœßÿ-˜6,;›v„´Çd~-¬)è[v46Z{mhˆÉ Ì‹Zà©"˜,846øz@6qÂÕùeQÛÙ_‚bbÈŸª~vÚ;¤¡`ë*17»ÃP·iéŽÛ¹=µ!‰)/ÚôORì¾ñÓã·G?Fß*£/Éwp×qøßutÃp·!–µIì<Ü(b“`Ïa$€› „!âW}OrF¢µC®É ©ˆÜ#š” ""&nt‡h%y,:.Uh*6:x€‹ðV¨ÅìÁ)¦ƒÜœD YÔËQ>Ô$&ò~ ë<¡þË1•þ}¾©û?>©ˆ¬%„MtôÜΌٌCðq·`Æcy¥–¾dž…ŠO§(¨¥X»(Éõ ­Ô×êïAÇßLýñ¸¸ÈxÕÎyibŽc¼‹Ç'®óô<ÛV#¾–†‹lIrÕ»ô®:TëÉyo5è­à‡ËiãŽêG¶F.ÝŽóX ÃÚãé–0…X(L@VnÒ艺êϽ‹«V(¦Š_˜r'C®“­¬0ñÕuÀt'$»•štÊÂF'$Yüò;¼x½Rÿ^D/ ¢Æ/Q€ô€É›®* È‡úïü§¾v±”zÞ…r€+ÇŽÚ©Õq4ôÁ`@1œX§œÿ›ØW–H‰èj^C"¹‰¹¥øB½¨Fq Cøå•BÐPxyüŽùëJÐ4´”€y:þes =› µ€u8~µ©¼k}/AƒÉyìóû<»Æ¼af§&~†›ßyz«=·t•K¤TW„Ǘ鼭â/ð¾J¯±/yk=| ÆFXÑop¡D[M,iN‚]Ugm½Ü4ÌË•S€6ŠÕrô±o<],Ö`½Gü ³{¤ŠæÌõ™‹ÐÛ‡ˆÒP+c\e ½”i?ïNßôr6E5Éi•4ÜŽ ›—ÿ[)ž‡f<‰¶½²9ƒðç+L£ÂŸ"#øCÆß¿àï_tqþi^CEHÉ“ÆÎOØÅ΅ͼÚÿôˤ?a#‹Ÿˆ¥â7oê =IÙ‡`çsùk¹¿ˆ‰ß¸¹ñ·š-q2«KO)è)ðê#ÕŠž>âKSà“ŘM¤…úCßQ¨ºwó·ÅìµQâÀfŒäÏT‰C܉Zÿk™ËP~‚»p*:ü‚`>oÝ– Ý½ß .Ü"io¬,cÃþ÷¹ý÷¹Ý˜¥ÿgÎmÚ’fÈ«8C A#Z{öe:rÑ?ânäècMµgÈk±&º™.Ñ¢˜ÒQ^@6«pAV?6‹I=(½w§êøà¤õÕW'ks…ó¡ÞÌÌ9¸]Ö\É÷àÑ`SfN›¦tS‚Î'6Åé«YQW“Ÿó_›îƒí?Æ[y½BN†;KWp͘*ˆ èüˆ·nöÖ„¬¶AO£/­š÷ø!›£Ü ³”ù#¨Ü'öWµ+ÓK<ß)}M±,'"gä_Ñg˜Þ}iÜ ä 9ú²­Ðr}PäFBøZ]’+‚O<.a:Pp¼¸ ¥½•‹Õ,Óéß’ª>Móå·(•(êèt9'ñ^™ýc™—ö@­¼åc½·ïÙšOüF-(c­Õ§yS«©jγúAìÞfH* Oãïc¢†JëäEÎY¯’ëîŠt6מ„Y(½rKb„‹ƒrD;¾F–<^g«ÞQ_ì|^wðaôÏ|‘Ü£wÌ9’íkUŠéÁRõS¡~7Úÿ¾«6„ÂÆìzØÈR+Åh\TI£óŒÞìh?Üï:µfùeÒt#¸ôš,ÄvõIëÑ Òõ YÄÝ_A_úòô´ÊÜ÷àC—¬%“.gןåbYi¬=[ÖÙ|œ•gvõa¡ã§³|Žº©ø§´ÌÓùh{L ƒáÛ¯êÜ€hã'bðI€ÜG Iœ.+õ¤W!ê××u¬§0@À¶%à˜T™é÷à@x¸€× ÞÜD»ßB!VÂü_˜W™3%ë¤Ý4ŽÊ'è—ËYC@œåå©À¸¸Î8¯Œ_gS$^ä/&ß,Ê;¼ûÿ„:¢N=§Nݱu)6Á ´jd’ MMWø§j©JÇáż:9iÚ9 ;A¤Ðm<§Úÿ:þùƒìCä®VÖ¸šÆ>ÜuÞgÀjbZŽe6ÉYõàÑÁÁ_<øö&Žè‰ D×Ù•ÂŹÂÜâb¬þãSõ{γ|YT5HÔ)}äôæ&®¢ËLUUtà7H(Æòg)å4ÒAjÁ¯G5$ƒ7œ• ÐuuôAç´@ÄI}ŸpD â†>ððûºÛDq·“2_hGÜ+ Z¢þ!ðPÔËRs{…aX y„Q–å'Ù]U“{Š/d{÷‰lœÎ—mQL8„ù(hÅ^?þééèèå‹ïŸý m'ܘ _}µ&4Žóü¶\®Ñ=Ef}®Î»yVUÆF¨­Ó¶»z§ÀIÅ‹òüñwOŸûÝÆüYdv©ŽÆ2S[æt#K4]32°7ª¸¨7ø"B­][g~>|è…>vŒpZ—ÄœÁtß³7o¿{ù÷µ#\#×#Ê!‹¯ ÃXµåý0áß§ŠÚ¢?ÛãUi²´šeC1³>«ÅCìËÇ;¶nYEZÔ½±(ïtü.1YU—ñ¢Ò_FW¨¬¬8kÑd~:üpCê¸Ö ÜgŽz:u´%ˆêðM†zAW¶ÝAê¨ØÁålª)´Îôæ,L¨×»ØoŒ‡ª.¤& 'Òc~®2™-§˜…Ϩ3ÎÛÆHGÚ &!D©‰nÀÉÑŽ!R|¬âÔô)êAŲòOæ "ê@ý¬>‡ÈUжÔY d·¦ÊzŠÎ»j®_›×¡)ºÎOˆ±"ÓFG€^ :£˜ŠjQ y>7Û\w…WÝèâêÐ=¤"|™aP‚Q¶ud£ÕDÝm¡áѽtßf8%“Hø×“H•ô‡ïÕ\ÀÓþYÿp§A ¾ýöÛèr6V„Òá¿ôfzÔ¥¦G’N—ŽC•ôæ™]ó«Tæ²M5òÀÆ$ë™\§E‹{mwÌ\ÌnÎç~˜qÝÎo°üÛs­&|Ñ‚ó 9S¤ihÕ¨çsS˱cu›8V²(ì>½^Pw¡Ÿ¥àŒ?×ÇØ®cñŒG¯ØuÇžß¼dA"mõ L[ PvsülÞi®Ž0¿Ô£t½ü_©Ó y=:€‘ ¢ÌMíR¤ˆé)×ÚYK·†1ä ¬¤)£z38‰î9,VÌ™z] ãÚÈ m‹¡Å;f5ì"0,  ¤ƒ7œÝux†b¼!Ã*ßøìüà5§ßrÝ»‡ÍIFÓëÉY™OGê&cv=Ä­ ^H‡´Œåì™5±)ä>‹ÍP!ÛRnZrGœŸt\2³§=Óš”*º»ïÔÛ€M+HH Bê9ö”M6S&™Ü¦4Kb¾6dŸ5‹ÍpÜŒOÃl̯ ûT‚§•b­²« äêéô«¿+égÁðüPÆa”‡t¦˜ÏX3WHµ5ÁfÃ$¿Ã‡ƒÌê“AOÀ¸ã p«á+±ûã)h˜ µ$GøNL¯Ccì&¾è‰TCT¡Ñ¸.?èýTÀi¼¾Â§åy‚ú—RÕlêÄCŒ+—^ާidLG©jÒtZ»Dµ­©}¿½öOYò×ó,›­©Ÿ;}6‚BPÓÑZØiHÕú½a°æ Z੯»QÖ_µú@¬C @]´B{TBbYƒÇ—LÏ iåçE”žª…’)›^ýZ¸µëý5ÎÎÓ÷yn‡ê w®—½Ó:ƒž'hÆ£ ·$:b…§Z±‘=ró\‡|^[íxN˜{É[µ¤øÀ#8³·©dÂ÷("Ø*šÀÎÔ¢1  n}•Q„?¼ux‡Ñ¶½–c³´ô6ÄÎÍÃ`{?/Bè„û;Á0tö¡¬ú¤¸ ⢬ÜV÷U©fcË:žÀNxœAvœM° |Ý:Ôu¾..ÙAÙãí ŽW¼ßù¥¯‘¯¡§ ýŽ€Û‹¿ƒQ|ôF¶n»ÑëøšØuÌòW{ס‚˜aˆš£ÀûÛ;Ëã:pžÔKz©ÂjNCŽû.á¢?wRf]KxO¦àÔuZÃÑßIñ(Zä×À’CCâ—:_;ž)Gð s/k‰â%‚Õ¥3Ež¦+Å¥SF’ä’³7ˆûÔeD‡òÏÏæê¦'€h —Æ`íƒßwó7xM¿õ“p º‰‰¦ž¨ûÃ4cB̳ 3™ÎW»j¾ù·ƒ †Ópüz½^FÂ~vêöJsÉã‹lø¥pR§t\%ÔÂuO±Rªùi1ºN:÷Í”&¶¾‰ûM(›dÙF¼qkÑ·Qâ,€ÓXïQ0N¸Œ|nø…ª(ci«²ý;h¾çø;|—sWØÏ¬ËHËÌßÙ¹+Pho¸Õ‹8„Ö>é¼Ä·N\€6@Îþè}6Ŭ?T‚V®‘×¶M#mOÞË>ðŽsDMч³/å‹ÊkGÜBƒñ†âm ãÖµ°² ˆ¦‰( vÞŠD§³"­Õ6³î)MùÌõÀàÔut? ìéùr} [RoT¿á1,Ó2†}»N>ï:LÂî/f4N¶ìõË×2¡Î0–ó­°©³¡¶Fê»æ^• ‡®s^Ýà‰¾ÔÊ*aÞ éz@JÖ-çPžIl¦Eò:S.äQ_uQÌ4Èo € Ì­“ &rŒ”-Ü[ßI±Dgëî =-Ô¼1VLM}¶0 8ŠaÔ…”‹äò( –³H¯ê¢E¡E§ÞUÝÅX:Í˸IÀqš/õÊ€ÁA`™¬ÊLg±xÊU–è›Çà£T0>’ZÜDi]—¹Ú8™¾»y7ÚiF6Ζ–9œÝrŒp_ÕÐémŠ©ç`b rÖ?ëw)ñƒÚÅ´ ö4b 2rÚj•‡ R»&–ãcA+í7¡\µÞçSÅÆjd_·8¦º¿H·Z‹ b‘n·8ö¨°‹ôQ‹s·4%3ÑŸ£¿e«q¦Fææñ=Þ-1.ÕúÄÁàÔô~ž)6©ª¬¸´QGS¶d¿{Ñ+Írï-j_ö_h+cÑ_PQIHeûŒq'Æ6x‰Ù &™ÖÀŒê÷ržË Ð:ü)‰g\.#1ó}Ú‹¤£pã¯T™T¹VïˆÉ²Èà%½€Ì< Ô ¹C¹eã Í‘¨ÿŠÁ:ö1Ás'ê…ÞÛ;FË×Å•µ‰ºstðÇÁºv\Ð÷*ËÐÒ¥å†þ†ÃJ,EVÈ> õÏLQr¡a2™} ´s5L]\UvÃÜ}Àîá8Ùë†dã•…ÌÁò*oÃÖ.ö˜XUÞ(0-®æ&…U¸y˜pGî!ÐRº§EÄ¢07V iNMÖE«¹>ÄÃ,‹8+ÐoL¨9K:›o÷³•³»Ì*à\b4ñl^-KÎQEŠþ÷y@àÔá_°0ÂFD{Õy¾IWGëµâ,V0•´Ý!Ùa7 WôAÍ»‚©ZìDL–3б 0ç–ØèX#þÂŒ“†õ‚˜õ²yC#ï£ï¶½º t÷fŒòa.üš4¸mAìzMJvã1-÷PåIKÚi½#Š…B„”FïŠPO·öþ¬‡¡Îö®HŒJû¾<“³7ðdæÞa•ú+ï,j–Â;¿•£Hw-èü£ ѵÑHj‰´ngºÄêfii²Ñ¼Ä2ù-f&ˆiISWÊ5„Œz“ñÉššÜùí WÔÜj[AõU'pVûìâJÿð‚¶\d«.F`ØQ§A3TAŠWÅMbRÜ:|× %E¾H+ðåU1,IÄwb6>¨–Ž¿8<9„.ßxªY¨—<}-ý¤·í=åËâ&6Ît—oÏ^‡¿jëpˆn’-…¿iÎa„øXfˆ ¬¢±Ä0k|»Ý†¼ú?¸ ©ÚÀHˆ›ŸÂîP»ØEàZÀYèùûV= 6Âã4r,ChDêш)ñ­È ejï‹I6›U:0%A#^í̦ºÎ#ö¢Xž-/1Y‰º~€ÿ–µe½÷ñþ±»”~ª§î„9V°X†ŠŠáyj¿PÀ%–ÛΑ;;Z9Ö@߆És1»û©5²™ÿ¢³”ðš™6e,¥¢và 5>ƒíð×Y»1tìϧâv¦¡àÉÀƒcæm: Ë[)R˜¨9GZh»à››ÜEJº‹mÉ@d-#NMk6¼Îæ$°Óa,ýìhyz&ñÁN]Ž/ë[Òƒ»å!ñž=žL²êîeCœìB\Í!Ì›º´,ÿ3,ï­´Boz`lVƒ]%ìÅoUŠ'É2Ô&ÛQk‰ž²èL¸±ÈæI}­g€©I"Ëðbq™®X¾Ìöè{9§ÎM/0é̤ y˜pÉ»í ôµÜ×Kæ0EÜMvZ¦œ„,\ØÙ ¤:ëFúÝ…ÛÄ@§w_aº¹ÛmdMíÂ×4r&­Šº%i%•I.ŸFÏÝè”Æ@AÄÉKPÆ.–墨 ªº´íR³ „…ÂÊ€ ½â‘Bv© Çù )däENDfDÕÔ×ÅDIÕqMP¥n<sƒ²Æò™43¿³^Îhðž ¿4Ëá~§¨¨W'Q0÷¯ûÛ û¹‰ÈNá~É·’ßzµQéé[(<"u¶ô8’N:`ÓðÇtU*BdÆÌñD¿ºg€ ƶc"š1•°zŽ(ù­£›¦©Èr̪cu¢]#zQÚµâê ¿¶+÷¯Bߎ1GeJë2?/Q7»£OŒ»ú=^Û áDÌ ½ÁP^Òç]§‚ÿÅùœªý{±ÄaMc– ă&„NO »1™üÒ’ki¿¡ò¨ÈyþARu­Û]9*ŒNà­n7Ã2Ó:bí+®õYùÞÄà@´AbRLQ[ýut^\Apötb¤&:«ôÙ°õ°Y^-'8(+6Þg¤Ù¸L§¤y~fýçv´¢(ºSc6@M²°åEQg‡ê0WÀ³s»Ñ1­ÔÞº“+ý©ŠKˆ¶ª¢„ܵòZó!êt¨@aŠšhJ‘b&e_PX JÑ4O/©™¯á5ÆÈe÷8пdÂ.sœMÒ¥pp›gvÛQ?Ô(F£3c ÚA£#iª;OçÅüŸYYàS-­ǃNu †3“âr¡¦Ÿ¢W3ñ9M'ÒL£XÏa¢ÈøÕ­®ŒF—³1¹&ÃSHãõ ¡1… º”¦5§ndXhºÂ‚2Óœqމ¸Ï3i°@„LÈv )òB.ŽÈtȯ±‡Pë×ÏÔÒ‚n ÜÚ$BŸ`ãj/`èÉ=â³`ÀÐf)bÆiI‘{)¼Hܪ¢¬Û¢ù–Ù¢"׈B*cŸK1'fGÐQG­–rÚZùëG…'åUõ[“3Ú–’A´é·âXò´vœU_Ã/ütbv‚v•B¥±oµ‡(_;ÐVQ±ò|»"ÛŸtÊQÈà¤õÁñ½ÃןÚJΜàWRä–s͇Ì›ótöÏL6'»õ16v0Q¶ÝåË`=ÚqŬ1"QÂó=Ó1íL ½èî*k| HõQPnºÖ.y¥‚Ý2Á¦ŒúÁijÚùWÄéÙ ¤ýHl 8ÄÙ­?6 PEÁtˆ^U‹T€ ,brqÈZ8¹›(d ©ùÖlp¾!Ñ7ÙK<儊ë/ } —0.+ÙLFÕ2§÷>êðáÒsIÝ™´™p™Á*?›CN‡¬ÅIH×uìm…á²êÖ»ÕTpªW.˜8šU7<“5¯´õ+r5+b]…ÓÛÇFñ+R9¿½ß8vȧ6ðgÇ ä‹Þ¢™‚4k‰íD¢Åšk%_$ƒŸ²a©WZÏ2ˆ-ø«W£ÛCˆ\øË~Zb޶«°³´›Zö'“nÞÑ9­'?Øôú ¹äŒÂK|AQ»+}•ÂýøçhržM.´{*íÆÜ D«™äŸ9aq)#»ûöø=Žé=[ùžH{H(ck‹„ G]ÃT4գ⠟!Ö F"'ú7AxÓ‰•óXÍSïò;AŸÓ.å'ƒ¨‹ÃLãnÄÉʈ rò•‰±½ÄLé̹&öÌp‰. ó~玱(!ØÃ0âIzÖF·7™í8PB ‰ /¥¶ç8^Q÷û©@¶\;µ÷‚o4wt{ð¿Û¶‘ÊY ëI_Å¡áØÃ†ÏRuN9Éô0õœkšÑÔ܃µA» ŒZ(ŠÚ¥Ëqn/k¼ªÆäúXBÕÎæ35ÂS,¹c1ô‡ˆXí^uïØœ‹†IrU°‰hd~a LQï‚~üôü»¾µ!ÔÙ ØWçË+ [PÌÝë"½2t{™™:ÕV4Œøï“?†ü2ù*OÒߢ¤ŒIýæ™vjK·êS̸PTðŸ;.–Vl9.,½y\3¥j„$y’—Ýö‚ÃËÀ »X‚¸özÉâFë‡Úìl?™ôÑi“ ‹>ÆT¨a!Ô6Í®…½ê¶},|ÂÃÚÂ,êc½aµ¥´Ã¹9À:7áN9…þxßÒ`G}xn_ \mf,ƒ8Y[Ø?°ÆpvÝøîÆ_18Tô'Ô1¹\ëØ:pgo}²1npÌdwÐ1laW° þDóÑõý6À·íúZ³¶»6‚\Ûw zËžÛž´õûS5Ëk×wÌò02­\‰ñ]5b‰…t_[N}%yÀZ/V«ðó=W«Bß+~’ÀÜqwôÁ4y££#Ÿh­YU ÞÉÓ@³Ür¥Ø¸ßÜ ƒf|0e &zb¶jô¤:ÏOAY ¤šážôlš°€-r?)ësøn!ñmu6lÞai |)Äâu ­˜sûså­¡„te¸‹»uD¿uß·†ø ‘”po%¦W~ZœKë¾ºÝ ž<ýî看>Ý6Fï+&ãße×ufX- ²Í!X+«á#—ªí,oň[I˜ÉsÔ9·1;@>Ƨ•%(Fi,†‚“kÕúw° Ì]еfðš¢hötë¥ølNFXëfŽ~Ùw…¶Ö°Íƒ AñšÊáÄ=`Mi áì&dëÒfHÊ‚TéÒš¯óôýÌ)"?DHi Ý1Â!N·PdÍ3T^ì*­ÓUêsËMX[^>jh±GŸNÁ ±(àÕWVc =ŸAžäÝß:7®×¼3KFWL É)¹!»²ÓÁ!ÿvc¬cZC Š|pLŠ+H¼¼1nàU ®¤èî¿\@îM'F¾jÇ `…°™I†Ãƒ ‡»Åác,x¼rrܼbлÁÉöNó>à“ýPUÜõö`U¯ÉÓåøŒ¦ bñ òŒ p>âø8Nœ#à°’hK€­i#uðË&:…c 7Srp>e<Œ†VèÍ7ÝvÍF1óoÀ´í§8»$¼,ÿ¢#¥¯Ž‰™êÅÝl«¼ý†Ñ/i/u×u³—œŠtÙQ׬â"ÏŒÛt•¾Om|¢¹óÓÉzrŸ7ù-¯ÊËjÚ7œ*Œ‹ög uW÷ðÛ‰‰²} >)óûƒàð²ª.Jw&œ ëV¶¹®>ÁÚzΙ€5;f.-ãôtœjÄÍ’µ<þMÂÞ"Ún ÐîIËR¸±nÖ¬™^&(øiKE瀂çœ溲žK#"coŸLlظe=×öÈ.hqõòh‡eóXn4z”ÎÉþœøSßèíŸDÛê½Ä9ÓĶÔvãcøR]E¬¦¯dËò¯V7|Ó¶ ·tæ^>™í¸ÕèÇl¶PÐ&&‡R ŸŠbtÎià ë/ÂvÛa°Ý  Åܲu—W2I\C .ÕP+^2n°±ÈŸ4A†›åµêîg%öõ3àNõ'ÓÙb?›ÆÑg­rü˜tÝþt›½¹óHïD˜[d\ ÞÖ¬+Òˆ?˜lÅÅc›¥ÁªGFÒS4o“"ÆüšƒØûF®v¸U1©©f.ÌnDyÂîÜÂÕÆ„z(ä7AÆ×Ž"P…¢k/K&G;ZSÄ<Ñc¼`òþ–­Ao¶Ø’8SÞlÜNûÑ7Š­Š¾Ù¸'x&Öû{|#…nÂì‚‘@Ýk¦ÈÊ{ ‘µú¡FÚMŠ÷±!È“Áƒ-Dön¼ž°¶Îo¤¡"² nŠ4t›®¹˜Â]óÙܵ»sêÿƒÓFd˜ÀzÝ‘'ä0´q5¹  ³•D±ä}ÎOmQ+Ž™$›¯Li«Ñ'žÁújZÇe%%ÄIrzÞ9iÐl8b¤‹Na8ô†@”wJgñ;ɽÆF‚׊ˆôš™2ΠàÜ9Ö {¤;Å9w4^v*àÍ4+‡q]œ)î0nAÆ7Ú¸ÉquµK˜ˆR“y÷ÎO.d\ ±Õ›†¨¢k‡ÑWyI’ „À ;ôU;r¹G^B‹ú¦Ã}QH! ¿Œ„Ìù;m†ØÔSœ6·‹¶Í½sE)¨´šdsmw?ÍÄ/éÛ ÷ d«©ßÒbà•2ßAuVµUžñ8W¶M¸µu%€‚µ1nµ&ð²PU' *ê 4zÅ<“^ÜGxÝ2¨1¥c«€uà…:JÜDZ¦y#ó+ï³²Ê .Š ¥4í 7Íd5@à ·2ø–¼« ¬¶ón$çPýjnø¸èI9dàV*¸kÀïv7±wmó»Héw©»}q‘“ó×fîaLQm+YÖRcådúêfN‘ä¡ßœÓáÐÌ™ ­»jÚ$»Ë`îT€ËkŸx'D[q&cCàÛÓº(Ñ–Rg¥;ëm†¹ÙaB”r(q¡’dXûQÙŠœ¯×Ù´L¯Bþ-³Moh›Ä$|Š¥'áÇÞ×íMg•Α@êé§h·v®Öx–‘» šÌ"u± ‰òöz´eEüiqEp,Þ<ÜÞœ³WLö³Pf÷Ó´¤X¸Š«‚ï”…U£kúknL˜iWaÔ¹¶m`3â@ê]`¼´Ýþ¬ŠQ¾60¯ÈçË,vLÉí êoÂ~MtÂÛ}Mswm'óDá8¬<‰Kþ€ €b›°“¥Zë²®½Îzîþ£ó°š¨Eœwá|Bµ/¢íÒñƒ20Bî§_kýÞMÔ¡zއ êV„#`°-ÁcëÒƒ×él6ë¡4ª‡TŸZë1—5‘»£ájíÆq‚FI{ÎÔN%má„oœõ²*tIئ¤­â»„£¯HzJÖ>èØÆ®F«Ûµo¡íf£zv-ñÍêK7 ¿³ñ¶‰à€H|­8¼X6ˆÃqØm)cT´j$|åYÒ¨Øñf .ô*Š’‹®„m«B¥ntÑ9îùÊ óÎ_㦩üÌfâPjñ¬9Y.}ÆÁ‘Ï瓌ŒS So¯¿§@®à|‹eöWh¡¼$ŒOǰWé¦@w^½)DÀc—„öL%°ë¿Å\DkÙ8Öbxvp[ìy¡ˆª$¸#,ÉHñ ¤ˆj²c¢-Ov €}dBÀÇÝæ²Gfc¦ï9ÞŒ±Ò7£þ€µ¸AºÂÚWˆYcÁü4\—ïÆ± ¼©}YËe€a©ªb’ƒ²Î9÷©—F_1` cÄQÅÑbUŸC驈r°¬²ÓåŒü›Õ±päU€˜AÖ®Ä 6ö~‹!øNsÓ Ú·HÄÕªnŠÎ †¨HÅ”©¸¶ òÕî<åù®|EÆ-h1Q•()wÉ^ô$;MÕ™$lkÄ¢QLôY“?÷ÊM¡NõãêQœ«Àsµê˜vY6Ó‰ösÿ®pÉ‘ëÚùÆík«¶Òv ]eX½eÞœ{m¦ðщ^ÒoéaHOlÜDÝ®”'®>{¼³O…MtÙ|Ͷ‰˜šÚ–¶CÂÔ.ÙäP¯ôº–7¦ò“ÆðGvÝ®¯•ºÓÚwyâØ í:--kÞªpÛÓµjýÑ…@:þ¹»4µ#_¡T&QFôÇßïE‡óY}¡p¾\,Mºû> Ó9‡³h+3VØLäðjt#E¨(` (͈㌥úuzv¦®GP¨J:LJö÷OdöAµTÕãý°ÿŠ_ć‘ïÚ+8LóÿôgÅUVz$¦¢%u¾OOª~{ÛîÖIAK¨Lÿœ×MuîþqŸtTWü …[¾XWÜ gzŒs³æ* ¶bZ×%G…Ý)ºysZÝ›+:p/^«éÑ p Ÿ ¡hp"7‚ãNžà>ˆ!Þ¬® ã½ô4;­5hÓøu²ÓlM½ÃüZ§Ù†ZE¨’iJF2b„$ÑôÔȱI:):cšh uÉ ¥äœ‰ª2’\=§MO·Q[µYé§À_|±Yª/Tš‚BÀ‡YQ,’æÉ±“ƒ•+z°ã£”bþíÎþ—ø?ý|ñù`ÿ‹/îûŸÿ|~ðåÃGƒG_b‘‡_òÓÁ`ÿaÿA}¹x4§‡fq³YVâÏ úxP—™"o«ÿ±½€eé,z®Öl™žeÑÛ¢˜]äõaôC™.Î!Ö8Ñ@ÖyÍÜ,â[¡Ú£ê¨X¬H–u¢ÕƒÞêbôâùÛ¿E¯Ê"/«b—õyQFO§W©¢CÏ»Š¾É¦3øò׳2B„£þ$¯úKµéçýlºüVÕûùõóÃè›óº^>xpuuÕGBZ”gàí÷…h’Að qÆ+JôüÙÑÓožößþý-öôßžM#zôÕÃ/ÿgí©>ö>Ü?üüáDojPÒ}——ÓAôo;;ÀÜ™¹˜­LpÑ¢¢(`ê Ev˜¨W«jgÇR|hNÓ{(­öO©6PVi©X|*º¬ó™.u؈¼DìíEØJô&;›â‡¤Ãg+` ‹þ$G©ºUô£P{‚/›<]•5A¯©[ÏsÅØ•œ\">°>ì‚Kì´çÚ<ƒ(»àß?Ì‹©gC(Éc@K«6ÒÑjþþ¡ZŽqe8.´¿[Á턨]s+P'}­–GÛíB+ ‰¥y±@Cn賆 Áwr–c4Þuã\;k)êqª.nêäÈÇKN¿™Õ|Ùs@Ôpó<[ÎÒ2Ú…Z»U†ºc»ƒWÏé…ÔüΦe ?éøy¬›37’žîÊaô+ÆF\ÄÉb)F¨8gõUÆþœØ Ìò ×6žƒJ« hˆÀm"…@Òæ¨ì}ŠFW¼ÏX<Õï”$q ¦ØÀÐtE;`B¯û‘'4×ðúbF®+Åf7:6½T¤'EQ²üŸ ‡šEX€m¨P}„ÍZIwü—é<=#s9²(ÐÁ¾ìÄ*Ü Ó2¹Øm\·ìÊ#Ù•ÿŸ½wmoãF…ÏgýŠ^k3$cŠ")ÛÉ(¦ù6ѳ¾K™dWÑÑi‘-©W›Ã&-2½¿ý­®n’–œdΚ¹ˆì @¡P(TªHa'=aå¬!űVÄ"ŠÚak¹ÔÎfS U†ÐqªM·4ݽgk›°KÂ?µ»£(©K?Ïx‘`gÕãõ¨ÝDõQFpv”ùF‡ž^üÑÈèØ½ã´BŸc Ï+ê—vU$àȪ&˜ƒÞ`¤ ÜMQ¨Ã½žBÓï²<§€ÞÊ¡s‚,¬fÖCíÆ¡;x§CÞHºáObN!¹kÖ1uÎtÍ™LVtNQÄwaÉ$Ñ©ÇCÅ—˜¶e²˜ˆ­šcø®…vƒ ðò& òq\WÓœ°×G7'• $DFÿE¤!T^Mâh-k¦@zŸ§™Aœ_DãýçF­é†,z/oMÔŽÍY¤h¸¦^0pЧ«,†‡ÆZ»~eÄ‹XmK6ûá¤Ð¢8Ñõ\´ŠïŸâö=݈m1»®WÏÚOzQÛ{‰«>ð˜·&xÑñß,ô›‡Þ²5'­`¶AŽU$8N˜’ûž‰ÌØzÑkq4iz\o7ùQ’(®\jÁÓæ…)ÿÊá8.ÎɈë¶&ÏëVc,s—÷jœ[z•#óžkwÛ1]§˜·PA¡N„p_ÀwÖÆqB5N¸•÷t."˜XhÙ ¤°ÓË=TRÛ7DÔÿ½×/½õÈÀ^­’…h^Ud,ÓŒük”B]Ê=™~¹æ~¬‡{B ©¾Â…,7¢‚Ô6%Âæ'K-O¤¼«HËÒ }Z*Rp±’C±ÀÏ£C@Ñ(÷i½ _§w…áQšTRXòCm7ò1-¡©0ËŽÙûwCì¬ãÞ©¬÷ý`Õö ˆ+ºÝ[®0t,ìáXŒi­Z-;í{µ¢MbÅ"¦äEZifòêú¡%xqY·Ãñê~ËúÎ!§¶L© TB ::UŒu„Fz„W´¨'šKxT%(pqŒ%¼n.ÅÏr¨$ ­ ö#Vß%¸7E£öŠ“bzGúNÞ´QÛÉ›t@Ù©‡"ewýý¾Èù\,Üz‹òzT€­Au#Ú”3‚LÙn+Å5ëm1UûFU +,çš?t?yçð#Yùv–SŒ]WíR&O°G=Ô•5ÉZÀkÂ*â7­Í V_ÚÕ]¢çU\6zMÁÎ\ðÓ’*‹`•EUg£ K†ýkÀö¾UÜft)½U¬µ(œÎK‘Š5!Ž¥$y÷ÜD žaÉú6t§}:§èâJ̬¬¢ íÛ·n§'º1>•ÝM\mä3RЖûª‘ ³RzBn€%­$Y/9¦8ž¤¬bè2Òl8 ŸM4“Ó·¢ñQáÙGUù†Ý£,|•}OO„dùVÀü¡êùÒ1ƒT++ ¹ hÉjôAtš·2Mkêᡯr¸^k&V7Þê¸æhVJpቭ1ZòÂ Ò ßýp†ªÂc{#.ŒÔŽ¡´ÚDKq¿Åb¾Æù|ÅAnF§³i«ÅºÍï¿ÿÞÚœ§ñyTÆ„Ó9;Ý ­aÚ7N‘'S˜°svKYÉÃ’'ˆ¢ÃYaµˆë5Zø¬òú©SÉ+Ú‹B£¬c= Çß:Ç÷éïÎqc»ÛjW5¯ (\±Ë›‘€³Å*Ø ¹ü ”±:¸Ý€ºŸk@;΀,ê ‚qrÓJÜÆÛjÃ/ŽÎºÉ£$ÛŒŽ~ùå8Ò«?›ÑÏt‰Ue’d“¯ ¡™jô½ÝožCÚÚ¥:2ct‘å="pÇÅA(ñ¥CŠBæè\¤‡šGw¼tÅ€ê6Ÿç1ÿ¤\Ê’H´ÙÝhsž³À°h84¥hÏA}~•‚Ø» ÿϯâ9~ç&€|ŠôjnùÎîqpµ*´ ?Ø(ÿ—n º¥¦Ô”òïæáX°T<¯ó°-{°t©…)µð_}º{]?#2ï˜aOQ3âÁš,|Ô,ñŒÅsÎÒ‹úŸµOL9éÈï“E´rmå¼ZÉa–lj]´öÈÊZî˜Ö¢süõÄ=ŠÅ ¥ìuÏfêñ\[E…U»¢Ò/ìƒ÷@+¿õC)ß]i§ñ«Ž/ö,<8ê W÷…ÔÒ‚áÁÔ1nz’›lm¢—HÃßö—qyO=G· ˆɬ†®nŽÅJÞVÔŠÖç øoÞ…¿ÝF5Ÿ“~Â8î;:£'¢äR†CA$K¿æÖ¢c¡ 05áÝÌ)€Öžætu…3C¿ŒWÜñ~"Íž«ôD¨ìËv?¬¬PS‘?G jÚ É_õÐÌxgšÂÆfÍÑ&e^6)84=ó-l²]Ç#¾ßƒ®@‘ª!;³Ò²8Í­„u\>E¤S0Ž­õp¾£âÿ>šCÍuÑÜ­@óÖçFsÁÒ_‰†D/c_tÆ€¿mÙ+ºì‚Á.P¨´-ê©•í±g_–;ÿNÐLh(Gû&Õ÷ÕVæa)_·6’[qvUü_‘ÃÓ°î;ö„ vbóøbM å?1?M†Ùu4Ë]VƒQ™`Nõ\üɸû¢jË ³Xv ÚnCuÈÝ]x_5dg>>‘»Ç§Pð]@7úÆn­‡îÏÉØÀØë1vÆp€“ñ·>7†ïŒ±Ï‘±#“Û²×ñrÎ^¨R¶Ó}FÖn³õQ|žøìÜæã«œÃ¬>½“OÚy+‰3±aHÉ]㎰¯V'±åÚ†Á\¬s\0V5´åUH ìܯÓ9uƒ't€aLžæY…aç¸Ó'r /Sq®ÆÐ‰ò{L1$æØbÃò¶µ[Ÿ„O©¿ê¤k^Êo÷BÜq€ã‚%è¬(´n1ýš“Qnq§EªvŸŸ™¦çއÍç!é…t€<«ø%-…À2X¤ç’îHº¸´þ¤$Ý>Þþã ˜¨P¨—vŸ€}ºuÍxñwbçAZGc¶Žö¾RõdwÊÙGüØÓN9½2]³ð/:pQ%fæÖÊ'¡Þ)sFHƒWŽcË©“gðÞs1Ærq8[XôU¾ ÿãEhËþ׌ 4ÇV?&Qú›'çuå Ç&Œ± ué+Ȉg%¹EJ™ô¯p„e* ³¾ú!#s­}S¶ôY£¯ÝEÓï÷øë¯í^XÖQãÛ}T:x±.°‚òú“0²:†‚õɛϯ£hzì“GÁPZ/÷È·»Î¹‡>%Ñ)”Ø4óõ×v¿…È<4c'‚tÖ;LæÒ½r$éfíÒ…{ÊãY6úÀùtåš'çÓƒ—å:‰×ÝÂô—¾˜O'³þÝ01 ¹Ê#CÖ³§ptn¯:]T|öQ7`òq@×úÜc ‚¯Rßk¹Ù4†Ì}ô#²ûf8hnâºëw7:î v£U@âû³¢X Xü÷Eƒ×®uûCßCsîËŠý‰Ôp0:oá$ƒ0ñEÑÞhaÝ =M`# ™^Ò¼­§šé샚 Ô…Ñð<ª;@›Ðx’œ¥sqô¬Ñâk¥=!bõ¡éê.T„F¬£«(œ®¶¢£øfDƒ ä°r1¾~Fi8ˆÖÔE5ÅO6ñ.|?ÉsEüÚf™°³|ä{G6×7Omi=5 žÒS _{‰_Ï/\—Tït5ÖWën[D’Y£E¤©[6(ŒrI£Qex ƒÎÒáÔ š®§¥áDL/WÓ*äa¶jØ ä…5º/ýÍE1xœ­¾ÕìÜt§p 3ËRåÚwýc± ôõÇýyøþÕ˜òž1ç±FøüýÞßþ¶÷ôÕ‹SÚaA.ƒ©å„uKB»‚á · -€ V¸Ñ¾úvµ“+~6#ÂRû 3wwµŽ5þýsÝ[3*C`{¦C^"æ^4 _z÷o“c#qÖ¼FøU¯‘áë·ê¢S„Žíß¶ÛíZ¡¼fdPžK}Û®… q@ûæªqe_ A³*Óoã?~½VœH¯æý*Wûý7ö­×^èÞ’ë ˆðt†J Oeɵ@öå9À4Ù,îF[OP$ö *΢Te…iÓ|´¼«*zÑ‘±ClÍΛMM Á*D©^›¶¤^¸ä¡ÄyË÷ 8㪮_¼«oƒµ4öŸzÙÞí —ÿzO/ÔadyJÉ``-µ š4~:X‡†!: »µ]+»­Î%ÐiFi·µZ-¼ÓD:qMö™Ï;ºeäÌ焱‚E½pÞ ÝÕ×——ÝËüFè/Ú‡¸uZ Ù¹u —iK¸J£ätq¿ç{g›Á2c8Wl’ö¨×YÍ^OÊžÂ\­.]åöΫդ3nÏ?ôZ§“°¸Ô†fÁHà”x$¯°”}<Ü,9éc‘‡ìÖEŠw®KΧÆiƒÚ:+Ðr&¦ä¾—6»:ÀŽÒ`Ǫó¢xw`vßûQ=m6\¨Â9qÉb½³¸Ìç?·[¥A+U£uϨrj[î3KÖ Ì!í>#;*ùæ]Ahx·&W_ËÚ±e¥õDc·V²Ñ6­^¿lM:h ;KV¥)_µ2ñ#G$@ñʆ¯ÿs=x–pb›Í* ÂÂÁôĆÂ× 0(¡y|[´Ó@å>}DÕËMÍt1U —K‚«€gК~‚ñ¶ef1°×êg!*v$ÚÓ•:Š—PžÞA§¸¥b§ÂÀJOcK ¹C4;piðɵð¾¬W«!^oîå•ó]ô‰ZÖ%­ÃvÉÖp߆ ¼“{¨3·§Ž[´P±Ì+뇃¤°XòïÖ(”f?þ*ÁHq/‹·(GKEDÃ[£eñyÑR ~´”» ™>‚ì|.,-oc5T­¯òŽPÄ >ô”Ág‡¶Ïb³R´*ÄT.‡(^+ѧIA”³V)W¹AÀŒˆ¹*hÇ=°²¶*À*„µb´2~³‚H¨JoVUŠÑN+[µä‰@M|±|×÷®{«Kv¹ß1¨…S¿ò–Ë­¢oÚÁïtd»ãîEë0èeìE_L+ãŽÞð”sï*fÊíja¶ÒÊ/œÔëλ§uÞÚªºµ‘Ÿ--Š`P©zÂúÚgRz›±Åœ *X6,3½j…ñZŽ8°ûk”\ ýÞ¡Ú:9[àŽ«† ú´R°–¦ËŽ#š‰Î˺ʗ\{¸qGÜǘ€ãÕasÁ3C8¨qS(V Å­*o0a¬À^h-7«U¤9Í[Ø{‘lD¡öŸÌÙBcô§;¸À¤…Óè,ä–ïÕf”g”M!Á«Ñ€\O1õšN³¹Á¾Õ!ŠæÔSºÞh—tbJív ]á(çu–*Ùh)¯dƒ¦«šd‹›GH[ÔM?¦!¡åPÃþŒÞ™3¨3È"ž&Í"B2*"“x@)ήÍ£©ºã÷TO³‰![¤¨Ûù¯cˆ7†y'îëO“ë:gGFðð£‹p¶[žÉHç$Óï8/'ñUâ–¼"§É'øÉS§¼%[;ˆ^cŠº»“U(GVî^çE v¹aR¯a¶ÆZè½—äwŽI~é­¤ö]¡Î?Šut¥¾Ql÷ÏðÓÂK]]¡±nG`µÆ“t4E÷iÜd)éoäùª’‘ûo˜‚º²?šþ=ž”÷‹µ0KoÇŠrš‘…¡^»H†tÃ~mYy6šQ ZHÇâËë9Ãx/´qŽŽÒ|Ñ?ÿÇ V–?&¥šÃxïHPu¤-¶’½6 ¾lÊ—Þq¬ YY&x8âÔ³¯}ôMÐAHÅOg‰3˜Æ¢dÅÛ.¬ng?Ìíš@1O wærXFX–ZK ½Úõ‚+2²¾ H+‚V±Ò¸H:[Ï$yî ]¡Y¦SéÞù§g¿9ká>/;üµ¸·Íem/€à LE-=+yvÇËœíV Öy•Œf§ñÄ#UžèœRhˆ…d‘p©ÆV‘¸EðUtë ¥”~R¾ÙÐÛ­Õ,whêûôà+ÕKŒ\P 5¬Â w EHô9îÇb‹z²á×tûÕŨZ!ô¬[e‡«„Ž@Îz³%ðkøf3R3/À›±˜*#U€?$ñ$;;ëYª²D¤ýì $A}Ÿ&@\ïÙ£¬÷.˧y’Ž136HlÉ„’ê´—¬?×+ßA–ˆûýd˜Lâ)r€gÓ ìHµÕºþbžº}]Æ+œ¾Ê®Ú t`nu@ðÊíÇy?ÉNÚ }ópEÅ{ªÓÖtý–AÛ+N—*KmbŠçŒ]^T»‡éhQkêM¸göº¬¦NoéLÜë´›.j& [½CWñpx§=êÞ²G¯“A:»ºÓ.=¸e—^Å“óä.{Ôýö–=úqv·z¸ŒŽ*ÖÒA_ƒkI ¢°‘¢(+y¤¨ wi`óêta¼?·¬©ÊkS”_D—X,-°r¹»jhç=ªi±TÝ8‚`1-5Q1P°˜e’ DQS¥, M 0•Ž'd†=Žºí]Ó£Ð=Ž‚ÙïvÊt=¨±,)ºS& i:’"DHºcxZôèUA Ë 'XÙ´ŠX‡™ÎÏñ5°ºÚryßJô/t0«ÃË”¾+0t‰“u«ˆ!*É:Vbìvs+®¼’«ŒÒ¶S,‘±c8-Œ¶p P>$xsðoJÔaZ,—2.œ'$O4¾‹2¼ xbÚq¾À9 û0Ë“(Σ<û‡–û ÞРêÈSƘj¦ÄÅÃÑI:€yÑ_Óà^ãÓÇ$Í1º£ˆP-½cbyÒŽ]Ãa"»Æ›(—r G€„ñ^‹iL‘;”ç€%îŽwGŒ¾ü0Áܺ»<õvsZk"1Ý·Ã)Ü2JÁsœ†gè8÷éJ^ W™Ò‹N ¹~Ž%ð Sï_ÛIM¯-—‰ÚæWíGŒÁõZøÜíæ_éÓ0jÒ}ØÊv{ˆ»ÏE‚—~{h'BÊJFÙìü¢×•ÕŒç’S ÝËdz2Ž'yR¯Õø®_ý ª¿yG3ö!™,¢ÓôV†9æÕÿþ.ªï >N¦q~Gy|Ý Jõ眹 EƒìU·$ûÄ´_u¸w¾áàWæÜïmD(£È–ÞÁÍIÙ¯ W-ÆpB.«[<Û;J€Ó! µä¼o+Îa䣞Þáć® Ì=KOûMOÕ JÓœ®R+¨U2·3áÜ6éΣâv.–§§ÙÜóe4†6ÖËÌš™ô¼¡Îñ4ti‘ØT> éÜ[(´ZïJæM:C=è/â‘JÒŒ|eš]W„^ )ßøn0ñÿvƒ®ÿ~i»%RzCȺ»Di5yâš2ô¦Ôëx3L”Á®²n:Ûû ùƃA»]Ê9.¼®öœ60]+ÁêL©è|-v6º4œ~òŠÄ÷‹ê(xVEZC¥»¥Å[…K0ìO퀮ûp§áÜ3!K;m‹Zw–soC²{?ý‚¶*—ÿC£ÀëwðëY…‡¿ …Ík8¬|ÚߣA!ºžOë;üÇgzKøUa)»×¼»uu|'¹Niî êl|î >-™– û9  A‹âÚÏç–r A%5{ÞE^ ë ¢ó(­ ´¦SibtV´œ óª×é¥=`Þà;†æÀêÜ –jg-PE7®`2Uß üi(·fðNþ)ðÊÑþ ÐÊ¿°êíl·¼1t]¤ôC‰ Â@R_X¿Ñƒ*üÚ¤gg Úla溌‹§ ú££óÐÙÃÌ`:JìBæöãq™]ÕíÄÏ%8 Öâ>klȉ Ÿ7£££œŠiÃذƒè{òXâ/êJY]‹K8H#Ÿ4¤+ƒØÆévˆ¶Ô©gj¬_–Œã0WxÞ‚>¦:Ñ(>áÚ–çTQöWxµäì àÉ`‘‡îx›íÉE¶Šzð â”.˧,§¬b%³­k§“ìzÔõÅvy®^~Òè.9i¸â=6‚2¿µÀzÅ% cv}¦ü]íAp6x¢¼Qð,YÅÞòB´põ  \g3z?i¥Šê¥u8ßHѱaÓ“Ú&O(¥Î‰òŸä³òFûúG}:tÚíG´ýOGþtvºßƒÓo‰Ó2Eðƒ QÒµ6h1ª(ÓÈt1–¡1'æà—n(Ý›&mJ°)]S:ÖdÔúŸ )šðÔ­w2D\×á„7L4HVÞÍ¥Ó`Lë&U,aŒ°†ÓÌc,Ü$Û` æ~¸_œ+k~؇ƒ¶Ût-@ûÔI8ÇùÆu2¢v1ŽF³«SNF@Ñ¥TÏõ”xíPDátº‘ÄyŠ:ÕŒP7 `ÅØ<,lrK$•9ÂÆ ëÏ®ôÝìû+/¦3>»Ê&‘ËL>û¨½Ÿn Ì`6L"…Š"y ;!Ðe!Nôiœ'ì~Å’HqøÞheëž$W¢Ñä×VXH›¢AÊâîäxã ½2má¤ÜT "晉BiónšLzéņTxAbDÄþ$Ãìü>’4Üxõñ€^ü›O& #0}ÀIf‰{‰a ¼ùu. 0mP@H~CÇEŸŽ”N ¦Îyžöú—£ìz˜Z»Ñk ’,r]:H0gr¡sb8Áåòp$仓ôƒÒ³Ãh_íÿ pke gS¨:á°‹°­ŸÏÒvãhOã Ú`t¹¬$Ÿ ˜kþô‘‚"è‹ì*Ã`³ßøô(ÝÊO[ƒdûÿ»ÌFÀº·ao™Gù壎œkFi# ½²‹\­y<“lþ–¾Ô–+³fbís€T.Û„ox¸áÍäÙG”èf„@ê½kþ‚¶['XªÂ´AU‚f’É„.YDÌb}4ªóêe²¸Î@rSó è©-MÏ ~zÔÆs:Šž%ÏáÜ9 ²–UÇ/áìJ¼yžadê#¸“Ö'Æ—Âùô‚î¬éU’¤IKŽá”PIJû+S`®6„\á\M Ål„ÇhKr1Üw¡ºa±[YÂÈ>@t‹ y©Tö‘×¼Í Ï1üÅV‰À%tíª^ê|ZWÙ =[—"ºc¢,}/´IÕ'ŽÓWYž}”]A¥&DwÿÙÞ®bÙ¹B4¹:a¶ÙZb:":tIí“ ºZ'¨eÕ _|ÔȈÏÎDó˜°ëî²ã,OÕÉ·°ÄìoØLF%#qðä„+íbÄf>ò¶ìCÙ)œ‚¬RιMŸ¿”hæBæAïfúݹM¨â%Mèó+΀œ\mV¤Ç#‘Ôv)HŽíùàÜ¡é «´:«¦iza*±éÓ,&ñÈmÊØÏC¢IJû˜‰×ìÙùEz6 ˜>د@=û8˜ßDãtž Å[sÖ£”ø8X¨´=Ã6ÁµXsƒoÅÒ0JÎc¼ÞÄÍx6œÚ"HŠé Î(%/®‘Ù˜¾!2耥 cö`^àor²Æ7t¨Ö |înüWaœ¹çU“ØosQÚæâvmê<À!’*gÀÅsäÛpîíÞo÷ÏËD}$` ñ¥âp~:v—š§™Ð3ÆRB"ê_ #Í›Ÿkk’¶ˆnLÜ*9ùúÄM>©Ê=3,ŽpÎ&r3?¿º#U‰’V„ÙV²ôú³ó›¦µ>7¢kÞšÑõLÜÞj=éj¹sx(–¨S*ãw`h>N•õÆ`ÍiO!ÝŒÝBü³µ75—Ý«r¬;ÅÐý9âwTøEí&P‚–œ…{þ]”Lûaõ×¼Ó\tšónsÑU’_ñ°é•qA÷,€Ø„`%´P%[‹Ni=¨æÕÛînÍ»ÛÝfu½~{Ý•Ú{QhÏ­/ ñ²êÕ͖׫fy½j´–Öû¹Ø^x˜š‘ØaVJS»^ë‹]dNrDhô•e×LðN&ÝK§²Á>®µ||gãs}©^b¿’œ.“^a׬¢Ì"ä<&±8^ÜéðÚw?¼ÞæÚÓäD+)¾'°äÓ­áæÅ€BTçø]–3ö;2¸ë›-¤m?S!eïÀàn"‚(G=7³|¯Sª}PâÏ%=d§P9·¯KYÌ/pIFnMEÙ¨ W à4(ÏÆ‚bÚbÓFð—Û9‡òœŽVtÐëñÏWðb>x(?LÿxÅP¦ƒWo&Wþe®}‰ÎfÆÇDêuäu3ì/žk¬Tir £íDu4Y8@¸ÝF3Ú‰êbDQO`¡w£ú,ƒ¡K#¬Üô½žŽ¸ô1¹ÁYãðãR.nháÞ¹ª‹vÕ´jîÜñm›«1`9Eè+ò·­¬o݃üLç"°…ÅN9…°´C!FE3*pµ¥¼Œ4©±OÐqÿΠsq´0yÿc–ìt²ØdK×®‘0çýd<ÝÆ(<ûs¤×ïM ]Ù\·ÏBMá>«‡Ÿc'ÅÃüŽvrãå'4Oš°¼þˆþ«ˆ×SÛÏ•j¸—<'I£ ‘ ½ZEQˆcåcxãUÖå†c¹×<µÎÜvÕÎØgT4DZZpÓŒC”ƒºq:ÖñºÜ$e¥±Èbl”×qJÚ/>æ 6d@êÎÝõ±€ÐpÈÝÕBž£­«{^EœŽŒõØï8éÐ#Ï+g…jÝO«†zsß§]â«›ýDëWá²þßxÁ.E¤æÉw6Õ¦ì !TÊ4ò¢Æ¯=~MŸÔ<ïá *,õÁ¦·ž‹<]èÒ›8SM.ÞÓå²Õߢq‹è4Yp'ðTÏ}o×ÖÉ]„WxLïTd‡òöçt˜X'’°GšàbN(Üb9Ó¦sžÆ“)3_æÁâ§@÷—…7ºÓMå_ 8¢[óU¾ðW È¯tŒ×¨¬r}pÒžñy}ør&ù8ÈHÇ`ªæK<>ÔlÎ#2VÊ— Œ/oµTl#F-•¤ð„[t\Éý댷yðýB¿7N¾4ôuöAB`71öj ë$V äÊÍ¢ gXfu¼‘™]¾å ¤©Æë<.2¦µÑç(Âõ­ƒ7¸¸›4 ‚ÿR¶¸¼È ƒ¤:++ÆËBÉ÷MuŠ!Ù¯œÑ;+lÔÂ&J`’IÙæ)ÛC€üÅ_#y+Ð[B53XcGqœ;ºG”/YÜÆ5Œ1·Žp‹æ¶0Ô ÃÞdøÙã¨ÓnSäËÈúħ¹O€\gŽY¹ÍÛ…ûv¹|zû¯$$Nñ¦ªf$+.^mÕámœ Æ$cÎShk«Ú4 µG¦é²·,¢åX†ãÕ\„íùtT L)%;ªê´»^HŠåÞÆ²Öç²cÇ~¥ó†sL$?Z$éô; ÂÊñs¦ÃÒm©@8ÞƒЦ$Ë.£Ì’/b:yu-“xpdÂ(dz÷G-6f¯ó1)ü½j»Ç—ªÔÏaAúvY”Fä<¬DÙ£Ù*"Ù¯-R‘ˆT$?‰ŸJæ²2¶2KÁ'z’m?„ãõý¿—FI,øpDO×rÙ¾…¯öʹ](ú§qžöÍ:¸m0z+/’=¬b@úÂ0å‰oÂb)‡4\˜VÉW®fcar$ð]¹Oßuæ*ªi R p.‡_ ±îCuЖ’ž- Kûƽð o‘na#ª†PA¤I!̬- Xަ_Š+ÆWÒLÄr´úÞØmR´é¨ù@:©àB”ð‹#ô"Ñ ƒ§"÷L/š:ôÝiB)—¶®'( ðÖ å›rîïcóZ]°CQS{} ßËŠJµu#«h9Ó\uŒÒ•®nŒš.ú¾­\ëwa»!ÿFȯqÉ»äjUñŠämco‰c5g{Ã?þ›ø\G¬j1»çDsfÔaì÷('«Aµ}¢dÒÌtÊëx_+7Y`¯u ÚG !eµáW¢^_½e;Ùrå]yqrÔây_.à7lÏE¨fD~Å»ìÛmk:}G\GUƒ]à Ú8è0œJ\ o0ê°"C!Øž¶v\ß Å¶—åòÎUàWyh¢/,Š*GºQ+R ÑÿJ®¨ŠÏé¬Zux©¸† ôr,Õ_ürXóÒ ëL‘Œ/³Ã3/´<ÉlHBÖ/Ìçdz«º'/Y¦®¥ô~Êä½Dï§Š•„ (ó—WÞëG‡4ì¯&ÇÆ‡˜¯?‹«ÓLeÃ6’ÐZÂÝM‡óPN°¬üUê¶ ‡°é#OÏG$>Ð]YŒ˜ëäxG‚àa‡ ` ñUâ†DuãÙsëTÌI®…“à5ÿo–g²ÆW§ƒ˜¾^%“óD²³LàA_¹£œK´“Ð;þA_a­_x)i¡~¤Wc]䦎]×\î³x¨ê›É8O‡ÙèÆ;wÜR’¼•ˆ$]ø¡ÑkþóõÓ·¯0VŽ o Öñ˜¼›P"ä #o_Ĩîq4@r@ã’7®t;ÁÄ}Ô½+WD“¦1ªa &¶¶[ûug§[N-ÿÄœœ5œ`Øðw§鮯ÓN:1•ffŸk´©¥kLT ý€š`Ò'¡*ŠXjç!Vâgò$TIS’”Ù¡¦„ ¤õ€Ìd¼°†¡´^JœÔ»Ýn£mÖ~í> U'ê#ßP?AÒ£î7!’àšsš.éØ7Xu6Ò:íPSLÞX")¾¾q7’ÑŽ©fM‰\¸Ä “sË”*@6p1µˆ¥ËŸS6vN$nÕHMfo(µkµ ¸M{Ê ±Îì Eò|1gZ‚š2yÁÝNÝ©7À×…d$Ö¬ÑJdŒK¥eý_B®Ÿƒ¤v[Fh ÷ÕVÙÂP [¥×ÿT¾ÊIZphEP‰&Æs’Hª õêˆk¯# ¨Ù"’=Û°Çáu±´/ DOõ3Ô’OË£éü"A99ý-éuÛ%ÔðvŒ‘áhժ̄ Î (ëœÝŒ]$Ž/âS+–J+.ˆÃEÜ2é[`C=› ù³ çl'+ÃÏ«!tÞX{{‰7|6ƜڗÖ%g;î º4gãüø Œ>e«`q~o^³ÛVJ‰0]ûß³”Ë]]$Ù³ 6@XFŠtôjœ2ݾòÍšJPGàBI¸q®`‹¤3-ÌYS‚OövÚ. »%TÙABN{**7¤»ŠÒäÚvT[p T)X;îå§HèL»AÚ×k*ùúÂ#4‘“ˆââz°ú7ԽធR™S„âùšÞ×ÛÍÐ2ÉNG³–Çh†ñ½G”ÇUuN=|Ùü€Uƒ|D…öelðžæQpIïõxÓÚ2ê5Qôøj«ÓÎâZ¾\Õ%¹x®¯‚>ˆ"†ŸºðCå¾ÊaaGSï¬î°ÒÓ„þ|Ðâ>uë°99ÆíIúg’qw²‚BÛO%ÆÂõ!`cõ².I~A9ê=ûø4›ë«Z¤ ~û!šëEV–2²hòéŒ`ÄnjJ{,×úËòƒi$ãéć ‘FŒ›îËîA1I~¸ƒ̤â-@aU–0yÎVd´°“$1/gˆö~¤… /}¥Êun2A9“X!ó$­™Caiµ ˆ¹¿W–!ã÷µ‹F-5¤Z¾ÔiÅYÍ·qV¹[9w[bŠxï îTž­~M¹!ª°˜µTQÒ;«&ý^=p¬íÿ`ò…kë¹O±}Í`·ŒEGv7]®¶ÆZ’ûÚý® Ñ•ü©˜ Õi1×$ΕC/”yéI_—Rs €U•O'ãýéæPÅðO޶ísE^~» ½ÝÝztì îÞú`PÇbͨ0~ÙõöW/Ù‹WQ©‚¨ß§$K8UäÆ^JË = „7Ší¡PéÒð‘-qÚÔ(¡[?˜Í¦(Àa &1g–B% ”Ø ¿¬ÚU<9OG7%ÉÖK¡Ža«™^'Ê_Ä!!µ'{?·YøSöYœÞßWUéx¶.Ãù³n†LD;ÿzßµ-ë<õN“þ±ÜLdÈavLêŠ)S²ÊËöØ•h@}­gŠFVÆ#Vád üYŽIUV"UÑ2Zfµ^bà¢| ;´F…³ÛóÓ&–˜®Ï†Y¼ÄxMã¯6^¯ŠéU ­¦¹ 4š9ºÒ­ŽQ†ðFE^tjíù¡£5í9¤xŸ~ɰ·»îÊÔƒ¥û˜¹=Ôyg‹a`ëêkð<¯?óî}]E}]Íì-«³´9·ÞÉVž¸ße/džn»™û0VÝÍýzw¸û oiÃ$ÙŒë£GÚd¦/Ŀآ)ã{ý×£Ï*PK_$ßO „û¢>,JÒJ:éS!•çnœë5~ üüe <دÃdä×á§Å:ú"ÝýÂvWìvqSpa8IK$,ûguúj;QûÄAê’'£ ã¦:½…öCdã.‘?›Ñ³ll§{þbRx³/„Ó*Ôv÷Wœå–Î`çO…ú\ bôÆúu·PÆ#‡j`s½¦}õÛ½Mæõòl€Âö{}_:A¨ Ý(§±ÛXʉ˜Ty?H¬ÝFÉDÒZ Ídy0”¾Šì^ ¾ˆ]gQ}¢Ø/ŠØŒŒ°Œ-ã,+¡¡Þ¿õ¸ÕÐ8ƒíu?[ƒKΕÿ‹œ([q«È~`aë³H˜Z~¿‰ÕÞ]I6I.˜ Ç ÓQÞÞ "Ý_"Ü{gWê7v‚M7ï÷÷ßâ–ßzÐyÐíì<|ÔÝùf§ý×e=¦ÊãÎ{€`N-z7g!/ˆ”Ä‹X”hb¨ikˆó‡‚¼GÆ-ô„üÇ þ¢ósMÛœ€ EÓ¹ü=¾ð;ðúE‘KæÑLÄUX«ê܇:ÛÅ­Q@[Õbk0§t7÷ñ¯SŽºò“‹7=„?ïÜŸáÏ ÅPd¾5X üù}ükcð¥Š[ W1õ‘%.Òá¢Gèu÷u½sÿ=†L™wáë|mDÛ‘…ŽaØãz÷¹Þ¼»5ï4¾†Âºû;0Ô…º(zšÑe¨$ ]Lc W9óúÂé“0&–Yé(Lãѧaè‡>«嬼J Tè`×éáý®ÝGøµT pzºÕµûºÕ-9¼WŒ!pgÛ9ècÁãåÜêÈž« ­$x‡ ;ÔŒã”OÈt°ºˆ)ᯣ4 ꔶʽ²ŠS¿ÕÊJ§ý’òFÕºîjŠ:þå`}˃uF,EãI_D1¶úb˜ôjðlµüâ£êõþÚnF@=€ƒ^çÛvá0þ™[ß*kþÏ¡í—{Mì¹(TÆšúà&ñúÕ– °+öσŸÞ¿`ý?âØš¥•Äiˆ9D¯JX­‘øïRh¿;;o<=¡çJ^Ú~ÔŒ¬&2ÉQ¿Á׈©ÅÎ}õ«»ES Ú`º6˜ dâËdžªEˆ§ÀP.“ég"Nòç” T+‰¡ÂFÙAúùEnø rá¶°s#ŸÓ[w™þSªþënØ4Zqîþ„ݰ¢úí·jþÉ[uUý©­Zòù¶jÕÀ¿ÜVým3ZmUCÔmÙ[vµàní €÷uÕf·°£/mÓÚü5bV¤Q» Z8ÀLœ£~²‚¿~hë¿L’q^ X¥Ü÷ÑiŸ*_d“ô7d…C ¦RéyÓseºÿ€Q¢ÐÝž¢Ã½N?©" EÑ»,ÏS;Ç‘‘ø1µi6®ÑæG©é2ê’Þxj¼ÉŸñl8mê6WÞ¢Ž˜ÈDØar'ÇW ñP'­!³¿ˆ/$))¥²nÏÊÓ…iä°€ÐߦrêÂ0xgœQrÝ95÷Ÿ¡ƒçéúG0V–l¾V×½ú*¤ölj7”U¤Cs½Ä$š‘¹u¢™FÊÐý"î_ˆ€ä‰CUžúÔ°í÷‡h8x—VèÞ{Í$pQ`Š‚7d¼ Y¬ÁáÔÕï’d¥føÒ§Ïu_à¤#ÂO‰ ^åEub° «hjl®„¯>†uíJ|Þ2ÁEOX•ãÍšö5PsXUCæS×1óÈcú;=‚vµK­2X÷öóXe¬«SE{C½»Ø9VÈœ“OÁû—@:!›Ì6ôILƒUe…uñSëÚP ¸«.Ž7Bïc‡”5k¹4¶í8¼Ú¦W?çf¡Ìˆmhˆ‘1“U…‡^&½-´}RP¦‘岓óX3g턤E¿-üô9'@ m9íB]¨fÞ7fY*(. %+§éF#zâGòOGƒd®Ç¥7vz\HŒŽ,̶¿ûAòVX°îưûf¨G¦@[é(=¡KÚy É~ŠpÃó-l²]ǾߋȲ鸀2…n­…•-h—ÿûˆx 5×ED·[ˆ¨¸} Ñ"¬öv!Q¼R‰xloü‘ËÛ eå²½BßÙØtSBûðÊÎü/[\Ÿ¾°>•–*WL [r‰¦dõ¨+`‹ÖUYKn­åv÷è鮈ž’5å gµ°rZÙjëÿº=û¿GÝ­ÎñýÚqMù ½›d0AQŒ+)sÖvózC¢s3Þ-aÚ‘Bœ:NÇ¡ÛxˆtîŒfÃ(á°Puô=W¶nºgz8N ¨•RA´"YËA‘ Ê„DÇ€ª ¸òÄ “î„¥ dSð°dd&´¹×†ÊÒ6LÁÃB¨hû)äj 2J"ÝT1BU7x“•Ö èIª^ÁÐ=Ú2 …š&)· ˆŒæ­E¡¦µÕ“ Iž§’Qhú,+D51‰QQ·°Ò¯OC+Ï¡‡B“ÀÕ›ÄfÎU"<i È[‹7çY°¦ˆ8ÓG]«žÅ}ª¥/¹ˆ;M€"wà &pw0E¥Kð4‘Q#ˆSÛe©ååá…”CÃà$pß^7ãÉþÒ$bƒÒR¤ýÙ0ž4å¶$Ýf°n–ü`¿ ÜÍwúú‘®‡=Œ|v‘åô.EË+æPsÀœ“Þƒ[GCÙ,WóazçÁ‘èØé¨?¡p'l ¥¢‚éS,‡|–±«”RQOãþåçÓOk3AYK;m©xo¡Ÿ¦øM% jµôYôÓZµþE;MÚi¤²-Õ´šÁ/Šéÿ‘Šiâ*ÌêÅôÅôÅôÜVL³.‡H§Z7MT¦⯪ÒL‹º8ý¬*_POc÷¹[¼†ÚHó‚;k.[¹c%µÊ¶+íBeºHÓþc”Ô¨Êë$R5ÞñY¬§¢&#À¢ÖÌ.PÉFî$Ë”l 9xÃæÏ£©^D¡Ö'à#pÍ”G¸µ>>»Æº€ý?fÙ¯uÞ·úZPÇž&ÃìÚÁ¦»ÕËíO¤´ò¨âºZ}=°Ÿ*íPKåëîUW—!¦ë#¦daYòË255[¾è¨¿è¨¿è¨¿è¨¿è¨¿è¨¿è¨ÿ5nÛŸ¨£žÆ—€yXgr:›Fƒ,áëð’~*r§éèœÕàM®î+©[RÐaÌR檈åJYÈ%¤'”s[$¥¿1 cq°§È*‚³p*ÍE6ƒ÷ù6ˆÁ¸U¨tßïY4¾³7€?(· ôc°PÉãp:íhêÆ¸á …[¤ïËã†[’æ¥ô\ +œDú¹"a®âûÈ=t+©Ä;gѳðmÊph®ï/Ç"šÁŒ'þ±Q6É/Å”½VUÂ'W\o²èþÑ}ÁE@³$A¨[Ø=£ZgSÄž>e¤3?ÇÓþÅJùAt^ÇÀž0 kN¹Å•4†b7ç±™$çiÆ›î†ÃkL`ÄYï Ñ=±dtްJ:$ɹn<%EƒÝºæ¾«žäA[Éq.Iµ`ÿVI•Ëú÷N1=J`“\ túù­¡ý®;Ú§˜]K©gË籨ËÚî±Â-Ïk0É(Ÿ‘0bÙE’»`¼™]m™ ÊŒ-…ky #<ýw¿kþ!öM ×){m¯h¿Ÿ{ƒÊ÷âŸ]§…‰±Vap%g-ÙÊÄš1Zõ2_Þ…)Á–ÌTÒ*·ÏŠQœIåA ˆ[¢R ²|úD{8¦8£Ÿ4£,f°ïN*fR0ç•5gRX>£+jåîz^ƒz”ðÄ-µõVšKázb#©n7zïi¡ÝEÅ-à*F€r&¾fmÛ€aK?µî&ìÌ)'\‰H2¹eq}¾÷¢#Œš7jk%%®pT³ËÕŽ[pJOA„7ª1¥ñ²Š5¢ëáÄ3xAÉ:öñ¨}=vÚÇ'Ð.½ì^v𥳻PA Ÿ¸»ÊNáåαŸtÁE@½öÕ Rÿ¢ˆSØÿÔ¯Ò‘LÓ˲˜¼ºnǯÛY^7ž×eøMôUuu½¿ Æ»\žz¤$_èÄÏÆææö'} ¢Ú£—À ’O‡$‚7#Xõìô¿“~QÔÖÞ ðqÌJ$\—ŸfôÅX¥–D­b‚~ƒÊt·|ö1 ìãBw8ÎÁÃ5¬2“4É 5Ãð0êêαØÒ3 %ñ]O¤„—Ë×ßš¦ÓaR¯½yuøµŠbœÐ÷1_ΰ5~RkFÃøêtG‰ˆÆHÃ'Óìä,ˆÕÍ($ ^VõåU˲&h¼ðÛ¡D¯æØÚ–Ï<ì/«ö$?.ß‹Ô_Þìö V\%4é/LCu!+Zי떡 ÉEl5ÞDïnd5c-°æs±e¨…Ú?ªé”ÆFÓ;Ç È¶‚…Na%̽|É)ZÃq2'«~†eŒ]®a=¦œË*Ë2PŽ•}94Z»ü\—/$¢–‘Qád>ÆÌХކº¦$–¶HîO^À×Q+ÇÍFþñÊ“kí¨}•K¾õºBÇ5ghjü°*²vìwseqË%X.£N™½ î‘pÆWÆåW웉öAÒb­ÇêÊñb$´‹b#¸ìã™óÂ;—Éc÷køæ­l£.JÉ 8SŤæ9'ÙÙYϺ¸®Ê¶HùÀD\Ƨ ô¤”vÞeùÃâÑÎnÉ„¶¶—Èî*=y‘-¢ûýd˜Lâi6éÕžM'ÀËk«õêÅG7bøÎÚ‰ÒS0©Â¶`Ñ2äFcè©&šÑ¼G@{‹e\$R}®1o'½‹U2ŒãóĪº ÄŸâúèEíoºxºX †Õæ§™#ŸÀ/‹^Û6c®¨…ss[Ï&´9;µ/'²Ç6šMœþ8C1÷õƒ­él•{×VêÒt}u5UqóN£I™,ê ¹ÖÒ;„Fœ(øVF™”ÕQn +OÎjmÝQ£QhâqðÙyQÅäß‹ž¢ ›'u@h$®r>t~uUò¾yÏèá5ÑÍ ‰•Y>õ6BêtOù6¬†Á>x¥*TLEßꮆO§ íD¸Wqê!ˆ5ØJ¬¯è2\r/3~.Uï¯n"ÿP«Ü³àH×Dʧ˜pH)r²QhÞJ­ª¥nƒÚe…÷¹…zçÙx ´C1`.@r•Ÿ”àF|íA4ÅäiÌ.²§,‰ûI!@ô¢¬Ã ·Ã‹?¾Ãžì0WBªša ¢>‡kRâÉ$Ë®êÞâÆRÅ£Ìfôš¼Ž3„c@†ºÀKM¹Hf ›;ä‰5Ä—4¬–4ë V៬³]ñlÕ2öß2[•µöT¢QƒŒN’ÁÉÜ|]”0¶ÃÉÂZÉ싃D ¼u}ë³ý”^þ©O0Á¡@_-ýê¼R®[έ¼ëèI/ªSS[ض1ÑÕÛMëL…~jTº³ÝóËè@ßÇ\Љïm²yÊÖ¹ù$&›,(‰Ò¼»õ°Í”øÕ˜ž°òdMhuÓÅ\Á•W‰º£‚m]45¶.Ûvà>œžŽÈHTŸ ãñd ë‡xÇæ!&뺾ßï_Ü€†…hZW¡1.ÊǸ°Æ¸(ŒqnÆh]Ã<—Il]ÿÁ#ýÔÙøÓÐè¬8Í“\³Ë]¹>„Å+Ë÷ïé2›ÍC‚e!ʵ˜ßBœ¸ Tú8œ_À֋Ϋ `3z®{¾öá8™„»j´¨“®ŒÎzo*EïX+B»˜DŠfKÖU";‚YÇb©¡Y™]õUà›ÚsHF§é9ÒÙìü‚Ú¹Lù÷>5ÊÀ¿.“îŸs±’9JŽ4Ù:|C¨)aZ¬†Y.¿oÊñD ÇD¡ _+à(· †^«D¶3h/fÙX¡&žœçŒjüV‚§£©är‘Ö`d=Fg³QŸBx1ˆ :,Q¹‰è†i¡ÇÀÀF[90ÀÆ“ìeÖzÒ:7È’ÂyBŠÆwQ†If®Óœ¯Dt åÀ ò ­¬¹Õ r§AýQ?»cZàç¿oE¤4fUÏFªÎÛù,bæuaå¶> Ù5\Áa‰Ô¤â Iêóev£áèÁs¸óaþÆtÍdšeÃÜt‘ Ç NQž^¥Ã˜ùHUTXÀAF)í—MÆJ'>׋òˆ~w¹¯¿Æ ¼dclÍ&}}Â:ŽˆózøLâÜkÐr0íÁƒ™%6P·º¨Ë†Ž(b…DBµœ“½ÊöËIMùÿŽù·Ì(êTÈÆÄÍêØJÃærb,¦ÿ[¢Ï)TzJ^ ,öà@{µ·—µfÐ`%SenïYÕL‹§gÚ–ŠÒ¼ýb%£OØè µ+žõ϶ªvVq¡#D®.‚~ ´ß«¡³AM¥Ì{PÞŠ"­^Õw¨|òôÛS×ìjÉÁ§…2Gµöé„l|5X|˜œkP+@²†)VÅ«kàUð\ ¯îKÀ`ëÚkíãz¥Ó(‰s:§ª4)¬$Kv øG©+€)3_¡L…2ßÓvPî}`}‘÷ãqR,j ÷%U¹åéIFÀ ËnØ=dÁ\Í !ol‰åg¢$ê#]Ã9Hß{õñi±î>CPØGS’sßPG3†íœñÝ@S«ŸõFÁñèó2YD‰®2TðঠµÉ]¹ð¾ùÉârnvN€ƒ¤¸Z¡.‹^u.‚­˜B’OmZ}öòoµÜæÖà ¿ÂCW¸iÑ[´çÓX¢0FîšÊ]%Ó‹lß`³ô\>giΔe¹zåÉäRð6DÙÿ|&ˆJŠ’•ñ¢uÞr·Ï'OžXÝhÜñn¯Q^¡€Q#ç•ä|‚—½áà5„a‰=Žû$±ßËQøÑP ûfTÛ+î%57ŠE«8ð»ÔºOÊ)Ÿuk¤4Êx¶†°„w3ë´ö\K/fGÔ»IÀ±#º‰—=:¡ gD¶J ?é­Dų6¦ïb…ìn¢½~?Éïtqç ñWå>«=÷4¨I³¯#«uàRAE†!ƒZgÇ ˜€Yz×¢Í+P^âpêAšƒó­ðø^gƒôÌH2Z¢ZolÂ6)džpIkÜoí©6(ÞVA*Ý#Ñ­¯C?}ˆÅqä4Uë´Úx›Â=R±ßorÅqC°ŸÚñu¤ö<0[ñŠúÄ´z"4G¨” ¬FmbÕ²õY++ ¬hN³z8”VuÍ%›v¤Û3ʾ#}ZŽc€·ÕAÎRÄt¡Ê.x¬ª×¸7X稼ÊãàfôŒÝ»a¿"Á($,µÈÜ>ÎFøÂZ:ÕÞõ„š‹ôübˆº<¥è 7F4þ ðô£z-èYÓ^"£LÈN®NÙ× ¼<-”6éÙG,~£ýXS¾|Ê0@æ*…Àòd9ë’Ε™ë]\VÒîøìÿU¯i¼©Éf2¹Ï4Seôwƒ³0Ç‹ií9eMÐbêæ–pZu ¤ ,h»!N 3x9UBé7 cÞ3?ÏèʵڲÍMÍ•Û LðèuWäR<µXxsr0­: ·Åo·ñ`°"b1ß*¢;r¢È+·¤ìu;˜î÷E¾ð#ôŒ‘¬‡ŠN ˇie Q>ë_˜èZfXØß)%©#WB£¤ù‡ïùphR)(~ƒƒ GJq‰Ã eE²)L{ž$Ú¹Ã#Ù3u¿Kq“o[ºÂ&n¨ Akð*÷¶”vFåî–ÐŽgZ.¨«ÄÄŒqd(¨¤™gêF”õÞ-±ß×kP)ÖŒps„ÓÅùy¢®d‰Y0²>ªSÐIXé€!cŽ iÿ²Óä¿]ù»ãZ#$Â3K‹tØOú€¹Ù¸±/Ɉñ$Åó®`¿ûØî½ÆŸtÝBÓöTXP„J1rSÀ-NHìvÈ(+%樤*l À©ªUÄ#òÒøÉe&*€cB/ÆFéîÌ}¤d¬°ÛUhîEG5Æ2n­œ¡ ÓÃ6kö ÃMº@fhw¬!Æñ/âÿÖkÞAŪF߬¼òd-G¾ÇEB’„V¡ÊELOõ¤ùñ¦q„˜çýG/-åH^ººª„7Ë=°ÈØZÍ71ï‘08 ãC)êå¸C}ÍZgK¦Oëý5ú|„\&‹Üñ„üfêñÚ= ÙàÀÊ3¡†”¸2[Q&ÄYšÂµ™~ 5Ôm$JÉúúîU{å"65ý Aw#ûF™P}ÙNëzK/.½É$¬ßòß V «–·L€¹å`Ãl8ÅZKÚ’XµTÞoÎ’#”!ÖièNVØ <"†y÷Uó<E5®‡Ü’`Nö1Ù¸kŸÆyÚg#˜˜Q¬˜Q†)«€Å¢k¸ÌXe‹ìa3iã?§wånìu×6 êJÜ‚b( &Ï ^fF§Â—Ú§ÑV»¾Àš[›>"rÊG À¸ÎV3ä‚\Q%ÏîÔ•<9+CѶcF½6ë²ÑÞ³ü³ïíFÝft VÕ†Gê«/ Ü£ô=1A1Cï±?Þïí¼xOØÅžÜëß+ÐÉô¶Xa9öàÆê¡JrQ8I„“™¨I¿w¥)ä^3ºôåo^oøPU›^ÊÕF$+ßàlÑä6(#5pÏ §Ùôõúð^bù@­“kß¡˜'-†\PZ¡çHc\@¼n‘3ÑÏ1ùˆûsì´æ(®¥ó\ÙÙ£uKv‡ ²›òD ×=c\ª¤SEÜÂwRYÛ]¼p¼â>´âñø«3`×!»„BÓ^ U>Ášú-˾n|3zM=¶§OØv„)ŠzÉwôMgôtÉ€y{ðºl”`²ˆS?I+áYLõ)°{[N=B'†‹|=IžÈU†Hÿþ)LUÞ+½ƒ …oÿ¹z\'ö…èTöQÀ\ÖOéÞÒqyÑk¼å¯à;p§>„V€lIë{ãâ*«ûžÓ}Áð%zËò<¿È®ÑáÓÉgtûÖMdPôÖEe@½oV!… àþH˜Sü•Z~«ýë£Vd'ŒÍvû«ö: HÁz»ùWø4t+tÿ¤f¨¦´svVÕý¯¦C’³þ‹î9rÄ•^§­®˜ì´Û:Ò|gxøê{™`úÓQY©XÑ Ò]¤‹ n‚ºõÁ$>?G¢Öýi·X%‹¢ì×Ñ?ÿùëèäää×QýäVmFhŸMϽZŸnŠ ÑÏÖ[XËÀobÜ/:Ì×&ZùÑŸV5ûë¯ÐðöŠâ‚wÈìVůÈi’îùds«ØXé—ú‹xä7'ÈÇ­wržŽô :Lú;å0áð=éºýeŠŽƒÅÕifa§û!päH².æå±vK%ó4Ÿæ`‹Ýh9¸@ŠM Ë>æ<^Ü¢&¾¯ç¤†@áêÚ^N#$²à_k¶Nqba>ÄÞÚÏšz.šQ ÒÖÈ#³,+R^:$}ðž_G¼ g~•é©V!ÂTw:íKÚÚVBk¼ø_›Ñ¿ïv#ëQÔù¶Û‰êƒ­ö£­öNÔy´ûð›Ýý/Ø ’Á$½Œþ}cSe̦øã¨MÏQ<ˆ¶îà³ÁS_p ‡á<^¦”¬p^?ËÆ ÒöGõ~#êÀ†·…FÝ—ÒÇW³Ñà‚ ?]DÙ©$lÂÙʇi•šÅNÖM¯)G¿ÄJ›¶åDLã›Dñ9uý(ð'©EW Q/çÐñA¡Œ$;EXÏ•³l8dÝþ9‘T?ƒ½\nMãÞá…€<'É0Mê8ºy ×²DÇKsIY À ¿‹’¡p¼ÐÑ"Ï&c ‚0bWŒ¥y–$„öwwŽ…yäÑÕ$ÃÇã4Ÿ¢ãX_O0òø0žCâ f™EDÆ)êXJ…ÊhyQ•üÊTÆ3ÖHØØt!Óák6F ñèÍ’ndj‚éoà­>HàÑ«ø4öž*G“xƒžr^]íàñ€ bH9Xp<;þ”Nz¥rqX,žë^ƒž…PÉŒÔÇàBétšàxÈ‹Àb‹æýàų÷/£W{O°{ožG‡?¾ˆö~:üñíûèùþÁ³W{û¯áÝ«WÑÏ{ïßï½9Üqý¼øcôþÅßöÞ?Ç¥ô*íDo_B¡ÍhÿͳW?=ßó7ª¸ÿúÝ«ýÏmo_F¯_¼ö#üÄŸ÷tÿÕþáRû/÷ß¼88@§7Ñ›·Ñ‹¿¿xsüˆ¼îBMoq¾ˆ^íï=}õ"z ¯öÞügtðîųý½WØ£çûï_<;Ä:ÏÞ¾9xñ¿°ð*z¾÷zïoØ%ªPÔƒŸÜ;Ðm•úÁXМ¾‹z–b ÔsgÓV—øí¸¨¦ð²sžÍ&˜“µ»Àe.™AˆSþÅ$9‹Þ«2ð8õ¨Øµ¢:Àü½£ÿ_¡ò<Š^¤¨jîͯ†÷«‘ÅñžU@d”¤–¾¹—c‚ñc*)öºÍ@ŽpVgUÌìDZÇvQB-6‰©³ûiJƒµÉƒ¤¶Qê«#úi”¢e@îi›Ê‚×3$W´ÄS Mé:™$à€ÍFðrÿ-Mœø33CŽ~˜^L²ë\¿ @LJ¿é…F„28МŸpù:NZ“ÇÕÔHµ=ì)OšÃ“fg@ÌÉXD˜4ªÿÐ F)3ˆlQQJùÕ@{:†ðÍ—ë³%U)’ŸkâV½sÍRRÿ´ "¾® 6ôZd(p‚r¬!ëàáƒt’P`¬Ü!p˜£÷åXDg‹LýšHõZÑk æ)»Âv@“/ó „ƒ3öñ‡a:ºô&Hb¾Kò½Ô¾âÂÈq@` |A–#N$Lû>…6GÍëê‘ã²m¸Häèv„«Êß>\ªGY-à ¦šh‚Ä”¡%òc¹‡¿î¹>j²ŒU ú <µçDW¾æÕg„|¯yK1$-ùJR2ªsÂâ®VjK¹<Åf"Œ¯&Hï_MeegÔ§øFF„RÌÁÁÑàÌ¥Ãð8°½Æ›²æîw÷°ÉóïBU¡¨„u6ÜWÎtÐN¸:ÃÁ@ƒ¼žç[ñÊ{à ÀŸ|Šð.{9Úõ2Ûê/ ©BUsãÄ^QŸðM%~“\µïuóÃêN3HèéásõQ^–=„:´ÖäÜ›.GÙõHÈŠò¶Á±sžî*ƒ¬©þÕ¤³E… pü«ln¦„\Ï·dÕ~ÞƒÞ(˜º£c.cÞ<ìZÝU]¥^TvµŠ@öµqX‚zfÂv ºÂoÚëÙ¹¤jgçѣÂwñôb¹¢PJEo~k´„ßDÝj tJµ„ãsþ¬§"|¸ÕÝ ªñÍ·ü& DnÞÞÆ€ñ–ž±û­MDâ"5Çj#ÒlÓ5Cé ªOB¸/*Æ/*Æ/*Æ/*Æ/*Æ/*ÆÏ¨bÜW'lf)ÀšfC®òË;kå¤Ë“¤–ÃòšQªFhsZØxzf-#-z‡¸áâ;ZQpT·ö<§Ãš§À„)Ò0À‹›t–J’íhF“9I66hK?™f—É(ýŽ´¼…ádÊÒÕ½úîî?mýÚúç¯õ_ÿ<Ún}½ûëÑ¯Çøë‡ÞqãŸõú÷»¿~<ú?7Ç÷½i|ô¶­÷¿æÇ÷ÿü5¿Ï>§ Ê/cU Inä€Ë°Ó“lr‚¾¸UäÏ“˜²ürºWéá€1ªÕ›øcW.ôoÊi\Ý¢Ñ[­³“t#™“dN×ÓˆáXÑ^,Ûé…à‘†njëTÎòõžZ9Ø}Òî%4G5,½ÿ«måÓÅ0±º’GÅÈõQ&̹@IŒv/2â錻HŒŸ¾µá_:nß ‰Ø–:¸®bèH ›C6Ä•ÚÏ´¬eMX›Áí¸ )½uß¶ÆÙ¸Þ.(°Ì.F´†þ}8®j (]¥ávœÀià8Çý >b‚ —Ñm=pL^û<æLÂl$´Bb/²z ‡—œ/þØõ´¡¯2pño%¸÷³ü8ôLÁn¤÷ êC‘íªÔ¡-½½Ê¡^ÝÊ `HØ(Œð’®ëGp–o–ò–ÆRÂ'àBý‰H€p$¹çtC_H¦õޤÓ)ïPúƒæ:æ½ö­õk—÷ØØKÌu–‚]aFU’À(¬ºÅ°(Á“ܰåõió+Ó„}Ü•…Ôá ¶#OôõDáÝuÛ§;D—T ¶eÈ<—z8põF0­£]!„ +PX‡J›ÅrÞ3“üÉÅ2i`°ñUÐ=Uñ,4ÊMö /ÑŸc¤sÕs`é¤>Ût„Úðú˜ŽìéÌHîM b¨l„ÉKK».!êL26ÞÒ:î00ÜP`G¡mÙ,` Íë—Smðßû^25¶ãEûø’ƒ –!PÆá1’©+ȤX »¶2c,,½ðVÉx¥ ùÖt*—²Ãb#–¡ÑUªd¥f+QMõ—£Ûía:ú@1ípÐ…ÒgtbZ´tâHÄcÒ(ó…8Ú9!‚FtFq“¿(ÈQˆ²A -Xf(²`•Îîqt5*](¾¬ª¹2&ü‚óy&«Œ„ÃHê)n3ŽCYýAˆôV´Ñè(%åŒÓ[X6kNuÒû‰äa K]†jÀi“Ž+ÔI±>ž¨C–uªÀ‹W ‡l/æCW±DÝMBú6Öõñ4ãÖD9…¡6¢' ¤´­ËßҚė!4,1dÝŒ¸˜T #ZÑû¡5D䎾eoØm,ÝŠmØi0¸ —7ïªN_Jv{×YoÄÎN^0t>ÅXÐé¶áñŠÆv½Xf,RQw§û5è·Ð½÷t)~ðp·Ûq®Ê±àübºuÍ)@yýÊQë¿#uJÔi=lu‰ c á°t¶:í­nÛ±Du:´ã¥íl†z]´æç ]¡³ÕîpA:eœ¦:â˜CÚ¸|w þí<âò9v0žŠÚ˜Œ ÃdNyÀ)䮂¦®‚xy:K‡ì u÷¡ó¬P'côyÖ§0R³±†ðPwh Í/ l (]åíKͰ9bY9èòŸÂð4s& •ª‰¤ûÐò¿)¦³¢Ê[Øg“‹ö³ÂäÞ6d@{Wº×¿ˆGç–³·®=‹ó~š~'ÀL³ªˆ ML=qŸ¶gr0’õF1Õñ7 žHŠfvÛíNÃàÈœ@†6ŽÙ8˜ÙŠ5miB2r‡ ;óÂŒä|˜  –gù J×…–wl#õi_F<« «l·c—}ÇîÐľI ¹ýn¿Pñ¡î WœfìG¶tÎ_)U}2ÉmKÛ#»Žetç}é¤íÐ æ7Q†5鋳ލŠkÈ»ÊPÉŸ ‡[l/6”:ò´šž™ Œn ÑkÈ¡cÅž1€nvçÑóx2Íû å¯z¾ÑöËó‹:ª«ÙT ° ÚC–TñFsi¡½µ#¸åEqHÝ]¼Õàt#ýà=ÀÓ™Pòd“.Z€öŸËÜèB€ìBjéXkZß®ómpifg”+˜¨R~¶ÍZ»¨@ùVf1x"švà(Ÿ'dT¢Úê¶:ä.‰–¯ø©ªû©·í!9³ØÔQ~ë ;z˜ÜeSÁÞ€< Õ®Ò’V™8þ¿nÃ-ð‹ ÷‹ ÷‹ ÷‹ ÷‹ ÷³ÚpON`Uc´„èH¼D¢œ‚w°áèéÉþÅ!.î fWcõ]X=:ÉZ\_½5bšz’扩¡7FõÖùñnËeEU„äzõã`vª{ÃO”¸¨~[r±zTx3Ž-È¢žÂWº˜1‘çÅŠÇrZ>t,Ód•f_A¼üp†ç*ŒT§r¢LD¶ÇCwžžhž 1ËHÏ8sãQŒ\ùyÀ( 3»J@Ø\´8<6‚w&NQœGõ&P6°Ýþ$Ësx3½NÛ¹i |ލ÷xüÏÃ1ùJì½ £ÙÕ)öö ÷ ä”ÀõíMˆ6ÁtŠç×dz!yB1‡™q%6ýŸoä‰\zqfñ «ºm Êyå­&…E•¦Ø_¦íísï-Ï>Úц-#_S”´`ÁÕy¦O¼qZ?rÂÒjÝ.·ÇiêBˆ(2Žïvû« À |ækŠ«e£ÚUu‰;+ À§¶}jqȤãϘÑÏ–rD]†Ð~Wßnö[ë;¯ì@Ä¡˜3¹qûhZ&1}§šÐƒÂƒ—¾[c2FäF M¸+pe"­¹°D ø ä¡‹øT"܃MMd`×…1`S†]œgÛêRb¹$ðÙûX ž#û&V¢mß,¾™6â›Ê†·Ùhç±>„u2p ©.IkÙ‹®x“ Ó “ngwbkJ•ñ¤¦†K×9j þš„Õš–ŠJ@~zᆭ@ùg!ȉ„a@K÷-–Ý¢µ›7OMRÙ_$ëÛñ_ ª©-~5°b ý4•¨àÅ9üXLÇÀrá]op„,Þün¾ ½;¤ø ËÉ@írYjP%¤:^m _À½2ÍP /æ{¶ïúQƒ×Xꀎ,]é±9yávï´ È*øŸ #õÈÿ¤ßGͦ7ÔS’Js)Ï¥O&Õ„‘Ä?b±Z£ÎXøªø¡Õ$…M¸háósõ òrpáoJ­d•D µ‚ƒñ©¾Ó‰a§ÕÉË F=ÝôÜáfƒ# =CkŠç‰ˆßå‡Bœã@‡÷d°R‰L†?ø ¯°s¸M°ï0µ-¬BŸ¬ÚJyÈ-`†wîF*®ÛÐQä <ª ³H,ŒK#Om'iÕGV…s7XóÑj[ã*Ëëe.¹uåöj”9ÁÀ£™¦’g™Ør-έ ‘^Z€yB¬M’üf¿51…K9 ëÆd Ó ³ª‘uü©'üˆ¡ ¬YÝÀÚ`ô¢']íOÙb8Zh â•DJ @AŸ¶CÓ ê ÐÚ6:½¤B\3é~øJú5Ñg3,œ–Øÿþ¤P£²¢Æâ–•xUP°|`)¸ã˜Jˆ*bÁZ8CÇ@ä&¹>™Y„íøcSíÀ fÉ5Heuâ”H2º¶G§²1Gªb8™Ë]^Œô¢ÖAªÖ,{ŠÔUOã‹g£­öûgÚWB[¦™\ÍhÙÃŒ‰ƒ¦"c»"Uòo›VÛxAùˆ&¿­.,E]i¬Måö©v;COÑÃ[­ý?¶<“}÷œ ¼T¶×Þ€êD"÷„€â*-¤ç¿Ÿ@)wiÒþ²bÑ*O8ü·‚ds>ç„8N¿]ââZXJ^\QΕÆÄÒ£¢»YøŒ§¢üÀEˆ7ðrœ3^}ÿæ@’שùà(rÙEòc/å‚Óçê¶‘¿4h5à Ác14‰¸ÔÖýLÃyƒÇ“ü‡yaÝf<ôµÐ±1ñ׎•IBÃ?fÿcà–”NH¸dWsÝܪ.oUh¨¶häuâRjXf¥kMÈlmUþ$¯Nj+À˦­ÕRÕzÉ<ºËø“UAŠðm›à†Œ£^L[®nNu¸¤dËpÐ3˜÷\ÊJ\9ÿ[g–Û^€?+;J±ÿlwñÕx’ 07Ú€]h5‰0È&öÑE Jäzâö€P2—À$Ìsbø#*Šl9ü…›eSÄç`ÿðö eìÇñab=‹…ª[C8`0u‹ º‚ãN­¡+x—Lj¡~ˆñ|¿©nKE¬^zå‰ ±Ô8Û`ºàåf(°×D@•'¥0C}‰‚9_‹Æ‚$¤u^)®”‹$Î7™¤àYtÇ Y..*£u¿¬zÓ*¥˜–(©þb“bI4UÜG9s¸A¹e¨ e P‘ÌmN®iZu,ÃK‚ÉaP¥Àñ)7â>zÀ{9ælZ VA@p«¬3§ÃØwL¬:›Q^¡'.PŠš"wý^yÄ»©'Fñ#?¿Ü¨=Kmàì„f°ZÃw½´­UÈ<¤.1ì4}¢²Siϰç¢îLÀ À¬4¥ð:„7X#r9©•+zè…ö¨3¿:Ïw7„ºŸJÆäDUœ£Ó[Ýßþê×_'•AáØ[ëÐÔúKç¨(úM¨Sƒ“\LgÁ‚ϲ_£¨¶7öÞr¦´×Üê°µºÖ*Àhç×ݔޠ~8¦œVíãá5U½L(äUL·z GÚÕ%v%³ñdPœÎç © é1”úþ-rî•¥ö†â“¥àûÏer²°9¨àþÎV:*6Çõ\ï€DåÅ|Œ—ɯv,àb»–«ÆÍä‘^64“Xeé¢n~1€ù™ÀücÖ"Î –5!îT ¶œº€ÝÉ ˜Ÿ¿íÍ?fA-ˆƒeü3DÈ ¨£‚¨%xœæ‰}‰¥AöQ±”mÒ¤*®™%#±UƒP@zƒÔej%½{¡Ïéïßn¢æ¶¥XqŒòÇÀ¦!ÛŒ¤ëoý:+ YCjxû 9CìTÊ%¸Ât&R;M³šv´J¥§R˜bðýùìhóëu{üçR7Å|l®?|üq80j»­Î½†UãVÕxüèýSŸy©PáCÀ¢vhŠ|Þíìâ²¥d¤xe@êÕ勽‚é-)Ê:énà–…®º%Æo ‘‚…d ø(üjs\I77Ñ´[sRt{Às&?TÇX Ù鸟¤ªtìoU¯Ña”clCb(¢ ˜%U])¶m å¿´å¢«žÁX_‡—Šëíë¿ð†ˆ´_œŸÁÒ¨‹š´X‡…Yz?šßÔŒZüdEÓ3VÛÑ“²¹6àQtšâI¼;Ÿ5»»˜Ö¡ë麺˜ »ešðVÈlˆAX,‚½ºA' Šs³Í–Y"?®lX¨ç‡vv KWÓº.,»ŒÌ-㽈–.¡|>D ùaù2ª\i9å³|Yå³`y?¤Û¾ÈNÓZßðsèôŸD§é¶õ>0ts÷ýú­êý:jöeG|Pû¡½p‚©1=¤ë"€–Û[ñˆôVX¸Á~^Cç¼Å6\¸¿c¼ùD|5èG‰$¯º©¥Ó¾Êac«ƒB~qäý˜Õœê §÷ðn=]…Ëpú5Yô*ß ë¬Œ0àÕè¾êYÞÙ ¿µ'| Ò¦¼é› +±œÙêâ¤è}¨èf[o‰Hj(ÌúN~]Âä‹ÎWÝåÔ«XÉÊ09{¤”˜BžÙ‘mTËKGæQjõH„{Â\ `#šBQèá›lˆ‘Rq ‡Å —”ý"8"V„£ß%/k®ƒ…DOáºÕ/D–Xx* ço‹­›Ö[6ó\]¡Ü¤ŒÉ+XžŸ›Ã~Ö7Óâ‹?,ö‚IîÑa2ýâp~|LáƒóÀ< ­YJ‚ î|¶ÈôôÄñ™úÌü· Ѩ$8+gXÛqw‰\õi‘éæ@*úåiÙŸ;C΀ñ/uû 7¯5»±fáš´õ¥"8‚]ýR…ƒo¼¬é»r` §x°©Óy’WþB¦0ªŒ8¤ ýÝ2µ1¯[œ¸ƒiX¥ö;Õô8 Î;\Ü´ø·‘†‚£\*0bª„dçCòke“ƒ@|Ó†ÛÆÆÑ_ƒ ifü¼þ6ÀœiˆgÈôËÏõ8]ßÿâá£÷ëï?~}oóýÇ££ƒ;ë-éÍ‘ãdåÂЊæ´1—6æ÷îA+ó£#l§Q¨æ0‡4%ØØúë;æŸ|8ùFÂŒ>Ä'ƒ™}ðÛõ<øa>ÆG—¦eK˽Æ7ŒlncЯ§‚Ðcû*PìÙ} k6ÅÝ­ßüæë»¶ Áœ(2¤¸ í¾ûñdfÊìÀû¥eïmßÝÞÞœö6)ßæ¨Ú€ªæIP³êüê˜þ«q>Á&Ϊþà.‡/DÍ2X%pBÌwgH—¹Oò=Z[®ìÖ4k¢)´¤ê#Ž37a‹.¿Ûu'±¿MºzPsÅId}´Ë¸`i,›l§ôF6Š}ïÄФž©…çÛÈl‚)ŠOØnÉ‹2zÉó¦!.¦Ù]…vŠÀÀ¡ã™Üã ª¹Ø@ÚwCð„"sº2Ï'Í€›cËÓ z7Aé8+#”IfyS-¬±q«ÿ æuœöíHÞË”‘ k¥ÿ6.GM3 sŒ1ƒ”"LBÆxI:Õü°€¬ƒZ¦V¬iÒ•@–¡ß2~˜x@)¸ó@o›Êr^åiBNPŒmýÚdÍŽùªnõáDDMJBß Dy¯+L_‡­$ú–X:¿SÑçy÷C±.BŠ5×I‡Gæ¯Ôr;KÌ×o"êEâ_Þ’¦šÒ!Vë}0Õ±&ö¾X £zRg–° Ó·§Ø²9• ô5­Ä®g!æÙáhûXé‹6-[ÕÚ}¡àê‹‘-û¢“ªíù»@©«]LJœ[«EHön‚ÿF?³å;GÈšW’ïfV«8-(v›uù¦1 !G­Øxª~õÙºa/×yÙ¸9©S r_OÔÄdÙRSl‘ksYÔÛã ø©«Ïm´GÊL¾Ä¥"Á;žr1¢gÆçl@çYÓyÔƒÞ,· Wkèù‚Ëc»Ó‚r?PF Gr¥I݃ü=¿PÂ)-‚$I± bGbþ´à{X*1ïÜbä¸ÄµSíÍ O oÊŠ~W…ØMdóU58)Ñju+É>}ª½âÌ‚3Zî2–ÐvéÏòœî®¿8ø¬ ›rÁÝËx²¹°N0²Ô}‰šÿ[¨Âdï÷453}ždTq‰2ê «Ÿ ˜È8Ãb2ì‚ÔµBœëBÊù°0Oj @•yò>ÛÚõ¯3eïæ¨Z2oá[­ŸÍdÞÑ’»‘HXا˜ˆ ¸Šœå€ÃQìÃO_ž åÁi.5â×+áŸnÿªX¶:À¯ ë`bxò.Bš%ó°üHÌU¿I{÷´ Ê 5ÿú°ÚW篟‚{…üí  ×p­þ >éýê˜JÖ·³ñä…Ä;Y„¡Ù%èI-º‰F7²Á8ï3òÎŽtÞseÀîDeêxÛ;)XFж~}ÿëÉ ì´£‚ëªÞAÁB©65½ÇtyÍ…Ž uý†ÌZ}>`ô¯; ¾Âù ת¥™ZfŠÍ£Ø›Q5o´î#4«…¢Úìà~m-–s$NµuÆ),Ž ×ËÁíW9 CÞ>f‹“<ÈFº5¬"‡¬ºœ–zb·_Oȉ„Õ‚.˜&ô=Èb–\ã¥S»‰fÇDzkÀ޽4•˜ÕcrV¿hfïn8L¸½±ïï ç"})p•g8Ã4™&O¹¹'÷æ*™ù‹gŸ tã[»ì[q‰?t¾+ðµÛŒ*ûꊸ¹^ö}û]__„éWûe_„úHI²D3S»yÄD^¢™Z’ U’6Q$Ãâ'Ø&*‡8.ÏìwÅàX¨Ä…ÖÙ¦žây[#ŠrÔÌûlŒ®Å:ün³¸n˜Ê—Y¿~MPoê±K€T’_:Kï„s`͇CE³-ƒÈ' ëX—€…)¥¼€)i;4iÛ˜Ž¶®òï 7{Ð"]í©µ){ c”CÚ[B¦ù§äÔ±ö XÑÅCµ/;Èö_¶“ï`$”0JåOÔ̤/m˜mƒ9à­<èÌ‹~K!hW¢£X÷…ÅQTH‰†‘3 É< œÓ®•väT‚î£,͑IJMÁs©W»Œ0T£·bŒsà{olÆBYß`Aá¼÷!(gÆ'Ú.à?á—v¼rƒôTS2+;+`†yª0¢A? ìüCÀ§•SA¡T$98®dP7t|Ì«6²a¬!κò£&¢‡º¸F1Ùç&|Wf~¸.s‘>Ïtœ"{º¸\K¤’5bä=C,078BŒ«õ°žZÐ_ír¾1[+tûŠò_ºÚ Czá0­8‹öŽBùéÔ^¶jªèZufÉÖcËÎZPPd`F:HÍls9bXý&¼n­'Û Q!™ô§^´I F™¤,ÂòÁñ¡ü"îhó7&öÀª|]!qÒèÍޣИ°(Býc¥‹ijÂ|ÔáN¼8rŠŠßeÝ0-Ò¡‘D”%S$PlìIØô'úGó&H"WëiÇBôŠHp:ÍÒ/‰HÇõ•ë,»Ô]! iA­:éB«´9Ë&èLÿ=bÙTÅ»j$"šàs°°Lçue½çýãT*œUŽ(cµ2†.\Eµ Hp¯´ .©ŒŸówà+ LÆU*Ež$Âkû}¸ñ×™Ôg†ˆ£lÖ,>NÌio&r ¢ŽgÖ¨/E4ünRÁœda¶Â…ѧÁ5âL‘ ¶"ß6ý Ý?<|dFžSy–‰;³‡8ˆÜ;úöéþÝ&à„ô:')/9­”+i"ƒ»ê 連3y½ƒµgözZPèâì÷ï@b:ËÈ•ìÞ't—Ê€Ì c}ãîŽdºÕYÆí354á}¹ºže„QïÞk3ìBŽ1Ro¢ûq8 [ói%Ižqilɺ4ÏCHé^Ǹ·þjÌQoq?ö©‡oP  ¬Ïþ %ñp|ô…øjUö 6ÝA±}J¡Lš|º^ú:F°$ÿÉïv³ôµ®ì.¤‚ú¹· à Za14ÒBÞ;q¼ëÊpmï.*x;<#¯iVÊï™Y©_œ\ÕIß}Q£IüD«Þh:Iw-ø@R2§¬åâqÏÙ£l;¡ÿ¤‰zu÷·bc\°¨Üëï¿}ùâ)áÀžÿ´ÄÆ{å´77ÈevŠ‘¢ Œv'íønA&§¿|÷òuÚ¶cíÞWø?ùl}µ½uïÞ¯ýà^øÙâ?[÷·¿zðåÖ¯ïmÃÏ¿æ×_>Øzй;N›¨¸Þôy>+:“óÿ±‘½ÊÍX ‹òÒ°óü¸ÈÞǃål'Û£rphQ“É9yÐ7Ÿ¶2ÓáÖæ¶Höêå»?@ä/Nýôd>;1G¶×?ÝàKLfð°èàËo§y¿Ýp¯¬:sCòG¢?Ä >ø¼Í]ömi*?¬Û«ª¢cDäa18„²‹ÿæåNöPùŒÃÔÐåÞ>‡p2e¯U >ÐM5úŽ2ÐP÷½Wo÷:ïþònmmm}}}íû.ò±%{}š¿Áp•ŸƒTnU+®1Ûdö°|BàAþnØòþx¸¶fY;d»Œl99^yVÑ¿Â|³¼\«S â[Kø¸ ÌšŒQ²ù îàèñËüÇó§ãQϬòˆg¾Ë'ôå?Ê U2;Ì 1¥îóiñÃ3Ð~`¹=³ƒìƒ9 4Ü_[û­ûÑl@q\„aa8„^ÕÉ{=ƒY½ófKTF ­Kì»w€á¶¦þ"‰& «dˆ¯JðÐlÃpoVG}eã€7ƒA ~¾új*,ëÿ¨;,r%Á½ 5rÜ`r’ïÞë|¹`@˪/Ú`|Ü›’Ay2÷ãñïWÕ’ŠK1xböÇGØÅ vïà*ÒP}0ï¸J §®Ñ ¢ÜL˜{4û.7U>6½êÁKîmñ<‚:z*rÆ—ÑBBT¾îáy·7žf»Ï AðR3.è­s¥¦—®ÉYihêY¿<: WĽ1=o±?ÞnC «CfœOÏw×·Ö`Ëò–ŽÎ0D³n\ ƒÁÙÍ Þ^0Œ¨(*†cQ|MŒ$6Ë›Û_}Éí0PÍÑx¶÷.{õ*ûÓ·|ûŸÿ~˜ÿ¿x•Ñ›{9³Ñ¨YøËM-®Hµê+ò Þ¸®juu‘wÒEƒÖ\¿³Ø'§A—†zî„h«†žð ±c:©=-Ö–€RX —8sU˜ÔÛ¨û0@•»æ7ƒ"|ÿZΘFâ°ò®¦7óí  ;8ÿXPïùæwD¾¡^‚–/¨J„@JÞØF·;ÌËQ·ÛØaú˜»¶Öíš!A‘l¿¬¬é¹!‹Öð”< ;"(ãtxba?hÖAŽàB»»à‡Ûñƒ˜û¾ïÁ½¯¾ú2â¾÷ý`ûþÖö½/‰p_YÀ}Ìán¶úQw–2û÷îÿzË”B6ÿ˯T‘åÝÝ* \Ôõ—K»¾¿½µýåö}êšß»ÿÕö—+t=™‘¨±y{3ã8!YY7¿þúËßlnÁc’ê‘·`}úÚœóãyQQœ|fÑ?Nc’ïA Þ;É'`$¹%& ‡ïÂËO,„ §u{ãédnyíanfÔC|¦^wh^˜ïGfÆf/Þá¸UY©êdúF•ßÁ͘{µ”ÓÇRZˆ8,´6ÔÍ˪gßæ‡„¾êV'Å`°¶FaýöíÛÙ‹Ñlj¤$²Ýc,°IäßB¬zSv]êAV”´` æØët:¨›€å<ý¶e!˜ 6Eîþœùˆ£ˆBy$óqvZgY9ókîd D f«å˜) ~@øD8¹95†A©LÝ5.À5/JRœV͆ÅÜæÓá½/;³³F‹4<»ëßÃÿcZ©²×ÿ§šÍ‡æK¿È¾þ—añæ‡æWsë믷Zë-u´³Þf0tP%¡TúE³1YtsË04¢íUFôµ?¢gf)ŸæÕpnäJèþ7¿ »ß¶Ýo/îþ¾tÏh(ÝOìFõ»þÝÿ‹Å‚îîÛîî/îîš-ïéq8ÿ̓ª¸·å÷÷|<884ÙÛ|ž½Îç˜ðo~ó œð;‚µ#@—BÍ€­¼`a•€VYPa•UfFû î’ÇY>„(›yv<¬ÍŠiŽ©XË ²Ú—Êìçý¢4T6YvbPz”gbñä¿ Eȉ!Ôì&®Šþ?(H¥ÛàF1ç¦ ÎÌÿ·©{T”ÿ÷ÿguËY‵×_™ÐÊ|–ý²gˆçMËq¿ÅÇ8ùÞÿž”†JöLÁyö¬0ÄÔTèý?æ/ÜȘ]=ÍÃê€ZëO²§¦ñ„ýìMI ‰ç†–™Ì~aÿžšöNŠã|šåc³™óÊPLÕ"â ÓWÒ°$ëÙ:ÙbÓ£Ö’¥D€Äõ¶W©w?®w•zâzRõ"Nâ×WÒX2ouÿÞW¶]ÏIùŒ4ÆËt–Ï‹í&ߊ~¹úìºË¶R^¾fo'ÜS~šF[2ö4çæqѳ+ñ0ë{Ñÿ7Rp~k°´Ç¾, bàãØKˆzÄ ³ªyvÞ0×Ìô¡/(vˆM@rR•¢ýéØŒ'n “eO²¿\ð‹5ûâƒ$ƒËæ{Cj»ëô`’Ç”R½½vvR@6Osâù¯ E±”ç1Òóf57esågæƒzgÅq1m±¥¼9›Àâ7š%c÷Z|6Æ 8H·¸m;‹TÓ«šâÕüP n•—€ „œ—lrº™½9ø²¨.£ö¨ö3SœÉz³ $!uØQX7`†þ<Ôa~Žy¨mznÁúW GEÝár±1o8\`—jÇùÒ¼TãärgX³f€i^î3ËÖžGÈE+r> §ƒsëImqÓC"nµOÖ(Š6>zÆxÇ ²è2p”Î9þ!š=ÓþÉmí3Üç#Œ¥›5_\˜F.uAƒ£fkÜZ ðëlœØ;4 7›æd wþ%Lv h÷´ôül”pÓòÆ‹2R÷ÆÃC´Î8<_ûË…¡@GvPíÒŒâÏ'–Ê Å¡f[›õA‡½4رnÑo¯aH.×Pv”—ƒÊóùzz—5YB› Ûl²/¢%˜¼ ð $†9 ã#ÊšG FÔÎ9åšLÇócJµU·§°È(Tïa°Š™ˆéoš+`-ì1`¬yÍ$&6(R…£& ¬å=¸X2£<5×а:åD•G@@†Vâ Çÿ¥t°Ò}" ˜$ƒˆVÑ^< ð†æUË"z\I<ô‡pÃ0,¡ÓjµÛ‡qh$zyÂ~«501È?ÀÊŸ–hS)>¨¦‘1䈩‚y¯Å»Þ4dXÓÃã¡ø9SCD´èè,†?qÑË5—$Z5Ðàu$:2Í{lC¹ÁÅCˆànäåšóKjëXn$tõ]NbØÍƒÚÛ?@̳“LzÓNPà‹¶ö@~Öp„^?ßà>;Áß„pß<(fx€ÀAy½æŽC‰öÙ§BýÝɼb7Ôš>Uhü†‘¢ ‡Hof¾X'; Óç!,^žvÖ~?>3œ¤™Gò*¦µXGfç aP¹<ÊÎÇóµÞÉèìÌ0øfà`Q>8‡3ï¦\Έ„' mí%R¾ófëÒÞ÷LÛV’ɶA#>ðÌ5’e<7ÿÂYjà]JJpȹWàH?a#Òˆ°i_ EùÐ×Ö^‹zV £šàdw~Ö¾3ɦ´7Ùb‘‡ç2î ÇM_ )6GÉÀLÑ,ô´Àõ„ô xª§ Ä;<ÁÛ™áOèÈ„kGsðXýcÓ¥âW°Šâ Ôly–£4ß^ãŸ^@—Õ|zA¬†Œ{ÃÓ­V›²õa#ëÑ£G™Sc9ˆ2 1–$ü€î›w·ÚÙù.~oµ(nG3ßmä ûøp·qØØVóÆŽù÷Ðükþ1gXã£ùºêmx”7.×Ü>0h+ŽžŠb‚aÓK0=ô"÷òS0²‹›Éõø‚V#+GcÜÜöL8´îA*$àÌì?˜í ŠÀà›)a7ÿ·À=ºåØTN] :r¸˜ñ}‰ô@[9t×€€ôØ<ÚBãeHˆ‰4Ö°¾¢¼XØ<œî—׺ û–ïGˆ•š XL“CÎäÂè€M(Î1ÛInÈê wq'É;,ƞ̞NŽ¢_ÉÞ° ¨˜,擌ðQ}³VëûãÚ÷¸m^Þ†™ !,2­À±ËfÑ9î´i͕Ħhq+Ù£!Чd°:BƒY "¤f/sL ³ÏEŠzyq–>ð àC×J?’}Þ;ï)µÙòJÐÃG—rŠšÕÇV€ÍÎO ãˆ$øj5U‹Üf'JqŒñUm>¦)æ¹ áÞ€&*lm¯É<ÖÍ̲bXY ›a)÷Ž @$9ªLëcÃÈv«‡¹¨# A ÂÇbž%užÅˆ‹½ñä\+ÿiŒK«c¦í}£45oç‡Õ¬œÍgÅ·|ʽXÔÈKøñš{àß=´0qHYu¬¥8£ÞÄÔá!vSM³¥„s3Ö¿Øáï“ë(2§p ®k+$=ò'+ô0lqV&Ü”:á& +«nJ©pµV©‘ü Šgå^c戄ËÀªà2É`žþ Š\(ñ„ 'Üf±p;cªâ§fˆ“11’Ò„ QÍêæˆ»B=Úª¶ #uD“Ýomµ¶ÛN§}4jÀ¸„7( ”SÚ•?Ú:Ø?Ú>0å÷F—‹!X/;ûÊAl#%C‡ªÂ@޶¯ ~¤åPbu¶šXý\­d0 8:<´ÔÄØbQÓLào Ue}8*κÝfoPY\«8KÍíÛÃ1Ø"ÑCet(+ ;8¥{äð4©“õÅ‘:Š5HsSqÈDFô#õµ¬thÒ‚C Qr჋˜Ágˆ;I.Æcж2ÖHPæsÒ‘4ó Tj‰ÚÃg^Øä¡®6‹Ò¬Cæ}´pФS½•m>RO‰›l¶$3•YÔÌP”ÕÜLÞ>\Ø?Ÿr¶æKrÀå§‹ªúµBj¬ÚŒx*uÜO(²Ûw?D¬xz¡ºaÊÔï÷K'oÅÆk婯‡‡$žoêjž«!o¿è•}#Ã’6l/·À"ÎT šqMˆ¢pw•yþÕÇs>%øñ RÌ =ŠkfU}š·¶Œ´#”CDôÂk=ïºeÕå¥m&ÈÊê=¹ótiŸ²Å]‰(ÎAfÁH< ¦ñ.NQSá§‘€jFˆb\ìÀÕB¨¬Jn–€¼×“ñÃCÑé¾}÷äÍ»îóg/ž¾ë¾Ùë`|¥E0¿I¸ìû7Z÷z!¤¿ÂêF IZ¥–tºJŽçkö$n•“9 “6ZkjÿˆÀ™i¨íèé¤9e2\ƒN‡¤¢«8ÜÏäÑtXІßê$÷+ç7Ÿpn¹ 6ÈMöå­ïHñuc]À1mV‹rTëdŽ Î$“CêQ+ì(¹¼)15;ïñ䕬xåô¯À;7øðß0k…77")7_¢>’îÀè²I5’‰Ê†«¨tÄ`²C·LsX½aîkDyŽ€ý—[‰„”;¯äJñcJk¨O?ÂøWãÙ § ܯϾÂÀY^tf“¸£kœ+{O‹eá‰|æ P”UÇß<*ûí#3ò–‘CË©Õi™Çx Øâå! Óƒ{bì5O²•6i+”¼©QúóBçfˆÁž\|‘ýÞÈš6Úæ§5n—Åv„íŒïñ¢¦ëÔÕ‘Ÿ•†{U€Ã?½€Æä ›¼TZ¥€›:iäQÔê*b‘õfÆnœÈ…Ýíw1¹<ØÝÅîèÇ¥d»š x‹ ®sPÙè†.‰ÙöZCVxQ®–¢ãμ>¨U£¶bÑ)„22ÿ0;f•U®ì@ÏšF­TJ°:¸ëv5Àº¥€í–»*Ìj¿ 튫òQ9¹¶7Qº\y5æ£A.ªÿ +B)Ú¯»* `Û‘ h!TA?g¨Ñͱú)­Q•§ÍŒSÑ]6ÿ§*rŒ;›O/¬(P3 ŽÐÃa°°›TÔQ%Qx÷Òó Bˆª“íŽP1»X m°Nžô¶ZØ„¸>"#𜤱<f£{À¦Âø`uOnÙh -<ŽÛh¸fŽ™. '-/¹¶üDžkÅ3œ"Bá®"›Ô†“žŸm4ÂÓL TØà¢äû€ä/c²ÃHC©+ô¢aúͲê\º¨í‡ºJßD'n¶so¢ó§¸˜a»-û­Kâýþ™WÅ7ú&nõEú9Ê×á2sAï¬Ñ }8ì~†¥Õ6h±6NHa rÄ¢¿Ím#³mFh¤Øíé® þcµnÒý™}d»§K’¨ŽßìFà¨ö*CØÈš¯É:{‘ÿù‘>P—^Æ%dþ=Ê+ðåÜÜ„.éêC5ÑoŸñQ9àˆàÿP2YÍͺ¡ Ò¶…]"‰µ`ÚðƒÏ=ưfz ¥ÝV„‡ 3~ºdk…¥ 1œ†œ•SÎàdCüÊtÜ_åa0Ì?`à•!©Ï²3#®òŠ™ ªŸ¸oäÊhä~fŽÐWÈg‚Ž6¤yª¬·ï貃î²ì!}6æÝDFñ¸´ª•Ðö‹Ì¼ƒJUašë'íÄìÒD||¤ÄìóZ—#L¢þ³ÏK¯_Òæ«SÛ5¥½¶«íU'ê’Z{Øõëø=#@’«ČԪzXW¸f&n÷ X–ÎÁbÇÑ“ChúXD DsýžG H†’£Úà|vB!Ž€‘;ÏÀ‰©¦ÝÞI9èOuýfàô7`”ȸHohÖh5 Ö®IH ÿf”dLlh9Ù™Æ}KÄ#â­ñ¢“÷ûv<­¨íx.Ã# áp±PÍÔ\5ìWã3KØáBP%k˜ùâ(sæ½CCþ`ÝPŸíP-ºº‰ÂA×<¢Ès0ª.i„JᇒuqÐX2ãSb›8ˆ¶š»/°·•ž'}a Ü(÷¤n ž-ûxœZÀš]¹‘…ÓŸäÁŸ´ÒžÏ_‚©½ø«æ ’Zå=P­ðV ;¥É7éM§÷‰m^æ’x–1©O” Ý-‡!ý¸æss4íXyÑ"꘺å¼ä íŽ“ëžJÏãß" §O»D[á¾H‡} Ñîùòëû[ÉÍèö!FÇozÛ1@:iîönvÿ«ä›;»ø•o­X9¸ð[¾wUo´u±OA£¶A4\‚Wjl:(Ö<¸ÝßÎ ùQ©¯”}›WMùõEvïãWGô‰BVs!Ÿߌæø9Ø6Ý¾Ø m'CÁ7BÆ»ãÂ^™€qš)ÉÕYûÓý?8ò6“Vjvj÷ù›?þÇÞ«îÞ›7|®ðÏIÂÒZƒíš$”_WaEɦkÊã;`\Óô‚3¼*s9µ“KÇ:25£ì¡3<{ÅRÓ2'Pì òÝÌgfM<¥hƒFÚðº0Å9skOµ wõ‡é©Ö ¯èÙ›ßC­VÆÓÈ„¡wå%CØ×¨RE€® ê5O¬ÏkUxbb‰6hK,ühEܼ_D Ì.Ç m)š›Uw‡öá|†v]ð3òNJ7¨ žÑ²ÐË 0³O ¦V›Ê»Æ• ½—–×Ãóehþ8'˜–Õbí‚rÉñ¢6ÛdZ&>×="¯|–ÑÆKa7x8@Š›>0<2à7Çt6â)²jòÉ0ê©»)öµoH´Çýè…Ù¶¡_uf Êà9¶ö Zwc+o½u0bä YB@áZŒÃnR*]¬o-,txÿ:sOƵhuÀÇFÂ6°Õ-+½º]é¦ÛŻӑӯÏù7Ñãyé¦À5ÅëÙ¬t"‹ŸÉ…¸¸ïÎê… ¦p8$&±8È„~Z‹úTc>½/G}%dVÍ‹ËÖ¾P«7F]d•>¡xb>)ŽB<ðÉ5#eIÂÛmhE'dø’Ž ªç8#wÙË(âH (dV]0 W `b¨„ðÛgƒ1$Ö}Yo$ÙAó’À'%ÛŠ æèm¸ØÉËlÌá;Žm"ö‡±£:žŒ>•-6긗.[Ü1'…íòåh$4¶ùŠ¢!O5¸2·XÆyüdøù1#7dVµÔ7Â𜗅9Jà¡Çç…•àÜû*_F8™™ÅÁéÅÓŸ©%çàŸM/¦Š ä6`Žînä.bro©7Ä.l˜O‘Ì©‰*®¬ ££V·]Ûi¦1ÖÀØ©Ï]³x24U‹Š…©™£¶O–±L:’ì+ b}ºž^”}$¸zÅғ샊åÈ’öf9ê æ}º¶ÄG-Ôz8²2Š|$€Lx(ÑeŽs¤cO¤:NJPœä\c9t “ý7f¦M¦ |mŒjÖ !g3d6`Æ,Ä'Õój]ùÚÓ ›7Eo>­HÇn"ášèŸÂ+îPíÕv)v@Hˆ¡7ÇÍG†zy“vв‘O¾ÈÄúFù¼Êºiw%F oEùéïC1‰ wtRí´.SgëòýcÀÔƒŒ¬Ÿ6¼¨‘䨖uåÌF¥IðCuã@¤r“£Ê8éIƒ{ûe =ãTœô@%ÙÅß`möþöåª º¨Xí@ÔÜZšyŽOêµ<ñÑiæÙ "i_^Ž7>dúrfâƒb¦/R=óh<Žîº_^ØØU5JC Ô¤nÚ1‚ýOo\qAqv<“+A}F+ØlÞÚA¤9.å°¢ • §=q'mlÐk«åS­ùÐ7×¾ªY¶´ál~ËÅ™>$Kâ Äv—­-ÏP-AÑÙ0Gꮃ,ÈП?´7®!™Ñlo©Þ’Ò?nΞ3Ý.DåYl†ùŒCûHxMLUéGó¡÷¤Üäàbé…cTQPí¾S¢²FÕ¶±À,Ìj„…²µã²7²ïæ„-;0Ü å‘Õx^ÿ†Qj¾úã»O÷ êûÑ­ê…³—´ÖÒ8Ì?tpt¦.n ² ák2ºBL2óØm4‚A¿+OѦ‰/ÅKÏRhÒ›o •öX9Ê/èª4ž‚<Àµß˜Ç g‚:qHìÍjbö’‰9ßg…\C| å¶ÉñÕd\.­kŇ„•kSŒnˤã6$¡ŠòXaÑeí¦·Âî] Gã«éŒæLãé¸d2ri'O‚býqK5m1z¡x­ìNÖ è¯=õÇãöÖ>Cà«g–¦|†ÈW×@»Û‹‚^Õp¨9l:Œ¡”G±žÎ-kB*ˆëÇÍÒ,‰® !€mÌ(Š›ĵÇá³ " …ÒP·ªºˆ[~´-rÃfB:lÔ:¤Ù • [W Ò•ÃÂF\pžW«K¥^Ôñ|(ªÝŠ¡|œ)ï5BùxN¼”DCùã£èÑÞå‚f"ˆã¡›šd¸x¼®Æ3m«±•*£+M‚‚äp2¯+Å~‘ÛFíÅ Úkí¢màirÕÀ*i›7[×ß9‰P"ž9 ë-K•ÀÛÊ‚„cS߉¶Ò ; `¸TiÚÅWA„‘;þ ®`ÚvSëa|(Î9•Žš í0; ÍŽ`D‰›¦±BB\Ö´Èe¶ûýô ÌXV›VK9Ö¥B¾<©ТQ©v‘½¡À`Þæt™’œž£¾a ƨàBŒdtéçG•á B¶ÐuÔf\)xG¸D6¬«ë_¨ÃÙù¤oÀ߬ ‘Òór•–pI0m l¨6e]Wõ19Hx¼ áà uø‡OnµûâÕ³½¿8ö×êÐP@r rýÎðÁ·€…µHL¤òØ8fïZj‰<ÑxÚ…j>q|á‡-°ð9\Ý]V|äHÎþE–¥ߤ\ ÿPœ³§a=îéѵ³¦CÀ¶´áUóc} û²p¶ ËõŠ!èÅfcÖ²!õnæäƒúÿ/ltãi]>[µ«­v€ÓGà hÍnÁõ±ö Î>ÙÍÀü»o{aØjÃW¹X6¥×vöâHâ÷ToˆÍº%‰,nÇR’&ô®¸,¯ÅúcvÀò׿-¡ *ækã¾w&pŸ ™“–ºöq?ôeÝ×ÃuËÁðÚ*ܦtÑ®XaÛ~óù€O&ž…õ1ðïój¦”¸‰ƒ ÉuãO5ñÝV‰N)¤¯}CL>}Å;€âSéëõäð›%Ì Áô „™ý3½æ Ÿì½::¤&¾Ü:˜ ŠçŽf1[_Å!Á°<àɺcýþÎæÖALOcBOM¬@ëÑ«P}³›M•´Ø`¦£î nÁJž¡uj]{]ð>˜v»Ë\Š* .~£ŠÚ(Ĥ±±SœâÈ”1C*l  ÚþÁŽÂ «©J7ž+¡Õ……aUn#Öì›îxïÔ[ãŽ,ÂÏ{¤ø¹9†xÌìbçøéYÎÁtZ pëÑòÖ­) ·Ì{\Ü÷Ý3 :Ž:õ_¬Œ¯Bî]šúg‰ùéÖAÉôîø)pˆ+ç¥ôJе¡+«ã?zE5ùºAHý$öfë­j®~+Yk8­m&GnñùŸ¾Ë„ ‹qqn¦ù±µÐ<£ñ~Ô œÈ„÷fÐËì4>ÉP£*Ž)þón¶ïx%3å£ò£yÖp‘·ªù‘}fnØPc mkµ¡A¾…ãJ𭧉í¼È,IÛ\Ÿ6€>dRà4ùØ9`œua~ý˜Ü’w¶þª¡þƒ€ÉVS…q†?˜  cb c#Ûåƒ 6‡c¦Å÷Fèá»Ô}1í½(ÙÆvAïELµÑà762BQÀîz bï‰QP«—ߣ~³q«Ú|Ô¼UµÙ­Z¥|dXñr`w­P6€P/vrI …œïb&(­/JÔüB“ê¦ËŒ¥î5R³„Û.·*39\xxzt•9øNÜàho­fí-#«‹Û¦wÝ`:#°Éª.x¹ndZ?£«Œ- »4@Å8ÐÔÚJŒ`P ®Ðøž{¤Âk 5ˆ+4·¹¸¹.›ÚŠxx«zÂ"†€C]Áþ? ÷éÒ¾R`‚xÂd•›EÍ5ÑVcÂcƒ¶ÙsD®kû‰Â³J›åÇW=.Ü Å#ª; Ú\<â"ÍlöoU\ Òjþ/g:ͦâÓg·õŸhc)p/ cÎ9\g}领»éÄ—Ý¿cþëcÞ(?7Ã(i¼9ûþ9ظ·`ÇcŽˆ^æ…ãåÜÛÞ,\ÈÇXå¹Ú$iÀHAö‘þ^(HÝ$V Ö¶=Ó”+¯æøg³›´Èƒñèƒd*•Åã,û¾âH#h¬9ê2Üä‡ùG$ÚŒ®a~5†Ž}hµHc%xŽcé LôÉ;þ øg¬ áœ[D$l*X,Lv³ŒOB°¨ÓÑôåu\’·º„WæL}×0&+ô\ äþ¨‰ˆò©ŒD N[òõ€ªe¬ž•I¶ÊJØ=‹s@lY W91(8‹ú€£çT OEI±Í7¸´‹ÆN0ãF0꼃ËSˆ[L(¡®*•¤Ÿ@0Äý#B Õ¤mTèJ»&F´W'^ 5^†lbà錱Á6 ÖkÉxö›¬q[SÁ;÷[­;l` 1¨øA 6¯KÃ,P•ÑOtmlÆ+’ΣM#¯Kœ­,Kž­FäÌ^³„B»¿ l¾¹_Ü_êÝ_<Áf©³ËsGÞ€›K°!–d†Ò+{ˆ UMÅž!^3i/åáè²f‚ÚD–¡ÈóCÕ} ß_ˆõ3{NĬò€Ò€&‘@_ãÞÄ-.Ðğʱ¡$]ŸÞKÚ¯AáÿŠ> îJ8ÇAKRcfÇPþÅ¡à‡‚ÿ¾fÞ+n’_Œ»³Ÿ…ê/ÆÝ?Oãî·Ñ/&ÝÞ`ÿÑ»é'5é†ZÄ5­@†Q*|3kGo¹è4ü›¡"6-iÆ•XÐL™÷ûõMÐ[[Ö~ Êâ[*KJ§tIzG劳ÚrôŽÊ™¥„;Åd9zWo…¬9<…GK¢wRî´€°5ñ%›ÍëFon4HÖ?‘U°”áel¿GÖ¬7Š¥yð¯mé;ÍGÇEÓÞ@¯bî늤¬|‹Ñ|ˆÑœo>Òßg6òu·ö'F6Ç´Gä,ªîGl ö¡rZ©0¾W6 ýÅvë¯^uß’j% u|&¦ á£(BÊn9Üýq„k¾d ǃMPt¤ÇV€êgÄW—wó‰6’î®…v¯js(f«´}% ÃoôWáãaOݰCÂÎÆwËLïZ7xÉŒf|C­¯ÕE3>’<.ÂèQE‡ænƒ}êÂÀs×)kp÷€ŒÂ=ëÈiºÙ!^¡1› ‚£‡‚:‚I[‹íØŒœ•Wd¥–sK§ÖÚZô¹†GA#‚QzãhxQ„¼ž¼^ >+väÕ^Þ™-ªõüMÆ‚~Gâ"ÑìU\]4^¢¤wUêíñÃÖK²Ž`»Ìk·§²Ü;êqÖå§]y&(C[ŽD¾Q^¡î1¯©|è÷™Š&_½A?“€Ùl˜·/Í ã*¥BÛ¼¼LÝyu)‚ô R\ÉT%pFU—×Å–Øq®—QZûB!Ù„jrw¨çX+ÒÂp%gMi®¤ [à Ié-šõÇÅÂãC3„;"U-›S´šR| ¸c8u„Ⱥ-§Ñk,‰ZÍU}kªü"œQ8<µRƒqÁÒ×’ÑÛÿqä8®¨®[Mõz*mŸOб ‚¬FtTGvb#GVÚ*È›ö(ò³£åxY]bxG¼9åË×>æœPd–/ õè”Ý4žŒÑHjL vÒÒ‹xh³­ØBœ®uÇÓò¸T×<˰çá3†ÞëaÖ 7 ¸vVcÝrjƒgÂÈpyóTÛáÄž·HQñ™¸ÉO uæ5Ãz‘SûÄ´ ‡Tˆ$P–¶A’®ÍÏi[ˆmT¬eéð¨ß$º«œWoñ—ãã&Ž+ò@¼Òë’0é¯.î*ÿ¬ˆ‘dz ÜâèâUšÂ"ÕÕ(8d¥«ü6æ.—š©Î©Ô,÷xãäƒ7Uj®¸õ{qõHQJñÜiÿ 6Yš`0ñE÷4TÇs©5Åru úÉ\ŽVBkƒ¥Ò¯xx …¿ 2°xh·<L˜ÆÃÒ›ÏÈ’‘X;:¯Ïd€’ª•í$4ò0r‘˜[TR¦b¾ý¬ ¿Œº•Jz‰¦…Áøœ€l{F«AË¿XÀÊóZ¦„a(/tLK¼ôáZ{Ë`›@7Mœºáówc2[B ©=ig° „,&ãa4LÚùÂ!^'¦¾˜m¥ë-/.%SSAkãê0hÇéY=z$ë$³JáfcßHÎÛÝϧ»ûǶ~ºûøüàÀ‘ÔãþH5º½ Qs.BíÛ5Í' „É’™Ž..Uù°1Ÿ''ù&Ó#:–RÓÓ‰ÙCÈ¥“Šû lÃèS@3/’Àùô.ÍI)@ªïÞ/Ćú@’te’gÚ4#ö“Ućþæ…af4Ÿq€9Ý-w©‡ŠAÝüu—b~~½L Ðü WvY¥ó"‡•ñ|šÏF>EµÇHÇž ÝUûíÌGªM[Çöö³¬CÖÉÁ©¬3Õð(ìm—ü\ä©Ò:á8^šÍ’¼V¬XÌæ%síÕ‹¿HŸ Q8Ÿ¥r¤`›zZ€\9®Ú*®Â ¾ ”ÅѨÿ øæ–a#_Š`*– U‹Gáw ”%À<ŽÚWŸ2ø¸]˜æ¾®Äo%¸¥öÊâíù9d=vá´6®˜:pqÿ £ó  œyjóH+1‰Ë"áÌéòÌÿa#|ìR˜’¨šÍE˜°`u¡ÉV7ÌÜC‘©ö× ðÊ—þ‰_£1]8¹+/¬1]™ÎtÔl¼ïßù7¸5o v‘õA;Û¶g«4<ÛÉ8Bã1YhÐ=“Ð00à°ÜE É”¤Fàź¥Ç¿¨²(‚t¤WQØÕ¥\½†äï¥[½jªÕϡ֋SwŒ×ÇËûË™{ƒg.ØÞû›ýÚçB¸n^ñ€¼‡tâæ atßj”m»ªéçy90»¬i„›Âš"%’±Z‹ 0 tÜVc-~Û.ÑkkÍìÜ'b—Gל¢VáhU©FêaÂ_ºq|!î¯kvˆØ¤tLÆÛ s›ýýÑêÍ«¦l‚ÍchD’CV^ïH«xåÌ ÕQP]ÌR•#OF=Ö|p<ž–³“áŽ)’e[#àLa˜; óaÂqKA3šhÄ9=¥€bbØÕÂÊÛ#™Ä)ÖÎ-hKÞïdOPAaö×Y>íÓ…oüç8ã.´Çm«=èØ³Õ·ªìõv)q=:k.´Hïm+T¯šzA2èΞïqþövxËfZ–a\¶UVMޝ5Ñ0(éÈÍqÆp “¡gØuXŽÊ!{Ð¥Ï6¡(›ÎÏAhŒçd˜J—¨-” d2# +˜‡š˜ÓÓÉjRó*~ ¤-¦3Æ3 %?«™jÓé‰3tŠgFÞrYó°èåst…‹›UM²:ãÜl% 5+AkÇÇ8HÆ(Ý´H:">Øï,€?½@Œ»ä%…Ds€=—žÞüÈ=´}ÇVHš4馟õçh‡­F®¼·¿ ö°0¾YœÚZYö„¬ìlõeuìÁS§ °õ›å‘Øêµ2öö" Ä«•U› l’ÑRAééÅ‹‹ÓK¼8ðM=ÜvGÑW•ÓÓpöáýÁ¥ìDPÛÈÈ…rÎG³rà)pÃ6ûD Gé0mÚ'dñèVìûQHôôìŒ3ÿK5$€eª'XJʈ*6“õÕÂÛ Ü‡‹`Ì‚™†3e'vzU¯ýÜ›:-=4÷ñòRí:Ú½ÁJ@w°'Ðãk¨&­Ñ|©1=Y-W œé´‡ÝEP·(¦æS€ôÉnÀ¤å­îþxÐh·Õô^Ò+_ˆÇ †I0ÿn« 5nçã9F^QvÐÜ—ðMÆÛ~öÆ…®y·fœu4WU¤uáÕŠhn¹–Àô ”ä½²_ph-!‹ªe/¶Tg‰#܆8ÞNæÓɸ¢ƒÚ;? X­Á#>P~puÜÂ`Î1sp® Ëm®  Ú{t¹PÕ¤m·ÆI‹kºôɰ¶Ó•Ñ­´¥<#ü+ZÉBƒód2›¶N‹. Ú¿_§n;dK Çí(-JgáÆœ÷zEÑÇ­IÅ[éâv4PÒ´8ccõŠKâFöp9?MÐ*ŽxwÁ3ÙB‘3µ‹ÐÎ(?4ŒG¤‡8ã<ö¬Í„˜‚£žY’„Â#5 *M‘–ÔñâШž´ÔÐùÈFGœÍ€Sq»zÏlÀ€.°V–¢ù–£MD8¢Òo‘ñÊÊ]¶¯~ca]Nq[·`–¶¯zÐcA¹Ñ.ƒ9 š}w˱Ý:P"©µJ•Ž:bØ­}Py nŸiçʃºªXƒæxF¼È+ù'z†,zCK¼?šwc$Z@P'YK8P1ê@zYögTF¨î­“)ÍãaaÈ$Åw©\L]Ô\{}zè¼ÏÔÕM4#½´à¬I‡9Ûµ‡ 8bnΩMê¶5v‘Îòó*kƒñYÏdmÝÙ5ªéœà“L8…övPCvGôó²³¼U]Iÿ +`œ9ï&Cè M€6¬²î\!þçš“f ±ÚWshò²&:üI¶î;¥9"vV4 yƒeÜ6Ì)9`( SŒ|êÆ7Ũ²H·á]µÕ…u3%‡†±6­Nãy*œvÒ²½ ÔcNRçMovÞ¤ØM±Ôì_òmÙà+¥“FÄFxÎvRõZ'.³¶CFk²|4!¤1~Ë‘y2/Aé Œaõì†Yj™CÍŸÁA ‘ÑÁ.ufçfÕƒ¾gÐÊb™q ªCÁ¦IðnŠï-ºŸ…X8ëÁÄ•ô9É&Þl« Y*-Y¸fä"¥]c- §]Ó‚x­MÕ¶Êžko¥Q fSê.ÔögÔÖjƒ²ÕyXÝHÁwE©ÈzÂE“&gªP¢_Ò”£ƒó™j…UÔµ½Ãš¨„ä%q;HÃ%5fŽ¬Ž ˆ„g˜ŸŸùYÒtÞª…~A„£4¬oÏl‹“ü´„È–( ô‚Ûu%{„m•½ÄB%ÁÏb,Õ‹êl‘˜þðdå’:É ÊÀü6x¤ŠŸLW^"ŽÝ»Ã¶ZÚ²h*8Ž(~ˆEОðMhÄv’DDäO¦"µ6*Š„¯4%ÙqPŒŽg'[ Ȩ5ë@¢D®ƒ_íêÛÁê×JÝkÑÆì§©'c\,åVªù˜YBHV ²cTÙ¦=ñÍò`¥+ UhG-8ÙÅ=©Ç¹ñújÅag®‰6²þÙríÐÊ’Ë—obçaOÎi ð˜9r¥½‚¸çÞ2!‰#œ”‡‹Íf 6šµJžw­ Qf²þv]ým/Sæ”S01ôs:°Î“ëÿ[ôErv˜c;´Üª´é$Ù&A7‰NŠznŤiQ·aŽVv¡c’ÐXJƒ!D– J¿coí-V®PÈíàN†øÑ~¡P@1Ð1Ñ@zßÑg´9þˆ!§Eq"*= ¬ž(ã`F“±"kVXC&µ-k'²÷9/¸íUm™W(ßE6¾Ä×<Ä넟½3>Ý>Ø=ݲivîc¿ÐŃ ;µÂ6·‰AèIS z…a¡Å<¿ ªòÀë7Þ"Õ¯‹Cv®Ï†/cPºŠD·@™¾=—»k¯]¶n…Šc{Ð Q•jcPG¶®õZñ#eÔN´*Å—ãñ‡l>A¬[c$Ž1YÿoÙ6d¦L¦ÈC` M5­v‚Êïz²–:ˆ«nÇU·ãªFÆÁ¾ñpºm^®DŒ+®áüÿÄETŠ5*O…oÓ¹V9ê æý¹R[ƒîyqiF½qov¹šSáPË%–gÔa†>Ý"6i ¶êp†Žoœº•Š%´¥ƒƒ(ɵ<ØÒOqM¼hB[õ]n§ºÜ®ïrÛò|ºËí°Ëm»OaŸlíxyE”êª9•D^xÐZ…q {iÅF/á4v½K®~®\éÏ£˃Â6A!"ÂY³Ç™B•mvj*Ä5ÂËêgf ù¯âN¸lFYºÇzN÷w²'1‰§£XâAjV &åF p„u‰a¥ß^Þø²9o{kø`'Ðé ï(Éš€ §ì°õÐWAæ÷'eôH¿$lSçG vR6ß±¡œHÊbËÆÒšbþ“°aÆ/=b ¹áHz£b#¶ÙNoÏV4ôC7t¾L vø¶‰šµyйÑ?‘ôcäë8r²ú-½ÍGå TpEïÃs4J±¡:šqMË^¨[·ÿvrh!ðF(Û\ 1±8JOЄÆÀ‰D‚$ýkÄ#°@·¦¸ïÍŸ~YåÇSä6ÒPö&½2”¨{Zu(og²j€öR6žåb7¼4,,³ßz„…½ì-„ °–Λ›´êÂâ5„Ãë¨q…:KŸw…>×­lH²ÒíyÐ ùøjNŸFøz–×ñ_l íYn8Û³f’ÇÛœrŒ_¤5zJ‹Vqª6jAv5È`0È Æë×ñ ÞœáH¿‹Œè§[‹´úä˜â±Ä7Õ‹–iº•êi[÷$¬—yú+®³¤óíU;ßV,õYìU÷|=0 ›%T%ßnšk“¥IŸ, 1ôÈHs´É8Ëϯo.b­†¦!ÞF?òsÚÔATѦ·ðÍ®4@#RźÛcë+ÈI7(,RfŠêXRµˆO½–ÍÉB±m2X0L™e„üô‚†öÜj`½Øp ÞèJF0¾ÂçºÆ?Î:x©éOÖLúH·|ÑÛÆl$2” Û†‹¥ \X`͈¤yÂ0ÕpžŒNÕBîÖø> YåmãŒïvú󃯡ùxIoC~ȬìÕ1º¤˜¶7P2ÐÃ@ÍÀ%l¹çÏiOoÕ&g]¥Ýý´¨–Ϊí쮬‰Óêë’žî¨~çàO)ž²¯I Â]&£Dc£{s8ÛàÆÜQ´Íh¢õ{Ì#…a8³½ú}I|—ÒQyî™ÜÀ7¬ïÆjØTÙ¼®rìvDÏË€Ö.¾’‰¹¸”’ ·SÄì©ß¦@.„ÀkEJQ»«Ñ¦Æ°Ûz”P¾ñ~¤Ž~>] !úh>H‡ŽäH˜M›#Šò ñ[3Ÿ}áŒ$t§ñwó»qïõ°ÐæVËòâeãõ—Ø;Ì:Æ‹µ‘½ÆB†÷óÚ2‹ÃǺa§`yqèÇû~¤óïác>DÙô#$qmp ÞäÃÃ~žîœîß;òÉ`ÌwÊ(q«º„=èr¦x´ªq«’]J”5Ø?p€ùš$; C[DUô&7‰7´cJðh5u÷Žë ’Ù&Ô©.i ÌHNVŸFÚ2S)c“²ä«ÆÒµAe§ptø¶¹ÒŒçÚ.þ¤Š?›q]k¥¹(@v†:ÙÂ#VN$ä1(?úÊZ?ì´ÏlÃySg#ÎíF¼>€“\ÒŒœB+ܫћ|Xâuue‡/¹ˆaÀàhã;ï.4,CqÃ.év³bú‘ùuq¤6óLRÊë@ dýõ‘ÄFþI+·Ê²zÛ| L¡;8㿞{‚×ñ Æ~ù}1˜ié¹D$¹ÑHZæ8e(ò|Œ|‡F‹§†^qF³qjú ’k ¢E‘ ¹¦®Õ…äxÓn7Ñ`·k”¼ñè]˜FÛËìÞ²NC«9ö“„Ë<÷oÉ@ݳºZÍ>"¬sËÕÓOSÄ.µÞ‰±ŠèÊao‚ù-#2€ÿ3æ‘Èi­9Öñ·ø81(XÎç ©¦a—ïÑ|Cw¾nëæpS+ZÁú ïÉ;Øâ7¯(”÷V®>†ž5ʆà©LnÚwDÌÄà‘ØµöK@ªCÓ +Þt™ò¡jeh †–$¸œ ³ƒ‚Ò«yƒ4u­RD¥45÷ s¼À :f0¸ŠèãìÉ{݉ÄÍ}ÀG(ëņ›NãV¢¿ŒÚ†Rkµc]f5Êû˜È¤Ògªü¤ûøÂÍnÙ4Œhd õ;æ®N“½ Ì*Á%Œþ’ FR<‘„˜° äqs:m ^Gû&΂é~Ji‡Üxûêg·œª‰!›”¸,£i4 ¯°(6ÑË‹¬.Û†?F7ò™¦Ù -Œ¤%Î=(?ƨl²%E¸ pFÑíÁý"Ù%9é—Ãá|æÒÈé}.ÛÈö?~<;'pè¿Ueà‡þXc‡Í÷J¹-ŸÚl5B”µI8WÏ,oó°M˜z(˜š\kC5®4¥(6 ¸Újsx‚+¬5ïŸãJ›qýÔë|q×jx ˜iÏËê„|Ýœ?|6²Qç°žO”U'@9xa0xœƒÎZ¦Œ‘ÈÉIµK&ªm¨G1”¿B'¹X’dù´õl"òÙ”’‚3ÀR(ù=F¾Y¤´ Ek¼=!¿s6 óPüæ^&ÖÞ¼¯òI«ñZ»ÍÞ b…d”A˜¥‚-š¯löƃA>¡ðCz®ow40¬ªAœS6óŒZ ûv~Ø9qìpFãÈüVUÆÀ#”q ˆ÷¬9ÈŠ\i¦Ë#<øý#ΩZíáÉ€"ÜU­K'k:[þYS7î§*g6Ï[*µÓŽ’…ª-¬&²•o‰Alìsš 4²ï¥¢igŬ—"Èæ¸›1¦·9ø>×ä膧Ÿ òsü¥öŠã·°âM*ß L÷ÀiWfÂ'!!µ_öâ+~HÔÁŽ€“@ÌÐßqö¹0³]G¦•èH@±kÂÝ¡~T$8ÖóB Ó —·cª.1,Â;‰À‰ñl‚0˜v`º²À´ Œé~s‡#tgPL‡õ;(Â>víøÄ•ŠÇñ¡€¡47·Új)K߸QZÎ%mÖ6êw¢š÷üšX˜Q„2Ÿ7ùâÊíDý¸e^TÞóýwë²° •Zá„BûVu;ÖGóUµ7œØ‡ê_Ó(í±„2ÿ—>6·b píR«ôÓ–¿Ò £é-B[êñoo¾'fíkç /›ÑâmÜý„µÞ¤U!ôÅӲߟÆsP  E÷OïÎNw’OŃ‚× Ü®ÇÛ¶ŠîˆÞئì4† =ïxMÕ4¢;pPÿ5n;šŽØï†K3ýT¥æÈ¯vbÉjé]ìÂÇE™zy[[\v’=Æ÷ñ•¡%‹ü¯åÎHœ”¾çËͯÏÌœª]ü¹h…h`o z@z`ož¼úÝ^÷Í¥®{ß2Ç47¿ïßiíðßFëff1ÄVY‰ú](¡[kàUÎK5BýÙBúžãÜ 3ŒxhhÔY•5Ÿ^465.[礼œ€Ä«ŸAš×B?‹€b¢65»Š‰Þyßl‚Úä}Ëül=n¾üxÿýÙæÁø¾/Ú”îÞ«g‹[:0ÿIaddÅîJç{OÞ}ÿf¯ûêÉw±B‡¶góàq«¹ÿŸï«÷Í÷­‡Öß7Þoî¾ßÐ6ƒS½½ÙÛ{õîÍ“WOkšÚ|¤ ün¯f RþÉÛ·/~÷*]~W•{úÇï¾{’.ÖVž}bfýúÍÞó¹ò‚ÀϦ,ÈdŽ"'9I=ó0Ê]„VbÈx13$hñìEr)áõïæ ý»û÷ÍG­¿Ãzܪþþþ …¬ÎQ(˜“Ù³Y15"e„ö•« ‚ª\ó¦ÕòNL½áBßî=OÅÇ·m+œ«á/j<°“d¼>G:8 zEÑÐùP†›D¨“qRiKJ}dBmô Å6¼pÀ6¹›aˆWÞ#vQ ù<óÒ×q:©T{åºCÆ»äleß ÝxÃAQÃü³Æ°<#íÐB€’Ãá#\?Éÿ*·¦Q¿ªUš+ðhM¦{h³øÀñg…²_M“ øœFÇñÓ†‰$8ŠN>krs*l³*yѱp›Ë«k¤%{"=Òp3ldO)ЬÁ¨8‹×ÔSsü‚$—lÅŸê$téµQœå|ÕBgÓ—5FvõøÙléàÇoм'9„èÛ먻n³m-¢n+\@ œ~›)]ÐââVáËpz^­Èr?IŽBñ7Ø…¾ ¸‘!dKê®=@³ÞxÚÏÊ™g›DíÈEG€)ŽÜ †³VEÁFQÀ øìÍœú"ü­ÀŸUµ½ï*© ž™@œÔ®•Ÿ•V$ Ñù´vZGtPFFe·§)JøÊbpbr)¢í)Â~çà*CFs³Ï?dùÔC5”˜.®8 ä4å|ʧ&yMEµÞ©ÛÛ­Ôݡ޲J¿²‘} í’beI'–DpG÷WéH÷„ÆyÊU·²©ÄY"æ‰(І¥%“âŽ9p0$í Æ•¸; ¦/–¬@0©úÂØi¾t;MühÔoˆ8ÉO É"3ÌÖ7eM 5ÔZ_0~OvYuøñkHuXLúvRв.CJ¡Î’cD>xi!mçî¸(géXHßqÁ¢!Q$™¶"ð„b³¥¤>h¼z<ÑÁíUüp£ÓÌK¬dHº´$×Z:Ü«çøZK§…MÔPT719ø~UoÖ¶Ù£þÃ[çtA:O„n ÆãI'Äéwr " í‚]]¯{âb˜æ¤Ø[°#E¿ -a©"BCì«÷–óWî\x«ü,´²}ŽûÕ£þŽÄ[|¾ÓÑrŒ¦sâ–«par“šk´0’†ÖB¶X3‡ÛÁö¡.O1®—?ÆHeÈCµ5¨Å ôÃÍÆãF¹uãâ®Ð´ƒ¨+Ì”¥ q€¦—\-`Þq¸Ù}L "?ß`ŸÙß@µŠGÄ‹ž:Ùj¬š¬ñ,G=³xI‰ËQžVÛ N¤¾9Æd#Ûƒ z‚0'œX‚š@X`§Š‡S½Ò˜ 6ž%kâ•b‚×wlÄ+÷lÓ{¶"ßô=¹çTÎE•8'ã8a¾Ð9Ç L^ùé9È¥êÏž7»êèv‚þ®˜yº+Š9Û¼‰'ª(oàÄðP×/™Or®t´èu¹u†9û?Ø;ÒHÙ`úÝ¿w€÷Ì·¨·ÃG›[ò,^0«Ë¡°ê„¿M³œi9ˆ ô>ŒÀ *˜F‚ˆ×ÁAûp,Q¾t£D“Iˆ¨üX«ªxu— ëÛñxPä# Õ¿Ž4cN¨u$ëiô²œ5®ÉÆŽÝÛ‘j…*›ª ÚL&¥Ò)C‚R34-íD£:•PØé¥üHÏa1݀ϊ|~<¦+IK<¦Š9ù\[’‚ϪÒçŠUzxW’ªàsˆ Â%ĪHªÂØ_˜5¸¹Î'×z+Ú›WB;{Íw”ûd4[UƃO,ç‘/ÙD½ ò¤bÇ0§æ ÂË„NT6-”Â'Æ eMá¡D°nogc¾ÖIÁÂÂJà[Ÿ§‚Ï/"迆êÄOÇ¿)³†{«±€ÊaúŒ¥H7糨…ÍoA†ì99øY‰Ó¶„6'ý>f»ô;ÓW‹† õéÚÒkÐû/­XFxKëÝ%(‹ÇCy¶^±SlÌL’Š:69-OÚÍè¦Móë’eê2€µl=°k?* ÆwÚè•ïüL7S¾E€Ô% +zXôuŸ–ºÞX³ìü„ú^ÄXOø-¡½uŽÙ‹ á00ËÐ*>míÌ•QàKŒ“®â%·ºçT »I€7€ÇÅÇIˮ¼ü~¯ûû'¯ž½Ü{ó6} Ju®¯çλ`ºaš“×4~Ÿ‹ÖÇÒU^u캵€ô!ÔjH_Ý D ŽlŸhl©Í]W¬3Oš÷Z­;ÆÖü#ò½Pà½JÿCÁ‚› œŸ5ÛˆlÞ t#ôO†•Ý`å\Êü׸mGf~þg²®‹þO‘dõ+ÐÆߘ‡ì5ÍÞÕ 4KM  *=ðü0@=Ÿùöž•âÈÆÐYP\I#€™çb4Äí¡j5/»QR £µ&V•vÈа›Ìx¡ùa¡"ü•¸9Lă#ù==¨¤¬º p‡¤~bgbù#{ƒÛ^ºóv²N§4Vº'™½q}’æ±[m6/röVvðÊì*a‹Dò†é€\Q¨d<í£}¸4&ÑÓÇB½_’ú” Ú-î6¬{§]ÔÚY—´ÚQÓ|jËûf„÷óÍŸlþG÷€¿ÜÛüM÷àv£•h¢¯%ZYߟðxúæàqs¿ñ~ýÀö‰Úåh–úâ¦;<¦«\aÐùd’nãaÓ¼j%›:¸ –”íÁjýÙa ´á@6‚)ÍPöÿó‘©úÈ›ÁÆ ÿ±œó‰|Tï$ç*0‚>‚†wš•àhO@†É(ºO¦E¯è“V»±ÙˆÀ²p4Û[ÍǵI#Êâ…)jÖò"¹¸¿j°éÕ8ù Û—©ñ¶ùµ´ÍõjŒEúëv>Ì>t0ÕaªŽmÕgZ‚^íf¹n¯è®ô=èÊm åÏ ¢fÇ 8 8yp_Ž "™ÍˆéûDØØÛª¥Cí¾ý÷ûêí; Š b;å´•ø×ü@=dcÿ\†‹)åÊFÛU0­÷ÕhÁPSx§FNqs¡%¸—ÃöÂ…·äêÊ#4"çwy ž!v É»÷NQô{/67NíkDh Ž˜&Ë»BhßO.’›†"/WžHdY=€á8¦V€M ÝWv6 dd‹lÚ~|‹:!¿ž´½ƒ¤± ²å d®HÀi`ƒEU™!%,8+ÊúIı+tý‡¥¢$ÝïgV£E K~¢¨véðá^-n‚ê_cb— ƒP¡µÁTCí€Ì*3[0Ô^uÑy dü$cø‘EùÊù- WÚ{X…ür° i5WoBþ!EÕË'ES m©°¼¶‘.²ê|4Ë?Æ8"¥Þ&üaî>6ÿä×3âËbëáŽC“ÄÐÛÅNq#Bƒo’¯¼Ó€â<î;ÛÖÊ,\øä.LÞü ÇSdʈÚG’OBãšíJµ“P5Ù<`ÛµC øCD>^ô Ã…õ‚Ù#hIðq–š·8Œ„¾s+¨§‚f@9ëÌÖâk]P}Ãué`<þ€Ž&3Ö ãÚ,]Œfûïïïü½ùx×–è¶úÒÄw°‘R÷°uуÖíÌÁ4‚ †¬ã8VQéË1~C6.²gÅp|£É^ع«‚‘4*Ìw³ ެpÙ¸ ©¢)´ 8J9‚0‘. ´£¢˜™¿„¦Ý®Uª^ÝünEº5ÓoPh;.Äî:¶ÁëßleE¡ùè<„„[û°ŽœÏ…ÝævëNãÀ‰tnw A²mÜöû]³»v•þ¶õ·µ¨¿mÝêy¥BÛU½?–ב{§Bé¿hi³;¶gÔ’fPj­…Åï©÷v4LÖPï\‰(ÀOó*Û͢ʷ·ïÜOôó÷Ìÿü½ÑéAL”i“Ûj%*ÝÙ„Ï÷¯^<ÿwüvg•J+µü§¸–²YäÐ ¹ìÔՖ䂯t(0Á`—óÏ¿!÷{nBò½½g­ô(}Ô³ù(9ø.·1HVM~µ0{+l=NR¨wŸwÜÛÉ;£³æuÐR¡ëe—y†u¦Áb"9 šä@i¢QòÏ{ˆ´Û7$±I ¸CÌN¨ 4}Ô 5:¯hy~¿÷ò5fR¤½·µy«¿“½5½õf|jzšŒqD5Žw?ìdÿל3žÏv²wx±eF€¹qGpNŒ(öð`'ƒp0:•š"’Xäñ2Žø]F¥0P¡»1Ì•TÂnèÔi$SŽå€é¡X‹,&™úšRÓf¼m­>`Ìc;†cè9…yµgA'Ëþ|RÐ 4]™'Ý1L´)9…ûm¸øx¿ï’|ðˆ° Ó]Ù“Hf0ˆ~9)ÁÆq:‚V{|=ÂBÊJ"dÒ `™’æÃÃbªæéÍzuðÂ4ÍQ°Ç¤f9(Î¥VòrXÌΠߌKžd3Ë\ -iŠb#g m7a¹üC§MÆá§×ë¶oÀeq ÏT’S. Œú¶ðãõއŒ ¯A‚ÍÖ÷ù×q˜lýÐét¨ Ár#ÖÌ퀽좦’–DíÇ¡%2?ürcSìñLj½Luð‡*p}i©€ùþÚkšžñ÷?©ï¯_× I&ðø6N=ö«z¢Ÿ& <~K¯Õo¥¿µKhèˆhoqýš¥NX× Vx¿çÿgï]Û¸‘„Ñý­§è‘ÖËfLÑ’|ÉŒ&ÊD–ŒgmYÇv²³Ÿ¬á´È–Ô1Éfؤ.±õ=ÖyóbuP¸t“’•¹†3ŠÉn  …ªB¡ÊvÏ¢›”LÂ_‰¿À§G8p f(xóx¢8RÀ´“Æ«Ý?÷öþøúÅÞs°º>–;*̃œbØ6EñèV@µÛ­2`=„ެõ2QCm”ÒÂ82ˆ–$lSÆ@z­/¾|\ûvOì6!gœqÛÒ õ… T¸ç`O>D+¶V åæñì¬S ¨œ¦­oá¶ÕÙPªuÚz›+~:hu6ÛÞñ=Q®U½W# cíE?°Ñù©3ë ;ho×§o¼GÝ ÉAžêèOÔ RÍv¢›Bô‚Œ©xÂuI­Ÿ€Cþ?ð yÿ疉нDep=k½‹Ý¯±Ý/± ç9Õ éc»nßqb͵Œ‡àâÎB°ÚÖá?ÑAn?P޼—ÎYµ“Ö†ÐÐ˦†\ºwaAKjWèã*l°-B©Ë$iwiƒ pt¸y­2qøa¾Æ'—¦îi6H@TŰP$jÄ×@èÖ >Ô0Ͼ‡ëZf$G]W”ž@àcR6›xœ³ˆðD@v)}.îî‘ãQkâ_ æ_ÁŒõW.ù×íäX-äNù¸»°„¯-uiÀTL6zaÁ3šä;HC‘Ó¤Æþ=^ˆø _þ¤”¢®Ñ1 ¥™™­âJŸ6ÿ ®Ï¡CUŽ™œ—kŽjh7û5ŽANèåà²l”ò¤õp:hý*I'ÿì’´/D;4ç’U_„«'úÈKLŠ[»‰·ö9òÛZLx[s¹ÀÚ"±Mˆ¤qЛõl‰ l5ó-â«ïÇ_„Ÿ÷c„_m¿ß«Þáç–üiø'>+»°DkCý­fe¥À|k¨íü½Þ(+ O¸Žõ«x„âi½ô2\¶ìTµêØ0ú;˜jà;6†ù}æÇÕ|”Wð]çr¯Ü“ÌÖ·æš[KÆ¿‡ß2—¾ïs”ˆþ÷àyŒŒ[ÜRêׯ—ø?ýÙüòËÍ'OmøŸMþgóáãG[nna‘Gééã­‡vÌF“à7³ŽîÐÃa>ÅŸN§Ùh”M»“«ÿXKÖ¿XW<l_ÛÉ|v²þ[x²²–ìà ²aòR ižæÉ»²~(fÛuGMßÂ…ÝïVµŸ÷ÊÉÕ´8=Sñ^;ÙR=YßR]Mö_¾ûïä`ZB\fUlw>;+§ÛÉÛY–§Åt|UÓ¯ª6óQ><îæƒy7›mÀ’äù<0’—Õ&ù* áË7UžUݹÚéÆPE–ÿS¦ØÇT !_ý?ºcøñÍ©¢š!=ÉÒ*1yùã||:üÿþß“ä« <èñAyòÍYŽéÆyvÒ­r¨ùý›—ÛÉWg³ÙdûÁƒ‹‹‹.ú(•ÓÓðö[ÔEûïOGøÌF/ÕâÞû¼ûîÏïâV`£zŠ¡GÙ,Kt|ö þÉÖ>05öó'€yžE¸°»òçü󚣀b°t ?S…Éšf`¹7´1lí [»xöwåû; ¸!4Éáí,ùóGÜ9ñ͵vd^Á¾¥Ó 2P]Ùr×àûbŒä®õ ::ó/X -h¾B2¾J&EÞç˜ÿX†3È'¥ }&h ”Csç Èd "äš{æØÉ¢›w©Q¬Ñª4n¡§{¢‹+Íò§§¥¡ÃöþŒ"¹jåVõE À!Œ›ÔuAÑÔy^Ñèìà±Á‹r:X™•ýÞÄí—㬌¡ΦY¥–*$¸<-§ÆÍÁÛ±‚ ´×+Psïã×Ý•pG~ù‘Çkø;M95\D·T½†¦êȽ¢÷â ÊqÖPT¬®FÇå#ÏrnêÉ´Ì)tq{öç²°¤fœÈ²œ‰á#yra‹k~‰Ûjg…È\´ åí,/âw5³TÔ¦†¹啪M/Ŭ÷Ï ô®"ߨΊI¥écéÖÇiÖKÖu‡ælôÜìχÙt1ÈöuŽú\A Ós„QbØ î}üêí×á«ýõŸ¾¾:1F'ºý•°ý1Q,!RAÂÅ ‰ŒBI8œÜæ¼ã{L!žŸî ¢É{ãÐê0?™¡k$ak+‹Zs[Ò_2Èœ0¶ŠëüJ¾‹_VØ/FlŸ‚LÃI‡™WRá¬ßǛܜýc„$Õ,j>WB4 ü?YÝ]mëØbªóc`p`W@Ыs„wð,Òß3.¦`0Ê14tWÀ¯¹WdèpÎL3íÍ´¤Áài¦Wž÷ \yúQíÃ×Ú™– ¯5ê‹ÚRÌò&yS̨É$Ñ$£<Íjá÷• ¢) +„ÏôÉån¢QßÜgÝ" šËu£ä d¨V"zõ‘$âŠÉY ,ÃÒÞ´Ñ]Ê9Òz9Ù Œ2ñ÷!.õtùî..³2ã€ÇU,)h l=aËz&ùð+ÜZ˜î/Îà½Ë¦ì¹ÄÇáYÅ ÅkåhŒ»dÓ3©³>G©õ+ðI4“e‡›º3 šöz¦CFª„JÝ#aNC  †¿e|UÈc’ÔyD/sPLކ)CïãÃO©Ù6+†ø©S¬ù£‘’RF¦S¦ )Y¥„ôür’!·ºÖÈWÔAÕ¡¶A8¦CÕV«|eˆ{Bû; @¨ïc¸g[ŠœÔuµþÅ»9P¿G?ð^;Ï{ਇWz ÌqÑSkxrÆ-+,gÇŰ˜]éš/F£9^~;Ðï Åü«âòŘêÀiˆ6|R‘SÇiíé{:‰ÈÊsžt+„AÕÀã´Ch]âcEÌ놚…šE%jP<Ü  àâd)òà vþ{ö¾`çc¬å8ÕRû˜sùþ£e§WMˆÅˆ9‚Ðáæ"‚QÂ<‰ñ R aZí¿E÷AmOuÔ\ÖgÈSÂè4vƒÜû¸º°z vhõõõµ-}Hk’SÆ%;I êQ”¡ «åà×ÁHKóî©Òñô8¿„žaŽ@­ép¤“·èš"uÇ¥kÃÇ òê0ÌKçÞ9SÌIä8”ÇùT5>(@R^LÅ XלÏí›*Ï·ã ó^=G©ªG`·“ÔÄPø¦8WÛ£yá‘‚+sáÞVX£;,-QÍ(n^œÅIÛEªÏÌÃÌ&C‡%gé™Óy$Pמ}*BLà;Î0plÜ #Nm;þ¨9@‘÷Ž.ì[¡ÞQ>cB¡uó ~"9Ù„É?°ÌÈƒÑ 0€c¹µ—¨x$‚òŸjéÅ»;Iƒ#Œ9Ô±qyÍJ“I-3®]Û¨Á&eàŒ@ž˜Z1uÏBZœ×È{O3ëIË%M«>÷>S°²ë(êÒ*¤©K2&¯6SFò>z: ãów»)…ßߨÚ)æ~<ŸEomQD´3šæq~‡Ó À;œjŒ÷Ý4ݨl7Ív´Cw3ãúš¬jñ·³Cø[6K;PL›"‘S{ÖñHÆ›õ8õÍ¥¯[®µ87}”$vk’³èéñ˜TÓ Žh7¾Œ‹H×Iuã¡fZìÂæÔZ" LëÞ4VOàîWÿŠžZô ŠsCùJÙZŒ#–! 7!½Ó®ä½¯>î^?xõñéõ5¸ð¶}x­u]!ük¡A“o6”W a]Õõp>^Æ’ÎVtVé½ÛŽÆ`@äüˆJërµ“•FÀý@é Û©éJ„OIíIMûƒØÌãp5›i“ˆèAÅâLåé]ß±µˆÉœí)lncOlÆ)f4L?s|Ö}‹h_K˜p#œJK`þcq\Ê‹:/•m{¥å@ÝG´0 òa1*Ø’Àn̰ˆù )QbWñ¦1wi rÖã^bP¹/FRÊE 0Ïss×lLǨ`Meþ)a @\2IÛ÷Wɰèô5ô:BVz²»ûzÅØ"¾ôpÈ;f~ØÁ¸Ó×h‹:WQ%½šü„ä8RÙÖÒQ˜˜XÖ¶–ñÔ’ðnÄÔoˆºæPÇR²Œ’I0+¹Õº1$//!<¾˜ÏJ8|&L?S6tµØ(’ì¶ù ë*_ñ]W(Á#5ØwÝú ±õóuŸª'†™ªµ0òVyD„ˆIù(&”û”’”KG:އݺ–;G+wcz§—Ê]Ø¢YŸ)»ÑÑe>…£"€Q%z‘…Ik£ˆ:" /g ãÄ’[šXZmúo1Ö$r'sA«Þ­3 žG˜‘ãÛQ†Ç½pL4tX2»4ÐÙãº8Ä$”¾•\%4s¥lÃ^TƒÈZ/d’ÍÃy/Y’ÖÑ8)ÎYÉ}I›Ì- cÇðÉ¡cŽ1m«dË‚ZH}²¿fa‰ÈÙVºm€Ü½ø†ØøJ˜P ‘öµíkGiVíbÚÙ‘}˜êͺbÊJ8–sœ,V婱HÎÁ#qõú†ºgè΢q}llÀÖ^lÆjùÏ›3–ê J (Íí}Äx¸§žµà¤f=:†C#qz¤V×$žuØmDZù©Þ7 ì¾ÕÏ1{K e p` ZêP-šX4ø…m ¹*–Á¼W ›S…(tª7ÿ¾ AÏo¡#‘Ú%ã*ƒ¸Ó©¿Õ‚Ÿ-:ZJâv‡:gUõ_ïùŸ#ýAÿýjÒ¨™H[YÆøQo–ÒŒ#œ­<Ñ:q|ÊšU°úQg^ÀÛé º¥¬Å}ü\šr0õÐë óñ’Âóøt¦] ý!Ålœa½¹˜°ÅP€ÛA’(ùxg9ƒ·—œƒÏ¡9©÷sjLw]W»OzèI½cÒ/Æ6\³Ö‘ lUþÄÀیÃ<=qœ›}0AßÃÛŒ–=öÚÅü­­Vçùôî¨õŠÉ3Á4®‡f3EàmÝ›&ë_ Ë0»<`vòáÌ™—±˜û NÂ*UºhM&vî-LjòúÅí±Ð4£Å5Ô‘¾f{~Ó3š™4Þ»>yÑCš2\dO]çn8wvƒ2Ç$ð«¾ìÔ)ël[æ#ÐÕpÖá[ó Ø¥íùáháÐMð)qcPXþ㛫',B:rÀíÅvþJÆ-Ýzõœ5›Â»-%°zf¥l¡¨Ù¯²60åo¡·!Œ3ˆÏ…~|”ë}>FX>0V9Ì2%¿…Q9 <˨btÞü2L3ñøC C·åŸï§xßú<ªÁ!%€ x)Oõÿä°ÉåƒèDwš<˜*˜È"Qï@­9Øûö;…¤°ìí‘Wùü*I“ÿù>;àzˆô¡½­z,VU ™ {¥È[Eù`B›ü9á#„=‡+Ž`æ™ÑIƒˆy"èb© %ðÏ)ú‹±sdÊ:qC¿¹èÀY†T#Fõ 8+Ë«é³*µøý ú„bi³Â\ €º^öûsÊ”Už G ®…Ã& ¦ž .Ò«Øá2ðo*@16Ý—Ó¦—åºþ5Û b$^ƒÙòãM¿tnŒ¸Á–É 3Ö“fmÆ(¼æœ Þ`#J[àP};rƒeוҡ_ ‘”èз¨šµ–`|,>lÿaÒJeà Wý8Ü'TÌö´~ØvP¦Ð!ˆŒ½¶h=âqsÖ¬@œ8ªRT8î€Üí=b„DƒÄGÕJ»Ä4bí P\¯öši+\¬Re*²d^?¬ÚA¦Ónw³Ä`lËÕî­å5ÈÉ\;¨+ºo1Çè‚Cóì®·t‹Ñ(n“.£ýYšªGÔ}ˆKsÄ:ýLPž­ä³Ähh† Á/ 9à+‚­¤Þq,Äx=>Ø<ÞWã5‡G2MÎÚ5 Ð;Y²Ýˆ\$„ªÏe»ÓuížiÃkkéRýW¿ þ󜄼`ƒ0ä §’ýÚ×”‘íÈëré°éòxZK´Õ|”޲ „èê,‡ú.Ý7BU&Þ€¦Øea“¡1LÈÈ_×èëãÒøû%e6ú‰b«ÇÅiÇœ·œu4‘Â2Sk#›^Q_P–þ·ëƒ’[±ŽóDq DFÍuÀfCè]X0¿‰Ë)éB¬§ Á2Ì®„ `É®«¶Þ)l¿w@·‚‹†eÅH@Tà Wå°ßO QMKˆhò.ÁCÍ=Z7åÙYöR‘ £<‰+·¾3Çè|]3á‹ÝÖ–Ò’£á‰Õûåj†u,Ë»|..½/n!.‰ [Äb^—¯ÒC t‘ŒO3÷?|_jpqäQw¨Aß„Žp¬K+w‘VzeçW¯‹„:ÐujñUR±=YÎìÍ X‚ZoÃŽ´A'Ô‰À¾Cæ¤'d Õ?ËûzýR¡,;Õé7É®Ywl5€á]F©¿LçCˆN °¤ÎÀÂ5µ›Ó ™Ðt0f‘„v|6¦° ú Ž'p+ 5Ž¡QQax¶äPUÔfXÔ4Èx[,ÍÅ ¬€QU³}$™ 7â®VÛ2 ¦œ^úvóE‡lŽÐ.è¥Ò tìlPætî@ˆ¦S1"õE Åh'N•Ü›v!L³iäÃËË#jcF·ZÁt£FM^GŠ÷àæÓ¥\b S.VÌþ€O¾±‘­ÒÕï!j\pDÞu¡cRéµ^‚´ 謹úìAÓÜbªvi4 ¨ã…‘7@ÑEaûàæJ•ãrN¸_i ÚèQ‚¾Ã{h’mš9¨=ll0LóukÃ+O˜ÁC½´j{öĸ3Hæ•1šÕŸ·Z7zŠ›žNºÂµž]õLuœHw„ô{÷ ,“ôY´“ßì$›A»ŠÇ÷(Ç¢ú–rI·• Jv©«d—ËUPês0ÈöÔþp#D-™×Úꎪ+/¼è Å`Ü;½ð°:®x0.Þm{A?–»D"»¢QF\:³ )µô²ÚÑWmxxÄ7Wà[¹$²W9ð–Åí„è„9Æž¤MCñ¾I7Ê3íÔCï¸*MERÌ6“n·›<'éøëÈ›¤æ߬V«‘Ö dèvâPEr…Oc*¦ÏÍÅ~‡Š 0¡· X‡Â!ÐÍÒÈûãrv¶ÜÛî¸sÌ«ªqv#Γê  (n_çÜØ;î‚×/ZºŸ×¯a9>½}·¨±%ö£eÄÌLîX'%w|Å–Å œ9V j Kü¶\B­ŽQ w™8ö¢6f–iÎÖ>Ÿ²VUZ¶7sä¯v’­`@ht9æ½þY9ª>\)TOGjƒ¦]ðvclXêÉ^‡¿Õ¬òºQH"h³w[xóËyaç*äõ7í®ÙÂ=‘†¨ºÏlT]ò6ÕN¹¡Ò…­³.(,:¬`ÕAI2hÌ|š×7øs¼ö£µÁÚ¼‘ÿ4/γaN®”`<„Ù,l+úþÈËÑp6×ZñYÖýØxc­Û¸cÅ¢ZÞÿX¢k)¿àe|£X¯qþë@£á—õþO•ôïçì]˜ñµúÅd· òÜ''jc†3}äPp÷ª'#Þ&È5É u Cª@Ài}Iß?”×5 :©§†ìFùì¬$=¥:÷-½â¤§óäF8¦ÿ«»Ãˆ(VŒ§là§ú¯é*»äEZÝÿ=Üh—¢”E[Ã~õúÕWèW_¡_}…îÈW¨¡1’Ã/æG´ å£†QL þDçîÜÿèÆ(H¦É7i™Ý¹ßÒ_ýw–ôQ½ç¤dYà.>åÜ“PíÅGbîxè]Œ1×G1̧6œm½AYß~9{aóÅ1Ü/gN¹«|¶úK¹/ŸùÛ9/üƒ‹ÓO„õýc>„Ô4'ó1ŒÀ±‰WÆ4«k_µÚûP¯;b{Æ@£Jrªf:ªÌEA¾ý.<¬Èžkuû_@® =E7ð<ž–€zŠ;$DÅC‡Píh _(ì™s,¤šKÝg„E›à‡”xk ºVäUq:¦8ÇVé%Vƒ–Ì~FhŽ\)Ì@ªN5+'ŒAk! B=yýúðèšÀç9{41Ùf´Šq[UVWpw TZ ó³Co½w2´Õ^ÞzLÈÂ^,I<ò'¬ƒiƒ(ÐB»ñ·MBl„? :»tš?2Ñ‚aH"¼þÔ÷-q¹‰mîšÝê3‡f¼è‰I$“‘" ã|v‘c´õIV ¯amØ"‡,nhl G,,m74¥ýcX½Ô»n§ƒ 2â`> ÃÕ mÔŠ¼àoaô[_1[*D„ޚߢA[ª­˜*¥ÃÆÅ÷ÑŸ=`Þ1_ù—ókM ©,ÃYpÂ2‘VÀ©çîŒËïu#žÙu4FÍ:ü•œ~%§ÛÓþÙPÀÓübZÌf`ÙÒ,¤aV@òE¡¢¨´Ö½æp¹^~™õgÃ+Ÿ<³r‡×n‚Ñ5‹}µZ`à¹n¡íÖjÆk¤0|ÔØ73Ëq;dsuå·¸¼îF´œêÆ{ž[5­Ç¦p-˜BÁ~á¬5AÍ ð  > Ã0Þî(ú³Î•—9U–uJ®wÕµѱ®w‚O-¹¾…ŽU˜`d !ÖýÒ,vʦ­ØJvZzIJu8%÷CóÞh* þùUîúu£üä®_˜É6­¿[ò[2÷‰÷ÚBÏquo·œqÉ c6•©of°íä"£ò‡øÏ³£Ð±éýøâ^ËŠ¡à¥*Û8°˜Ú¦ïUÛïgé½ ]¢R]¹#ªê‡Â õ÷‰!¾ÅÅ6IXŸ½ ù‘~oà55‰Eô c*jȉoSA0ÍNºn—Âh ´ØääXrˆ0î°Ü­”FL¼Ø¾3Gô…«r¿«é¬õ>nPÒÇ£Wþ‘îºÍ†4&TäjWg’ÍÏŽ©cF"˜¦¸‘X€¶Ïço_¼|½LòdXflý¦‰mÌ;˜@hvB ‡®N \'9ØÚ)Ê Ü%¢ÃÝÌž…XŠ{ס'‚"IÓÜ?Ž7º›µÒÕ?jÌ8iúòÑ?YÌøIý7þü¶NËFeµËÉ]HC”x®‰.é%ãðú±pS kÚQmo2é¿d¸ÎÛ¸Ö,çü÷Uö.Oí}oFx2ʱp‘XQ®¬T+%aíC!Ôš0„)¾¢;ÝÞëN²ÑNîG%ÛÈGÔžphgÙQ ’1is71ŒUŠÒtsÝÛlºL€íä«d’|íVºy¿®Vì†bàãr túÞT¯ÍOáj+z"ÞMècÕlw-z )—òª£$°Ÿ)B'OK%H)’=N¾:Ö_¿éWÃy·<-ºù`þ5'‹„Nå½Iÿä4­õ|Ö Gl… -£!gÏbì’ùõ»XFAÌú‚]b; gÝmªûÉŸÈ~9Ït½¶.t>í(Ÿ’ݶ"ôu÷ÃU)äÅxz>¾Š&9š“í}Ñvsú6ì=‹ö;Ûsnzðd -2Ý% ’®&ç£c²(€›VË9H™`M˪ôË?¾]g˜øOÕtnsharåš(Çv•S/è*=4ÜNò¹Ä&·™ØªGüh‡šª~ÁUˆ <ú0’VdbqRÎÎB¾ ËcEÌTwè¨<0ãQyž=ö~-#÷Ö¬|H,î;ãhœõŽš†ªø@EL wüžbQBÝK1A]n[çÒ¼#¾Œœ {Ùh©9¿)”ƃl:ÐQaz$‰´½N.êYô®¸ À¼¡@y#BsêoöZÅ{l¬q¢¢Ž™s*°Ð>hÒ›³÷‡Ó£Ûá㠸Ņã8‹ä+ˆÝÓÓ{àhÜþdOn=Û¸¼lPm?o¶;®©€ãÆz=_Ô]×Úð2еÿ ©¡Þdlàu>þ–c\12úÖ¹xå“ÑIŒŒ”2Jnš—!+÷‚×gp¯m¿·¦µ§ü‡Jlës~ä‡õ# ³Èëx~e;Òcðz Ð&xØ®’”/Jl'o_î¾ýcõÚ²¾Ä ®-üF¡n³Ññ [Ï/Á‚Y!Qשmé%@<@0Z³Ñ}uPÇ+·ß´RtY.Ä0A•'Ô™OYƒ XȼQz^€+SÝ@ˆû~'±ŽŒÝL'‰Þ*\ž ¨CÔòuš\Ï=¬€‚æö™T»ç:èSÚzß’•Þ·»ï[îØÔc´=Ög+ÆLÅ@‚"mÌZ,þ/Ï*Ý»˜%-ÒO‡çy·|£,/ˆ„׉ãhËxG‚©(ØbKJBeÙŽÃYOóq>UÛ“«±²ÒÛ}óæõÿôÞ>,€û6Kò&–^½¸€,Åx.VKw¤X&"®xj-AOJ Ac[ orQE2Ð`-w <:õÊDózû¡˜$&ØU6–d¤j’èâ&YY¬¢Ñvh€i=¿œäý#¨œ œº5êÂMCÓ ;œè>צ¦´².g>P ˜*…ÏùŽÉÅ™šslæ+;™*¬±Âåp´.9cv|×]!úëy—»uÞÈá®o‚:KÚá¨{:-ç“t³}¸¹½¾y»£êUý:ÙìF’G„" Ò\sïdQNwü´ø` ¹&)àHhFM¯ê@+¹·Fô¾ÓvÌ©oIÂX_O²ÁÀœ•ÙI¢-CÍÇ¡BîH(«ï[ïWW½°z0•‚­ÄiW µ–†¿×ÐqÓÑLôcÑ×g›ç0Bþ4öò)“+æ¡0@F'8q¡6j±°³“´>µ"(pYb3œ™Ñ#Q«,:ZýÞ¿‹T;¶Ý1]î#ÁQ Nãò¯Xp±Å¼lÁ”pEî“¿xoý˜ÑÇ&EcŽXAíP&B+¹)lÊ)zý¹˜¤ÜÁŽ‹t¾~å"Aw(ÒŠ¿6¥«{ òhEnjž¸ró-Mª´ìðÏ=[×'áxsDßè/õžæúéß±òŸ… ˆ;lêõå%i‹âPú>/fú@}'= 2¦¼c$3¥}}˜žÎñˆÙ ú£ÃñB5Ñ€WW‰ÛJ1é:ˆq £…1*Bðu y%JëöòÌ-Î;4Ýj¢ÈP ×ãV;B‡NY!'Ëœ)®ÄÌAÀ8WŒçy €«Q˘|áÉx®7¾R¿ ÙFŽ+»òºª*»Àîãã.LÇÄMdÇÏUÿ*8ýL[k-¼ËÏwvZ­m)¨¥¸×yÿ¾å4ùcA ¾;åNÜoy[iÐN >à3›^hØÄñø{­p—&#µ+ ¬t§6·xBÉ–° 0a*#xÄW¤ 3×5\Ú³{´"°ZUé7äx Eküëàî¿OÁMO¯,¼Ò4gúÝ ­ÏE‡^«.âE~9^Îé°wPT?27©t`™7|T”kãþέ”qua1Å5\ùW’ z¯€ã ŠÚH~÷ Æoõ~Œ.pÍâ[Ê«õþ&­WÕdÛn§ @EOnüŽ(QsßñÝQœ`>ü¦%!ѹ𡍥–™Hm7®W÷Þ¾ÛݶûæYoÿõ>…v¨ÔÌÃ÷Žà?ùêëõ£/¢æ ç:ŠYGõž€Âȶ՞"m³&+Ü@•’ÛT‡p·´eîóÎãö‘‹5aÝ¢k»£å¸»>Mn€wy°Ô;Ø}óöyïÙwí¿Õú ˜0‚Å<Ì3LŠ¡4ÆY^M²~¾‘iëð/­£û­¶·–À°†ùúOóƒÂ{i@#Pþ°}¸¾stÿk *É‹«¦Áõ0€DÍG›SlÍA9W|a ;¯)a1°O ãBsqXsù5ÉY¨ÏZ`> Õm÷ ¤Ës…XºK̽ú|ñŸŠ”:òÉZòs>-A¶•)´œH÷½èDZZ½=xùâ]ѵµ>±|âÉûÄxÿôþS[rÌXÖwÛÿšE¥³bX%¥ÂP»Cñ dŸø&)†äí— bón¨6¡†íý±¤í^*£ e·Ýì˜ÿ†WhÜý+:¹z~yÿlPo5íÆ.‚$¼?ØÊA4Sä}û%2Mð¶eŠNšŠÎÄ¥!*ÐF…äÞÖÎÎæ‘¶P‚ïë€ (©Æ"Æ=TÙ  úEPÝ­í£mIô>fi°_Dâ;„v„ìvÔÙLcWfRk‡½ƒ¥9ªû]l³Ïò9o#缃¯Gœ*ò•iàX?Í®|üTý£TÞˆD¥C›÷Ñ~H—pæ&L»q_2±Zu„V©žw„oSÇ:fh[2{+£s©¬†¯ßv’ýƒNòƒú;8°Ê H[Î[&Â}õ[ý„¸¯³ ‚|Éå8èUCˆ±ð8x° ]ŠÀŸ¾õÖï×¶ÚR–;N£r$àPºä‘–*>ôµ‚ñWû௶¬l[ФjïPÁlǦ/?J,¥;ç©u²} GDjÌ?ð…_õä@=ãûøÐ¸¯¶|õë@¿ùßüÏ>Áó «7­¬‡Ò®´º¼å)¾èg3ýâ|Ñ?ƒão|WÙwØJ«ãóbÜÒ4×v&hW#&æ¯<è¿úØ××F(û©-h"¼wµu§çIäÙøÞÜÏérž”´Ûí¶9° ìIëÓŒ¢»(Uì7¢¿FáÿÕ€ju ÝLGmê`¯øbëq[ |“ýÝÓÙVÊ“D$‡_Tcµã›PTC7ΩXÕÜup¢ŽÚ‹JÍÊ+(µ¢þE?›MCjIk–Ò’ÃM}$‰ëp£ûøH“üÚŸ­?•gjîÕïMüù‚¾?&šÔ€Èàùo±Ìè lq#Xb”:ˇy¥¿œŸ­‚Öíc{HÞêÇ—ôÃB%’UË!<$°Uv?Ÿ0D»šÄ ‰ AÈ¢²4.%cäS‚õ»#Aã­[uhUƒYý±áC½Líûîãßùïåç°ûȯX±ï76Í &vE}ïý>ï·~ë½÷à?Ü4L cããñêüMï=Δ¿ÿ~ ôéŸ~¬È\¼ßÜôÞ÷ËòC¡›Pï·¼÷x©eë?ôß[ŠÄ÷¼÷gÅpزí3!ZáõŸ²þùzË{ÿ´<–Ýô[M‹ žÈѼ²é½gò4Óó;»þìð7½÷™}+§×¼ ì ôÓ@qúvâLxcŒbvÓÉÜŸ)¹è8ˆ <ð“oÚÔ AáÝïµDZ ›àÅÞã´ÇÞ×|ÖÝ)¼’è0­kn9rÈ®÷LS<ØùÌ2ßé Ó{ÜPm¯6]P®±Õèq -YAǰÌÈ Oÿ½¤„'mMw*!ÓÞ¿ Ôi1¶¬Ôª´‹iÖŸ¹:6ÝDgpXCä|G+1s'šYå[/á½>“A6˶Wí½€¨ÅKæZt'êp{Kè¦xM:&ËáRô*Œ/éE^¤QNò)ƒ¢ç£Á1sÈtìŒîi×­ åþ†Ãl¢6{Ì7’꟯߂ÿžúF*Ešœ•ÓŸ_eÓ幪¸%Î÷}¼Üß! ¦ò– Á¹õ–µ&»ää•¥·‘<uÄIr•܉¥˜$`ކgl²7˜Õ¾Ä&»/Æ ïÁwö‰ä mQ¼ t—§ %B¢Cü(«tTr8·59†t+Ù±€EádÚº¨~ìmlllvGÓÓVûpƒáú5’ +u‡yvžëÓBIeGò™7Ph0ãã¼b7Æ ¤ÚøŠ6ÏóÔ :l;°Rh:ASÖOºÃ2SƒÓކŽ•|Ó3¿Àq£ `[΋樂@냛mÜx—vgxH|îÕëHñ$ÅuÀó5߈UmUý)˜ÝZ$Êe³ µÍ‹l8¬ˆ òw#ëÐC*éŸ}Êf3¿ÝÇ D ˜¹-=Ææs%•;c žì‹ÿVjL3ÝRlL€¨òøÇM§¥'êïÃ|<ÎDzè‡lìU–ót6N>•3;M#wšpòÏû‰;¦/Õßiæ>Íün-ã“ØpÝý %˜<Ål>äC9*zR×’ZÎH}µm*¼õÇ?º-ýNýu‚¢á“ƒù¸¯ÿû¡T<£v@Üý ´É–6íÿ|‘FÎDÁ·òç –A…ðÉ^ 5é¿cËÓ4&˜ü`ž6·0úÊ4«*Q›žÈÊ!•ê©<>&EûÝ høDÎÓîÄ6} ûÁ<™%ÂÅŽ»ðÍÙùIÃï)+ÀrÓ°Û¹ÕúÄïï÷Àùew ••bŒ³QÞëáÙK¯7ÊŠq¯ÇG0ºXsßõ@xn It io9—j…ï2Œ²ÀóÐmÓžHÈjâQ x€š""µÜK2-!Ðhc;w~ˆ#=¯)k©³&KÔ8qjœ,QãÔŽßwPŠ–¥ [5zXéA’€šÄ°¶ü1°_+óÝa,PÆ(ë-%7o|‰ÿÓŸ-% olÅŸ {wrõk˜a>UºÙËl|:mø]Y?³$Ýùî¿Û8e :L®è’Cº×N _ë[ª ƒmâ'ªb»ó™R¸ªíäí ÙÝÓb:H¾ª ÊC•wçãb”!ÒC7›­ÊÿæåvòÕÙl6Ù~ðàâ⢋âw9=}o¿Åã÷¾RI@î7Z#„Ë“—/öžï¿}Þ}÷çw++ ¼©^œÃ餹õ&/ Ê>úãQD,¿Â:À4§$‹°ÆÐòÀÏá»Ì¿J~ºq¥ãiy1ÆÈgŠ<;(ù%ŸŽåÍÇõ\Z•UV1j$WbF×fÕã›ÁtêÄ€Îeß°Ÿ²Šò³hñ˜VÀ;¸·òâùÛ;‚ˆcíá¾ÑË!‰Ò(aØ}Ì®°mJõmNÀ;¨tE‘6¡8„³³I`pÈK“ûÉêv²ªþÁJµÅòË ‚w­ˆ(¥›G%` fg;_>î$Ƨàä[ãÙÚÎ;¨æÇUþÓ\=rßXk ÞW»?–Å8Å>Ý7m¶Æ;™ôœD9ó|6}7œç‘Ü»¼R¤AfÁ£=<\¸\7>Ù|\D}?¤Çž|ùxs™åªHkÑJE2¦hÕ/4ã¢Õ«qéõµÔ²½ãEøÂ..€Yªå*ùižgΚê†Ô~CBG(ËR;ާ‡£¹+Z8à3yìv)ó|Ѭ–l4bh0 x}ÆšÃ[zeü3à¤c(ƒ^z) ýô — U>êæ Yìþ€ÿ‘ÜbiiîÿŽöo}XT‹¯.LÅHUÎÊá ÒÌŸö¿È–®× î¢P˜ç:!2^0 Q)‰< zOê8¿Okfnw0ÀH^gÜ5‰]®Ï´çµ´í´å1¦iP܉£-–suã‰2½´LstŠ †ß™ òãùé¢üêèi$1dbž/#lg¢ÜC\<ФH~:¦[ˆŠŸî}4ݺÖñ°ø”n›„^nɉ­3«ë6¶#ôg^z E%'-¬°€ æ¬ù³U3+/a¡“Ç|:Åè R‚ gAs:Zó+ܪï°È›:-rCÑÆnŠ¾ÝªSNÇÈf¶$jèðP-®Ãóg!Í‘¨5; 9ìbÖº§µ¯óÙN=£U]VûÝ+% &)çt‡sæ e.¿Ìû€ÛHr–‘ûW'eßrE‘ñ¦ùŸÛ+² å‰qéEN®šR=â$É–|…|¢AÉ,Oæ¡­ý‡Mƒd{ûAÚjÒœ—\c¬fÓëOô­å)g;êürtŒÀ€˜!7BΩ †¨àBÊ¥Ç_J@ÜÕ²ß $Q€Žñð÷{Æfé•M2àgòy9z20Ƀ Šlp¾3rn­øð÷]uØ·[h–\|®U§~ :Ôyë•ø4«òˆt~›ÅøKJê^摽«9ý T„3p¹·Ap±ÉߨtD_öÁ:R¹‡>‡@—•ƒbì8½XbôÓ:™¿©ca*mœžù‰ReKNß„ì½ñä‡Oxj«O¥f%kx½¥_2VjéÃcôK,Ù¡9¤\và. v¥lz¥^¯3­I”ü+ª€7R+‚ùéæ—3Œûç }á¼üª6þCªD‚ÙSó[&F„©ì!¯ÞINŠá Db€ð$ÛÆÀI˜RÌ^@5NÄÇê wÂj1ƒÿ¡ÉIWw=„'gYÅ1TÕ´õϲñ©ÂÌj;ä7¡¶æª§~tÎ0}©j!ñÿ#jßÒàé³”¿ê-{Bû;)ÜtÖ§S:Ô£ÓÌ‘\-<.Mu\]ýŸS1SPŸC¼½[Kb€r<éã]ðzÖÈ}´]¬ß5˜P'ý¦ÊQ‰ÏI“¤Äêï—%oÚÛ@æ€O¼G~v½àX¢B2ýU4Á\I¬¤;²¤$ÉÛYÉÁT™Æe-±ÎµÌ7Ã'.C ’.رiéΩœ§®8±ì"7kß(åBø ùÀâtUM}šçƒ4ÂNóÓRIjè˲ü0Ÿp¦Áÿ-甋“ra ç:)¦’—Òž)+Pyò›Õ&÷D‰ËAÞ/ÁÌѳ8dš±¨z¤x‡šìéX“ËLÀ+È9%r,œ نƯ¨ãܵ¿9ƒS_ˆ^-c< 8öØc†¾ºM2Rö²š óŽ €Æõä&;(¶ÊfžÏØG%˜ÏÞM]`u6¦Ûï¬>„½?ÿáwY2Qù»ìí¬º»³Y®Ú Gn·×†¦à;Ùlõ€ßG7¿pB»ƒÄçmÇwµ /µýÂs3©ÿ"fê[îέ`wv٤٣¥@iÑ&Žõ~ÓZvï™mYR½i6"µ±Zä\\À¶¶Ì\òt8õÌmXc¢oU)&˜TjÎT/ƃ¢ŸÍtŽÚç ¨&ÃìÊ‘–ß·ÎÞàüÁAºÃŠëÝvž–•,nß`óKìZ±~~æÞeÔ‘“çÎ60Çh£_ý«o<«šÙD]íoñ^Úlð‰µv×&Ÿ»<{ùÕìó¯iö¹k“ÏÍwí*ë»Ûú8‹bë&N¬7ÛÓ£QÒö%›WýÉO<¶¢4ô™Þ}é´Çu…gèéÅùQínbn¥¦ÃŸ1Á—‡t¿r:‡ø0ŠƒJJ(È¡#'j»¬ÎÀ4Ë¥ã¸ÓOCòè`äé3Ú›ôžand³¿ìÒÎ’5V€:¯†Å'3¶ÙϾìòàù¸éòe2}Ú¶ïèö%÷“&.Ý —ï@šÓ#[϶¸–¼U q>If%_’×¶¤QIöqIÌ—3›ô¨¶š!²½Ãg츷íºF,¼úm¾AØÁ¬GÇË7æHKÞ¬• ¿zÚŠœº™áêhïaß‚7æ %´· ŠjwXàÂÇ8ÇÁs?€øEV€È6+†š},XɰRÊ$Q w;ø@h&Ó+»A9F6iÞ¼ bGUàì`–‡93kAÖlÒtìÙÀÆåñ¾c6ëÔyDy<«Qÿ®\ÿóÔj?~è;Çð²œ`ZìÜ‘6u«¢µNr‘Sc3ííʇJK)Á$U{0´×ƒqi¢•¨Ÿ«·Ú…–ulˆ&n.m<¯¯¡²¨ ët/ïF6Î`S3ªÇ-4Íèþäè2±èï½ë„óAÜçî6u\‹Ü_w™e¶ï˜é®vlã®SëD3M˜`C]zÐÞ®cK}½ R€X…>òá¹\Ã?“ skFíØèwC½äoÞ{èàKýZ¬ôŠ—=Ÿ^5"Þé±g2¦îÊvh¶áŒ5„•ä^¥÷>öãfÔólÆ™`ëÌG©_dz?A¤¨î°T[3|Kñ'~k·ƒ”µÏñŸ“E#Uæ‘g¢û€s˜cî©cJÕ2¼Âüµ¶wÌ· ÕzœäÓÙ•¾%·DÂO>Z_NUØ|Dñ(ŸðãÇ7.ÒDÍ[eÎÄÃæöG°#üRQæ’» 3·–üç‹Áv"GšüöÑÃÇÆòwë››ë[¿M6»ýøÉöã‡ÿ'±ýIþÓ„$++ ¿ó‡&i~®VB¯î¦$t±&Ü2dFÁd©öu‰‹8€sYÎzê÷4Ÿt{& ')¢'FK8ÁEc–ʼÉOóËÉ»ìôT‰w^ê²®"å™mlÈ©xÜ^wSSCXžç½lœM%dd—¿Ùxf±y`N‡s30ÅHaÕþ<Rp¬bÜŸâ­c g5P‚ó°œ`êp0pævb+òÖU²uÿ¼‡ÊªÕOÔ’+NçÅ쪣6%ŸÕ «PðO&Ÿó|¬ÐÞ]Yy•¯´0\ r²‡=¸Ô‡G<ß¿y—œd£bˆ%Õ,@$v<˜x:ÌúŽÑª« ?-«d%ªüþ#óY9Ê'jyU]¹Hü¨xçEÿªÓ˜©g³{6 ¯ÛªC{¢Ëf€¨…L§t ’$_q¾%èßË@5×°ïχÖ ¯€„yQ W^¸H¨mÙuÆÀJãÛÀ‚ZûOUV¨†Ï\'ÜÊG/¶&‡O¤có¾>G³óÚMVàDH¡_M$.)ªj÷lvJQ*~p l›ÈbÍËënò<ëŸÙ$UÓ¯ìx¹º8…ÀgYòâ£)ôâÙ5„¸ªŠÁu[“^tQÏ®_|,T`IÂz{›óGm@…ãòÂMfÃ$`U›Š_*¤]ñÌ™‹wß‹épeÅí§:$.çJ™Íg9†xÏGÇ á$,u@.2¨ qñð€qš÷s5£{åÅG.¥Ç2­K²®Þªo?ÖÊ4¸m%Æjc}ª†‰Œ/»)-½l'ÿ…#Wßڦ؆WlPžz%WvòIšºv'ÙÕ•䃕L¡¡U<ϬThȳ"<©+‘ñTÿ[8̰ _ŽVJuØM\kÐHÅ7Eì^Òº÷”:WE6Ö™Öl½ÜŒR÷ŠªC˜TIçÝ•Í,=CàÑñ€BöUj…ª5“_•Š!­© H±Y»¬~ÓF?ÿQ®6ðZ­ºÉ”îð’‹Â5éûåxˆ.ÔÊ.Ѥ@C#*Ó‘CÍ’t–ç«~d}Œ šG=V|ñt fGUæXÊ,¤»±xÝo¨Jc–‹YÙ3ýeÿFJó·.µ­Ã˜Î8P2bÜ–Æ{ ¶ð-£x˜µÁ6œXwñzmFSÓ}œfž*5óáÀ„ÁP}?S˜‚­P§þ€®šMJªæý3O­Ú¸)¼5mØõ÷àƒnn[¸õ†Vƒ>Q©îrx$%¯ãš¦²E˜º5÷$K‡©½àÜ|ºåõÔ9µ'ãܧ¸Sf®îÇ0Æ =ŒÝQˆ‡¡ 4€^>7ž™^žAíÔýY¯Aƒ|¤XK 3‡8Ëa–uñmÔa1*˜I.ΔHaÒòæ‚ð@ó)·.×t¥Å”ÙIê²)Zóÿ4˜Âú«Ô¦óc éFÊ/RÇÕ¼%Ÿ¤ËXîTvCm‰;δ¹c@v«É°&.J»ãŒÇÉyI çŠC¡¥¤dp­DHÕ­ÏèHí—P弟§¤õàfãŠÆ’_zu¨ÁÕöÌ &FJ¦î æ «ýLÉ då9Å(ÎOØ]âVŠlé6‹K³5¯sÛz_²(8 ”üÁ¯ozL¬^交Çu«Uö¾nÅŠj½*]µŠf—_´¼e†J莞¾Ô™ùc‘3R8]Á“E ì4šo¾ñ‹¯j7ºàÍTàóÔ4æ\7¿±pï Èe8¾R³ ìl„ øÍËôFhl0UÄ[úr€~‰¡eˆÿ ’ýx=ôÍ1© ¦«ÝÂÒ¶à\®Bª¼mÄ,HQ ¢Ÿ–Ð=»ý-:àÃLåfÚ¤Ì<ϪÌÉxtQvð‚òO?çx…Û·+ÔŠ³(_`7˜ùÕÌhVž,Á…©Ì‘)`;„mPž)j:)JÍ­FòÚ”pšÃµÅ>iS^ø÷ÎŒXýYz˜¶*²¾´zißå°õæãùc@¦°}äŸi Y©ŠŸYx'ùhQWœø´Ð|¹Ø-º‘H›œë xO¼žk}×é8½"m9Ä˨Á‰É®=å,£h#&=Ø*à²"Ø]+4ç«yKsi+Žy抶wì…øqêi¦·(o–IÞ:Kh™:†QzlŠÖôŒüÑ2Ûòtð&+ù°¸";š·.ºò+Å•£œVï)‘ç^µ ‡>… jfªƒ£¶¿Ý›‘Â¥B]ÔÛ,;lîŸ}ÐòÛV쓆H‚™cÔX€ óåûÉ6m,ÅÅM7i#öÓ׳¶’Ý11óhY«ŠÃœÛ°‚ö>‚-êºCFiòyÞñ%âS¾­Y·â-t]ø»ÁP5SŠË¢þNzb­dKu$:ËkÜø…È…áesbÄŠ:»jØÀwàE ²8) ü¼“²!¶ß°³‹QÉÅõC xÛ,²Ê˜_Æ×|¼8¡Zxé´°BPÛD]Sû+hg 9U­ ‘/öþNÀð‚òtŽ4žI¬0Òq> ¥ÍÈ@à3›¨1¸‡sèØ¸#A»ÎMu1NÍý3ôÉK¼Yø0¿ÒŒ9iÁw^4ñÑö*_k6¤Öf*;׎wB´°6hKyÓ|H_fÚw‚vÔÔHh1¡¥›M@'Ú†[賤ÑŽ”aüµ%V³³Ä˜µZÁO"¦p¼ Oiéi_CÖ§ÑÎ\3¦;:8=qã É)˜Òí–$$I-«>—©ÕN;¥ož×î]ºÑÆK¦#JÈe°i.bÝ~«êDö)gûjfów»{7˜bÇ€ã*t& eŽÃ„"L\R©qÏ¡…Bv†ÈBa-¯çhÌ 2*®àþÐMÞÉí ù…m›Mƒ™f¶ƒx3½µâKÒ5ØÞ/±Ä—Z$j‘éIŒalGnûG`«ËÙ,(¶µ-O gï)Œsj¨àÛ £û íK’nk6³JÕƒ„5Ù]xˉþoÌ9Zxä+µ”`š²&ok]â€ï)_¢´ Š]?ŒÖIç°ÆX±„§H˫۞ó¡‹×˜ÙÎ_¬ÁÇbÃøF¸8©ÁÄ÷Uã†xêÄpàÎÑO‹tåÎi¾^X“‹§Ó+‚ìÃm苜®ÿÙÐÑYÈšì(o‘Fe4|ŬP|Vý‚èºf“Vo¨UãG–á(=o}ÊzvK‰žm#{:ö´Ú™\ðKÖåcÖz¶#-ý.I!fÖC¶Së\ß0ðÒˆ.‚å›MâÇsl¼äà⇇"A=“I VèñÄõ  ÉAJs+&ëT¼ëÄa"ihÀm1–ЬHÓÏ€ˆgVàmô2Ö:‘~àN¸è‡+›ºëU‹Ú!ãF¶©µäCžOHU…S bPuÔÎÈÛ£÷‡óïê—rIp4`„ÀÛC陣ţ¿0üî×vf3uPd½Ö£Ë]mÔT NdBawBÕ(ŽžÃ­|ú¹‚“kWn5{RÓ±›»;»‘§D¤´A©×C+~µ®ô[…?nVdq%”æ ¦‚j».µ²r'Àì!h„FçÆÇ–_¥Mܶ}N÷ ¾Ò±G~¡Uòv‹Ý4SÓ¸B…?•UªÿTt­µÞJ¾HnDÍÓ†®!ϨÑ·6 µSüP ðY{÷úÙkª§j‘ç¹biëðòêç£÷ƒ/À!íç{ƒV|7 M—¸ïè« ±Ë¤Ã¼FÛ$EÓ÷æÀàÎ2  ÎQœ½%‰íú]ÝJ ‚1Noå±7P›_Õ¯Z´mY°T#Ø IBÈ.‡U¨ibï EˆP–h¤ù¶”ú½ÿÃîË϶ùFIÞíõðÀ³×SßÀñ¡×s`…”‡†®ˆqÝ{ÄSgöp@¢¿ãà Õau©ßt4œ| W¦4ÁÄ>sïY±nŒ¾ˆQ‡UwWÃLñõ[³If–+aíÇ9¦¶»=“¬˜VúòD'1w*®ÛìÁœãƒoûEŸÖ—Žô¹«Y_ ¯Ô¾â*IŽ7Úíá)hî ä¼[é½à™{æz{̽– #vÃQsk·uÔI[O[GGÇ4ßÊàåþÓo%«,Q¥2‚´KÕì÷¾nÁ–y*Ê<å2Ga‡o»Š¤.ÿî´ÇID-Ǻ€_«X;Ö¦—Ñ/È5.jÈVsÛ©Ï?®­»=s]o8Ä’§Wócì]!æ6UJÊ-&LJ;òý’áÚ¶Å…±„6ÏŠçäkÂqôô±†–dÓ k.ÔHaw¨~yWÎv¤fŽ~(T=D$j–¾-¥û¨~j cDAÑ ŒãlªeãÔÖ.<ƒz _Џ‚M:·3Í+Œ ’¶ÔÄ'Š“Úßëp-t™zU]W…›æ9Tli $âºÓYï‡ó’>1(O-›=.¯`îÏ0Ì÷‚‰¿!ýž=„^ÄÄ\Õø†ÚÙJ¯öç‡<ìZ‡îrêï¡0:ø‰;«Db†Ñ(òepÊÔªRië/)áþà»ýŸ€üž¶db(“Ñ›ü²÷Ó<Ï‚÷8“Tf>.ÎkJ©™þ¤&˜ÊíïïÏrzuðFÇŸfG]·Ï tôÌ”F·CÿpÉÀãdn¦é‹-¢p_@|ò…Jvv’V¯7ÊŠq¯×Ò‘jpŠ‚¸´Æ¥ýòËÇ_rÇüïÖ“Ç/—bW. IûB­éIfoãÑáyéäNŒÚ%BÏŠ@µ "ÚÞ]„Zf–¿Af0Tf¼x©¨5ë=á‹x$Ùt.zM ÍU=¯Í¦ *ûEeÝõüá,g”}Pün°¹ê¦`há„´”`ßÿ fŽÃüqZ>q>ëÞŠU+&ûärÙv•r&`]+–”÷‹…%& z}í…;U;ÅL›^I}È"ipÛÚ$Œ-*oxœ  õÝÛš ¶±‚­“0Un`”kœNè]ïX•¥æõC¨5¥ªgÚ76øpI'\Z".‡U—Èr£ïrx¿sßÝŽJG,Ï>Xy@GÃ/ÐNVyès˜›I¥Õõzáo¶ˆûý²èÚ¦8˜Ùe1šDPUe‚“D¤S_@A5oÞ G¸tź):o“î&)ܵÙ@Å2aP#ݶ7Ù™½Š4j|œY­÷üöNŨ)NŠÛ9T¤¶›§Â›$Îè-Êœa ›Ôb‘ê£ãþ6ü‡ùEȶE_‘ƒÔÖyÍòVysš+ ‹©R( ECÃÕ8Ùùî¿×«ÙÕ0·Ì·ñçîdÄÔtBôF5‰§ù-<ƒßã\RbЋBž\i»”0¶™Öj<ãåðÔLdÓA™Ón=¸ ÌÏäÖÒcI/TÙÐæªVµmK–òŸaËi>`Ør¬ZÄR–™Bcîšux´ë\m7F´âà1n²EýQå{¦$ÄSš¥ÃÃaWË/ÓVÚjßßÜ6?;­ö‘@Ã0†;†“ù¸O½ÇSÅa:œ¶zê·ÏÖ7ccšæ“!ÊAgH”9ÒXÔFæ"Ùí8qfÃÉYvœÃY%VãMÍi8fJ"±Ü8ƒÚ;¤R‹ Ô°3"ÄEªŠî|2 ’ŒÂ‡î‹h?t»sT‹v†a\®(}„º=d8K!t[lßpö(âEœö†1L†Ç ¦z´8Œ£kM7¢¹ÿx>Êt°Ü·eÃõsfp¶jÀö¹A(®ïsìÙ‚ —O ŽºN„d¯Êg©\{îŸ?54#u~\.Òn†°F"7ûLâŒã…!ï$›m‘ ™ à†ý­zü9~äº,jko~ð ö¼åÄÎÐϧ9àH çƒÓ«f×3Ùüö2|ù÷j ªâörC//’a>>U<ŽËaG|IÑmÈ“±dìÄ€<%Úl‘ƒÇF‹eAwÞ†‰±X8+‡ìq&ó,ø½Ñ­8'œµb¥c–u1ìälCŠ'‡êyw¨/sϵƒg3˜<ïŸ)˜þRt,OV"ÐÍjbé8¨•«Rd´¾<….Õ¤ ðã|lÆ C­ 㩟 Ëpù:Ÿèö*¸e½ã@|à´¯Û«zz8£uLYQí^}µ H…Þ‚ ÿjÝm=‘œ |lDè>úN©‚K¤§*q:¹a^ñ…7¦½„“ü@B zŠ3elI× Th oeò!°Sö«î|<êæƒùƒÿ;ê÷çc²¸>RÿϳáƒkÊÀ“C¸Àª–Úþ2˜“1¢›<ËO2¼…‰åΪ1jàÍ@¨æ^ÅfÞ¶ ¸ÉêõN"ŒQ ƿИ$\cÄb\Nƒr±‡5nN™ m"K“fŒi‚xÍpc`fÞc^.1ÕM0ƒÁÉR‹°vlÁ°|ƒ$ ßÕ—¹RêO‡[Óì=¬èЉúv9bÍü²å‹[dác ö'ðÁ¥ÿ&¢@ך—ªùù„òvPØ2‡³—“ó ÌIkÌ/ÕF°* #ìŠØMɦ§ÕÎáÑ n¬`¦# Ž P ,£8E÷KŠe·¡ Û° ¥à0¸ÊŒæyµ‰V³A1ö±±ïò*'Æ®£ÅJ½œ“sSÌ\‡RJ’*¸%….‡}EÂŽ‹Þ_~T:)N9súïj)P§óÎ_GÎ DÄm}°ÑsQœÖÐjý§%±N¡êkÈ'´ïö „3˜0Ùqt¤ÏŒôa‘´;35öpG[ödEéÜ(Ãщ gƒ~©é¯r^åéíyÅgª97p0Ðxn°¾àšD˜³¸ñˆh™U í×Ì0AyR¢ãf­j7W¼ý…·D!YÅqO)$*¢E»M›X0â˜8œ‚„=½ÚŠlصW½Ëµ57µmƒX_Dj¢Ð>¼¶)´d`Ĥۦ•Ë_l©ìÓN²ÑáμÛöñN˜6‚û•[nmJ›–\G²¹…¯£œ•Âö öÜH3¦†ºLá¾Aé,S]­“Ü´»ïÇïÇðÊÙЗû;vZkväÿAÍŽ—ŽÏב„nêNÓÉY“ÓqiIOÜ3,áÅf'ý„ÖDx GöÄËUï¬X¯§¥M¾Šu\–Ã<óœÃ;È ·“ÖÜhʧâ±¢\Šêå~I7žü÷Hfðzœ·®ñéá¢âôdÏÌ¢ð=÷Á ²mâ :Ù2ó˳⸱ë=÷„ÿôÇC{§nÞ%K2NâPË.ÀLßI5ï³vôú¸e×#ïZ¿-L?b#—æR¾Ê_s“ß6 @bwªZIÂéö¾:¿ù´Îþê´¤ _1úŒ Úy”ÝjšG±%GÏ—šDgZZÀï/» £ée›Oy›'ç°…ÁߺÝì-k>XámÙkHu£JO•ä´tRv=½TÇ&ÿ•ÐÈÔÅò+ìvç²ÝŽš ÄÞË #¤«nÊÝYÿ÷ÑÊ ¥sÙ¹R e€”ež&,F×ÌL¥ûèê®…€ |§ÖäjXºn9ė²ËÀëd;ÒM}5I;txÞþk'žѬ…ȸtu£a+ý\õVðÓÀ6¢cÿ RZAØ¥ìÚí¸É²¿u–ªÓ—ãe«[GÇßù<ʺ+òð ¯3ÏH‹ ›W˜„…Å™qAßW:ÑÇ[ÆÜÒ¢Ê#7ˆ¶©éBb?ÎW1æÇÜ;õi×Îáfg£³y„çYPì0s{»“¤`~×!‚?«…Žý´¶á·ToßÒ–ÛÒ¦mÉiÇ^TTÁðŽån}Åì@ôøH‰Õ@Cg9dÆ+8.—BÖíoJG¬¶¥­”¢]¸oqå(ô~oÛìû¸8`ßײ©J-ñÛ;u?YèÔ½õå£Ç›¿|?uÁGŸ<Ú\Æ©{\ŽG¥Bn9.ú‹œ»÷EYÈ+@±|oîBqÜ?vût«Z{åäjZœžÍ’t¯l©Ñ¬o©¡óä匇M.Ý7óè6ÞÒs¾Û˜Oq£(°!+º *Å Ïh_† ã=íL¼B* hÏǨÏå8YÅ]C!ñ[ '®¡”–Ý)œ#ö õ<å‡Ãâfa59¾ZyUôÏ2…»7 wù2×äà"¦ZÞ/†Uò§.þ«:Ç>Û‹½Ð…›9™æ:ü/³ˆgtVN£þålÍëx ’D»j'°%Pù÷€“‰¶ RMÜ<ÉümBƶ·“‰*AKâSJX 5†V¤„V¯B%L,Òuy'ÈÏãšåË'÷“ÃuhÇ»ÝOs>Ä~ë´-q¥O;IšuO¦yΞ•Éz¿ñÖŽs}–꘴ÞC¥‹žá3BeŸ?_²¤»X<è‚dÿ̃Jw¯¯Ú˜O§mÇ" `58 `$á“ïEØðŒ{™E–>`£Êqë·(ljÞñÊÂC!˜i¤ß&¢QzÈÉfÐ=“®Ui~Ùa˜$§áá¼£òzÁ¸±?Þ¸?¯ã ¸ AáA¸õJ²uݹ&ÌÏŒàx§ Q÷]‰a÷KÿVùZOžé¡ÎÇàw˜;«ÀDOñL¨±žà¦:„«ƒ«x6V©­þ`Ú]MÞl&ŸõÏ–ú§Ûí& h¼¤—ŸÛ«Éë75Šêª,™ 6Û ˆûh«MpWkzD’w'€ö_!´ÿr¡±U5¿Üv,øïx~š“_ÚÇ”6R·¢Ã˜úc°<멌¤z·;ŠèA®±¥_Žh\Ìùeìafdê÷9÷¥“è^YPé ]ŸÁŒ¸(ÆŠ¡!Õ·£ø¾»fûœi‚*E§8[3_UPÚývrù_W[Kñ‡XzJÅZLq+ '&¨yç6]<¼1ý_l´¨S)Îw5šû¿ŸŸ‚!xQÿ×kƒ‰w9L]CÏq­Ö¶3(ª¿ E9íÆ9p)jÀQjnHQŸ®:¶ÖN#ù%±kÚõ¿?Íó}¥(Tw¾éÏ6úSWwºåKîrG¬_QÕê"è ¶V!M#h€Ç9nlÓªÚ£¢ÿÓDSÃÄj¿ÁzÑþÅ‚îF·ÝÐ8lvo”$ÄÛó]Y$ñ]›­c60÷ò˜ÀæŠ(‡®OkþèñÐí·ùì%œ¬G-æuÇ®îJÍÔB¥6®j—*"ìÄdbYfm¥Ž©q¤úbkp~ÓÊZ¨Ù´Ž[Ú‰³©ÁẋÙùºDŸGP9Î/zµgÊ”0sý¯ÑG—<¶Üã°€Û ‘Û=xŠ8F½º‡pÿÊÏàUt\…kB·ôkB@ªá=ÿé9˜»Ã K#<2k߯ß6è¸æV„ê<«…i›“ÝrcÚ[ì³v‘còU¬œ˜¤ÍÌš¦¹À":Ûvë!)ãý å  ö“ª„¤ ƒrÜ‚Këȵ–¢µøè믴ˆ‘©öhlf.#´ä ®ᑳ¾€²Î8®ñ„†o)®Rë“»å#h_ö/rÛÄÑK“Hî=È3ùÀŒÿüh¾Þ %@`5âl(Í’Û ¾|7MÆP³Þ²…²V”pùe¥ÕÒQÎàþ¾zË DZ˜åäT¸b/U Š.—¨µ@Õº ÐA_ñ‰¤Ú¥äÅ3ÉÃØÄpàüô¨ aéÔ39§d7øŸr:Ü¥æžaX{PØ -8™!½  ’uHÕÕƒt·½Ê›5kPé[nÓËݶÒÙ8I7Ц"½ÕƒÕîBè °—þƒ=.¦0ûÍòs(b+Ô6¥ë{@ F÷CÍü쌼¥Aô‚<´^ëë0膡 W-fmØøHÿ9N@ñáý„† J­½K­{zˆUð,•¦°Râš9¥s[óÏk“WJ\ ;ïð!ÆþúÍszÿ½ê¦ŽW±“¢—JI¾Ú¡sâOÐ{„ϰ½Æ1¸}¶ýÃÿ4R–Àï.ò|ÆõÖuá·_>P_`LTÈŹ &Ùȧär‡a¶ ë¶PtHfTØô@-VÍyYÜ„\±º;+%™ç=Ì¢éÚ–Ä&l€6à’¶¹ú²µ*NÝþÐN ÏÇpd‘§n;V[©ØŸˆ˜s.îÑré†àëŽy´â¼[{ eÝ/ަij'ŒŒAÝïÚºáæ£…J=œð:/e³s¾P.&©@@Z«Ë$d½ÝDäMSÂÍMŒÕs‹lHÞ¡uÈ‘%ŠÅ„’måˆø®ïæk%¥²ê¤Á<ùV⭦ǔ ÕžbÉ—¼ÜT@'Ág >òáù$@=ß\Œp¸yÔ­æÇJê˜ÍÕZÕPRý%@8,&o?óe!ÓP™v5 ¬ƒ².‚TÂ=æaîgHødãYÞÏ1£³¬íóTñ ÆW(ñB8EEGTýVÓ|¿—Ó^Œ&Rq2wD ní¸ô¿¦D?Üò!†Š’´&a^BÝr`Kb¡!ðÁš°Š²Ø×!=×™Ï)c=ÿ<ÜÞ^÷ï ËÞ:âTÀèP‘åF¯ä-´ø½ßl¤xˆ æ®”H-pRœ"“†;}~b»õg`mQÉ kæïõÄfòqZ Jöî<«~ç®å÷qA—py„ºNZkûD#FÍÎÆ§r/´›Io˜eòj™‚ä,qØËkú†'Pýb(OF¬BnÚ2[Iæ£ÖŠ.`2 š’Zµ~æñ«_1à÷yÓ‚w3AV(‰J &f+¬xØõ¤'ÂpÜTâ¸þkmѵ Íl]Ñ„¾ÆíçÊÜX5‘ßͱ&â3yýö>îW”6D æ­ßûx`ˆ0~åHšÂ =ˆHr‹­›š}N,²+0rÚ_Á|ÙWÎ|9ôq©|¦úéF9pÎ@lá]» j­¬)D"'ü6\h2åºêù|:½ò‚œ¸­QåúÙˆfà¦BÝ#fÏÀG9âö¤éŒGŒo\ 1:>3ø¼Qe}öÖnìnÃIçZ¿sÈ¢Ë÷m i>­ÔÏ«„÷W¸¶—IA9=Ôå\N»ÜI†'N‘‹3¼·kG@GoM}çèÔΓ—×,´î"švĤHÓa=¹qúG. LáƒÑÓ¼Ì:Ò êMZO›z˜t|_O²ÞÕÛÒõø@£¹b¸ 7Wdýð=pn ‰RQå¹!»"zŽ>¯þV͉|&³e¸ Ç)ój;b~jŽhJÇ9W*$ÜkB9âÁ š`<Ïô’–ÇàF³uRQ¼zÌWߎ¯Ü­%8,QÍÀÎé§¢¼VbƒË ¯{쵸£N†VÚ©ZVw@S¬–z]ìO$Z ëÌÙ 1y•+U}q:Ï» :Jj²‚kõyžþˆ†¼ÊZÉE9 ó‹/hž-#iQ%®M¤–7 Z3Šc.„ÜÝÝQQæÞjŽÝæ ¤„T[mõØ*ð:-¸×2§™²(ƒ«¶ºˆšë üä!¼V­5óø¼Héæ#Õ"s5Å7´¥k´ÚmN[¸„º­ØÙЏí_…¤à˜ƒô½XmD _:šAôF¶ H Òš¨×‡`1z»×Uqœá–ó›FnaÛ†ŸK5®m+uè@ V 9KÍÐõýOõQ}|G…ÂK¤øoÜò±Hƽ6]}kV,Åaƒ J™ÍúgNˆ ¸’ È­ô^ÕÁÿ·ñŠˆ7­‹ÏáÅ4Òt«íDÂ7ä@Lþ‰ì‹Ñ“žý5Wxð?“My‘gÚ2†||¢ï0m¹ÅÜ T¦ïqlÌ](#´1”»Ü`þ‘'I\H¶`;¸õºXjcJ}—¢šJŸ¿=]Xö$½ÖnÈ.nÃ.bÜébÁ’ˆ8GÕ`Õs—ªY©ïAU·G¥Ò«ªn‚"nP7ÛQþåæ«Ñ÷«fæ¤7Xp,ýÃjfÌñ«™0ãáõo4MÈÜÊ„àÂðÔ©[³¸Ï]‘w¸†N Uˆ dÆÐa?á)ŸÚ™Q ÎÉbòö$8×ѹ¥=ÁÖxJ…ÇÝ’m¤Q‰ÒjÖw70*Oªyn\£¦ªcUûJ«˶"& [ÙÒ­D²ÕM€ !ð‡MCà¹?Â#ö½J²c.î$%¸ 1ªn0R±'Cˆ–qkBBh[bLãu¢ÄÚ­é Á>D\À]çË"Ä,ðçËP<.½g˔Խ~m S÷lò@ß㬎ßSðVÝ”ÏÚC¦]ÏoÄœ-wîaèÀ´…dúbݸq A™g Ê  ×n!äø²E0™~êÜðƒwˆDB’î¢Üy;u{™Ý¹b‡¬(¶ YAêfzq±ßÖmŸ·ôåÂA·6·?ür ³¾rÕ‡›– D#øÝ¿òœÃqp©æ—'7‰û󋤈]û›äˆåßÕüXᯟW…ÍáÀ»üöÇb½ˆlâM1}Äòê(´ÈÁå´¢Çô3úÉ›_:–Ž…4ã¹á…º(u,O©—àvn«Þ¿ÚýsïÕóïÞòÖt¾ÓÞ(WàÓ‘ZíÓ+UTÌÚ‰… A»óË~žrÚ# ííó½×ûÏ …FþU#„ÇÂw/~x¾O²(´tSý'ºþ??xŒçC>™5Vÿªïî½{ñzßÎf²kŸõ)Ö &yvôȦý3‚°¹± 0Þ¾øîíóï~ÐÐõ•WåÈrZ¡Œ{œ+êÌ‘WeÉñ‚})AàZí®Nä*7ýqÜ“hwÕ?þÅ.EÚý縜%/?ºÀ®ÑWÅÉŒ€¾ÂüÚâé××véQ‹Š²DV…vÀ‚­A»™gyÀ’öVå3/÷qßrFS¨ÃlóÅadhLBþÄEayº¢„™É;‚‚Þq>Ñ0±VOó“©Yó\¯c'f|—+D¡@¢VÙ¦b— zàމ͇wûm*W¼ ²Ô…áƒNõÍ16ëZäŒ2½Y©£š§a'–êE<ÈHœ_äûñT­øÓ1òU âŽENß[ós5¹WË¿àãÔh¯DFCšpgã‰$wÞÇR„3èk-É…éÁQ\ä/™"ü¦ ÀgÅ(W˜Üy²¡EÒ_³~ûýbm‹«2zÛâëå\â÷ÈÙT?1w·Ó¯q© ù3Уù%yR¶Í­÷.^ïFµÕÁ\ÐÚC”nîü4„=C²uüêv)1$–Ônbœj˜(Žífâëk)£NøäšoÁ÷SŠëvåש0+é˜@yÒ#‰¹C)YŠ“« šX³z@@;æAo×ÍŽn¢!1“eö@p.uû ’§Š…dÇ Ç-LÕú¹jk£AIX¶šÇ—’H¨ÈïãAL ‘’ˆ?¾f÷dÁnÉ1—³`SH›iþÓ¼˜_åýM$]G’¹à ÞGè¯-Å/œ™£7,û:Í|èšê¦ a¢ñ*Ö§Wè;^# DúãäÞøŽâÎiô°j†¸\MBÕ½aZ)Ê^¼DÁI6;YEÞ ºDý||«ÃÖÁ›×?<ó»?¾~õ¼u´Lhþùt¸ÓjΊÈ8y°T¨î?ov ˆ÷ûI«›_æËõê¼.|êH$˜Aµš1 wYuÿöÞý±mãX½?ë¯@¤ë´HZ’í¸U­$Šíä¨ñ«–Óžse$! I0©GŸ¿ýÎkŸX€”-·9ßW¶±H`wvwvvvvvˆb8ãOÛÑvË%kUž cgAí«G‘‹iƒIBå™2ñÇFò!/^XÚ€4.X늟„ÒZ«Á9‰ŠsL©‡°sBåÌ!{ˆI(EA“™q…`qû³’ìØK¶ÞõÒê`÷ÀôœØ]Ì&`—¥ˆ²$ƽ9Ž·ˆßî”]ŽŸï tÍÙµ£hÄÉ~ÌÌËøH”k )ЙÀ€Ül/MýÒ…œHGšF=ZoŒhbH a»Ì‹Y®ü- =òaž"*»l˜çU6XÌÙkZ¨’Àææ°]I…Ùv'ÚÎOêµáÀAe]Žï-ÊÙ=õè @1¢ª)‰ß——®-±¼öj=*Ï@zÐeݹuRc‰ã§½¯ÙI}bPœ¯B$-›ÒÃL³V®­4²©"xú0ŽýikØ4;"“ ²gëó]VÛå>}?»éÎܣ̴R&°J:näSÓÑýk²Ðq«¼ÉiÊ%’ôeÒT¢bY5WùëÓÁÄVˆaÀ šÒðˆB…öq41Ï]è=ðw;3Û{‡þ²Á“ªXâEžÂغ½Påc¼˜;âNany§q¼4•º¯‹i6I ŠJޏg¿Do3t­Ä=üi6.ÊÛ‚l2~ò„(»ú'õ-TUªevl|OɈÆà›òô8­«"5”CHÕÀÕKÝz”*ŸnÏ({¨d²eةާX µ'Ò…ë¸áÝ{ùÊCÇûÄ©i=Ó× Îü3^Ðùq“ (+o´&‹Ùy½úÉ=Þ‘Ž•2[ CÜY J­+fåyRíµAë^óøn5ËúÍÓ©‡FT“Y=Tti’õåÆÉÙVC¹ÍêS!šP¶W•€õ1:£Í)[9+7§jõÒ¹ªh.*¥~sK… ýa•B˜"V% ÿgtª¶= cË;¤ñZÓ¥[Ͷ›T*"Ï+É3†S­÷o|+‰Ô¹Ðuev:{r¯²~{e%¹;‡Õ¢ìð$«Z&»/>éÙ}âÝ‹þ{ø*ÑÌœôôGRa›ÃteÛ¦üv;z/)é¹ÌN—"çÒ›Œ2öq;¦ÎŽjÃÆw»®«OmÀŒâKS"»9QfjDÎ$øQUªkV¦“Péw7(¹¬e®N¯iª"ÀU;zwÝ &2]VK&ª#¸æg‹‰ú:?›Qûøã¤XÌJµêš@q ˆFt½~óŠÓÙuI—/iäëÔ¯ÞX²_KNõÉb¼÷à¡“=îÄÑ]´Ä«zÈHÒv”pçgAdÁ ‘yïtRµ;¢˜P v7eäèê ‡!â% ²@PÚ—*>Õ7gp=z¸jòî~üõöÇ_¯â›ƒ-F Dÿ2÷œP+×)f˜sã®ÕaCŽ‹Î|oþ驼WÉA½²+dh¾éô+°Bðóï9¥ÈQgM7ªù„húëëçXÉ$ºa|£§'yå1¥þáÁh=ïØB`å!ÏÂûÆYÐÈ è »œ‚0h7Wà†ó9S[¸®ü…ÓVPs˜×¤ãDCŒàóžV;ÿkSÂûå” > ÿÊá}C"ëIviÖ¼Eu4ÜÄ4Þª…€«Ôw ?’HPÈ9Vªèô´Û›Òuö8É7ñXº¹]ß?õQû‡‰ÇÌO–×”5)Q‘¥Ö.eçÒ›e1jü"djý-‰ÅÚÈßXs¬fÃ÷|¯³m­øtˆÆÕœâ“à / ¯e^L›ögYz^³7÷¼õ›»Ô ÄÁ¹²éÕˆ‘J¿XçÁwS÷·»¼yýòDÅO÷çògn%ǮʩÁº+º .¹hÒŒ«jŸc/V9A¼Îfxd_O  sE‡$=>Kv‘Q0ÅI†f$¶éä,ë߃Jm˜î¿ ×_5W'Ë*ÿ03J ~%ÖúT=…on¢> þ+•Ý&P†í.ªdtaÛ¹‘û6Mtµ™NÊËlV.¡c9“T¥¿¨ˆ³*·¼ç½ ã˜òz©G‰?ή9W·Ü[³u:جúƒSâ|"VÉ£Ó”A'˜¶…³©ˆ³j«ÌpëªPz/ïŽRµoó‹|ko¥ÜRæ)‰º­B RÒp‰ï ó|À.ÜfÓ©%­0ß$×™áÞb ^ûlh‚ !· ¹àArDÉ àÀ'²FË›{é’%”±=È/V¾¬n ¸!Y)üœRCׂV¼ïÚú¢ êŽ?ùÉMN. Æ[U¨ ˜ú±º“eN•Vݘ¢ønâN@ÇÅ<‚iµ@º„‰ÔóØÕ/6™ „SÀ;ÔU?œÊH hÒst§¸“¸5᥹B†9*J³Œ:Žk¢ìv<½yY“ú<‘D•ÿ‹H€w)–üÈMJ¶$é’_G «}ÏߟRúC+½§Nýëy³!¦4¼áB03[ká7‡U°)²:"!r~Y(™’Ô!鼳±eM‰cFK…ø„ã\Æ’À C–aíèZ!1aÕÒbBZÜuò9™톸dMK`]‹ÜÏŠå–X'2‹.2œ´R¶ë×Û9¥Û"ÔýQ¿ÏÒ‹L¬¸³‰ÛWKj¸fŇ8¾’¯ƒô•E-.¦ü ‚¸½ÊHiùà8ÑLú£m$}y–aM.Š•yÆVBjæv‹À™D|±(UÙäéåc¡‘0â5;b+³keˆp=ÉY¯%5Û‰±­äz’õž®A¿ö¢äè¸}tì”j0EÕƒ`iK’!Եɽµ+ˆÜáþ¾"%>—pØQ¸‡éQè " ³G挚)¶£ÞI²9¥'>¹&;/á’V;³lŒ†õ˜ª{NÕJIöl›šµ‚C£HIj¶U4t!ØÁɽŽyƒÝlÅ»&‡ n›L·&PGo´+­hÎÂãDC¬ õ=@ÝYª ý«=¥xóš¡§ƒí®üyp§Aõ¦jÕ›·¥ìMR™ÕóOBˆë“d ¡TS1ÞôIyy€³ù«››ó2£* úvÁéÝ#å"Œsê©gÂøWÞMC ˆXY°CN® îR…;n$€8Ž{G23c4XÒdqs V…‘6K-9¬3¥`dÌ9kjžÏéÀh%5 ⽆ú™·+FN*Ö/?Î@O@ªŒwÍš¹p™,lWÓÙ–£Ý Î µ)­çv¼Š è „Àéß.+{ýž’ÀíwÖ’[ºgÕlQõÛQhËaQ¹²Ð‘³7œê@ª»ÏŠ*!³I)òF{r ù2 ÞÉZèá"Ï2§Ù¼D+}X!µwÿТ³u ê6Wõ½pºœ¿¼]èG©Ëvªœæ¸=W„.}¯Vð`:§î†€‡ÔCbÉú8‡ëÍŠÆt:CGCâeôšvÂÎkEå‘I…ôº%9)â•ÈU:LÀÌ0®@áZQ.´D½n­‡\àÀY«Áv~vOèH]áÀC4ìé©Òvôì—Å£jd;ï•îvª÷žêêñ›(‹}Ñð½?¾÷øájÌЪœv9!÷^Ôß \ÚŸàa_ÛñÔ}ì=2ôÞϘ޿ù¨÷4Žæ;Ã/3€÷QǾþ 6ÌC½ROfY…«Jd{‹¬ 3eôÛ½î4aù^Û^XY6¥};"ºÖ¶ucU+ÁPE‘4Ô$Ú ’YÀ°.ÖÒÁ¬ÀàpTûi‹ŽÕ½0õ ¥®Ü¾™ö°Ø²ë0Ÿi#òJ‚ȨLÏÆµ@–õÔ~iâÄÑÏ•*…£ZdeѾ}(9»Ç §`u…¦è™{iàò^éŒÞ×äìÖwüË-Ï—þ–¡5„’¥h]’1øCŒ!jÚQÜÅçÉ)YçÙ˜ã7K~ŒídÄ7ÉûëIKôo[”1 t%ñôøŽAÍ=ß!ö%ŽìI:Èa“¡ ]x°&×öùì´§¬ºä¸Pgï¢Ça³–•Ó)£ßͺ>«Ô,–‰•lE`.ÛV‹‡IΙ1:+ÕcÊW¨UGi+ãOTɳÚ€Õ9â[YÀžuî¢OÛzϜ<[‡ozƒ•¸+S`v;;Ô'8€[¼|=Ú:Þ<¢NÁ·cÝ£íãŠê`³¢(âzÛ»Çõ2®2&ý1ro Mðƒ›YLäÈ"Ê(¢ÔDKu¾º=tÛÞøÙÈ…悌½¦³âù•RâSX¶ íªjà»m‰H±,°ûtBPLˆµÌ’‹:uÌÚÂçÐÒ5¢79Ä“N¿ÕåE¤L!½3Xd¿ì7¾¬» OYA‡|FRŽhªuÅN ¼ûo­`R“r®b7Yg§¥ýÍù1ø8œÇL¬GnËEn.ó¿c— HÓÒÇ] ße6ÏLèǙҪ&jÌ!´¦Pß-¤&ô†×¤Ë®H©ÐEì2gªæ21%·ËaK³]{ðú‹ùtzœSfÔ³&7au–³pZ6 5 aV›Gé1¯)ÚoúÖ>£¹º­W÷•Ui“‰Ü ´Z«Ž®-z®Û5ŸèR:\ŒxµÔ »QûéÇ&®#˜!âÝ]£WI»"ׯ<˜ºôkæ©ÿîW:Ü_©ÃË1ŸZ¨wå*f >ûHˆ<ç6ú¡7È'"B,n(îÚü7 d¤,˜NŒ[È8ÂŽ9*Ä ´‘6ð®Ç¡Â‘XÙ”nz÷\"nS¢Ñ—Ù¥@Û•#ÏYÛø4j¦Ž-ÃHÞzŒ4¼q–NðÂÙgT8ØrAŒƒ—Öfì÷³É0®vPÜæ«·fŸ:mú0¡1ÝÒ®EæØaÉøJ@IÆ¡$KÞøªüe'tµXÂMPhPRØ; ¨|‹Y~šS¦?V’å87ÔTܲn¨éòiFÙzھآô§ ‡þ©ÉH,–êȹ&§{ºúKr}s')Ò¬>±å›ÍUhhž¶©w«¥&/ŽCëß|íÙZÑÇ+YáPùŽ·uá¶8‰Â¿él¯¨wŠ‹v@ÄòBã*«W Ä=s {º/Ú]Ýî•E7Xo Œy2_HìVÊÌô,Á] v¼Á¿„"a»yI”ª—´ªú+Íh¤/J¿M¶¸©eaB]öÛDæÇ*eÉðËg+pI‘÷ÿ¡}[È{òò§¶»µ™² ‹LÂâ…t7Ô³®h6Ãomfëoªv˯fÁ†Õ±±¡e]V—v?"÷Zz’=ûÎz-5Ý÷j©SºÞWPÙ°t´²„­®=LÅãÖrÐËäàσÞ$ƒÖÕê6¹‚oÜ:×ÜAijéÍúòvX€ªÄ´°£ ²]¼-ë99­æñ²›Ðz_Á±Ð.îxé%O>샬1Kó*/û؆¢ûóbœ¬‡-nqÇED໢_%¹ @l^T«ï`%ÝOÙu \¼U½’$Í^ˆ]h^‘ìêqÀõrhºƒTÏuj™šFÁ \Ø6¢Cr㮥Ô6tÓ݆ߪ†.˜”ÀG±éVQ½ÝËPî ?ÒÖNX†»¨VHýüíÔO 5‰•›ÉÜqÇ4­9™ÇÚÄâN€»¤6t+àj>1…œ'¿/–ÆŽ_¼WáY\ÿ´†ë7›>sšwÛé¾Âa¸ÜïæœÊ,~LÆ4Ά9úgîUÖµ.'qY¬Ò»Ë—V¸ýô†‹K§Ö0ª<±¯ 45úÉì¥"6Ú&0Ý¡=}MV§ÙF[;ºò·j“vÝÜŸWVÝÚÃêôíº‚ ܈$ÛݽRJQÞ½´ 7ïŽ&#¼Þ11k7æÉ#Ka4Þ Bª‘±x´ÃZKnü ¨uaf"ÛJ °Œ¥õ8 „Ò—B‘b>u˜•²þ^Û|!VÃyÖÅñRö' .ºƒÂ¥„vx±¾ ãY·XîF1jôP±_ÖW2'çYÛ8Ú"{8 Öš|)»Ýîðó¬Ðbf̼³mæ²åSÁ祑ê¡ñâÀ»JôT?äìÁE0"áê3EoÓÈ!ô­KDƒŸš+?ûãç¼û¼ ?Û ÑùÜäÐø¿©&ä} [‘Œ2¶eóÅ^è6Nw»ñVNUg“ç =8ý \Ú¬(‹*uÑøt*5zjoX>:ÔL—Ao¼g³âJÜìNQzÒp™¨nºñÎÇú®íU7+1-€™°}û»3ð%?¬`T˜ƒÿl«äŠmqNVØ]sœ°×¬m¾üâÉkª2tº·gvÄì‚ ¨eL¬h¤uêÀi­: ÞµÙ*Ú;si¸~ ƒuÚiü4ìe¼ñÜ¡æDòZÀºž Õ2û¹Ùp‘$L›« „ÍAˆcªqmõ[UŘí6¶L1–Mpz‡{· ÃNæ{•¼gR ›|s_s5 öG_ü-×Póê"¶»k·Ö¶oæÖæO7o ¿º¤cƒðâšôQ|×îù&ÁY3Ù@ª1¯¸ c0ÑÃbÉ,æxÙq«æu žÿ E;e…;ª4ùq­ „–•Ë®ÐãßE'TQôÂ]¹ÃXxI'b;á@]™’T?PàyR_%$H:ü¼HMú!øévM¶@Èv+SƒÄ2ß©­ìæqàâƒúÒ~¦;= ð£6ê)tö˜§Ûmh¿ ªqç•vÞA¹éαÍÇn2d¥~¥ÿ…bÿ/­—šâ˶¹ÙvW'hèƒp•]`"‡6æ0h;yÚ’Äb¥±e[]®¶ÅÕüú£[a`zPì•æÒ¸âÑ'5ÝÕá<nPZ9qúèv³v¶ìdNêRÚU°lM'R5FS•²é5gkù+­ÿVsÙä5'Zø+¥;ù-z³J%i ù+×}Ãd|Ö’7üûЮ}£Á}Nwá÷¡Ôm®,†W]«Íµ:IÇîj]Ĭ7+MZ'qʯRùÏëK gwŠ7×±j,…ïì¯eÓXRG6Ýʪu~íVk¶]­.ðk»$àÈÖøWýä ï‘L•,"Þ¢6mê·(¥n¡FfëŠqÑá‘"·sg}j®íí­­¯¿~Ðëãþö×vîïP‘‡T®Û+åú œqébµDœgâ-W æ÷X1_ÇMÓ€Ôæ÷¸÷© >ê’w|bbAãªY9zLÜLÑQžð7 ™;¸Y®™‡P¢7GÀýóóqüSR-N†˜kqŸ¾xúöÚ¬\­‹Ùy;næ<÷mSX#3¾àôyJ“EÑjdXg$¡ôÜT›]ýõ¸â¦ò»Íñ{I ¿›ˆ^álmK_¶©M¡&$¥‚èÚ†J7Œu†î?®Y-~2÷E•¥‰½šÃô à˜sZÌ0„"¥b*þš!·a+~¼Êu LjÉñHu “g:pãŽY{—o^­2Gp¿v«+ÖÇû?>{ùt?zöâõÛÿнpóÁ¸Wú áK\ªÐÒ(zÂγ²»ÿöÕ‹]* ÷En N,},8¯ß¼z]þ®çeOõ(grƒ½ì©>…!­Þ§ý×2´0ަ«MÕ mu@ÏþZëI/ûe58O{/ŸýXÛ¡ao’®<²çÏ›F6­<²ÿ<8|{¸[EÒçìåÓ¦9› WíЫ7M¨¶¾"œƒMs’Êj€Tw€VíŽêLÊÊy4×Ìûªh>øá‡¦þœœ¬ŒeTƒåU©õ´êúR´†rZ–ÅUCËM‹ëã‘ÚÜŽ«û‚µ»T·ÅŠ‹½‹TНº×¥"¾®r@}"lË&zr–¡ÿ´8‚ÎÒÉàŒìUiîjæïBb Ÿ–—öžl†ˆe8½3¶V¿A?yþêðÙÓ¸Y‡B»³›LOš‘16…O”"bv½JØ-é:ºQð·.‡LêäfÕ†4ÈžÀFt÷¥c+\[X6nœ¥M9U¹qcœÎÎÉŽh4²ïêÓ2ŠÙ¢ð,]”€€˜½ãÀ™ì:³,J¦Ú ä꺅–-(¥¡Í’é^»–!„`pY•½hÿ,iýù3QðžÔT¤³Ó²%H¡G†ÔølvÜ !«nåL~¿kGH–Gõïôït;+H% ©YF2ÍĶo¶”ðHð»\H_rú×Ìrbùäµr;ŒõwK_ž±þ_@u|ÂS°VNû·FlfTZ›Ìb‚ß®½(¢9•sãßû Æ¿u´É˜Ë‡Ä×ìJ}sLquRy>¹°¼?’ø?ùêU)»»§¨ö¬z€pÿtiG­ç{{ïëÃÁr÷àߤj«˜L.]ÀO½6Ò…™V«×‰^eµÕϯìm¾»ÏÓq˜:oã«jyKš K¸`¡[ÌB"í¬å‰B¶÷&D2'¼l*—}#(mÒŸ8e%/ÕÙ²’ë ®’ þ½ˆÝû-¢ åiBÙO¹¤Š?Ìna_víu–-¾Îÿ±«/ûåöÄ™ÛûX"»VÆÒ=r+C̦ÿO’îõ[®„®PŒð„4§1]Ç÷ã/ÚG¤n›ÛHðq˜‚[Èô|=ò®Ž)‹¢–u« <Õ³ž–ÙüˆêŸx ;ªb¢ÂÃq«á[G­tÿpkäjŸÔí² ¨pöù"f]¼áðf8Ýò*êg¡Žgd“eqB Æö¿SÁN«A'ü…ñƒ*ò/ˆ ýçÏ;Îçw‚šÚÈdø%W…ZÅþŠU1­þE«§˜} ŸåýÅ< ¦ªÖ®ä'4«5·CG{¶“ÙÛ:P¯˜"ï4›S5†Â2˜¨Æ”‹ ãufŽ’3É“þÛÑÆxÁé.zéEš„PýÎvšÐæ Z†aëÞV¨÷æEÃc̙ĮÔò“©0­Mo§óMô3¥N}ß)ßÅxäÁmG; Œê þÆSxäU?®V¾ù’ô`†–£ <¬H«WB«Ïmê†*3¾µÚPI˜ éX¤XXi —Îäßt½gÚþ¡yújuî¿Ot:(­¢ó6ö‹^ýh§‚æ†dðNå/¶i}Þ”.Ýò>mÎo´íQ[ß¼7Ï|©7@uîùÚa¢—ÛW"Åd]'•}=°UZ“ç¦Üí“]`гí¦×ú¦«þs¡kºJÄlè}miŠâOܺþÝu8º¶×ƒJÈQ{ASåê\qŽ­$ÔèfÊî±+Þ&OÏè`‚iÐÜà¢Â§Û_.ýü¹úvC©‚UL;˵“m¡—eéøµænj6xp=u½^8À­XáMýòr£3~êó¡Wc¥NÅ}Ù™¶VäK{žc±aµ2Ù~8*Õ&¢Ý™å–¦"ñàmŽ:åG—>„’(ÅøJHeã\»³trš%;Û!ßOVvx­HÅ¡ ëewPL¯¥±RÇrÄÞüb.Ê 5I½ñU»ÇP¸ ŸÕW7Mª„Ýtné©d‰~ÓÕ¢K¡H¶lUÊ'qúDRþ}¨WοÂbIƒ­ð€KÞóôï6±…êùø¨F½|ªNÒ«¤²%h#w·hà³RhRúPØ}ò­¤z5“O­[ ÍÒ~ù¤Ou+gYS!pü¬„oƘfWˆ£Š¸¨ÀqÊ=ΊL(2‰Ï¼ò`ØÔìoóWx–À˜²>Õ[á CAW¥ÝÀ ôoAÿÌEÐÐjO¶±a:SeŽG,ՉצÂÌlÄ –“úAÈÁžlKWaí†^ 7¡UÎ õ ÈŠY=’é†I]'Îú¤´s%‡n ûÓ3v”ܲš@ñGÇqÁ ÇùÌ&<1!X¤ 3>&ÀÛ™`Ï^SÖ­’˜-á òÓ!å81YVK×TÓIÕl5,õÒÎês¨vÔ¸_Ù=È®ˆÕÕ›‚—¦× ö8Ú:¶ŽÃÍ•ðSÂa S…mTí ,‘]aàÉÚâ+.òOêÆ’.4ÉŠ%Æ@=p^Ù`+#éš(QþTéÑVK0í'Ýõ+UOS04  Ï|v¥OÉÞ=!‡­T¼«>P¨ ¬£4öV1}%œ)žÚHí WGS=ë¹M[Jÿ@Óð¶¡q’|Ùð—PûfÏïŽÚ¹v«ÖjéeGrG£`µX•ŸT“µ§^ãr%‘˜ÃL[{ÁDÜÂ÷êøÃðŸ/ïp“0€ùüù kR½Õ€|ùt9Èšnaˆ¯Þ,x0žÞâÁ‹×+€<9¹ È~XrÕlcN‡Àzf±Ë’Qí 3¿z"67ÒL“2'œ.m]¢F›uÝ)1ftõö;ðÑZÔë{½® nç_C6ò…:†ÄnÓCÏG§zævÑ(µVf\~P›ÚùÑ€?ƒ;q`¡Uø“´¶ÄF¥;}3EQyVz#E!_–½— ;ËAÞŒOQÀ€ÞˆSQ|™å@?W¡d°à›r+Oi*ø$~E!­þÙKúäXkv\ü¥ñ%Ø †ÏAæK_Br‘N&ßÜסÀøø±9%oÆvùO‘À¨çô7¤r"ìO3Ê´Ti?±;îÆØßn[í…ãíÓyÌà’ emÀƒûáaÿ¼ oª',å%ð6Å{(«œã¥6…O Öü8J­fړ.5» ½Æ(MÜkìÂÑE¦O1”.™3 I†K}¸À¿Á9ÕiýèØÍgí×8µŒ[Á,fGˆï&NêúÓâÉoHSåYx-'i©MnÁ| RK/SÃJܵŠÛ!UÛ‹¶èî¸ò€Óó–Ivä”Ù‹î3Œ×\hi¼¹ýêިǎB<Œ=¢YY‚€_‘ÂІ‹"Y‰Ù}ùûÀ†u?º‚' .  .ÍÞõùéY6£Hí+lcjŠß·£|Kú­mQÕ縩 βtNe‚›–UI×1U<°ªµÕ quþU¢³â2[iNAÐÏø6 §:’Ú¥¤a¥ô„|ׄŔPm‹ç“âóñé®Wú®JHß`˜êI9ÏÒQé ½f¯“¿3hGo’kN-ò«´ôZe ¼Wwêµ½W­Bš ÌͲIèXPoJ'BšÕN›¢ñ×d®™âÒi6ü-‰¬»™é”“ºÔ”M]¦xÖ˜Š‹{fšz§Ï©ý .9É—:Åà{ÓAÛ·1’Tø šqªPû­pÂßRI=¯_Ök+æá¨&âØYšˆãÑƒí­‡œ‡cëT}´}g§)Ç<›M`2—%à8‚Ñbž£’(R+ËOÉ»q8Ï0Éë÷ùl=.ûß Ê2ë.&ù8õ»ÙpÑMß0ÿ¤Ï³áe %ŸÓl=Ά#üòÝ)&ûEÕì /» 8ÞM°ê7·˜®Cq”‹>Z¦ª'…õ­K>òs–©oÐã eŸ”ßhåp9K§ú÷õ4Ó/ËkóužÎ­D!*H¯w’€dp“û¹Ìèö}€dJwóÅIôŒ“W¿e°L9am^F')ð¸Y›Ö]Á–µ»k¨ˆ¨…«ñ¨›ÍÉSŠ›X`ÐÚÐú¹&‡û*É'ûÈ$4qÀØÕÖnɧj#z“.FéÌJ†‹¤EÓÓÛj†Öù ˜jç½Yvš]MÑXeRLNgÅb -%S4^ž)]‹2~xÂuhSEQÖ<œ¥e¦m&8¿1CŽT4/H Õm“„13ií9E–F÷´»k´ß|óÍ’ÎÇi?$W›­ä×»­Ö·C+¾úvwÿáëow¥€XV“%©ô­HËùL²+Ïð•óHád#:L'È-Cr7꧃óÎ,“ä?&-:ସ̆_Q%}Ó?˺'ùdˆf³øÝ»îoï’wß¾Þnî΀l,åÑö1V‹·¶wî?xøõ£?þ)޲Ùk†Ô¼KG QðÆŠÂlÓ[rô;*ŸX ¬\LqA`žÏ;3Ü‹TÏ<½½Eï!!íÔÔ­x¬²(Ù9i>Còˆ“XêS7ßî’ú‹9g%¿ÄaŽÓó `*Ð`µìjÝ)4‰ %ãª:fÞ&ñ¿Kõþû›ã»ß´¾ýI'B­Œ»DZ‰J”S>H‘Háé陫U ܃‘妓kÂo9JË3IܺÝDô =?òÓIA2c6vkF¸y¹Q€­‰ÔVž ¹„h[‡Î®WZ…mdÂËÌ9á÷\ [MQé9‡8 ÒŽîêÕçEp¿\ç ¶Ô¦ÿV;ípϘ\¤ÏâPléƒaX$ö6ñ½A›A˜©`Í™Txiðåóà>s‡lõ}#ú±Â®òzÞVdeëÚmmħ7¢¿¤iô·<ļóu´·ØÄZï=4ÑëO‘Qü °ÓJmDGWWÇ!6"å0ExãhµÌø$?¥zߪ­E=H²ÈÕ…åŒPÝW ž™pŸË«F$¸!µÑÿp”Í){=I xº =#s¼¬â5œ”Vã“à#¦Ç†ƒ*ï,—Hü@ç³Tõ$JÐ[rg™ò¬XŒ†pLåQIé€ î43áx¿Ñl1éÊVÁ. ÐÀ.&i6["áEîº]àeIÜrš (£½É kš‡u™”2Kgav× ÚŒ%ÿ™ ñÏ4¨Üͬ*S®ôélóò€¼ €çEq¾˜ÒFðQ.2SY› ˆv9ÀõG{¨…r®Ø'WKd ÄW4-üÁtÿÜîÛXåPŠ€¢} œñˆê÷+;Àò“Gqç?ÇW·wÆññG%ýͳѨ  Û¦¸Ôº Äb*WaÂÛM( @£qvšö¯á\£fJ–¼¹lѓǓç}Ä÷¸bVpl@ÕBÙx-®).šÕZ8}8ƒèuÙŽœIeìE‹Dßãa'1>)=&¿ ;¬½£ø/ûÛÿW/žá–†ß{ôúÜRWÕý M {/'E¡1”Þh ÞÍœá!ò­[Ná\ecû¼†|x í΂¼„˜È`ŒfÔŠõ?)çCÍ[à{±˜›ÙŒýœ¬ô+ýQ18‡NR’uÿ<»Ê‹yfÉ®´òÌt‰ýàþ†ì'µŽJLøH¾<-B„LB¶ Bäu†¾Ôw 8µ>aɘÇÚC¨ØÞó67m}äèSE-]ލbŽdÀ_¡*x) Ñ(µ¢|ëõͳŠKâíõ4ÇJ×mËpZD15|²ŠáHÁ‚;‚“Ç<‹Å%T`miCCÑÃÒ`ûQÿ {eÑ€ƒ«ÊA‹"ä_O>Ä»ñG`´øG¯<Ìgpˆ§[Ïvô—}cú‚èB)ÿßÁký„¼”™ccGùòϱn'Móáþ1+:V$¼ÃaÖ’1‘Ñìê”F@Ç%9DõAŽ^um󀡛g.”g²ꢮâòp„£ÙZœÃÀF¸gÁ¹!27æƒ t^S÷õÁëgÍ #1’£t¼ËÁ,ŸÎÑ='ųyŽ€i[ÇŒŒm»†™m¬É7ÎŒì'p!㢪6Ž&h£âK `<ͧ™ÚŽa²€ô­{Ü6¿ãz’FÚ°°¬ó“J9DÅ`°˜ýÙ€‹y¢ö ~9%Ç#Çj C;Øê†Ã!óšâéF9Æ®ïŒ÷ðíÓW?¿Õ;Ÿ7p>Ç C7œ#¢Òa¤ªšD%YCT‰5%tj% õ¤ a­»È÷ž| l™e‹?ƒPžC‡G×QzbKð°,.‰PëfÀÀD'tb¢‰ ³ VXx<ú£CŒ¾Ú‹¶¬+&ºEbøÊŠMâ¿ØKõ$…-aø•"Míåötmmƒ…Oí5J¤;’÷)‹–î~!EktBRÈu udšÎÓSØï’rua7zBì¼Å œÄ0pÀ©;9Éxw;δ–´ò)‰ 2ªÙÝi5CwM'YøÒ£›ö‰¨õ›®ØÔ>Ϩ{ 8Ô6ÒûÄt¥8‰[|c&Ó³a/T¥31‡ß–LýßQ²T4¾ÌÎ1ô ¬<¢ÎÊn?½FCàFü=~ÛR?°l0>NÌ×f°ƒ”]8Æf#|Ö™ë7s ŠI7œÄÇvus‹ïý\Bk÷äÊêÞÓ¬<ŸÓ{Øú‡Ç(ußd©ø&i»¤M?Z¹÷;£ÚA¼5ƒžÌcÃ@öoÁ7°Þ0/Ù´0‡&ôæc¿Å "¼¾Í« >°Xàæ® rH}3 FWÚ½»æ¦”õ:°ä¯‰etm1‘O…hÌÈÿ]]ѹE(y›ÈI…ÄnÕmPÙSðÊáï¸ú/¨G܃pm5”€Á¹ªÞÖÝ´vpƒ?G š‘ÑæVwkO—u ¨zÈåå«ÕG@I5^‡pßø™‚}d¨¹²pšm»)öß¾9xù#üÙó¶÷æ´;Ëp{™KfëG‹Ÿ¿=š½9þ6y·Žÿûí]Œÿû¾µÖ[¤–á‰G[ê’Ž›³yÏÛÂ5~iôúz~C” ¦™á|?;Í'Ôu’“•rMãA¨ƒÕb¤–aµ2ù†DÉ&CÓÖ­y9Ÿdkòú¤4ºD¼¥Î6žL^)©¹­•«Ï?˜5ôÑ¥6æz¢„~ÿ²(æŸOðþ„´‡Äò¨ô&ÄŸ£î8Ψ¶¥‰ñ®rê0‹96 ÆáÊôŽ2Êíé{®m-`ü 4ê”Ì… s7UŸ½|Z%¢øÝ»ß˜5x1˜1Ä>:?y&Ï[†ÈiÈ.¸¡.ëZÕ§5CȰ×Ô§º‚†­Þ½ƒƒ€Ýq*BßÜÖuIðú³,=WÈ£¦jÚ æ ‰e@w@<‡Zã…’õä‚t,Y Gq<´ý² ÛW1&{û—¸Oãõ ‹ÛHÊ#w¦w­.cX1ë' ílõ`¸,"ÿøbã‘», '¹S¶p¦3¡/d1¯÷ß>ë¼ ð—¸óí»áflsÀVâ"¥:Z•q0Ó ú7gÓ (ãP sŒ ·X3øläÄl$~:^Ö¬~¥0—Üm½6ÕL¾üùÅ÷ÏÞôþ¶ÿüçÀ|&ï†w[ÉQ÷ø[üò­3·“ŸŸ­¶I˜éEõä¨Hç¿ ‚‡°lš ÌÍök–m¯6׈$ü’¦Ô¯VEaS©†( j0úlJ3¤Ãþ–¼MYXgmMUb6¼o¡{´DœåÃ!,ÿ[jŽˆÚ€M¸E޵&÷2ìÙ“\ 'X2²–»usBîhp‡”Irß± ü®nbøìLÂ.ôŒh—ê§ØÐªmbÄuÎÊ‚#7Ù#¡:Cgˆ»‘ålÖkàõr#º(á^—¾5!žA‰Ù¡ºn—m9â8>”V8O·¡E.Ñlé½G?»P¸oYâ>) Ûm[µ&ˆ_ »¦ŠÅÝòÉ6tì¶+ºŠ® HZq/‹ùÁxÊv‚Ù—^+8<Ó¬dYÓeé.‡£Â*„RÎã7¤ª~lµñÄíêhñ‘>ÔÈ©dæA2 E—æãø@ÑW­'ƒi³©g÷ýBM'û ðÛn>fàðµ2üž*«ì†Í-Ée0*áí0Àéë U£;ËYÖõƳ"ñ:ˆ„üTVœÝ€-èEQWZÑ7âèè’@EË«Og´³9hïÆr»M„v=6òÆXAÁ«¹bÄ6s]¤Á>ùµ?FN”w³n[”Éþž&¥èÄ1ÁžU:Ü¡­ä%Œ?HAÇB&å/õ}r\>ú?ùpšÍÓù|†Ws?P€ËN9¿Y`ñú†‰¦ŒÞãÒgà{=©åÑX[]ùàMXTôqÔó6X,ÓùY‡ïŸGÙɼ3/:l” LM…áG—À½éâ{½•‹[i?*™v«§k««Ù†çB“†RNˆ—•-Iv±x—lì† M9¾¨&¦ƒ7·¸;>ͦ³LÔ°ÃlPÌR¼‰ýƒ5ó·×1ŽaF~öãòT 8O:ˆŠ°»ÐM$N`1Pz}^è5ŸM¯Ñv¼ÂÝhˆþÆÀñ+ˆYY¦§–½Æþ”Ôˆ¾£r´Ÿ‰È·Pqiµ¤ç^ˆ•Y)ÛJ¦#`yÃk{ouQÄH´{ƒÝx”NÎÉ Ÿ¯üÙv@àÀ0õU+^=KïqÁ XH™—Ò|jL®p¿ê¿GžS €J49XQm~ugØ1ª6ãw“w³U2è’escPœ|ûxïݤuo&ß~õ®D\v5….ÎAd°£€ã´1¼]ƒ²q>IäiËAÞ°˜ÄóêR™¨ñt~ÆiÝÀ*r‹¶ýtsO{À¸`Pñw–Ó‹G$+(ßQy—§£žÄNà?+ÔC›e:›Ïݪ›è©ÅÚw†—\6L*„K÷­•¨î>Hùb¨ Á0Jð#Ó‚Aºs¶T¹^`}´¸Ã뜌Ï'Jk,«åž`ýŒ]ÒŠnßY]ŽÑéD‡¤Eû, m8EYN™%&ª©’Jø6éꞸÙÛZV=\Ú<-.‹\b/JÖU´˜RhWËï î”ëþ áäCM-."hyMÀJÁ{G—ŠàUÛ§˜ÉàýÀ ´ ¨÷8n`8²ä.=oGwïž_2…+š.~áNIà•ë=‹}ÿ‹Ñimp>Ê.²Ñžq_ÒBm:õ rhâm8¨¢`œˆÑ¶½”ÿ¡Pd6›–•º‹é¯%O쇭@qå9S‚+áÄú*¦‰„J;×7"Ú)lô^ƒ¶6"U'°Icí(6pbClþ.ÅmÝŠ^&Ž^qNd Ã#Ìí2 k(".‘Ѭ ¡,º³á‘˜Òúê,ð†ÊX ‘°‹FÿÒ®8v—Ou¯FÃ'ø­êÓuŒÿu©_N*ëÈLì2Чt×¹™·ö(]‹?Bm¨¢+mÏÐn/”I ]õ`ìæK{õ+ÕÝd„ŽF"|E­-^çô~&}éá¡C èÕc[ôä|õÖ ËayKn¬p#Óù³è›Õ[ P/…€fžP:Ùcp7^x`Ü1g«‰×Þ Ì2`O§ÓYLŠ)d[£/Ð VÚÔkÆÇ]‘V,¸’Ð(.Ck³Mú»)Pƒ$?£‹´ŸÍüµ É»ýwå] zìÌÐ;ÊÙMjÐxëRÆ“oÇ­ÿàx=\…g¡(ŠqYÅ‘ÏPähÓ"¡Å`ÊcdqS¢¡†ñ‘",4£‰‹? ‘‰>t#zn;ì²7/àÈvf£R¶È;eíÎKr–ÞìñÿvS€\vƒýö³v\gϽM¼9¤ƒù8êe\·Ð Ä Ux»X¼5ò—[özòêç—oŸ½iG?¼zýüòà¯??‹^î¿8xùã­ºI¨bJCðc±¹]À\‡;+£,EÇŒ>üR¦šž°–^Â+ ¡ZN`3ŽËÐöÜrw0³…TÛTª¬ ´Þßò<›p2Ó˳Û:nStܰ]?x‰¢U zï+í‚Ñ«+koSÉ–ß3,â!*Âb6ò=)ä£âü)¬Áãj;Evcëu&eÖ¿fkǬ»Á¶"Jh‘ì/Xß.hšË£ÌÓA¿ÔÞIóÊ&­›ÓK½ƒ+‹Á“íßÏòY1AºÖiɨ/¤,§ƒ½ ÂÆž &‘!YžI_\ NGÌ”ìFÏ«~ Æß ¢×²Û×&›îÓ½ÈÒœVóÙ0YF¬v9ÔÜZem¸«Ö1c«Öð¢þÉtÔ˜Ôª×hU#_ç"£¡ ´}‡=íbÑTˆE¢ÜQêÇýýÒN´jdtê*ø³±—F3ïñt„ÖqªûÖ:P)ËÁ¼D›ÁĪŽèipËœ.?’óEK¯-ǂтK˾ժ ›j“k¬÷ÉÆðoüém[Õ=¼íìþ„×䉖, çÈTæRõÒG•£¶ÚâËÇ‹`•7ëgÚäD^:ÎüÄ~#X.2l¢Ú³JIÝã%POaR^…”È2G?PFPÖ²[⟠©a¾û*ÝS1õT]‹Ç6×ïB[C§‰¸)ÖVÇÞUpçá0´ä?gì¼RV³íÏÿVŒ9«S¥ÇµÜq‘ÖÔvzÍé ô~{3Û0º•xd3ê¨B –ñ1*.Ðlàõ«Ãƒÿ×qÙäæ³k±ACŒÄ¤ˆ•¼¹BûÏ‹Óúšé/ŸdÑ1¦(ó«ø†¨ Æç zSIçqÇÆ—Âu¿©R„ëˆ𘨴ÎÎ+âÎæ+*¬É¢™B4Õ@©‡¯¶Õrh?šÃº¿) “»E.GeÛ½Ÿ²ë~‘ΆÃj¶˜*‰ž]åóQáN€˜%¼'¤§JcO¾(F-´vIÎRÜTñÍW…qrt÷ëâÝ€Úþu3nâ4Z4²8¶I:ô<”ÄÕØF`×%D©¨Œ6¸ËUÄfÜo¨þoÒùK'×ñJK€"f|çìÞ¼¸GeÌjÈ!.¤ÏpùLEjÂݸRFb¿Å܃ŽÒ51»sÙœnŽvÉ´‚Wò”Ù‰F×3¸ï¥ê…,AK 4hC ¾åY:DË2åo£ž¡È-µñÊ¢äcYŠÆQmå$ å¨~ ‡@´œ‰¥ßH2eŠì!Ñåtµ(žçÙä×<ã…Áõ8:U–«´ÔDwG€ˆ@^Ç;®Ÿÿš©6y‰H­/^Õæ©/È¿¥å‹Ѱ§6–k‰šG(‘xþ¶×¤z ‹‘O)ð0‰)”c܆F¢=!¤ž"ªj˜——X“ÜòŠQ“ñFk„Æè©r‹-ЊQÖ‹öuñÒËs±J¤ÔÁRÝ™É J½CfÍÄ,ã…!{[Ó%ˆKö‰hx¤Í±1’4^Òx-¢"Z:”M”ˆõ¤¬]K_Ov˜§@UŒë ”¡¨_U NuÜ‹•¢7wÅÔ°â…²Qä=vËqÉæFõ¦–€¬_Ë{W˜z»«V®PÔÄV9œ^¿Ú‹D»¿X—Ìe~:ÁHœ¢…c¥9ìÝúËù%&>?I#²k ]4:ƒ"ðID üÐ]ÉÑüF8浦Í(ç“E©á+‹šBi2ÞìZBÞ™äe€ º¸yU/¸MÛf,¼Ãªá„Q  sP¬Þ ¶¥Çz;Ó;³{ƒ©vi/µ"Ø© |™¢RlŠÏÐG“)ïdìY2˦34¢o)ŒÓk"Zº)¡øhÜ, $r£MzÝÅzÖÄV:Ze£@¼õ±µÑ¸Àª5,0EÔtº&aÓþ¶ÈÖá/™êf yѺµ´a§¢Ñ®Í³æ-sn¼©Ã°²8?vðSù~»b Π¥å:C‡KéX:£óP orņ~¥ExĨɋٴ(uIKN{­×C2…ùCrJµpX‘T¢¯ Ò—-KE"âéžÄ¥³h-1w¯‡zs±öèB”0ŠG§Ÿ €Í°„ÍŽ»Ê()ˆÂXªÉaqœžNЀâjÿ1aÈè½H{@ª“¨ž²ZÐûÍ%ŠÐ”¢c¸‰B\2q•\»[†íëF2Æj;G‡p„Q X¸÷í+(H®›pWWLÚhÚSŸ”§‹]sóJ!Z¨ÂŸ|p‰MÅ*±Š 1µJñ®:*l†ßIï"‚m·‰°®žŠš+Šjʭॱ YáÏJ3ê97 ÷€O´¸‰ó>¨xi1…?à‹…S×‹Ê S†awYôJ"Ê,Q5éyÁõÜE Áb3²¾úkcÌqH潨¦)A¦*Í׽ʇ îÑësÒóÜi¤/&ˆ²@îhýÝâäÞèQÒ¥ ?·Ë÷?Y²n ãèV»Þjµ¿Ô~7½8íípüÙêþñòlšÓïGúÓÃû;;¶ÿø§G¾Þ~ o¹0ÞDšÏ°¸*² ðÀS4\©EåêϹ6°ÕîUe€A¶)1K4Í œ¦¸H8Ù3äã8ýóR×U Žó j˜€ØÐ\ñ'úñƒ‰¤–µmZ`Æ+vN3Õir†¤ÈgiyFwà¶‹Ñçd+Ûl\­bv ×Á,N¸àÑ¥Gå.g‹^öoM‡ÆÔá9˜â+ˆ­ËW¬¶“·«qYiÖ•¨ÊÊ7±ƒBrǰLr™`hL’6N¢k´BUœz¦³õ‰B‘nmÒ2R2¢éÇöês¾eȃÅ}•q9´[Ðå”Émƒ#y±x LY%¸Æû¾B*ý©6$ÚÃ/kZPlͨzµç‰Ã®ÏÙÃ$9á†G…]Gaäê!õÐ÷t{nÒC¸*xüSøñ“ðc1~:rÔÜôð«½À= ÕA J/ÍÒkâ2®z¬!1îú»Ù»É:GD§Ód”ŽûÃ4ºÚnônÎÿ­7Yxò'¹:Šy«8nGð×O|¬ÙTŒ÷0ëïæë+äYoKw e|Œ™ñ4jlW kØâ†ßÝ9®.‰'G =çë®·ÖnËN¾jÓ¼ß!ËlµSjÆàî•ÔkÙhð ¸pg,¡’m|Ѧ­ ÉQ†.*I0Új";ÃaÈàI7|«úö'zËP¯è56xI¦d)íÖÉ™ë]éš§]þ‹Jœÿ]ü÷£5[ ÷É4 öá¿ïa\îT™ƒ†–|. Å—ÉÎï9px™žïcÈŠÃZ6Ĺ·7Ø'¯ito/·iËÎawþý2X߯ –ÞR€¸Ø‘ôCIÎ÷ µ·¡e›ö5Ùz»¤rMÖ_õÙ&Ç:r)´ÝámìNÉñÈv£;'ëuKOÏùºÔ¶öÔkß!‹à£mlE/k{¤°¬ÍóïçŒ*§C²à!©—BPç² äòwàûŸ!óW‡fÞ…Ö'ÅÖuÒŒz!LoÀ™Ï[fÉ -õ¯úU¹ägŸ¼Î±Â•M 6ßSõÉÀÓo÷«Ûl׌ES÷Ê]¸ÛÛûìŽ ä¡ÞbÂ=ñ¯Úùbý¿ŠKSèX­²uædôÖ¦#Y¿˜Ÿ}µ^GøÙ×ÑË£;e›þ ¤>yûœVÍ ¤S^æôF“+ˆã6té³a={l’4$JÆ®°™ÅØÝØ uȉպ¿R›ë¢{RO»ò|b¦ ™ÌÒ-‡–j¸€ý ÒÑ¢³Úh:˜%›óû2|4§î‹žtÑÒ’ÀT†=<ãë§s`#ïmÏûÚò‰·ƒð3..2@³»›Rµï©Z5„(·³)<T ªnnÒ¦â͵ôs-mÀa÷}Ë#šw\µúíåµõ¯'ø+±v¢íV‹š .eö,¯BfBA?]ö¦iNÎÌAaa?§Wú¤·¸)y]f¨è‡·ç¤v ÓÙgM¾»¢õ |žˆjÉKÏSØÁÏ’a-±¬†ÏxÔbOÊò½Ã™—ÏÞ>ˆTdm&á~C²¦½ƒ† »GõDhSÀ"~ø¸âF`ñûÀ[ü å]˜¿¢NfžucrIj¯{ž]—â´ƒ%¡5ˆùÁþ`•#Z„àŽÕ®ç-(…ÝðÄñ\£A]a=OB¢Î;>¼Ù,¯´»X—NO¬û+=ч턵ÄMjâð¢ÞÏälV~~‚aúÄ ì’å(Q±ÜÏ# Ÿ[ö«ù2æîíõÍxÏyÝÖáòž—ÉùÞ9¬Ï»ÑŽ;ráüÉøs·*þQ‹_‚ˆ•ÐЃŠ>*Lu,CœÙcþAñ¹anWÆØ>å¾Jœ-v°‡­!îÂÒ1m‰~ïkQh¤½&jò˜Éœ¾2 AÃ;çdï^>›ÓŒèÛÜé„§*ÌYÓ&ÆÚ_ÂWSä“}<ê­ß)I“%ýv”jÎf 2ºðlßT ¨´õ Wõ(•^®ÂE /]r>ª¥+Wia? -‡Zát©`ú4«H¤ç{¶ z¾’?ß·#KçùÔfB« ž•—,†ÂçÙҥ؋žVö;^y‰Ú.í8[O—ð4%Rö×2ÙD¯”?Þl'øô‰ô· Åôì¹Tlm´Ò„jáÐ*Tœ—ö<¶qo¹Ñ”-ePö =-ežšY‡ZîD«~IîÌdýïîÁá³y¦K5M̳†$¾(ó´dIäjÇŸÅ>+À4­[:·ËI×r Ã+þ7{QÜë¡·O¯‹õ‚ÜeÏ2ûW1OÓYé<Ów€"²Š¥§ÊÔaam·A…j—¼î+rÇzMÏEÅ…ð6¦ÇÎZÉzÕë}»ŽßËùÞºó€lIöÖ½+ÐZüY6šʘ†êÖë»’rWô‚Ô}qŸHgh.—t!l3†9•ÙXCw2îNvEá•tgÌo¶H„Vhƒ¬íŠêñÑq[ºE‡d“€R "G»F»r1>ÁûwmNB©=Z ͹³ JwÖüö;kw«‡^gb›Ãù³E!Z/(´Ôwñ„»ˆ–&ºü£±},ÂÎéP_²ÙŒ\—Òr4h6G&’`•±œ¼Ie+·œqC/¸‡’*JwÒþÍ8‹·jóq§ÏŠK˜<èm8§dÂUȽùìSЬd«ó iRÜO6Õ=rŸÈ‚x7_‚ÉÁY:»'ÉÞÉ8 `±t§Œ¯ûV4Iû ]q—×VÒáö§÷Wn¬ŒX,Ú6æcã²~A%,f‰o¹ý'&Ú0‚)@Yë8x᤼îâ¡hÝB§êþžÇ·qÜâ¸ÚdšªÀëýçûß?{ÞÐÉ·ÜI´FÓÝIçw’"£˜õ_Î7= · ûŠEF®zc"\s2®¹ò7¦¶Òç„{Z¢„‚› Ú `F8 ýéaŽ ;W¬ŠÄ51SXfŸÝ¹&® vкZÉ®(é,ýV`5<¡ ` çˆØ6j"„ê_WÖ¶IÊAlˆŸìb´mžaÉØ-–®k-“É=KÜšçä–,†zª”^Íöm@°˜6ÐØ#G0{>KèÇöngïµôCá¡Ê¨çµ¦V¢%Å ¢P¿Wá÷öôTooÅ:$C‘Wó +o„¯DÝ– FBÌù¥vÛÞ1¦H!±‡_Öz@R;ÈÕ½?ïÂãä ÎíÝ­”ÁŒ®úùÚZOK™z˜{ °×ލ]ïFWÀ᯷@¦½ÃûÏŸãñ;ÃL^è@båræ2rD/Ía Žj/|³ÿ‚"ŽÀ^è"h[_ðMˆÐè"œúóˬvHIê*J´X2“G`1É=0o_½ÝÇw¶×pY̆ZVCÞAPÄVº:³5I€¬Z\SûºàÕ%,HÎäUœDBRAÊã[íg8°)G “¿8ÕÇ¢MPkúÄ'!ˆº é»z4Ѽ`T®›/He–~óGv¥’NO…¾£)GXgSY¯£Þ„ù@ªæ±70>ƒM¬-,«€JµÄaé,B†Ý(»SÉ äcÜ¡a¤‚ûôѹÊX³ë‹™ï0 a>à>j½—Ý×ä®nßÓ}™žKo)H›= 68w¡,Ò˨º$pí(!s*L‰]^¤ù¬Wi ðY—µ.Ί¥¤Xi‡FAr×êË ø¨öÜÌ&áÃqþÎQB´îcD9úõDÓG©³°SµHÑ®Š¶‰§WÆá a‚þÍ¢W$àúM?§ä{GÛÑãÇQn¬=f ÕdØ›.p†c'º ¿”+•‹ºA62¨(¯©iOZÞˆþ ³èéœdfhN±k*”¨ ˜@åÇÑfÄߢÿŽÞ›kV+ºš:V£2Føò·‰dgéeïd–ýR»|ÙÅæ»ä\OYŽJev«¾¤åáhxG´×£Š×<£=ø¸rDþëÍ™dVëÄ;äªèùÍÉÉŽ3È 3<³ÐéÿìzŠæe¥íq+²º}‘raäЇÍÍ¢‡ÝûÝÆ´DIeìdWã=êT'OϾ)¬DžãVhº“ T˜q¡jºU‹î6Õ‘ÎmF,ѱ@÷a«iAÎò^ùËÍçäu–ÎJJˆ :å/ ôUªE²††È¾F6® XÙ4Øš}ÈR^LKU¶Â‘~¡"X#9®$By¶¢Àë0øaÐ\}@EP¯Õpp¿æSj”ÍkqœS„k€ã»ç—¨ÃYŽá”3ÄOæì"ɱZÍAˆcXžg×(ähXÚSfZ\f”z^RVO Š—š(×û-[0æ<.]ØxìK6hÎnJ]%¹À8Y03÷a󲩮š»žÃÂQã6ö¥º˜é4¯ &žŽó›P50Âi'|¼â `;À@4,ÃH4ノp+ñoÕ·Ì…}:~Lì¥Ä¾9 0"‚P.a²5عɺ‡ƒ¨8ºSàEJÕµ$,œ¨Tô=fÃÊ$mZü‚\äUŽŠÌyÅ]@òDŒ‰àw«Â0šYGï¨g\KˆºÈAêŸôÊy>C¥øÍ' Wòk†Ò9(êøàˆ`Øç½¨™À*ËÖ—jȹžk¢nµÜcŽp%1W> ?áʬ?è~*­: }[$3)™öˆtÐZï8š&U]D%±«i˜ó!6¬i`-CàëDKEp(¼\äxÏP7ô©ÅžÖ:Lzy‡øsÕ9þ*oÑŸ+FŒíÍYrT ÎåM1àô)|¢€J¡M>éhFé2°Ò4Á6¼À !”ú—Ý31UÔtQòŽ9ÊæóL$}·µ\œœäW0c'x%&áFP#Žùž:¤/î´Xƒ!ã¦gy8F\™€½ºcnmôRÇÚØ……ë’ΘD¬ñèÊ(¹ÜÈ;-ñÀGhŒd8Í#‰ª‡ªÚ•SínKž_pw šz~e?¿«Ár#N>g¾Y?ÃÜ D˜p “?a*­êüTíO.·ñßÿ¹Üvßtè#Ì«Ëè7Æþ)ðÏ e•Úÿ£j\þ¬Ô(~ö‘òQêDD–«5é¡zuge JíRXEŸÒWZΫUuŒ8`“Ø ëU^¨Ñz¯ÔÉFm*Î]–Òô§°Åªº“È¢ø’Zg, uy3RÝðw„~©ú]ãØ‚[÷í*@ç!ýÇ'ë3xz QPô°+áþx³sl캘§7ˆ*«,™ÔÕ<ÕçIÊ#gKáÁrPqÖîl9 YÍØäo“`.ó¿dC¯•xe!´»€—~Ï[wïîOöT\hŒð«C%ù-¤¨Þ MÂï•𫱜 ÞÞ9ô =PæT¹Ê ®nR4@˜™þjj–š“¿à•hžEs!žzY¢aqâEÕŠ¬7ˆ‡§PÕ1:»£3ˆ[àÑ€¸ê&·¦ÅºÐµÂïI®;Áp3±NZt庼ÁÁ ²ó«œqž7!tÉäðŸ«ÆòXàßBäR!Ò—"á¿û¶$©()(Jz²¤-Nzr£': Ô€´tÿFÒRˆÀV%¬U Š iL᪺Ñ&èF¸èg-WÓ0E&©Ù„Š\ÉgJ@s%´\D´à[%Ài Î~[(ÉÏÔ‘MÉlNiue`¹¥óÂé—ÝÒy*]Hé««ئ?/Ô{µÕ—I„¡ð>.[x€n4—-¸¬È+‹­ÿ¤+¿†A5 …ïb¹Š©ªÂ±i럪9%“„ÖNâ”W`äw\k@§<h¥òZ uHÉÇj–̾ˆÔ[±ãÀÜ!ƒsËÆÈ‘ÒÙr\+"§oàQV­’‚‰éÀPÇòÈjcWJPT¨br⮯„gJkGÈ$ãç…Å Eh̽¢åŽjiçìAnÃ|–zŒTà œK”D0KLܪª‚î+˜aF…ðíè¼å7€ã^#¡6.tÀzŒÇgïJ(ù˜.üP\Ü zDï›YKðb4„îÿ@‘Á'!áNËqíˆU´H{Ñ$»´<´4Ñà0bÿt^Ö'ÊîóÒmjõD§K欖;âG˜ŽRš`jx«­3E²¢Ìoè•Cº~*8.¤éS—òhP{•UÓK´_¼‰é¥¤ Øþúþƒí妗^Àøeæ—:€{DÜŸfvyÓ°ûê³btÿ[Š˜m¸³³>la'æ.‚ ¾¯|ûL@¸É@Ñ1š_­1Ʋ˜ ù¾Š+ ùl2Ÿ]GG/>Ì>¶_|˜ŠZ;ÂMÄ=9è”ÙC5=¥°å@ 56yñáMþqïŇ·ï?¶Ô<Èx*^)ðs &óÏ4ót ;9éøÀ½úâ•åè:äW·ë f$`˜×ټ׿î}°çˆ A}×;¶pê us&ˆ*i{ ±0\NÞºžþò†G7€û|ÀŠÆQõ;ôÛ%öÅú0Ö®¹®ûP š_îk`2&ö2š’ÉiЬµ’WÏpC'ØŒîoøX2™œ@wt 6ú„þQÄ혿E…³ôÂmôè²ÇkÓ¡±˜Þˆ{ø„ÖH(-”éX?Ç`ß›¯4»2‡¥þ1›[´‚æa>0m‘Mœþ6}Ò;A×Xã?̲_0¦¤5 0”ja— à‡"Nd×ÉEkWmÀD7ttq¼i Á/·²Âu:&è—£ûC[m„¾ÿ5E\Ž“íÌ^–T·ó ›Ž›Ä–¥b¿G/tmnçlK“ämøEÞØ™ØÔd ÷ÅmïÇͯ¬~+Ã6~Ìû½èèh‹Ú¸ ³áqà‘®Žþmò©Í«|¼°’Ù8²úeûT(xìÍ£_]ýHqty|¬¿ŸS <§ŠÕøš¨Ÿíå°l¤Ù Ã:vÚYÖ˜ãöû£ŸýNq![LÇ’$Ô4ðüƒ á£lÞùLтߒ!ùf·j$ p’Úá) +PïÖò›±É£:«Ò徯 …k4ºÊÇ(¡x«H´‡Ãè+¹“H_ý¦8Ä„‘‹ ç1û ¨ .'õÛ0ÐÕνշ¼6Ã-©šúvMá1Þv¬€ÕvÂxÃéý£µTŒòöè}«fãÿŽy¡?Ktd)wÌ”áW_Ú–fºÐ{.D|{ÛÃÍ÷]˜ÅÁŽäú¤«ÖÙ(7<ã}õí{'j²IêàíýñÊùÚâÇž8¹Ý)ïÝ)Õ„~ƒ)åg¦VŠªWýXtµrvª3Ú¶t—9•YFSˆœ«x>VNÂL€²ú^Ï÷Þήƒ3ì-TìK4äÕåEM6û| Ç1,Ĺ[Ç;Oõ-JËUVà/ѤS»èÙ[ÑôK›´o›¥“Ñ5aÂuòufÉg˜‰`rMCǾ'˜ÁÄ+ Púå~²ñ8‰—Ù 9¨sל`ñÒkWÖÁ®£Ç¶ô_ª L}’Ã{t§YÎt$üØä"Eݬ«ýŠiÙÒÕyÎ;`Äš~NùÎ=X܄߼Àc;Çe'sL6<5ùÄiÙ¹¨ÚƒF:1­Qi7ÅŠ„oÓE1,v£t84ºÉoƒtio=;Xs™•íÛó¶ÝfÑÔ—àø7 â·xŽv£>dî>«ryÛÅñ±-—å'†ìÃá/G»ªØ±SÙc=•kä9tE'i±A„«‘l}0“Ím3 ï[Lj¹Wb’oAh3XàÞ¤ÑO-¢œ4RƒêaË­Ù“Fö¢øj’ÿ¡àý{—ÑoQ¼rïDr²÷Ьlí¢K_sÿ¿®¾°»÷°»}rçNìú5›Ð|O¿»qÃLYM!šþáŠQÿh57OØQ ;Ãú¾ÄwU­›)Gº}Üø;%le¾‡1*ÇD¤¤ØÀ÷D 5IÞI ¤ØŒØº.°G¿¹Ý¬#Œ`„«<úfO“XGWx™Sëðú(福ªÆqwöŽÓ)°<*0ªæ@vê×—¿½›ÐŒéÐ.rQÈL¬„4TéNÙÙìÜ)7¡*Š9qÇ`¯á¯Änõn@úô§T@×™=<”BóêHç ^É+þmL£`h£Ü¬ã;ø³2ߣ÷†—UQÜ,bêÞœDž€‰‘5¶fLD`¢›Y‚ÄY‡0?Û[[Ý­»^Oî9‚eµ½FÂòZð¥è(íÃû0H/8gü†¸?”⊠{#Ðvz´«ë e_ü9My¤nnïÒóoâðÈôò¬ZöãéâHQXðZHpOÛ3²ØŸ‘#b€Ttؽ—«ÃÆ-A’*›ÚõGOlTV[P'TSÁÃ×C< òÍm7)6~ÔqE?ÐGT¾yg›fALv‘ð8H fíbÎ;ÛÖ&õ¬í+ŠîÄ›ÿPuÿ±«ñÔm1öTzHžSV >¢Dñ0X5¤X#CÀl\$ψß7¾I!õI¤¯`V¿D‘ÄëoLÓqÛSéK‘·ª‰˜•nö»'žâd7¶Þ®fåÝ Šâ±×=™â9¶Õ ‘‰nLY ýÖí¥7¬lo=Üyàd=ßÞzôhyŽõ{* Ï²«U&é3R á‹Uõù—d.öñÓ][ã„ÂYMòÔKÉüˆ:Ãf¥Uù[ݵýÒ$÷€Ú×’4zPž\{qKØHò ª2ÛÝhˆáEÓ’Ù‘Ÿý †[k÷å÷ =ÞÓ¯6á[®EV"w %Iä"U&xr2‡èéŒåtŇ|˜-ü»#lc”]¸ÑÊ\¦µmq+(ª‚hms@äîGV.ØÎS·JdYVŒÒÁfÍ-Ö¸¡Äù­Ýh«½ÝÞißo?hw»] ó½sǃ¹Åb”²aâ(ÜjVpWγ)Æ‹£üKïa.‰Õ\J{ ¡³ FŠÛ‘Ófyž³Î½Ü>¡³ïÔ‚ V2ØÆ›µbjQ~Q]‹­™Ë%E hQ¨mV×\^ÆùH¬ß´œZÓ|p·©³,"³^ûÎÐUÿ KO£†Â1ã (â]¢N¿NJÀT¹£÷(°[ùÿgï=šL¶†aëªX°WÔºI ”ÐT;( XPP1@€Hš)ݵ÷Þë®eY{ï½`oX±‹½‹×öÏ™™§%Ý{÷¾ß{¿ÿcWHžgê™3gÎ9s -a'¬È%4š`MñC€“pÆaâQþަ[˜¬3h¨¡(/›¬ÑÂâ˜!Eœ ›9ƒ J«$YOcCèXj/ƒ©+ øBÈx%$="d¨Vh@¡Ðú‘ØŽF´á‚áFØ„­l9X)G:z¡º£­½Ò‘KUJ(  "M• vá ¤~ÄódFÒµj!€¤’Ž=¢€$]¸6G]pŸ’øŸ”Ž$í8äSÕ+ 4ë{ŽÄ^!ÇtãI4õ¾EGÄ1Öl!°©• |ÇÍŽÛÇešð†Qlâ•&·Œò‡ÉC®P§©ò•(vr¸ Å2øÏ=­žãûþuƒEN`Û&2ëÑîeIÝ Z9eri3Lä˜ïÔÇÐL[¤Õ…WÆu× `‰IÚbdÉtU8UÒ7‘%ƒÛ Ñ~a²!#›_jA⢠·høímGöžEg’¸SNµÁ¼]ŽÃÅ‘ŒÞ*S:f®èjBžrÑæòüBlUˆìr¶@:~Åæn§ €™å8ä2ªž€ðó…”ò"¥˜üd¬¹) VÌ™ŒÕµÙ^(ÜÒ—Ð"uºþ>Œ,rÁS»LŠ•&W£J£RCì‰t:m+ãfÀ6Ò4ðS:úÅÂ$ˆxïðño$N4l©;–ÜfR:¨úû3aÓ× MCÁw ا±¨\TØT¸”`BU“Ãá‰ÈË’XQáxpÀvtDGKeÌG8­™ü¤n4kMg°Õj• 30º>2Ù@Í¥ôA²E8‚;ãEÁ„¿ƒ´ÐD³®Ô(CINƒ"˜Ê8^ N f/Øäª?º&ŒN™”•ÊØÏ¼ea_[-åÊðSpˆå6f“ãlÝ2ÚZžM䤜U^6¼ ¶Â”XeÄöRÁZ ›€óXë°]m(¶Ö©u‰é¬6Ò­öNË€çE‚B{Êqu²U¸ /Äѹrß=ỌÒdeØþZ3uÔê´®LÆpŠ} ö yÎ.Ð@K s¤kÁDj%r o„+¥Á•†¬c¬‘áÌ9—±™I!Œ°¹ÙÙ(•é~@iœ%öŠ.M $‹ûžÎ"I‚nF‹isŒÖm+bê׉zqÌKÃôDø,âs8ó†À]Ç‘àðÔ0LòŒàå£HQÛÝØ… ®eEÁØltŒ+RÅ!ÉL§á0r™Fh¦©+„š-"3ãv[Q¢Øšd}'‘š1”qªq‹J¨/óymQ¨µB­y…`¤QV­È˜rÀÃÁ¸¤,¼e#èY´”9óù$#ð=„9$ßï¡ì OÍâ(à6[ÜÈ(Æküpº"ÄÔ!ȃm~ZäúÒx®ü¤4Ÿh¾®Çñ€ŽÀážyâgžüg­1›À XrüÑZL|~5«³ßF=8R•,Î4FÔù5Nž˦߇”£g? qž’Æïé›A/Iœ‚þ÷išþ)~&>±1ÝÈUÌ|F y¼NcgÇžkX!cŒSéÓÁÇd$¿ Ú! •9 DÁø1Ù¥Rz+" ¬ùª$é™BÒÑ) ò·–Ž…*ôvÔïÉwh…Ø.UTz^[zƒ.V«ÂÜ:-ÁX¸2  E\z¶‘̉Վº[î˜PˆÕ6n…1u ¤ «ÍèqÆÚ½±µÓià:ÔK`¬¢åy2ØßD±Í@•kºœÁs€#V• h à]  ïŒ¨Z˜aù¿aT^Aù¿bL.„é²Ùñ¿aD.°\·2 gæG Ç­ŒÆ¿g4NÏ[ÄÛñÌ=Øb~á®*Üà/ –;](Wó[e­}Ñ®x%˜Ä¤<›N†wDô ¤€ÛT4oF{ŠÉªÁ¹>0OXl ¤ç•£{;Ú[Ûj¡vÉ’ÙÞLƒøØ>0N¡Ï ÖǃÜ1Ÿh Œa2èÓ”½)¦Ñ¶Ú GÍ– ÚQHL+pC"[ÛȲfÑ& ßD6ÀÃYžb vØxB–/!{Ĉ='yk/拆bAº ~ ârcQØj¯tC£¡þÜh±w¬< Þ‹'` ³©°ûÏÂâ€.>ˆ0™µ¥ áä]¶[¡ÈKœ¤îl/d7€öX­þ?±¬Z±±Ðþ­­À½ãï)ý›âû¦àSÿéayX$ÄPbË"#X<Ü|þc›Å•q¸¦†¸‚ÂX‹Gñ!ÞÊoÍ ëÈõ ×$½ZIR4:-Îý  –ܤì9Éä) ¤"CÆ&©£æ612¬Ø>Ü Ñ±@Glk¢ÐzÒ"i¸¨2Ùby'¾+)ðc‡ šáªñ®k þ@äîf¢îxP."‰Ü•y$u‡9õCf”*|ùR€›%áA›•A¶ ccüÿ)‚¯sfØ€ÊæqÅP =‡n Ì—‚ ®4…”·I]<ªe7w ^uw=AüEên C­KŒáÂÿÈm|)TU¬™ÍæÆŠ^ˆY7€Æ ?Y—ÀÒËÞe‚í[@ÂëBÖ4«Ú,%¬A.ñXÜdWð»‚ØK~8ËßÀÎh [£ÿ9q ×vuµ^Jbà§KÀÀ€´K lˆL¦ARÑð›8žY•ÈX–?Ø¿›õbefAΚ-§eÇ6VÄéti ³Áö4VêàveÒii•îmáš ’]ae'N(°,`|Éú° À/†$óËÊaÿjÅë…¨”Tˆ§/uŽ {t’‚¨õxƒ"•ñ©S½0§¸^g4ª ø‰^iИÉ=:Ÿ[ôT«4*‹P¸)@[i¤Jk`Àé’†¸m67ñèQŽ€óÖþ‚ÔÎ:7ÂU3':"ªS1âL6!Œ²q8®=¿"KwÌZRØ&‰n…â1þhLN.ˆâ¢ I¯¾‡2ÎõJˆêFÔ@32*Íñ:WnÀxFñ쌄 YNOÆ„àÚ6&™Ô ùcE<ÙSøñŽŠòH“Â&½ŒEµÄàƒ©þ˜Ïk6VØ,³™‘#úÁO¢À<ËDM›6•R:ÝCK­Ž¤5À¢œ!¦Ô¥uÐVM‡PÀÿÙDkË~{³ðã(æ…¬°ŒriÄzƒX~H`¯°á VÌrZÌ"ŠeÌu"GU½£Ú%5KûhjR‹ÖèSÔq·7ô¥åí( Y?Q£x0Šއp1tø8í^\éÁ˜«+žšìuh‰¿¶:æucYÅUŒÀìëÁ5G÷É=È35¾‘³Ò3ÉÞºLýÕ9klÞBѳχ†P]È@ö \îaÃoC^º*ž!7¦ân´€„ŽÏ‚³Ç-aò€—–ÚT÷Ï`£Ê€€M}?2Ãh^à)~K±?Þiȯ€–8!««°Gs¦˜×Tó>Xe°ºð¯.Š%ÁeP7›kÉ£öLz=‡7:ÄÇU‘-o«>Ùá Žæ.—kŽ¥KðàÇ6´Q :±<|%”„÷B”‡›"mè·ü–{àßø³'þíãmíæA¯C1ç‡òh” …ñàkaô@Fz•ÄÉDEÏùd„±…â_&âÕÚ¸Ãf¡íÜØÇBAñßæÑŠÞÉù‰eßÏÙ}gÖ1 O“[t‹˜8J ÷ÖÒ™¢ ÑC« ït–ï;3²6LÆà 幚‚ˆ€'1Ü@(R/È•„z6k…T%üÁëyëûy¯¿w?O=B<¼å^?îØ k⻹ŽÃi9QW…6YÔ†Eþ¿%Õq7|• Dˆ3›D'f2ÃõÓ$ÂØFžc £>‚…þ˳“ RiiLÆÚ”ožÄ*AÈÉš ZCÜ ‰ó©L7R,Áá(p:C T‰TÁ ÑS±•³ÎDÞË8kW$L¤`[qNj“ ÆÉ¥8A¡e˜6:(‰|“ro<o¨= ¶L”"çfåVصEÂci¶yðC²&º®´‡¨džË>±cè¨Lç1°t2‹“•aÐ5†—Ç À%bdo#‹¿¼ŠüXõ$«½¡âïË…åtüö nc¡ r Ün4R×0æûˆ0‰ske2f¼|:=’!¥ aX¼cmœ¥D&CR.D!+›MŽE'…˜I f|þ‘4Œ¾%˜Õ4=4º¥M$ÄqJ@ ‰Å0>„¶(eC0kÙ›qæ=ø‚#µw1•ø.Š–cÌlYy I|ÙD"çÀÙœ¹²(^õRtmU)ø”°@0ÐM1 ú 9ÑäC¦{ €Ýð– ++kÆçëÃ`‡}àð‘»[!oed[IZ¥š8ƒí¨bRÄÐ!Ã)DÕÌ*錴وSG‚Ñ<  hk“dXì~&¯j—†aCìS²Ðß/µ19lÒ%!È’Æ$*ô >ù<[n£àšR¢ 1 €Þ3 2–LXÇEÀùZ¥3¡’˜,ã&`M’}Ê@º7Šâè/8¨43 7Q8…rÁÆMý”a„\Œ)C_2nð#\<ÚÂh+š3žq?®¼N¥¢æìĬ¹i: •% f¯;maú™}AÉ:Cýx‚oâø¯ ¾ÆûïÅf^{5öü»ÃTt<ëR¾ÇÙD²%ÿ/¶=túÇ~P·|ˆ‰:+Sh•™(ÄœQí!Ób…CÑ÷V¢6ü*X)U¨óD†ÚZœñ'u§Å›¢›2Í$ W&j˜H]2¼8àd« 6ê¢D ÌXÜ6Êd"Ï&"‰“¡ "$há<‘h€6´¼©«—ï? L9üBd2,p•‰ž8˜y¬# Cz€£ÜÑÒÉç#Ì5@6Mo¥W¨ˆ¾•7sLðV†¸`”¶%;\#)D)º8E,\b§3þ{Ôób‡‰=e"¾ƒ7(…±ÈØÁ3á ¹=óÐÑ¢Qˆ'`K!Oã Ž!Äò[°§¹OŽ\1O®[ŽùË+æE‹qMqE¹büuÁÎj&¿õ(°Œ'[ÆÛfôÞèÅ–‘û } ÑêûY‚§”&¯YÿArl¥ûä)¼Ïï7’ìGGþÄ›’xõ“± };]j?‹EçÕãŠXô m‘O±ˆŒò½"= œåÀÕƒ½]„Ir»ˆ°”¿ûä†Ñ¬Åfºô‚‘,{j¼ð4ÆÇ }ºŠ’y}¡ˆÁć&z¥òS¹$»È£Ýð%‹„™5ÖC£Aø^pk˜_ïC¡;à°ÿ0ÿoº/½ð3/¾žÇÑæÎže¶7•¨°ÊžWö,´2»×¬+s¯¬y oÆ} `%/áíé%÷ôð!O¼óÊÂK¨ŒqE¬ºòý{ê/æ½OaêÔ•;0ý{K(*¬T«Z%p¿]¼Yý¯å>øfB°j".I§1&³Ì¥^ }N Aº.ˆÊ¹ÂH9xC‘"+ \J™˜"¡äI¸ÒDJÄ‚CS‚ʘĺLÀÕ$w1ŸbŒIúÑŸo‘啼}}c¶”õ‰ññòiüEfGEV™‰}Eô¦Uç`LÄŒÄr‰Ï:[M¡6)RÀ÷‘#¥¤?]Ï~}:Ze­ÈÓ ÛÅâÔkP’£ø‰èr*Œz­˜µ¦(á’B º· ïæÞF§KFÔ=Ù½3nǽ+âôJw_ï¦^Q+¡ºxÊ2¤ ð­U2ÑH~87zŒ¥áJªLcùÊÄŒ—r 5)UÚX¥!+ïþ>XìÂÍ ]@ã°›ª vüì b‘Ø­ŸN¥•pË¡HQ¨Ô ƨéÄ7i\®(µš‰HÜ¿ðþPQ“àx]b¢JɘzÖBü±yÅÚDtÆ'ANˆ e“Ä"fرúýu¬".Y¢O2(qé0Ésî©1‰âuf“[‚ÚlL’0mróa>¡é‹Ô &R S.Lè¬úâVè øCo§!¹ÐâÌÉN Í×ÉR:³Ij°‚¨W¬ÑO§V*´v-!ö’Ÿ¨53xÊ;´fð¢MpñüÇP,¸~b×2Exfë¥&`䌷Ø\aVA€1CDÔ¦Ò€ï&D ƒãýDü /’{ùÊ.{¹Ê=\=½Ero?o¹Ÿ·w/E íZ*X5¹Vµ̆ù÷‹Oh©×ÅêÒË›5ã?¦dÌ0«Æ|3ÔjU,ßù W‹P°òD¯º€ø«`Âa!D.{!¤U# ÀÄŵY¢JT‰nv‘[8¨oˆIŠc;bŠ€ÐBˆtãZ’k“•`qŠÒΊs^+²ÌØüÓPîæáógkÓA)—š¤Cx£ert" MRš X´ÃJH- ˜„•¸X‡ê¢ÕBM‘$Ý*mœÚ:o…G°gUëÒ•JzC$¤Q:\¯R+%±¼*E¤Šp$ plŽMéje€£^¶y~n¾JM3Çæ’¨>þÑÎR±E6Aƒp)ÐÛ±ï+nBD71N§ÖÐôLʪäßÛá…N›ØÜ?ÖðC5˜™È=ôiQ}šG»pµx•¢í ¸í (+éöG¯ýà›Ý/N $”I0Ø*cÔ+´¼vŒ¶Ê$úBÛ‰WÚ,cP*“IðYhGm«Œ Ô*Ò|³Uv9ÑŒPSŒÊèm–ØJP€´3 ÉV™~ 41RȾÙ*“¬3(¹y%ël•1˜Fn̳eðÉ ²Õ!S«s1Ž~1‹&ô£8oƒbüOs3©LjˆÉ„ýà°zŸ+â×è]4“§JHŽ%X³ˆH¢uÐ%56­bPì¡Ïž?R¼Å‰d¨°h”mˆ×£%Á—«XåÉ2èRµÄ#ÒéŠ$v„kÂ77FF‰N\‚ ¢Ê jß-4A0£da@¿Å¡`?¦F{ }2šc5ÆÇ`“:/@`–q€'˜¦–(–i5$£•”jâ+7[æe?4Wä–~­ÒŒ“o:¸“H ùiqÈ8f%&ec Ìæ‡sCSCd¢·ÑÎyrUŸfb‚P;Yni #ÁëÍli 0$*-mÊ! wÄ0á®p\.RŸzEã68 †‚¸9öõ–2‰É™=D-™hÄ-Ì¡ªx” ߬q¾*`ƒËb7§Ö¢¾­ ¨9÷V´~zØp•;A-"¼Ð)P R–Íúž‹L/&N…y-мáîMɬm•$o¢ù·`Ìíª Ï­PÕ +t6"¡MXº¶6P›À„ÌJ³€PNà ýÄ+%Åñ:DI.#à~Ÿé›paÔ`Ò”gJúQÐ~¿†Z óáë"Ç@0_$3l, ±ºÇ‹ÇM÷ó¥! qg±7Æó¡ ¢ö5Œï~' ôÔ$¾šÓØ]îØ®¢µbˆ–¤NÆF©Jb8!R,¬ 4BÛO2iÔØé„ŒÜ 'ñî"ß LTRBŽù®k!×D%ÜŒJP¨K 5îÜW%Rˆ½EÍ&3J>@úœ7~+²ìØF3@4P‡„ŠJn—ÇSæ•ñĦĆìܲ–$r†À°‡…mú"£6é"¹eȿbƒ¥ ‹RÀÝS»Jw˜Ü2”æ@Zt5ã/…(?¤µ`O’g›“Ʊ)9¾ÈÐH,7Nlj#à€†˜U‚j!ب FÓ¿MÝH÷…û7©ü-„¸aÖžF]!Ï© -¹?¤"*ÁLµ œ©Hû±«Hþ2[—¨—µpE@ÙÔÿ|y"1;DëS Ú’áÞ£àc´Õi=•ÏDx/Êá?3S7Ä7 ¦o5Sæ!׫]ûÀΠúttÄ:;vÿÃÝâsã‘d Ì܈«•® CÃÒ›MLe¦P’—ýHfø(B¨cDKmÇI¸T?@Ü6,¤\¶˜qŸ|ÝÃ?q‘4År B…šÄb§µÒ’`XDÏ$ÂeˆvÄl¹)åýûð_Þ‡|ͺ‡Ðb­ŽBLg0Ç×»Ølš vÑï‚#AIýÚO$Ø"´ml(ŸƒÑU"Æ£jntŸ+J¨ýn• ±›L¬.ZÄÚÀ×ùä¦Ö¥* ©UIUW8€ÂÇfæVØ£VoqÂ~+ìªÚnÊRóH3öðX8©õ@e0Ûð–Vûn›QâþfEâØtðü@Ø/¶a ?BâW ( fm@ <Kn äß(Bw†à¯Ì…u:Ÿ áý(’ ç`ÏCs©›£Íæñpa»t JØ|pcOåÒ?dP!ÛŒh¸iÊ^=’Zô…Œ °n‰É*ÚëpJ¥Æp‡g$dű$GÆBÈ2E@³kdlîˆ3lXtXh]›Jå#í˜px%XÎ0žYQŽfÁ'K@:1†_ ÖQ;È ‰ósÈ`98GU&~(¾V2»ˆ Œ³ Û˜¯íÁsokQ®>°S…ž&÷ Û´:‰‚Ñ7ŸL3ö”{~ïš^'’K¶6ôΑ8Ha5sñ‚Õ׋änr™¨«"]£ÃfL&ˆ –MÖÝ£©»ÜK„ ó¯^œy{{øxûÚY(éAëÖ¦}§ÐðŽ=‰QH¡"ÌN‘„/ÔÈ áŒb‡S%Ö7c@Svç,…Ø€pÏ\ 4È*àJ5IŒI°Öˆ1Ž™G_âD§žû ü„`$&Б¥‹Ât MÂutG²%&ޣРM†ðZ©‰U3ލ´3:f4†DÝ0~f^ô ‚DLc.ˆ±æX7Óë:öºX‚²Ô›¥T•-² av»Ik¥"‘«+&$4Tºu)Û°‚jlçgvPØ•É`/w`‚Ñ™ô³îh l¬woüJÄ_9„¨*:¸·WÄ©ÆË¨ºÍÞhND'˜‰€…2š4ÂïYÙ•aÏ’º‘Jš¹{8(ávG£×Š):ƒ-üœJ#0œ»QHð÷npIJ93µSë&é%ëHïà:YÔFŽŸ4Êœ¾Út.=á»É­ ž#c'Îç*¼ñ­2no;4YµŠ$­¤qÛAѦi0ŽŠ µ.Qg6ªÓɃ6h5 Ì—0IÉ ¨Î l`TEá²L,9jIDMW¼[’™X½FÂ%I 1¶è°hØí°tv ð1£PƒÎ%È¿l›­ÓáÀˆ¥îßñý˜+M¼ëâ”8¸à†—ËÌ I„¯0²êatŒ€‹YžçºÑhÖè Y,b=ùU\T¬ƒ!h"iÚ4N*J…«/,Ô0×vèco;•–\¾cŸg0 ¤öœdŒíƒ$CŠ×9ÙÌŒâ[ò+ÙÛ°Î8d>tWɵ*Q¶ƒÚ 'JTèETîh=F‚'’¦¾R =IÓÆ`بĶ'Ü„µLV–8¸0ax‘Á¥`æL‹Ú4òö„¤‰œ¢w¸í( &‡š»q‹ßør2j0(†ªðÇÆÁ°à©Þ]<Óx¼Zn@1‰¸)5qFàm¨ÎxÜŠXÕî€5ƒ-p¤£-ÃŒOD0#„v“R£OB€€ÓLà¨Vj„W6¤jœEf¼àF²c¸2h©0ƒ¥Ãî*݆4ER'Q1C[µ #fæt|õ‹^£9¬’(å$ꃗ„V¢‰…‡OW¨WC¶$"PÀ~acYÊ¢QW°‰pBT\K£3²*4lhq¨Fåu-XT)!Ú1g^LûƒD$’5pàB IÂy3´"¡”üÀm!.4E”*ľ‹5jÌÆa¦ÃÓpè  †+çÌD(À¾Ãf¦tîÖœdÆ€ãC!° ­”X¢‹S5+‰UŽâxÂŒ1 Š—‚½‡÷:]1kÍ£ ªÐÛ­0â«IŒdÄkѰylà9‘¬4¸ª4ˆüÓѬ”ŽW¢³WEw*ðÌêkƒ}ùÐä H¿ •n÷xt*šC¨ D‰¥z¦É{ð&IM-Ä„ö#΄ùFK€í²i”5x‰ÁJ(8¥î €Fû“k1¸c‡ˆŒ²#ˆ¶7[P[ÄñAhBa>"•³%8ž…‹§^gu¸`ŸÇB—ä¬C,ìœ¹Ž¥qj “Nžiñ†émgŒC§9ß…o°M#ïpâzEœŠe¨ ϶Nôhcµ*8¸ :Ô°&Žp…&@pX 7V鋘jPèeL´Ûöã’0®2ª g q÷ñ5›¥µ¼ÛOøÈp^àâИ¹é&"’ #<ÂBUXȘψõ`±è¥„V÷¦j’¦D£ÐKÔˆ6i0‘8%0£„wkaˆk…õFÈä´,€ŽQ½ºÄð8xHpôug\ {2bD*å‡Ü`àì–€¸h 5)À×,¤M©`…¦Î WYV€·Î£áõýÌ­ÇÆYÅÇ»ñ÷C8_‡ï»†à’—êÿˆGÈß̈ALÙ8Ç Á=‹ëcµÁ‘@b”‹uÔØ8Äo6 ½ &Þ‹q >gá&ÂÉ7¸¹2‡WšjÑb8né…Ó#iÃÀXãb@>B ôúGÀMUR DŽ„S0«?á™R‚Âä(J1Їkr$ý8š $ yÏ|s#ªn¬ÜžvÕ%¸"vL ,16Ì„ å„™qaŸˆ¤mÖJEb{°ý„^̉-ƒq‘R@"ÈFÇéTj™v™Xx#¨PW&œS›Ú=Á1j 7xia*ÁOP`3·0çI,^¶õ]äÚ#É%œÕ:b’ÎD`í×Á–k ß-ñãÅKe‚’Ïüü–㦉íéB;)&ˆ×‘5|´XÊb\ev™ÚNØ’ kŽ ´1–Mà3'•©y ‚»±08j „ Â#Ðá»4´›™æ°bÒÈDDà¡ÃÜð˜VEÐ÷ÜR¨ø\Ž‘KÛ+ØUP=v4`ASà’Ú±XÅ‚XØ6¦µŠ§h†ƒ{®”r2€Ã÷°’[ߪ;Êiâ Â[£,­âGãÃà/ "0¨I¢^M#Y +Û5R2‘‡©­T7Z4Æ-°-:)Ú‡¿¬äH3Þá P]ÚØ`l…o#'túèt`žî)Ìÿa *Ê…„ÝE_EcL¢–Æv¼ž:*•z¬ñNÆdœË(…è§´çBv Sž­RÛD¡ aà먚Á ®wtb²á#W;7ÊrËàTn‚}‹ ºé°éÎÁHoÖx°UÚ2Ò%”ØåÀ’”‰I,gÏÀÏzÐþü¸gÖð‚, 2‡>±Mð\Íñ[&//ço9jOEbLÄQyŠO€YWu—¢q"uÚk4I ÚI¡ÇŽŽÂ`ŸVËÄ‚YJ!Y¤ƒþîj³•¬ÍhtHVSˆ(x‚iC4A|$VŒ€|œ’bO—\Øq>ÑÐkX–£”†ÌI~Œg• Ò¤–«GÞs“ ã ÆÍ-cÃá勵ón̼õ’{‡m‡;‡ï1ìX  'šøÿnHh/ñ:g.‹AeŽCL±KŠFz½ Pƒ~Ç”¤¦Tg‡x 3h h ¨P8WX$1`» #q92êÔøøÚ*!^(î±ïÚ ÛŒÝ?b»FSåƉ¢M6i¢î ˜ua‡~"#hÞY›`FŠ&£ð=¤÷EDÄ *÷T…®ÿ™+[j G Òì-¬äÄ*¾ bkG¨QöŽ Í€ÿQ§sÚnr[G˜5Þ¥2RÆË¨ñ“)Ã_¢ö‡° ¬æÃ".3ÍÏŒŽè;‡% q\ªt‰Ê… ÆmaI¸¢úE‹üÑÔŠ [IÒv?;Á 2FéŽ}N)pÂ@ü‘‘G‚Ñ$ü?ƒq­Í± 8ÿ£è‹Û´Â5^W~\v‹ø~Š8XvºüFz¨• &W“ÎSDÎÑ#ñ4æ¥}×™1ÓH´ŽˆÉOM¬Pà8o`Q8¥8&R¨!=µÃ ÃÀÁ’HÌ}5œFòˆÚÔ(™ë"¬ €AáûdÆ–Â Õ©Á·%j3j‰½ ÄnW>n[ 2-¡”á0ÙENp¹Ÿ L†§2Z&€†ïÑV%yèmºÿ î†*Ñý³¨£6cpDXI,b‡(„„‡ÔªýØo Ž7"6ð‚ ×+Ø~ãS øPðÇÂÎ÷ÅYA_Á0DPÄ‚m6cÍþ±u¡YaÝ~\]Ô½ ´Ÿdä\ (‚júQoBAóø%}I—Y!ãe¬À€Åpµ" Ü ¢£”ȼ“A‹mï˜=`0ù’¶ˆƒ©²N€©&Ú¶HHÔûYøDvú!DSûYì;Wí`J3Üc†ÂFƒ”'èGÉ/å„?Nø\û¥QÁxûñ›ÿ‘¦Iófm<8HÓ”VV[VV HB?†$uë¸`±fÖ»ÔŸ}£Œ-`cÕ­g+èÂè*/´ˆ¸;ƒÔæÃhi1¡rÕFn¥Q¡€¹Pi®ìüq¤Átþƒïƒ÷P¸tlî¨4Wû àå¤Ò3,Ï‚‡õ…í1x}4.Üz#^¡y@÷?08—@ä-Þ?¼P. ˜Í* µPü ¦€€?€˜Fo2 6Ãòì‚LµâV­Û´ j×>¸CÇаN»t ïÖ="²GÏ^b˘¤-¬6à­F,#¨zçáæÏRzô-Ýßúx¦eœq²ŠÏ,"{€ÁS `‹ Kc«"Ô³]ŠÓèP¼‚» ³žgÀF0þÀ¶²¸=àq„<ˆ`žU@ÆlühTÌ:I#&! Ac€ ”X£®¼páF–¢QËeBÜðo ›êŒo¡Ì”•Úª £ü0mù6*•†võu‚È xx³Iá“Ü3š%‘2†Z°E¤wrâ’°<Ù¶·Ö± 3Z…_øPð[^^’n¸µá/.`÷© þa6ü–™`"ñ~89§Ê2³@!`¾§j¬³µÆÁÛv¬Fã ˆõ/ÐL¢q@²¤Ú*Ø¿—Ïi7|„qà¡×ÂT@_?íŸ} ÕþkâükÁA„ÙŽÎ$AÖ[…Yðœh Çhë¥þ~®/¹ocOycmއ§·ü»àWèU߃<uþ¿oPaš³ß%ãÐ6Fs,1·{fp">Dh¬±mÕ nÁšãÃ,ƒm›â·­öô%Ø”&/…y£‘†ˆWTGTÀ±î`g[ÄDµ²çQ±X,° g…ðº‰Å\Àc2-Lg ?¸lg‚Mòì…qæ`bæFŽnkc7Ô&ÍGE8OþL0«¦2²|"ßõïF­KüÆ#Ñ*±Ÿ’øÌ:£2†œ1˜-È@µÝS§S…7£Gĥи`çYjÓüïqÈ¡ƒÒ ‡ÓŽ CÄ € ãßE%%äëÛÏï,šI‡¹IâFã1ZúÖ›8<$¶5®ž8ÍÖ>Æzùúà÷òãÓ³¢ÀßM„â-÷öñ‘{’">ìãÆ…f»ÁŸÿ÷~6”î¶üh…‰MPƒ–”9=¡oþêÒ”[Æ'çAeüЦdÀý„†2õ¥¼.ÍX;EúRá2Ò¡ó’wšcª Ñ.4BK’Ç‹ÖÙâÀhK,ŽElY6€bIìqþX„VN %}-ž‡±”BÏñ8(‚YVÔ«â’Õ¢‡uã˼~É ¤Ìȵ+!§áütgQœÙ¤KH°}.""'aW1Íï»Sá˜b„Ò<ìh¯ÐcG`6¢Z™ 2•!£Ô={DZ]¹µ5jt:S’ÑâäTáXÔ§â` 1ëó°j“uÆ%‘;’à ØSÞl`À3kO-Ñ«as@–h A !€!7Õlu¬XÓZ»PïèÐqÂç`aQŸ8”àcÃÂR²Û‰„6¶T±f+éØvk$b¨@#±A12ØpºÜÕ$ ª0»FècÁé¿°Å=\Ûkîbž Ó W(‰r¯x´…-Â~ˆKÁÙÆ xQ'éidzìSgñRoP&¨@‘%‹eRœ;[ €©£e¨¡„ø"ó› »R*äB,G]€ðGˈ·Äí'€(ëI‘(¸"–Ãa$¤ø=sƒÊƒ çó¤ÒÆIpq©åš¬°ÅÿKP<ÂB´„è™T`ÂÁFÏP§s!(;’ª4¸’ͦ!g5oéµBK;nD`¢†HäGæh®ÖUnMêX-nG…i€¨5­ÇÛ‡ÍçÓÉß·ú–«iå¦b°D."¢@äùÿT)Ðû‡[byfcW ^·ïÜ:„Ê‚C³Í+J”™ý¤Ö¡:³]’JÌz ÍOø¹Æý‘Õg-}PÝ%íA£§ª×†W†¢x Á‰76·CÝ´+t è()‰b `rGŒx›®AnX—5#²g\Èj_×l>™ä¿EÝÔ°biƒsSbW–ϪÚÔ5´îÎÑÇ;Æev >ï9sM÷¦çÚ_½‰¦}p»MÓw+í‡7zæR«QµÐ½$É¿nhó±øç¢õ~1&ª;ŽZ2jà‹ùM?ù¶sÿöpĉ"Ú-ªí*oŸvXªsŽ}¨ÃÝÊ]+Ÿ³Û•Öò§úÖµJÖó—$mlþ,kéø%wˆÜ[Ež;tüâ_ìƒ%e’$¥Tm×óÀô_ì]JçΪr˜•7zh¨¤}Z·ý ó'ç¹5x±t¨kZHß›®Ãï¤õÞ¯š?%¯Þþ/m›4ÊÝíyS|rÀÞñâ’ƒ:ìwSÜ,•·é·üßòƇ|­Ø\6qRûü?‡Nʽk·pÿÆÖÙâøA]÷gæ©Ê,š’׳֢iý#?¬öúyYÏÓÇòw?–¿aøÚÑëæÏÊëþ“nÍäcy•Ž<´ÔwYà‰ßÕ¶­Z8æçg©»Ëíy5ÿú§”æÓW8É^•kQ½nvÈ´Y¦Åo–U-¬sôA¾>ûduÓO[Óúv¾»ãtÇŒ¦§<¶í_'Þþ~RÓN“²§W:•¼Ý¡ñ’»s÷×Ýž1bPŸóç'NÖ´.QFç»]qé„6ìâ´)=R“g_vŽÎ›lzysÕB]œ>ÐïEƒ.÷Ê4.Z4êøÇþ}jŸ?¹ý=[,¹ÿ~†ç}ÿ˧J~,÷ôÛ­¡ÍŽ_éÝ+þüÎŒõW¾6õpQpÞ3B·2¿ÂÂ*OÜþÜVÇÉ9Á£ãÇg‹ÄÏžT®ôb®¨ÊÁâ¯ÚîÛj¼‡z•Óº7O;—«ðòe'ÉõþQãë¯>b\ëoÊ….?7üàyMÞÔ±/8Žðm·¥bÝz‹nï¿»‡ï±Akîwï¦ï²®O×WÅV”‹Š¹7\$ÚxeþÂÑ7Çm“去Ò1å×ô’¦ï½ÑiCr`—¦)u²·igÏ]xg±Ãð2··)gÆn9º¤§ó€S²é‹½U;ž•ï”ݾtƒ_µùÜ>kÒ«*ÕÜs3+þãŽ2÷ÊiŸ“µîÛÏh:gɇù%ÛD¹ž7ÔÕ={ô )G6ü¼ bË›Ï:óBÿGß±£b‚ó7½u»Zµ½Y/~ÒñÛÚ¡ý6ïî|¼í»ù馛E2e>·ÊªÿjþðËó½¾.vœ¸uÚÙO/½ü¶ç¯»7÷e¹Æ}’í½L«æžÉVï™}7n®×äé½Vwq3ÀájãœN÷›¼Ù—]µæþã®’¡­›þûÕ¾vdYsÈ„M²í¢ß—Éô º'(r~ƒóAë7Žq.7Ï´¶·²cÉ2q1qIË—]R•®­ÎêÚ «|œbk ͪK£O 9³Ò©¡}ˆhm„Ìpêlµž#UeúiΖž~jè°Ø5zvh_uÿý†S;œ™>ÓeZÉScþºä4òÒÑØñ¢™^ן]>uÎûà‹®«I|7竘xc[ßÑAŽ/zœ:ÒÒqÍ8Ù§ ¯›:žÉìÛ¿ˆƒlýÀ®Û‡6Ø“øfæø³ ûæ´º°Ò¾l«vŸ£.ÿ±øœç¶¦Aáóæ9…ÕÍ‘^©•à<Ü)Ú§þ‚Ç—wÞ_s«9kæý¶›]?–}&Ù°`Ñ¢Ô-[§œ~~F|Q¹¤nýÄÅc½DŽ9sçãµïÕ«ÊÌljp?EöëF¡Ï)­•5-ëüæÔÐÕ%µÖÔè³êzÀ¸š5}#–¬}ú)[uð^·ÓkZÏéà¾mòîJÙ×Ë.{ê˜Xöð¬Õ'·(gKZ¾t4{똻ë/{ˆÓÚxW­å¹¨×0_GùVÉ_ž›~¨ýegü”Þ{[7˜ê³édµçcvÏé•òØ¿W'ݧK;»íëzò÷¬iß­íù2êðŽƒ—7è§Vý«ÝÙ6×ü}å¸gµ)üÐñ–úa9ÛÕi–fMþ­lùØN%6{¤ªÒêçüf.}k<: w_ЧÎö*Íö]sô&E3EÔ˜Û%:®kå{(V[ýͺٗ/:mÞôö¨WÒ£M·Ÿ¼*™±`ÖÇÖg´e_8ûsÞuÌné/êý—k½}Þ o«å¿½:^îÂÃì·•/]ìRjî‚Æ_—ézšçÜkÚékvÅä––¸*[5Î'¢RÈÈé—ŒxowéÈ…Éýrw¬,µëΖ÷6¯“?(“Þ»úšµ±áç²£÷×ùieÎüréÎ¥\oÐuS£íŠºQíz_ż•™ÒœªŽe·,:8gr³œxŨ "+ñµë÷„²uåCŽ)³`Ó$ßk·e~+r6ß~§5D®3—Üñ¸úë¨ÑWoÞJ˺x¾²zNœß¨ ^Û¤÷ì·ô>°ð§iKFÌë´uÝ›oòªß4íóÂþr×·å¶œ¯5jÞ†K毜nn1²â>«Ò»óäcëNåvü}ô!yÐrÿc÷¶ÙûMümü§þ•ï¦Õ–úó>W¬Æä§¦m*ñPuCv§ZÍM¿‡æÆ¼ÝúöVÚðú¡%õSoür¤‡CfÎî´ù¡•ewÊÍ;à=°j§É&‡nÚ×òçÐ7攑êª{4Öç=·MY]WæÛâÔêQ+sæ©®þõZ1yã¾ç«†flÌk–³p_]ß-gö¯èþ¬ÓO¸ÇŠ/v®ºC:iIµëMÇ쥿º¼AËS¿O­õôÑ­AâÍÇž?ZÖ¡yëüŸ ³ûþ¢(oÙêMö­œ¢v³_n`¹÷ÏC¥Ða_±0nÀÕ7€X`m‚®a¤îLÛAúLøNØsçìÅZU ¸{U«ÒΜs¶cƒøŸ45v—)*SûÿÕm×l”7«ó%¾ÒÛ³¥ÞvÉ®[f@éZÛROÏ1>\8pïžÍ‹öþ­ÏÐagÛUºV/Åÿ™¼jµ˜Ò#;è/JŸ®¯Ö®Í’ecÝk­¨²?tMV熱êø?9Vw/·fÎñUýOw~¹Ìob·ØÙCÅc<º <ûxEp‡¬3ºëíåà¹"ìű–Í2¿¿°ÛߘñÃ÷Ü]2¼æÙÈüÎ ½½õÞÉ«åÖMìZ¤¾ý§rG\öèüK×êtjBØñ¸åÇZüüȹ‘©YnÏŸ=GæéÊ~É?2A?÷’Rhïp.hܦޥÊ%ë¾}Åaä¥û•ÏËr~«Yѯõ¯no »µ¯×KÉ.7ŸyU£·\ts«¤Fç ‹ÎcÌ÷»åØÃR´ßºi‚GÑ"EŽå/EÅdáRˆl,E7P¡õh(§ÐßÊî€GÅÀoI>½\R¤kzoó}[.khÙJʪÝ/9D4¦Þé¥ò:ÇŽÝfùq‘ý›¯O¿¤;:ÙÃcäìŠ/Oš¹áÏ ÇÛG\ð»UíDÐõ’Š ç–y®¹t1è¯MåãK{ʬ™±7oe羫^«èðøÕ›7m¯|:¸]ûkÎk†¯ÉŸsô\•?¾Õö°TêÖã‘í+ ØïôvX…ƒY¹£®N¯ÐfÛdZ–þ74—3E ›­¤°Ù†¦·WhãoNgÝe×*­*fllp%>º}¹ Š­"Ú†úo˚ѹÞèbëKMî_wÁÉwF¼j\õ¯âiûœíÿR_™]eqç’sÚ¤vê´wpÀÚoŸ¿¼«³¯v¯¢ƒ¼‹MÏþó¶Ûƒã³íõöeO_ÿ°þ^Üœß75Ø|²GÕ¹«‹\>±Vé‘ý"û½ò­ÿ~ćµ¾u¼éùåhEŸ‹ò#ë=œÓm½g°£¹rjèq… u7E•jîø“Ç𵛫oY’6wìtÅö+>¾c6o~¤ÍšéGïdœXöâ™fõšÍuÓÏzm¨u®RØ·ƒ^ä´îV1?­flÉI5Ýc4{nõ™”’š±jdÚxqƒ…;†þ|ðxÛ™Û5ëm?}Ý…Û¢¿…ytAòjo¼âõ½r›+õéàÝÄs!å‚Ce.n«Øanþ‚~O•yŠyŠ÷#7…çܘþóõ¹ãfÖZ6mJó,{çÔ:³y—w¼šVëEÕÛM%•ßúhKÛ…)Cïl\”&¿ZáÊ9IzHJm¯»çfÿRïãûªOÂ%S[´y\Ù»Íî_«ÜùôêCíÑ™ƒG©ófÝSÿz ÇnvíÞÓªÝ×—¾pÜû½c=Q“R€eJ­½¡û®b…QŸz… Yú¬…0«7yв§ek+RTU¢A`ÎÌÀ$ÑòºSkÇ íÑ÷DÅÜMôµoï<±s8·tmç;ÚmÝâ|nuôÕ‹É;6I=æ´ nyÆÛïv¯~º%å¥ùÆŽv’¶n¾Ò4ÒþÄ–rÖæÚ?ü:øa*úÿæÞ7çk yÿ¹cíïíºÏÏjZòeäúÒ“\ò±«qzyZåúUóžö¹>ìVçW õد[ÑùÕô¡“ÒòºTÛ;bÒò³Ò£·"òúW;ñjÓä!­ƒö:-IŒ\+cE¥3wÎOh2©l17çüh÷KŠû=S&­OË.ûÊnxxåãÔ_Nxfô Z4%ûå±G=ÿʨ•Yu×âÛoœ[7y¸¿Æ½OEc*ç)«iÒº'}k¹?røçä>ê#—»*~kzöôÛ{ݶñdRüÛ#‹ßo­öyLR¾¦è­.¯2ó½JY³8ÊãFþšÒAöÇü™3Ìèù±cµÌW‘žöÚ+-o«óbƒÉnÈÃÀŸ†¼ÜÿAùÚ¾Þç–û[¬ÛßþkðžÝòμþRcF£üÈjóWkq`ïʯõí¾†ï²¼ÉÄ#ÝNªö¹­¤ÏŒcCÊæ×/úíéþNÕ>‡d5,»íóÓ¡¡õº&Mé0îØ²É×Ú”¯×Õÿ^—) ÖŽêfïã¼4=iLëŽ= ½þè\E1Gó8aÇåÚ¥6l™\¥·gƒ¤)¦«ëvLà–x¯ôµÅAŽõOg¦Õ®wlDøOUN®" qø›-sÊ¥&v•,ȯ4”ùÍIwžæ1uóÉY/œ“ÇD^8yåò‰>ï=>U›×¶É‰ÜM×N>>ìÔê·%ÃG6;¹Iu+¸‡ÃuÃlûÇ M{ÌÿsŽï_¯—+Ž-#ßß,¶ÖéÇʋ÷?*óeS=_/E»ÏI‡k—ÍœöþØXïjÁ“ü¶žtštºÁÎ#Âþ4~êL;W±·øÂéÊÅʉMrJÏsª6Ѭ/-/±sê•þ7“RûŒó˜zúÊ€‡ôêðs —g9P|‹ò#~[Ë•_Ý[šâ«è®ÿ³B–bÄÜ2 ›MUÞ]^|Î.ŸØéwÁ!ÅFÌHžµ v½¬¸UÝVgýU>@ÑcWk—ª½‚GG_ )žà<Ï;ö¯œÙ ÍŠåjª®õ ÜònCòë™ýg?Ÿ¡x¶|Wß2®eÅG3³{­Î®ý:yÓÌKÒM}–Ôyd~ééºÌG&§Ü÷é[ãO­ªä«9xîãÛuïÔs™v&èxÍ¡ŸrvoŸjWÒûjË:#n…ÖÖ,žW¦ÎšZ!­¿ ˜àÞy‰ë…Å]\6«ª.N™p`S÷ߣ¼¯é:?Š˜°å|… Ú%׿<÷ÏÞ©Ú÷à—Y•Ú 1¯Ê®£ZT×Ð6ó—fI™A©vÛÆÝ\Ú¥ÆósFM¼áúÛ¨>W–ìö{4iÑø3»Lo2ïRØ‘È!Í5ó¤KæÚ¿¸q˜.òfˆáYÅKåÿ˜0FÖ,»} ƒcÝ6.-×(Ï?Ïl^´”·_ûQ‡d_j—?qàÁÇM±³ã5ñô‰…÷Øà¤r’.=*}cI—ùûy·ýÐÏ´è‘qüÄdS‹Ç½C›Œñ®ýG¥•k׊}ø>yx—¦n.•–gK&¨ÒJÕk°jΖ֑Ë&¾ë12üÑê´Awãû™†FTñ ÊÿEpE9á~¸Ã'Ÿ èˆ8¿µË&¥U´O6MúlÂÂã$÷ª~>+«k\ùyv^uÿi–_=_µÄ"ãžðÑgæ^ÉëâµÇ)hUöäcêEíùt˜’ŸR/2iúï«òŸMªÞ?t“Ïñóªò-&¥VÒÎ6øuׂS×ê\¸"gEï¤~³†ß­µ&ÉøþÃRÍÓ¢~½¿ç€ÎKwÜß˱r³¿6?lxóRD~½šÍ÷?9s;ľҵ—®i!K¶¿é³Ñge·¶#nþ‘}MUJñô]Ÿ©½ã3¿ìÏŠ|vãòæEšÁáêë¥Kû®ÎL›¼¾˜¯“ClØì–­·¾KéòµîÉØ«ŠáßÅý^¬Â¹Ü³ÕÏWWKt¨ylésÏI½^nŽœ^f¾|Ú±_¶/‘0Û¹ob"^=T{ÃB;¦~¶{Xzf`Øú¯o Þìç?kÊ£cÎ{/¨Ðhk÷;™õ3jíl<­“¸ßÔ¸¬·3k?{Ý3rÏ[»¨êƒ’û÷éó¥u“£¿L:]+e|•sëì{Í©ä4+îü›PçL×ú+en¶´Ïs®{xÕš_ŒŠñO>.­;ãýPݽí5šÍ:Rrƒ“÷Ï^s¦u­Uåp‰‡~×.,ˆr<ùÒYÓ±»ñÈÜ•™oÎy?&¯Õ§hÖêų͎¾UWßöᕜ¦¡gú¸lvXð¦r3ß>'÷‡ä¤»ÎYyykéúÁƺö¾5ñçT÷öG\]¦ 9cløèñÁÊ'a1#].éW=“®uØtr¼Ïîgv/R| %Û?¬œž9j|€¨èá:éÑ«¶ ­tecÃÈí;ŽÉé·Öà²ÛôóH—)=ŸGG”LÞÝŧòOó~Uîøt{ó¸)tª¦çOÇ+GkdÇœ#Whö¸Iè‚kqM¯5ë÷‡ËÝ?†OÙ˜îÜáܵE­ˆ ª>ÛKû}1ÏYúj¼ñĬD—QO^äé{7©[1·ó‚Š×wï<½fÀŸ;v{Ï»°þ¯–Ë2ä,µ$;ÿÕ‹â7Ö/¼q¤Tì—a7Öï¼æ}ãˆøcïdâÆû™C§<2,´‹Zû±ÛìWÅ*–¹ôìùØ÷—Z¼üãê›÷OŽeïHÔŽþüå㦃“Ö­lqé|‹Î‹º'»®ÌÙ¦¬9êõެWg)߬i¿)9²“é·™Y#{UßyrG:oÕ—Ú‹×ôr{2ýÙƒwóKDö|Ÿ3§r¿”ñwE¾léûr¬—WÔ1ƒ¡¿kêŠ#¦.ï&DÌÝÿøùe÷À±û$½ìqóp@ŸÇ»¯&+»*ë}|šÖêtd£µé7NVo»ðýª„å3N_Ü)sNpžõÇö¹jû 5ŸºeÎ\övL×õÒ/ƒo§ø9wyV½oõkÑ­ß&Å»ù—¦¥gNšï9ÇëLí°SñÓ|üäÝb;$M+Òç·Òe·=ˬ5på©RõŸµ¼u²÷ò™—]éú´ë—+|u/-Û1÷þï³ÏÉ/²õÑÑ[˼®ÉÂüšŸR ܹòÖ×ÍÏwµï¼öø ‘ÃüIiß]˜[þdlèºCÛÎ:p½”óÄœHUÄÊÖæq´®ÃÕFΟ2?^ÜSßadíN÷}e.;µƒsr2{Þ÷5Ô|Ó»np‚!(=yÉÆSŸ¦$ˆõžsf›ËWŽ«üBïž~dáݲ7-]1sÑü®iové7Ì\Ö¦÷ÃÄ¥Ûg/Ù8Ø'j_õJ›vN¼6jÍÃv- -ß>WV¤Æ¥ãq³Ú%ß÷y:èc™òÞ«§dúž¹!ú˜56÷Ì«g?}Ú7ê–‹Í]P®ŽÃ½^i÷CǶޑäsfWH^ßJãî„ìIRiM~Õë "‹–ÿ½ØôKãòªÞè:¼¶}èïëÞO­ö{Öµ¹³zèz¹êÌjg»z6ú³×sßïL?2¶X„_ñ˜¢+ó7þe÷þÄæ«˜¼óë,¶(¯¨zVv¯'z\þ=ÿÆàûŽ—Ênkµïi‰ë‘¨o›¢uÄ1••3gŸ}¸þñÁ¢zœž'îSæÃ_ÎÚ7 õ^7öšç˜çzw¬vÙoLùÀMYãJ¯¿ùüTn±-v¡E·V]§±Óm;¹1jmßEƒÔ²st̾ •Þún9ÑèúÍÜÜ*3nöh9jÑ„œcwN,o{èú I߇•ö~º²bØÍÊ5ËiÛtÇ:»€ñWÆù^Ÿ°S_ñ“Ǿ!òëãœjîhï4±ŒÃýÒ54œ˜«r8âµ­~øÙWµÊÝíòεa^ôø;Ÿjœä1qÑ«ÛÌõulÓ þ±ïüÅÚ{I3UUæý먼²a×+ žòí[åÁÛ«ö¼§œØå̦êú‡fMúÖ|ÔÎêæm5^&åùlª–÷G¦~éË[Ö¸ÖFÚ÷ò‰G/ò¶¥¤eô‘ï{‘yöÚ“ù»Ö´-àнY·Ä^ ½¶{Y%ïãqî+ŸÁ'>Sd>Ûý,õÓÍž Èg•ÜC÷D¤-ý5w`ëH½1¹ý{ÙÍßó«Ny,õöÒô]t`“]ø—ß.íûtöd^—ÓÜ=d·:ñÙϧG¦øfˆWîšt1àÀ“„¹eœôžt{wÈ»æ»ê<ÌkxsÒ¤×TþûÃ4¼lzàÛæa-gì ­0©}Ç¢ŸRÖèÒ·É9éUaýÎq£#î&·8yâ/M¹yOvªWÊíÎm—=Û\ôç¦s-R•-#ÆŽÎê¸2¼æ³²>õÝn·/•x?àÕäCÞõM›Žyñ{êžÛ“$ ¾<›9pÒô‰ovºçî©°ltœ,ç•óÚOMMMÔåŽÌ³ï¤XúËð[NuÊ÷¿•”®\µ©ë¡´ë/ç ½’ûn`Ö5ñçgǦ)›z~󑨍âõ3Æ»¾ë¿åëù:ÝŽT¹Ù+é³ÃLßòµd…{ãþœ¥¯}f}©…‰Cª¾jÒçöYsžlÜ642/tP½[ûkä ªÕâÐÊü2{d=_yïðåöY—Uµöt)Ÿ3¢ÔËåÕŒZvóÕÞ%.“E_JT½Ü°÷8÷ß¶Ž™?åÔ•áný¶l›{ßívË’ÉÍr•¹sèqÚ›ZÆÍíž0®Â¡huë”SÅkTœsiÇœ3³Æît(ª>|j•Ê%êöîñQ‡*½«8$ª‰ÁÓ!Y—°<ìÍÍ䪟|Ä{.õ[cöù”¯ƒ«½Åš¢!G*†ö-_¤ÈK‡Âd5q!š¢pHhÌÓu íTL^1£dÑž½KîmÑ¥ÂØ1+Ç4žP£Ü”á}Ët«þçL톨w©é¢Ÿë}.úUrwúªìa~/w?8eúãÍÇw_‹Þ®yÑß³GÿÉY+”ɾ«®›Óf„¢Ã›ßûÎÖä%¾pYóið‘¶]?tªe×µ£Óö£Ö+úÖ(÷qá…WwÖXß@ÿçµn $7¾Dîâ_Ñy}Õ>ßه,>žú-!þTËÃFΈ¨ù.cõÉ%-îu(s÷òÚ:¡7Ê®~|rKêéu{goұʥƒçê·4ڜԢ|ñJ7òZfO8_fÑâ5Ií]®7¼vËÝØ oÜ:äôŸ2êîâ"âQÑ#µ¢îÁ ëÆ}dŸ:¯×¸?פ9\ë¿ûVϵCKe/‹ yy¥Þ¬Ÿ/ŽH™±ÌõÁ¾G!Uâ¶,‘W¯l’{Žº1¯È™^ã}ž—TŽ-»4lrÐØu½›ÕüõâÍÉ+ªµkÍã_Š{WÙÔ²Ig;Î:4oúú¶~#”ê*~÷Ž÷-Õ÷,bôì×wõHàŽ/Tà®[ð"’¥Û̠ܳ _-º¢Þ°W=xòºŠ¢±$­¶ªZmFÇi“[v(=¹Äôk¡¯dÚ”»õ*—Ø_\—·'Ï­Þòê%÷7 ò_÷<æéÅ^ÕVnÙróCO]—g©7_nmç[gÙ×kyýo¾<µ«±—.áæÉ]ïÆìþТmÛQW<lš± v¢²k³§ÅþR]9¬.Ú´ØO¥õejJúNþ¥wÑa%ËUòèÖrá/_J~犻–«›k_Nö|ôÆ»yŸÃU¼ºß-Ö4§õ„MÓÖO{ðªò•]ÞUbÄýžM RÛG;5ûlÚÑ<Ÿ>nÍvWèÓ Z‰|‡ëŽKbîþrxuíE²Dû±³'V\©Ñ?Of—75¤HûÒ¸y‡Wz7ï~zS¯Ùkú¿Víº´ÿ©wóè»^ž—2:<9s,ñuÏØ÷ï|PO8“ß[¼èÈòZî+Y;fvàý]}î–¯­»ïØsÀúó’-?<«éGÞÖw]/u¯³ú•Oì’ÐÚ1ó².¿zWL{6?h‚®²Y7óÝÞKioM´òðVÕ ‡Ÿ×þt·™÷žð»·úíÊÚŸzìɱ9ƒëf¬Õ„íi·8±ÙËÉço¹üvpÿ‚+GêÜ|p?Wµ¿åÒÍÇ÷Êö-üø%hÝõ‡eFŒ¯žSÅ7¤ëH÷³×Õ%æ¼_Ù'·iVxå23 }ó×¼Ž+ªiÃ{5|¤4Ìé8¨Þä'Z­*"ͼR¥Ìˆ]õ¾ùwª¸.»ïå¯ÄËÞ´”ۯӰܸ'—ï¨VÚÕiÕ)§N^›’kbŸÔ¯ì³Or®æÄëÝ×´æwŸ—Ýߨ¯Ÿóñ§.¸­^¹vATŸ[· U=z{îÉñ K‡=ɼÑM¶Àkë¦-{oq{çÙ³»YÕ3ÐíhØ·êêòj›;&Wî_ë×¾Ó* »eÄÙ.'þþ ììs¿=ÊUq+[¢Î®ÐБõû‰{¸%y×Ñ4Ú ˜Ñþiÿæ1}—e?ï6.ÐÊãÅóÚÆõ¨X.åKë.º lÓwÞ´}<ªdõ¼™°÷ˆ÷Ù)Á÷‚/þÚ×8æVæË—–:^œ®Y¦¯SëMÞ!ém¹^yf´ÇˆÐë‰Úà¢ËÒ VoÈŸeö k÷4rsëÝ'g¶œ–»òí_yú9¿O_Øoxÿ›-×>_LÝ¢Sxø‚™>"}ÄÞcs•!¯ÿÚ+ Ùþq·“æ¤ÿÏ›–í[¹{ð’ŸÏë[âi@yÇS=³/%mZ‘cÐÏöÒ¨o|=yý«Ÿ:´•fÅ&9DÞ×Mh9å·\U—¥o#:yÄÿÙ(vôŒQgz—ìÕÁyPи\û•Âd¾›g$Wçú(îLó&¥V z¶Öðó [í¯·=á;ëöÆÉ‹f™®YÑßPQ´mÞ„u/Ë3ä§xx®_é=¾g›[Ÿ=ÿÒmþÛš™Î-îlï»hNnlâ‘÷aåÛv+÷üSÊÒ­êàeûõ“Óºïή!Ÿ·m÷OSVÌ϶Ø-ä¸ùö£M—–/YÚ»¡ô|¯nÕKç—›´wÒˆ²!—g˜Ða³ã ŸR±ãÌ6t·0áüÆð÷7Ú,¿”ñ1¢mÃkãÃúg®ìê|'éõÈ^ »¸=UW¬â¾rwÖݬ¥º‡ÌpvÔÂðaiТϽ¾ø|uÙ¶C'Œœó›ÉeÍ‘ã) ¾¯°ô^œùÉoa v1ÓLò+Æ/AQ+W¬{±.óÊê7e?G.ˆ¹×nJõ*¡âYm2Ï<.;Û»ÏO‡µ:ã¼|„_@`§y÷›—ËÖ‹ª08øá,EÌ×:æÖûÁõ‹Ë´÷>®wkCNïÌQIb¿ÉÝoupv•Vÿ{_ßFq5Î}˜£-P ”c± ’Yñ‘Lâ8G¹ˆâ¸b-­í%­ÐJvÇ¥åj¹)G¹ -w úJ)g |…Rø€r =(Wi9JË]øÏ{sìÌì®,'†¯ß÷«ZbiwæÍÌ›7oÞ¼yÇ?›¹ºþî÷Ùõ¤/ú߃û®}ï?]ulݾ÷ìµóŸvÜâ¹³ÎnjŸ=7´ñ×ü6ÇÛ|ê{¿=ɸä_5›<óñŸ}{~uÑ›k×½¸é/õþcòEÉ^?ÿ×·}»_/:æþšû=s±óØA·´uöò‡£ï^ÿ«ëyyþÑ÷§æ¿iµ¯Z|Qêåé?¿ýWÛ¿ï#Û_Púþo÷¿²¾æ‹ùhÿ»®x¬¹û“÷Þ#¯&×î3”Ùçµ\½å²¯¼üÓÿpþ½ÿ³¦ïqÀ÷zã•w½ü‡wËãàÌ{?õt¶qâæ¿ØuÉûÞýá¡ÇÕ]ZwB÷1ïÅÿñÁ¹¾WwÑÁ/-)}Ü‘ÿSïNGmñá~[í×ýß;r÷Üç¯}{·&,Ü¥}âw^øÆîÓö,Ü3ùÒâK ÿqÉó+¾¿ïïó÷ÜþÁϾÓiñÞc&ün»·îxú©×>:uÕO÷ÍÞtð·6¹±uð¶È5g}ò¯ tîp]çÐϯýxÿÛæû뾋îz!¹0}Qþ§¿ýû-ó¶ó­»üà©+ßsß|·å“Ÿ—?~ô„·Ÿ=~ß[êuþï¿|òŠÍ÷þŠÏßúå/8Ÿÿö…^±éî÷½¹ûë?˜tñ¥;^týÙßÛþoÅ“~wâÓM§å&ݽdzÝÙ=¶}âÁ¾Â…î¸zö—¿>eöþçÙ¿íÍ;¾ZwëžÓ.ÌzãoË=7˜}uÇÎN;ì1{ëç®y¬®xêÎ×Ýý£¾XØ'Yó™7ïš:quâØ}oß!3§¸ò¥Üiç½qÉqOï1ñïÖÐ6Ïþå%_1v8åŸ×Þ¸Ö<ç‘-²_w~Ö¾ù©_XþìC_}õ;¿;i _œ¾bÅûß9âñÇ®þ>þ«oØíŠgŽhÁœ+ÛÞ9~ëi§¿÷Û¹WÇ><qÝ­[n¿õw·-˜2íÑó—7þñ•îÜc×ä[mråû± Ï>ìWÎÉwž=ñõ~ñèSk_Ýí×ç¾ðÞo~xüå[ïñØ5ß¼ñâŽ]öLJ¶ºí™7§:2ë…co~ôõ¾\ÿ›øŠ¾4µñ‡ O®¬øîMï¼×táÏ·áÏß¾o·CÖžw~ëuÏ<´§Ûºüƃ??åä¾—>}Ïïö¬9¢s‡£Û×pðs/ÝwÏÜí¶^¸KSááoM:íÄÁýNûÒß/žxÑsÅ%÷gÿV÷÷)_þîïŸ:kÞÔ3ýSΉ^òÆ¿Þÿù¡…9‹g7žõ·ŸÝ~ÅÄþîsû§õ™Dr»cÞ5?;ÿ²û¿xÚªI#O\²ûúÞ5×~ôÁ™ß®?÷ëwßðÖàÙMO¯)nÿÎß~áÍÕ?ü• n}àÑù ¸êðÙŸ2ó¤¿î¸È²柛ßòÅ?<»0=ZŸ¼éÛÉ“Kÿ5yÚ9-u‡<ÙzþÕ]Þ5îé}î‚•W/ZSþÁwòçœÿðä}§þ3R÷Ä5_õÌ'Ë…)Ãë¼ö†7ÏóÁK_Üíµáìïÿß™9wέ»éœoŸpfûÄÈ‚“ÛíÜ=ÞzeYú§Sï»ñÐS?·¾±ýkG®^wÿûÜüË®'ßûöOö~£yêŸN¼ò—ó뿾fýß:ªÿ^wÑ©¯ücÓOŽs¾ü¯/¥ÞïÁO޹¾·þ·¿ºîâŸþò¹'?Þíñ¶øäÇ›œüÇ×v8¤î _ž³e×½[&:·¾â‹™Á3îøù9Ï·~iËÍß;yÇâ.ÙȵÇ6mw˱ßÛì†-^Ùìƒm_Iñdëo7+fZf}ãÐ+7ßrû‰Ñ-û0:ØsXd‡ëbÑÄ_/ß÷¹)g_Òõ|òÿw=räÏß_Úc,œyÜ öÑ\±¹½û^s¶nÿ‹íŸzÅQ³ç?zÉ”w¼lٳΙr~Ï#GM¹ð;±hËïoyÓÙ»VWå‹ïµ¿øò»¾yã Ç~|Ñ×.9zý Ÿ<Öòñ³O¾µä_©½ï:ë׬oþëÃf½pô/6[ôø¬_¿yýù‹k'_{écñ7¯¶¾ynýv[|2¸ë+C7ïX2ós/ï÷÷Ov[üü!‡¾’yºa‹Ý_/ÿå©WÊ_¹÷‡¹ÞYßÝüOûÄþzåæçõ\¹èÀ¬¿pÇÌ3>{ÒŽ¯\ùÇiùðÏ~}Ñ:óâ;.œ×°è+:¸×ã ¶vîþYÍßÿzÛýÏÍ­3¯˜6ëÀýNlªy©ÿå/ýà”Å—þùžKÿ¶n÷hz— ;=Û´oÝä{–?²àø ìnì=è¡-¯¼ÿî§¿xåžçuowÖu ßÚ´ùÉ»3Ñ G?¥Íüào—·½|ÛŽ¯]zÔç~Ú¾ufŸë2½ÞpЫ߸uۧ놻ïûécïÿ|ï'>ÌnþLl“=éÌþÃVÝWzt‹÷î¼è/³.<ï¦þ6»ô¢Ó6µŠÎF´~âë—Ç޾핕OuQÇçîøÈëÊßl~ï>/^øù†ÞI\pëÌSŸ}wÅ}Ǿ’Ÿ¸Åª~z]ëîð‡¯ZgïµÙË¥-úÒ6§7Û;^tü&_=½ÿ¨i?yôÀo¼ü› ¥½»}ù±ž[¿qó#ÇßÿÏ/Ÿñ³éµ_N·ýpcÓ‹¶¹ðÐWGnýÞ?˜tÞ«‡ üfùŒã®ë™ÞuÏÝtb䑟Ì8õº†JÉé¿Ùáw‡Lÿ»]Ú95kúÛóî}|¯ÒvÎ%ïíÖS߸jêíÛ=1·xýO[Ù{Õö§|i˃~¸Ù}×ÿõöµgqVíY=ûÍ{©d=vû1Ãö/¼×'‡Æ?|a+óñEÍ»ÿò«ûí¶öˆÖK®Ùãõkcu|片¨)ØÿïšýO¸àŠ¥—Ôuczý5Süôòk–.þÁ_vY]9Ð~ǩӾúÖ‡ïrÒ‡S¶™¼Ós ¾úæ)'¿>û[.œwYâÜG¦µ\ðú%MÏ,ŽüþÒ9÷Ç^[záÿ¼u»Ãκ~ÞÔÛ÷žúþŠÇ_¸í&ÿc×{ïnñ—7Üõw\ÿƒî<õ™¿põW½ùÎÞkOylùœõü ýæÈ+·ý`Mä¡ Oßbè£/=æîDË%Ǿ´Û­»®¼àÝ­ÿû'?8y×G.ýÔŸæüµûø®î[oØ?w{ò©¯e|ûƒ?­ºíÕE,zûìÛK]x滞7ýòõÆää½»þä±ìÒ]ß½2n_õÖâËÏÞó¬o|ï¨î‰÷õþ£´níão½ú§5íy×M}~¼Ã+GoÓÕ¶ÿ­Ûï¶ö”ÍIÿùžµßºì§çS»ì›Ï¶õ¡õ¿\½ù«OžÞñìV¿¾øÔÒð—Núë¢hsç}áÕç~üZ~å;sWþkùÚ­ßœûÁÏ·Ï}o¯_??pÈ·â—æŽO,™>õéõWÞÓSÇ>þtë¢w®›öÕ'[J.üòiìÚsÞ_+Mžúâ3GÍzª8ÇÙëÊüç3O»÷[Mœ´¸Å>fóI¸:í[ÆU‰Üíß½æÃÍ;L}ë…ÌלzMçõ¿XéL:ë¢/ŸþtÓ›~´ÅUïíùâGÛ}ýäÃ~|ç7rçÜ>sìNÿÜ|èüèq3n8!WšyÕ¾W=ý}~óÊŽ?zöœ7æ5ož½¼ióÜ·{·úýÑ|éˆîó±ÛCÛüÉ’žo`³ÞwÕY7¬Œìöî±Mÿ²§ô¶oûò#ÎøÆOϼiÔ¸‰kÿü‹›vüêÛÙÝvÆÛ?þ0ù玷Ï~ñky};Ÿ1üä·õF~=µæê«w:d›…çþ#>㔦÷7Ùfæçß)\öý÷7ÙÔj*{Ñ¢«8tÑIowè¢KÏh¯ßwWçÑZÿ<çÝïÞ?¼çš}vt÷Ùôùï?zWüù_×??ÿÂÛ»®9çž½Ží{èÉõ7ý¤óèyl—|êÇçÝô‹[¾}ïüö£gnõÎE³îYS›±í²ƒç/½pYïì+Ÿ¾e÷·nYÜrëc]½³ÞüIéÈK/o~æ¯ï—Þkþæ·œ}Ò-ÛÿèÎ˯ßiåÉm;~'ºû„g^øäÌo^uÃ9[õÿó¦å[^zÙNï\°Õ“©V_ÑóÜÍ/îöã‹7}ò‰#“;ÿè®+ÞøàÈ‘''›É­Ôÿ‹[wüÑ»¼»kÿ]Ök{<½öŽÛgõ·¿M¿ìë/̼duá>·\³à¥ƒ#±Ì7tu<¿èåâõ÷'/ž÷÷ÍZ¾»Ûì'?wÓSgÞñèI‰Óï½íÏ™æ‡÷ß¶åúÞ9_œ¿ü¯©ïíúÐ=»ÓSßß玎Ýö;Îì¸è_Óo:{ÊÌ/¼õÇÖuSYSZqEvÖOöyâ“þ‰Cw>ùôŸaòž7ŸÿηìVóÂû¿ð—wöù}|áÌîö£Ã)ÌÜö÷N{mù7=lò1wM>eõϾ0ãŒé÷û¹ûßµøÖæÝyï~Ïôêoö{ÀÚîÕ×¾ö÷§çÝxÕ­›ýê¶GÊ^jxeù)7î³Ïá'§^zâÞVÕíyéY‹ÎÛå÷‡¾½ó?îÿÒïæ>5ïÀÔnÏÜñ³âaWðâVgݰÅ.OÚx΃œ¸Ç‰; ¦/ymÿß~'¾ë?ÝU—þ¹õâ÷Þ~襦{ŽZ5é©ë7}ì—W½|ån>±ó²“j®}ãw¥ø]‰Å7LÏ]ùàì§zŒ§®ºì‘]¯ÜºåWïtÛå#oäcGöÚs{ütï‡N:bò™dÃÌŸ}®ù¹ž«÷ÜiÄ9ýK—ýë¾Û^Øvuý÷õnsØNW¦8föÍgîqË©ß}{bËùK®xâö n~{µ5×~¼õíß½óå›Î]òòaë¶ZòróÏNxøOn¿xdÝ´ë_r¿‘<ùáß^Swý‹¿~³;OšÞpñÖëJÝÛù~÷ë/úÞ„;ϾkÓòçR'Î8ã—¯óõ[ÊŸlýúå_¥ý­¯Lâøg_=mñ­Í^öü7þ^¨;óEy¿þº«êžn~äð97ž½ßÞ¿;ÿ›?¿à®;N¿ù¯?¸í7gôü¾õÆß >ò½SöêxùéÖm¿»ô©/œzêi[/ßq¯ÙÇöí²Ç=®ßmýe?úÕç–~oÁ»ëoÍ^¶ÉÞÓŸý±»÷/ÿvï'[öð¾oìÚ÷öΛlòó‰ =Üt³]6Qõ‡Ü¿\ Ôâ¡W•=vQª%CÜ!t²ãž „_ŽâÇ C’ðêHo3FH4صÉo†:U3ºMŸM6Šª¸Þ®z›9}ø²’oê²F3ÂÒaɺæˆëã=ª¾$ÐÊ$¸—ôž=GQZ/=tË­(un»É ¤O›6Â/ŸßÐØò­Oiâ+„&r²fz´˜Díå:KÓá’…­mÆ"«dfÌ’ù¿ : æÓŽH,án_üw òofR€ˆèš\V‹|³Œ¼ƒˆ28î•‹ò0¶Á…ç~Ñ Í Rb¹jàh¶+³l,_4O`°`4ž Vc§ˆÇþlÝJgq¬Y€Ó*-ænYtp†Ã' Ò-׊r,à°c‰^òŽâ¡BYæØ(ÏjX2{ã"Å!©÷›VÀ¤È,õø¦h5Ýgg3E+K Xžv:ÊiŽÕÞ¸Ásçã ùñüõj'Ô76aRÙ˜dDZ"^0(›ÆA ŠYYå‚´Ù ¢µÑ nr‚CçiZ¬3Séâ½íŒ@v†HWL'ìíéŸ$É)I×­•’ÞÓ„ FÀWÔ)š“¡v·™gK™ÀÐ „’†S x± ‘M®>Õú2ÊQ-#bZ"q#"AŒø[fÃ´É «¶1L‘Šý|²Ê -Z SxZ!ˆ&¶Uu Ó¥PºøiFo㟹™A“”\Hæ¹h̰2Yø2«·hflX—iÛM” ä¡ê8òÕ:š· ¶/‹aÁ\\É…¢“)c|x PƒFÎ)-u¬-pÕ­ÁTðP7oB&°^3o¯Á°Lv =ëK¦Ûîõýyg0ÐVü Ÿä±µâ"Æ?M ÔHž™†É͈\·†³Ühn¨`C‰¡¸´Ëv½HÐs+Ÿ¶¼Öp‹6¼ñ±`Cù’™.Ùi¯<8•¨ùŠ3æãF’æL0³®Ÿ0Cº–'äe€)³Ö˜Û0^#WäuJ FÆ }NÖé…áù:Sƒ¯­Ÿ¥œÙoA m“Y­€£\ “ŽUƒQáxFû2Zƒ(v°%¶;9 ÓÄ=Ll+m‰x•¬ã{¤ÿÝCÐih޼ƒÜ¹êlÂda"óJu4 ¨a°rØÌuÛ½e§ìŽ4¿½Ñ"-B¬Ï¢å 2}e £4è ÅÎ×Ðñ¦Òy¯…òïzÌ‘Š%úÍæ€(Él  y2èl³üX–4fÓp,˼´)þÓv†ç§wzx&¶ñ¤!ò|‚YPÀÆ‚Á¦ÈÃàOH.<·uÆ!… “vGXZúÞ²íb¢2¿0jØò (^ ¢]";ü€,²C‘‚mÔ.Áù.‘Ž™ÅŒ?-àDýAt9‘¦\He6Ù‰ƒg¹rÍ’#^Ó6ÜÞg÷”–—¶hƒÚ4¿ŒGïšc¹ì–½%¼ D?AØRH¹»ž‚21Ì‚É2F?Ùu±¬Ç8»õ¤—A'Ò}fÐÇ0€?y…8›±2LJfˆl¶všÅp BY•cX==v¸7¡5ËÈ2 9 ‚ÌVÚkVy¬5-®ía´D%2 F³ÀØ$ØK#OèèÔg=lŠVÐ+Ë,f­¡ 7… ‡@)Ê·¨=ƒw²Gz´[ïhÆ‚=„É<úôÖé áE9FÐjÁ¼“¯º,9ç•*D¯c"‚GÈø8 “`YQŒ{Ä&’TÉ:ds‡S‹uHkÂÓ-Aj+¸ìC+^¨P ^ Î)óD‡•!ºE¢o)ÊÀ^Êp¦5ø»…VO©Í)ôj•C^ÈÝ-hýå0—Ê‹O» ïέ=¤¾Ƶ´èʯIõòïK,¯ :FeUŽ^|±I^Î6‡,×+ÑN¤(:S °Åò® A‘_ù›2"‹È •*]ã‘NóßÖ s@"†€J™ñêÅ¡â'6¬JjŒÆ”Æq-ˆ²-.ðTlˉpƒàX)Œym™™ˆLk7dNS4E•_Å ªokÑà}nøÁ•š ÛM£•·ÙQš¯\9¨ÞþõíÅ£5¦—¯0L¥°½¿ÊÁUÓ®Î1£>:Jc¾ò„pçhץɰj-KU¿2H¿0 ¯ªœ¢ÊMù÷èh’Ò(íŽV= Tûd°Qšó•â'(ÂQø>qn4.¢—€Ï%AÚB€\8J5Za‚%mÄ/eŽÒ†¿B@²”UDÖQ +eχ´Yœgg­hE@¨A‡ÃR4¦€¡BIçš5]FŸ)r˜Xn‹1£ÞÆ ýv.ç¿%³·×*¦@­~°4RI^/AŸŽ âîºÛq­$f˜•CJb$éf¶ÌãHK#¡j ž»Ò¥×pVƨ¯È¥.>µåŠceyç|jó)cR›óÜSS››GSÑW“{ŠMlëÒÿ§•æ<ÕORSI¤Ê:–GÜp1í ‘%kdY«XM+Zt[¨ÅåÚq]+Y}]»›%Ó•Tç\¹{TtÖ¨ÃXÞ¨¢G=¡_׎04½OÍÎU× jÕôÖ¨<_CCzb_Q5ÇÔæÐòìr)\gîÁAÕ9í ×婽Cà>褪&§­ÒÔÆLß­á%áékÚYÞ1 T½n‰)åøŠ¯‡LåYË„ ã<à¯ÅXHÄÉ÷|7a¢)ù ‹÷@¬û…ÃÝf)ݧ¥Oü ès»¡–{kù:»p¸—²#RC<¤©e ßdár@ôIy%oº òc Yb p\‹³+‡fžE/¢[P‰ÎÊÓ‰í¢ñÉÙ¡1°v¥äObÆðJ;KY7F¥7­\5 ¤®vã£_­p½6°*ï‰ §m6‰LáÈ€m õr’\/èÄ SðÃ’Áb¨ÕÏ‹˜É¤@ \•Ë«{ƒ) ut…¢—wŠ'´†z^Šjž@K/ô,úÓ¼0s……Ãd#ˆ$CõØ •,‘³žÖW Ü,}'t¦+ÖÙàE¤ö§‹—…¨–—TŽC¹´MyÙõ9á—Ü‚h…—€¹óbG«-prŠùJ©‹$²â¶$TæÍðÑø3fV¨ŒÐÏá@¼&‹ìO·z‰K¨Hõ:9‡¼ó’ P–E#5ÂMĈR™ç’gTH¯`aX$¶„‰Ø °AlÀx&Þ ?Kz-ðüIÔŸ€žM‚Æ“ÐZÈœä·×Ù µ ÑÒ¨+B¦;º.âyme´Caª~5"gbëÉ[IËH¬‘NøUa uVZÁ¬ ¦n’SÂvA袕v6ì ¤&’n(¯«æðy¬‡ÑÎqò ân…µéQ2[i°6 yV½6?{6ß6Ìñ´á”îÍШ„.Mf ÿhC^Gá´>êjƒ–})ÁÅþ=–ö®µXøb£¥ªHpTEzÙ±/«Ð¼nˆj—À÷ÖƒþWÑ‚ÉZ䨱C‰{6£fŸÃ…O<é·èd4&`y1ÉîDø™Ž7|Yz3<ºDæCUbY(…yô%‹z¹!ÖZ(vHâ-rö{ 0gàf©4/!€A3„•h©Qú5sæL±)ékO‚rƒ‹#ñ4‚QÔh3”êÊ]š^ÍÏ"Br!éÒ…¯…[Tl,…Bú¨-úä§áIdS¹Y)`¥j³·±­fìtƒ½ÕÅ~¿Ü ]}äàÁãw Rƒ=²­?¾ó^¡~:ÊiCK↓ø'‚D!ê“/Á Þ·àÉÝΗ-C/ª›Å’ öƒÑH]DàÃ5‘²¾Wr¿|m;‹ZjãþÊL Ü¢± x2Üà¼9 Oý©Fñ5pyV¼…»pgº‘qX矎µGµ@íKu#‰ »‹bT¾`UÔË5ÆBý̯§in¸y™ì¾+‘î³Òý©4¢foÈÒKi…B2‘²ÜŒÁ€Yé2ã„|ßUÚÔQ¯Ò¦NijnnlÂ"Séc¸ßšZÁS^¥q;¹MêŒú‰õdå€)h‹Q.õÔOj*]²µR[6WrÑ6Ö›3þãÝ!–™7™Å~c¾9X$GýøwVδ³ 7ã–õ–Z%¸’X]Î÷fﹹǘQ€‰,>pzfõY&³–Ù“p­q½Ô«3ö[iæ«ÆS¦€Äú†æú†Œ¦¦–Æi-ÍÍGûdìG}jæ˜%ÓHKŽ5v筙‹ZUëY˜[EwU-7ò†+`fb>ºy¹p²i5V#@æH2ÂnÎT½¤ÉW‡˜ŽŒfÈ63h˜¼ªÖÊôZ¤75•–w2±ªÖ0æ‚Ü·rÞÈ7&¦Ñ7T€û9¼lºr)È©†]>–»•ûGæ\Ao ùFÐ]§fUmwÖL÷w;„jIûÔñ2Wpð.K~u³œöx}É»þ*¨—3 ÇähÞK&7ªgˆ€T“Fºh1‹ú¼5hD­\¡4«ÁÊ„l fž7û» D5å¶gxI‘è"D"K4JÁà1Ç6N£Î #BûN/ŸÒ<ß¹k”ó¡œR…ÕísÊÙ z«3†§3å­,A=˜„‰> GêÏ U,y1R_èÌü7ˆ¸@—íÖþ³d!ü"7¢÷Wd6è: Ä…Ãmtq)P?+Úw½x -ü\¤+(ºpx!YVô7[LŒ@”Ù¤Î5Üq‚z(kJ¸yH†-#Ü }T ¹ž¬µuMr]Òà|T/™5tW‰“P…dCÓ+ãi‰ðXB‚‹òŒhœú÷äêÙä¡'0Á{`”Å{ËnzéqKyª¬´Iu[Ò½9Ù €tBâC0Ì @vAîIE&j½ó!½âW·ì- L`eØîO0á¦ò  Ÿ2³ø›ÿ¨‘¤æ fÉ3è1ËÙˆáAN*ÜÚ¢hñodó‚E莗Ü’WØ“É?ܬ‘@ 5éd—Ês](=C¨‹,ȇ¹²SŽÑcBRcTiJ`tK6p&*†ÖB-Á‘W¬UI Ûš§°¹c•t’¯Çýá™^ ˆPÄ4­òÞÊàäÛÅà†ÀAËs ”ji­g­žR}¬X0ê‰3¾9$ú–!èS'á@'7˜Ð‡†rTtÆ–K¸Ýi&Fè@´2Þþ"kW‘Øç ¹2õÔ1#µ£LmÅ©¤MÏ%BóQR~­‡ñp4úX9L60þ@Ìw½ ÛUjØpçª=é)ÃÎçõGô8¬_>øR[T,@BÈèW‹£÷@c{qeꀜ¥Í×”nhÊv~ÑU#®N¤øO‡¸µÏGa›5”åÓ&ö8wÿ6s„G¤ Î…TDë3Àöƒl=hBx¹D„ÁK‹:( ¯¢6±±º3²ì›× š?X†“N—‹<»9ÈGK€ s‘|AÉ–2@   ;çä¥wp#â@×Àÿ0ô± ›uѰMÞ´q÷q ÏA'L²‡Ú Æ9"ˆqs!ÙI±ß”“"}} Ù2€ÝoQ5!ôF£ƒô‘Þ”Eæ2ê‹4všöñã¥/4Ú lßH¥G wU¶Y­4A;™×¨·À挊ˆmÃDê ôÐé¶X](Ѝœ®Ê‹ÜÝ_áþÔÑ7ˆM),ÙYZ´ƒ\õµáN‚ü Ñòâäµ9;#Íà‚JSè¡ Ô_UÛ±ai`ßÖ:l`®lçAP LƳçV>Su¿-ŒóðïÐ묕ï-õUÝqZü´×ã±ÀÁÏ€8YF;á•ã¹Ô³}n•ÈôЧ SܽÝÛ …@¥¬VM¸REnÓ -H3ãZœ0øˆG5dûHíã&J«%$¹¿ÆÝSu†%db J¦¤ÿss¿ µtŸÊä7`ò5a×7û>ì3áW°yAAàãïãN Zÿ?KjÀˆPNµ»…‡rYúçøfGù¼€o°ÁÇ‹3€R[¶¯ÉS):]ν^íÚél!=£{{€Í…—^¥Ò3Ö‚?`Šu|¦[HžÈ¤Õï|Þ]u)| Ì¢^årV"móMª‹‡‚± Á.öT ìl¥üdæDŸ :l7ÅÏ9Õa¥X¶ÐMÞP«:Ô Šð5]E«%û0«È†PårGSZIb×¼Ÿk)/YI H ÞZ&LÇCuøì­¾²CR°‹ðZ@‚9LjäN£5 ö! =©*˜ãèçOÚ}“ZH0#ýªÅ_Œ{©Wå½rÊeB`Q">7Q VQ–§ëSË'åeüÚ_Y¸wÌrÿ¬”àÜaú ý}Cë©ÂĽ'T©WU½ßyÅÍPîpº8}Hêc‹Ü1€yw*«ü²d`XÈ{$:Qߨ/:*-8Fy£ÁUšâ;ô:xÕDY“ýÁ ÐEù‘ ™eÕ„ßo^²N†vLê<ÎpKÓ:xÖ•¦°hËD‚ô7L~éœà¶Lp»Œd:Ðxˆ² bJÍI¤ê„ú¦¢Q?Sª ŒD5«?@Íncn3Ox–J[šó˜sí‹¡¶ˆ6‰UräÏ'¥~ Ú]RW욫ωºTTOF§€ìÐÙŒtc†8R]Ð+(ĵHüÒbõH Y ç‰‰ÁÐàíº¸Šš$¬ªIB0­!;“ª,(øÐH«‰Š ³Ç¯¤r—_±#±Ô¿Àf{::Ҹ؟½‚áʼn£a¾™ †Ï:eñhRc×H\–Ì€,#x‚Y…ã6tª²Të?d‹›*I=ÜäUÀ‰Óx2•ÀÈ”\ÂoTisØo_{ MZSÒ|k-ÒnTÛtÀAÆ—Tudô ½’´G [pˆ!ÂETj%.79©ÑcÕáâí/&l0€•… ïµ_ ˆz}ñK UHpwQ•¨€Áé‚„¨›ÞÂÁ¯ÌvÑÇ…p#º™Æ?ŒU·+3³MvG¾/³wì¾Üý7 Ê;­+5"oµ†ÞˆØl•Í*ˆ•Ö7pס¥ÆÙð­ÈÇÝð¡†>{þ(Dxþ²´È¶ÛZÉ.•¹Õ-·Ä—ö@j8È핽Ì%L‹~ €ú¸l_e)LF·Åvœ¥é4ÕŽx¦‘è1M>bÐcµžË…¾…Æ©NƘ…G\Èã]WsÃm‡:wÐúQ‰[$?Ä)Ÿ#G}zʲ­,¸q`Ä&ÉútrŽ+Y°Ùiæ‡Ð:=‡`È-³höʼÒClÜP×býÕ"]4qî“0Âý–„±®ð«!3YoØ<åÙÁ\Xl°yó’°yw&ž=,m› tFønéF|d0ÀSZìÖ¥>%ÄYô¥¥1©|ƒ¥¤$%×ðõ‚¶P±GÕú¨Zdñ®¨–þ’ypp$7'f=ãnj숬ûå éì³h¤¨NR;Âò݈ùérµ|9<ƒ¢¢Ìà…È¢A åÂØC9¢™×m&ŠRW3­ é[*]ÈbMpì Äa‡ÈÄ£[‹l›ãP| ¾ŒWoöļd¤‹:O6a}@¢²ÂšG­©°Î–hMô‚t‹­ ~á]¦´JÀp­"¡¤¸iÁ ,@Àä&I቉æle&”úb+É£ÌçËsBõ˜]âyo„æ\@¢º«ƒù,‰ËOJ+²”‹Z-N},Ž.”V"N›çÔáê8ε‡ò€ë Ü;7N—¢ì€v—gï©tæ¢/Óøs­9w!V y«Ãµ%za©_ š]ÉÙNkÚ› 9ò’ô´‚ñJéM1²Šo…ò9Ek±##G¹ŽZ8)‡n“€ÒÉ cv¾µ(–uP+œ%ƆGjüáZ«sô¥#w1Ê£ˆ²-1Óg¼€¶+8.–úœÞèafàzb» êÛ<>>ˆ:eÞá Uq £ÕG7Ï“A^§dn³‡1¬ªn…¨·ÐvHjƒm\ôVHפ‹®ê¡²B{Ë®ìP—Òáj±ªîz^¯)%±K:å8] Î¥à?cÓø„¡s‹9îÔ‹«tŒ(!U,Ã2ŒÅÜÙ¯#®³ª§­ÙSbAè@Ey쑪ÇÄ݇¥Êë¶8&t­âÌq˯ 3kzl7³³ÀèÓ‹U:[$j€ªÃ¦íRÄá Fê|0Tô–ÉNÈÖbÊ ÏßœåIÔqî§|‚Κ¹Ñò::¤5}fÙ¥ûøK³ß*¡3ȨJV±2êÇ„v/$ŽP , Èv$ðHGÆŽTx­è‰q)‹ RÉS™yõ“¨òò*wóêY¹'ÓÈ»²ìE7RÊÓ'N$’!áN(C‡ £B­ù!eÖ¥X v])PÆÌ¾ó¨ îöåA¨ô«ÔyA&_Jv“ °C¿^¡þA³ØËì®–ä³Ck±mØæWVU¡±dMD}PÐä­*0Ô‚ŸÀ/>@xq]´ýO&á]A¥ñª 0µ*D¿ùaT5€à’@és} ŠU‚(RÅ T­ j_ øëƒ‚·TU}16˜:I4(…F¬¬j&य़AÍxêæ iHRVÓ¦$OïV^²jˆ|!9x ÎîQÊ'“Ã#-á<_î°Ôƒy6;WÒcà)™ «r[ ‰á‚æ¸ß‚³‡RžIˆW0¢ßxÆ^âQ¬FE—]Âì¿ùH‰0A"¢0,QÍyëG…ŒJü¼zZR/oé+Âék¥†ö̬Ök6üNR± 5~ä ^€óOëY§Wº+Ñk•¢ÐNœ`cZ2©¿t/“š ‰viôYYˆ‰Å“²b'… Á½µÔABéé~Dõ¶…×®þS&ÕnÓ·²Å…V= SDüòwLïƒ+E9øBË‚øq^ì<¡Õ®”Te‡%¥äÂ*žU3ãçI*¼³w¦©^ø¥$…bŽoôI,ü¦¦ sš”ƒß¹kŠ[F÷FÍè½Ùƒ @¥‹²[€È„ÐäPw²ÂÎhfÉæ•Q¤<Å ë6¦x²©¥ÖòªI¾S7‚ Môý‚D©Ë‚’®"²)nJèNJ`1RW®TSk&â!‡EˆQu…SpËØnC‡áÛJ<@Á›)t¾¥œ¬ƒ.hè%Ÿ’̨ È[rˆaÉͧ=5hº)ADIÉ`>Ȭ|3‡¼+`)jº/_5¾ú_ùb4ÔfÓìŸXF'T,D{´¥ ²™qjò Q~ÂW vúiLØ©£ ÀR’›öø ꂹDer$Æ}Š›~zà®Aç`à+ûÑßxÊ45?8œÐH¨¹sº3¶ aÓ¡§kD¬øþàÖ/ÙE!#}P+è õhC{Iøšx6é¶Ð'}sõ!5D‰ƒ)©_Q§h( ]Ž V»X1RB”÷5n(æ/¾Ó#ððOáɃ†~ žžâµŠéœu' Ôm@<ÆŒløù·°îHÙØÊyk ™YP Ï2] É«šÌû/k¹.wDF‹˜vχQ÷ÝÔ4܀̴䰬2”€M=4 È,ÂÖH Pð :lÊÝ>DÇú* Û5súþŠP1ᥤ°¥Cg+€¹^Ûªl@£Ðaz:ÄøS&<”UÏÚ0:<ÈÚgêGºJð¢jW RU Ý/ ‰ËAÕÃ>üNO¢ro¾sVÎI„¾ÁOÕ*ÄÏšZ–بL P5ÓYfä=³«A+2¢$WcõÄ ×aU¹ƒ:Q3-t·ÖQòÊA·ÍA U÷E«Ìì0€`ô$„> #v ÚÈ{2»d:F蔋†C¶3ïÊYJ¹Óåæ…` IÖM%ü…› S`‹ ¨ÓC鬿;a%â2qyÌ‘Å<¦+HRd/2!ÙMœt%m"hhl‰lxiPJäWG B öL s˜ÙVäúW¡7¥k®:%ì„Yà,VZ ÑI6×ížÉµ2©Ì(_1ˆ@R`·>j2G¥?Z° /…¼ì+Æãð‹Pú•=|ª˜4¡ªË?¼DºÏAƒVÁZ”N%4´-V§Óî)8H'øˆ«Á€Ýá¨ÀÐ0Ç2ÇO*rjŸü­|(ÂØá@b"´;óÄÀ10¾É-–¹À‡»U¼Fí±8ýчQ‘RüETW€™:­#¡áE\‚†6‰úEé€ÉøÞè£è¾ÊÛ‹é7b½ ID¢GW\. VÑOÔ¶è¼»2W”¤­$ÍÊ:heKñήJ³„ԗŶƒ÷‰tõá^KW—õ<[ Í^ˆ­_Ôë™‚å… WÃIÙkéä{[uûññýe^þi!䪋6XÈö •LÂH¦1„.á¾#+3_&–Õ*y ’Ew’\ÀùX¥ô`%ëœëÙ)JNÉàDÚOØ&(¼žÎ]T+pO  ¶©hèÍ{Ïú æÉ¶Ë¶a{ÄÏo-.Ã=åœ@¹]ê‘Mî–§ÛàÐUüæH.7"Üê%ÃŽ¯ \ š‹ ¢Ût-jüÀEêèE35ô°dCàæG•„*…Î¥ ºY–‡ÅK:hx|YÌÇ…?'Ñï]•9³Šb·c©«*žD§0é†üXàƒF­-9ÉÕs´ø¹˜'ëPjÕe*QÅïòOÓÆ [¨™äתŠ)H~õÆ­r2Ë6ßU€ z4&7Pµ˜ð ›oI­n0êyŽ/!ªý£^-›x\#ªMzj¢Êõ!ÅZÀg¢(T­pýNåÏá‘·j7ýÜÛ-dÍ¡q‚‡ ¸‚®t$G;Sê«”rUÒöÈ×ÒP=æ£1Xýžƒ4[‡7Ù*/Íz/$«=vR7  fï~!€~©ÄT„ž´Fáþ]÷+‡ ) >@I û™4¦6LfY³=Ëô÷°…‹îú¾)•×ÔFL’VX¸‚uc’$"“"Fdb[¬oŒÅ&bmY&9Â*:õ´GtA€Ò¨¶®V>îÆÚ™øÀêäáQ—·d Ûô}ŽËƒ’4(ÂÔ©A h þrfDîf;O¥9Ýao;“ÉdWíAưnò^HÕk;ëëë»ôAÁÃÐa¾rï½£?5„qGâº:VÛ{ù :#“¢‘dd""<61J0Vè#“Cf ^‰Éš銌֛ ­Ô‡·RÞŠ¾ómÈ™È RT!ÈI„D 4{õ@ñ>f i3[d2’¼>ä‹¿Í]˜;SÁ°úš”iAÜÌXªm-nЄѠÎÂaÆKCn 6‚‘(«Uvܲ©©€£·qf£J´ãÈV_ÿ $ïTË\Ô|² A柔èlaÓß•H[h¹!“CPÓ“¥U S¯‹×à>ëï©|Ê4òq+ª3ÀŒ’òVÌøš5O˜U1h)!¹*%òHH@¢˜5’†Èeˆ‡šÝ(eâDŽzÌdË“v?j#.ɺԪºÕ§€G“%FSñTÜŠqE‚ |SBaxì…2@`D«òd“õQ::î›ÏqùZŒ9d¡G[;öï0æÍB߀½66ˆeaì2v/€Ö}2ꌯ¨¼f" cþº5&Èój¹4Ö»MÖNO­=H~Ï_Í|Æ.&. |wr}˜V/ÙhŒÓeO¾uU(ï–†²V’Pjû¦¬SLÖféôP-­*Ë'ä°R.PƒA>0×C^@'±È¨ó !äúFøÏÚi$‹?+­¢Ÿ ¢VïËš€¾H Þßžè \VF×3d ·sØ8[•†æ:âÿ¬€i¾êTˆõ7-wBÔLþ1:³f·•MÖÒB5ñ¡˜ÏÖ.ëÓ ÍQ@k8Òìü€ívd¬©Ÿ i†ðöN›ä9V:‹áÅišö1¡•we%áVí ÿDU šãyÓäMŒ¿Ôˆ†W˜/æ: -Ž&d•òÄÀÕ$.çÆ††Š‹¸Ï,XIÂýlLªÝTRL}¤êù œÍú™\ò ¡‰À5èÍ AG×Èû)­Dú‚ õê1®“µ3&T< ŽM†t5~:g®|È ˜k— ñÒ‚¬Ÿ)¯NÚ»Zœ(¾DC¨Ð‡1Q|0”ù  –Jm1N0¡RCÚ©ÛóŽÎÒ­ñ ´°*£QÄg‰'iEñGü¼ôiÄ42–AÞO'²€Íë ÉõëƒAÕ—X3‹ùÂÂÐ yn#´~ŠÿCBè yîaR€ÀÇÌÛ=öHHäkÅ1¡K0ÄTÓlÊÆ€Y´Q/¾pxñòE©¹sæÏmñŒi½›ŒO½JãJ}`¦L@²a¤â°ÅÔóჅn¯Y¤VJþQ×TÙlã›E¬óèOJ›U7Ù´AML»4NZÿP#rÑâ×;+ !n¤wšÃ°æ3#B®àÈ _‰rÃÉ~ƒb`¨@7ÅiEY‹Kã5¯µŠŽ ¦4èøãÙ`:lqý‡ÊÁ^ž(z"=J„Ûé{¶ì2ò¼ð˜/ž¢UdYç` ûÝQ¼ø=#fl_ \žÚ S¤@$i/@s<· #nð.P‰ ¯Ç‘×¥¶fǘ ØdQ¢t`iȃ&%(ÑN܈xÌ0<ÝNJ)áÔ7FºÁ lœ¢ä‡ 6 HFAl8ùð.¡– 3üØ9‹±^uví½g÷ú‚q3]"¾d¼¤ð<ý ÷=QµÈ ó@_0¬€!‚YaV:Ú±EAø?OìŸIb’ht_$ÌJE`¤JfEËhñbè£:#ù§óÛ÷¾Ü7|™{Áb$„käÈgk\C¦F*‹V¾” áÌFYúªr$„ T\ú „ aã5©‚érleEDݨÅöZ+¶ é-?—¨\ ÆáƒJÇÁs‰s"aŒ9ö6Ü)å„.»³*R6CJ 4$„(î)ª£Óïˆ7«ã ^ê°h"¬€.N³L8SÛ® -2Z—DÖ‘Éæª»ÊBßJ¾Æ@‰®”J-FÖ¤ûˆX0«'Þzb!Sñ¶—»ñ-D|+.Ez`èLl„´Bë> ÓI{á]œC‘ÒbdÔ¤E§ÜÛ'‰ Ú6H}i@bfõñÈM¥ß˜/²©$ìo0·ä7cbZÔ¯Ð.w¦w^FCóåK„'Fãí®¿ùFóV#5ãP4>ðcë_œ Q江MÐO«©B  ù¯)d@0¨æOkPVó(…ª=<»Õ·š+Œ¾2ôûÔE/4³™_{¾[.@$g+ã_ô+kõ¢ ­ΕªfH,è‘Ç¢£ÅD~½6z4~ÎNFã$!ýÓù‘VßßEŠGì–Øô½ BþªRÆ»—"Jƒ`ÙÆâ«¶™÷plé3L+Z ·ÜD;Íúµ]±hgký]±HÜ(FV5«š"<ò:Ï+C¾AwR)ˆ¤¾QŠuƼr>câÆ™EuãFd‚–Å,ßVAå÷¹.ºÖ™™Õ&-xç[´J/êÀ¼Qñå3`öÎ0²!´I=r„"+#â­ˆ¹©7:[öáú™#ä_3[è3GŒ‰ÆlòƒÌ«9ÒÕi·¬î’KÏ¥{ÈHé®ÎÕ-ý´ “ò]!Å·Tlh6iÊßG–Ä9¦ê… ÈgÂBS`Xn=:‡°‡‹B(gÀvZ=Ųˆøm÷°'aáX¼°#£M3|ô½‰.Úv\UpÓ—¯ö{ÔÅ+Û²>êR–c¯³Èõlqó5-ô6U.hÊè!UVT^hÿÃ+Ú¦u„)%®7–CÆQÂcNÌû­ßÈÿÉVE•Ýä­Mœ{÷ @à@¢—å¼Pf%BÙKãTŸUåž(y¥XS^-ÖT%P3ì´*µ¶amyEAQà:„Έµ*„T~¢ø¹]¼GD¾”X#„'OT?À0“£ðÊå|ØÁñehUL–oHLޝÍ×–orhlmªzn _Ö“j+BÐ }oûß97+µ²Pgù^£ƒGwbw|¤WR{EÑUº/®–Gj&ŒI©ÑÅyKinEÆ>Z Í^x¾]ú·ÒŒJhN1Þ³Õáêç8ƒyciÑbŽœã3Q0Àu®4I…îÝÞspdT:gRzgÏmRz¹ÝAL]„«Xy¡É …ÄÐÕÙÐb3)Aº%Ú8÷Œ¶ávMìàéî y-òˆ‡t>TØP‡Þ…³· ™à"äσM $‚牅Ai½ãFøÒ;3“!h¶2:‰i¿ãÆ0KGÛØ–»`’¢è%"Ú%P›¬P0[Ÿ×ð¹¡æ³"y–Í:ƒx1¯æ É«ƒ,kª$ð*%ïÊRõli•P‘rVµòÊÐeù‘‰Çá¡R\ª³ "mPõ{Þ˜k³ÖÆÄ"8«$BW¹t*IвC;ªŽe™U%/Œ²Äª”6l­±e0#ÈëLZáKÍ_–/5i¥)ËY^oþG¾—ƺ¤"c®'buKÕàz«Udî豋àƒoçx:N]\¥B­ Lå”ÌÎÍmÇu(Ý}M‰]ßñËXнÈ!Z0²ãÜßÁ•ò‹yðØN€0¥%€q»Ò4ó^1J¡÷Ì}  d¤hVˆIO¶ìöé'H_20‰ühK¨EEIz:Ã="õ‘èð§·ž5qóYZ9òÐa¤ÄJ£Êz Ð-‰ÈM"''A%:iøbˆÕaOýÁ4.,4èÝôþ‚ bÎA¼:çP²î0¹ûMÞ‘óX€6#E÷fSáÍ úOG%„AâSðuŠ£Ÿ—‚%(ßÙ€ÁiXŽÒc|؈Y‡ƒ°å·Rñ™ƒ–œ6ÁS 8²,k+«a‘0 ]9+8‰Uiôì´÷“ŒG© b»À±v‘<IJ¤+¦@ ÞÊÈû’žÁ¦Žgr.Ñ»åœY‚$M|þí|¡,)f í ÂÃ.û+PŠž™4üç Xòt°ûò×Ò‰¢CSÙy-¤fõ;ǸHäWH ö¼ÜäléëKO²†”(Q2„”ÑowiË Ûa*Ɉau4aqcO`³RÉÉÕ//ŒûŒB^^Ø( ¶0¤%ŸkÊËã>"-¯¨Ñmõ‚Þ™m¢ºèªfrW)vlbk«yÇ ²ŠJ#Ÿ²©¨iÇ*f¹˜9ŠÆ°)“ŸÇ_ÎÔ¨µ^6 ÑòÈ–ÒßÄñ Û Ë‹¬ÝL0õ`UVÆ+1L€Eù·+”/þ»~P#Ƭä ñgÅ0ß* „§RÖŒKIÜh¬zÕàŸê—Î\ˆ|¹\waÚ:Íä!4µšé¢¹59ú0ƒ4zØ!Ò˜q@œ4›ŠK¾±ÏJW…}¦1šýWJB»&L‘kL2‚¬¿6J„evÃ\GõÉ_·µym€ZtIË ãî½Æ›¿­}ÃPòor›ÖTÈLñÇw­9ÆF½ã|¥ûM4ð‚Y*öê „n•c3‚(h êL”ˆ5ÿ{/R Q¥ºq¡” )¨#*p)ð"ݲq&ï=ÀHœdï©Ä[6ø"õq¼/á³—µ~c¦ÿ·¶£,ølè21þCqô,ÕðݤúƒôêSØOt!FHIÆ…‹&ÒSÍŽ#ƒ5”…µÁ’lµ$ç ±ÕIJ$ÐWU"q!»ªfdEˆMÖ@²A¶{0‹1yƒÈ!äˆ>£f‘›½I€ÓÉÊ ó1Yù§©0iú“ø ¨R+Êç–õ(Œéw˜ØxËŒóá"ÃN³K!ÇLq¥ÆŽ9©öŽe­sçBR)¶1ׯ{6ˆÐ‹¤€²áBj¬«föò±ôÌwþò ÒÅm@§¶¥_ÕuÍß;™Å–¯ØÍš±u² 6^©rG¤˜”v£ôÏ¡ÞØB§÷˜L©W†ÆJˆUg–¬Þ!š¦M®½pXœ¯Ýž·âLRRJu—±Ò OäN˜üö†ü:/Óœâ[êûX *tn€Ìl¯S$qc× ˆòã¶x5õ,Õ†r‰$ò Ë<µÞ ð¿`¸È´Ýä«À‚Ê€ëVØ]EY¼’ìÒŒŠR·X$xÌÁÁUl4••RE™-ߦ´•ón&ÕµÙÌ´9£üÔŒ¯R,³ÆD›Úy.­”Ù Y=¨HÉRÑl98-!.ÆmÛh*"°gè†LZ € ·LÆq |pwÝ’þF4 &õ,0Û6¥Þì`J Ò…ÒnÑpE+g‚f¬(RVeÁª’)ƒFhäîÚø¼Ö“ŽL.wxÁ©P²söZˆ=‹/ê©ÿ˜à™qß!Œ§~;ÀS9@QF÷—:§Ù»|H£‹A…d¢:"¾hÅÉB{/–BR¬ ­•$ w2 ðrò3QG\ž‹FÈž)\y~Xsíä\#ê™Þ‘Zñª#”ÒCölÈ,K„\¸1&òl•éÌÏW‹6oÊɼZ)i挤<¢œV GIijT³A¤è | ‹ÌM0FÀðŽ»’{µÞ žÃ*ªg&tÙõÃk ƒ§u¾à Ô(§ NžÌhU·,RÂüszö6sJyüøæjxM/^²­œžÚ)ªgw°¨al^;°Ó5šÂØÅˆ€”—L×ÊLvõħ’ëªDã->n3@ ƒeŒÈ·‚"úÂÒC5“¬ŠTjx] 5–)Õ#ûa¦Aª¹lTMüç4ž‹TÊÆ˜d0"qQŒáEc:À’H¤Ã„̬ÂB‚³ü¨rÄß^Eãh³<4¦3K÷†=öÂ6AV~¡,½ø¢M³('¼÷c¶{QˆÑœŸJÔ“ÙT VKyÏýãdñÇ“ÔMšþÒíìøÙŸœmÓý©4$«!ëÏW˜nÇI?Ëó•ôö–€ Ÿ:ãp ž8S/Ùyš“@wgmäÅiC—·˜ÉFØý›ë-:u³âzkR¸[Ò]’”õRÇe‚àÅŸ¬ ¹¯tkƒÌ8\³ˆeX|ÝDI¯§s=VA1<ÿØeh*còmRü_K9K§8§}Ò“t’ØÚZÌê ýçYÝ88Ê)ñ”@fº_–J"!—K³U¶ôŒÒz‚5¬õ‰°ˆ‚0ßðaŠI¡àòÄÉ›¶† `m£<á¸ùVŠÆ‘Ñ3‹9â¤øVoÙƒxž.„¨Â6ÑÊ4Å?Œ4&% œÌ/2ÔsÜŽ+Jtê\#ðª%‚’…ÓÙ– O¹Á¿mÂG‘ÓqzL3.ÅÂm±û{jæQ&t£'SbèRÎ=ºƒkº9Áº'4ä»-7XhÈ£ÀP)˸²ÉÒ‡¾‰Îy9©4ePp7RÒso—×ÈÁÙ;[ò]škƒ¬ ”¾û”„ uuSpí× pZ¢½n·,½&žò »qŠ9Œ‹5Š™¾Ëe"Õ'¥Ìb¯+ßöIm$Â*K*w’nˆ·‘h¡×/õåÂÿ8^Ô¤¶¢üá1ÜÊÌ S5h!Ï’›€/ÑÚ¤ÐCð Sì§ž~µ\ܨ eÔdLÀ=­·ÓmvÛ@ÿvZn„{ËT¼ ûeÂN‡’Üš™ îp:€Ø˜ÈCº÷<Z=¥6¼†7BA«Jf›L4<±ÕBØ z gæcÓÓc§md¹»aº3f1ãÁÿw\ºž:çÕâzC—:-y7ŒyÂÙáÞÈ—Ç£a­0³e #aEk½Ž¯ Ô­¸Ï`I‰p|ˆ‰Ú±QpElŽÛÕ%\޶—¬BHôSºe‘_X+Õ.2íΈT‚ë"×rÊpv&üD<È– ˜O[®‹YœéòäQÌA’–£•.àö¿2p„Aý™î¢åβ«IrRyÜ]¼2œ„j„ë(ãg^×`Ò¹4¦›–W(×PW”ÕâF„‰hÈÓ»-‰ad¨‹©“›ªY´k¤Êl«ÉS~]ï¤Æ :K%‹a‚© ‚ýeåûÍ™¤›LY²=Œ¡Aƒ ^©ÄýÚjpX´Ø©»‹–©*AƒïÖ(DL@ZGþHä“nÊÅJT5Vb[ïÿ$yìò‡“ò¸mÚ­i8A8ÌþyÜvmŠ\í+w“ºírIZJ[ _G ë ÑVÈ%¶Ú”bTU]K¾‹oœÄÊÍPʧ+ÙKcš"×G|Õ4ËÓ:áæ(Ì›`Yç<ˆ°¡¡½ð1cÑ#Iy[I‹¬öK¹E·AÜ012©bgX 3UëŽ)ï·ÅÁ÷`dwéq?ÈçW†Zþ3Ì\Ñ;¬ãèaõ09‹sofx*™²Âí¾z `·÷¿Á¶0\¾æUI½ûÌJÂì±:[ºxäfð / é‘-‚ŽM|e&˜ÕFš8?/²üÜÁŒ ³%Ét2¤ÑLÐ*ußcmÌGîúBÇ̈€JÜŠP1ù 꽤eÒs./¨’§}Veúf¶h¨Žj\7Á¹GCñERg€ù'éVv@¾¬C¨’^‚’À ß(sãtéÌÖé|Ë‘¤Bõ”€gy¿S“d´ã¿¥ÆÇ|툲£Òõâ¸)AÏ<ÇÊ9F›CøÚ¸E© +Ø C"ZoA"ò„à9HfA’J÷PU C‡xîÙn@f×ÅKKk–.…ï¨Õ¯%j/eï–.­YßWÈß­b7”ò~± s¬™Çržý¬=ÄéË×ò jkà=|%ŒÃû‘ò¾›¤4©ß3N¯ô+í8ý6©…mcÑ’üË5kk– qŠqx?Ë`Ø\[CóPIDrÇfvt’¿ŠbÒº —ù(/¶}~Ìjm{Xmª7m’‹Y(Y`ž€'º„ ›~Í ƒŽÄÁðœ’qrªï“Ç=ŒÔÓÙ²«êz…¸C`‰Àcê B⯥l¤ÜRqÁ¸7å)@d9oX¥{Q8}ÜÐÁx;—Jä5ŒAáÞã"TP;Ñ`Ý©ÕÞôj½®Å+d=[EŠ)°=n©5¤P  On!k—XŸiAúÞ{À{Ðêöcãe@ •ÎØ&›*Ù3⬠-ëûŒAÓÍGJ<Zí¯7,ö–s¼G¸íYû•LÌœÕýK7É$â/ÚÔÂÃ]• £•m–ËÊ7ãԛ҂ץA¥é:ðchŒ©-Þíž jB¼Ií!ù¨÷÷`Æ M#òk6˯Àªüáò”Eë§Å6"ÞÒf3„%F¶¼ŒO@hBp¨ˆÆ`Ý*›µI”áU,{ŠiÓŠóFmcSó”©Ój}Ó;Û uVÑÓks<Ô‚[E•úeô9ƒFÖͯ)#‘„ï@FHcGâo&2Ûôa¤1ÒbD#œª"ŠÙJ Cb¤ k‚‰(¦ ÁUšÕ*2Eô‹ù`S‚¬¢kHÓ!¹ xç 2ðDŸé¦ú­!>Q-ryúµK¼d22-"ÃYÙð–•YÞ<ë¨2áVF!å¨ Q3פ­B‰þ½ó{GÒ#kÌ1h6À*ÛÅé¨Ö˜$ª“Wï’#2Rô]à—Lì~[¿‡Sá4ŠkCv9Çá€L7/ðOÔo.T4Cc³Ü–‰yzl¯5 áΑv©]©/é[\ô<#éI¦6Ž&Šq¬Ì”ïÙª¼kbr2¬K_Ç’IQr•9%j‰O‡‡7Í®_ÆðǰÇ»;7¶‘§žžÕ§Öö†J¡²²^1,h5rŒ£ã '䄾7À¼²üÜó&p%¾]Bœ¡DDdœ!ˆ7F ˆdTÛ^Ú©± „F&N$;|AGÛWŒŽ%äÿK9K_QŠ’v]Ì@}³»:NÏT¡ÇzÚjjà£N7Р?e»®*J¹>2ŒÓeÍn‹¬–cy¥<Í4ía¹hjn‡:IR¼’_çá–QÈ‚ i£œË "E°7ü&Ψóém7yÑqPžÊV7EÇ)1Ü>"ˆöÂý\¤ÁÛ±L°p ðpMPÃn´'ÍYfÞõ® 0¼&†RèQZô$*)#HÆ.„’d— œÜéòdãÌf´Ð|1ëá:è3‘ªZip#-`´)ZYÂ:#Dô"½HÉì•~™™ é)i4xZ¨8æ]‡iAó,G´Y2’I´Ìœþ ÝÇè™é°#+!¹-ŒûÖ=”beôÂ~…PÊ2¬åŠð¬—è%«€’̃Ε\é5',LªÈ¢^¸%ø—L±r•R<çlá .cF½nÓ•jÌL šLˆHª3ÒéUQÓ \’±:ŬvÁþÉ~„zp ènèŽùzÁúX¯är„èG@Ä)ÂùÜ8d›cŠSþ:tžèk¸?I»<´tD\“^~¨se‰v(ºøô¶‰1NÙÌ*ð‡M†%¤FLàÈô´GdãJë åY9Ñûp="p¬ùJcÁ ßýSÈûj$Ey)-g&³È&ø_sq0HU^r­h úØ4Õ¦‹µ'’ƒÂ]¯ –Ož²vÔâÔx#3.3¼8”Á–•ÆÊõ2î‚b J¯9þeŒIv-N>«%ÉJ†qóÊYj)’§ËSõäëÅf¼«fØí#ØìSщoÀ%ÏÞ-)Q½2"GÁ$/×4J9 ë2Ú÷MÒêò3@={ÛÂ=*„Ü)Lç:P‘œ½Ç^cX`Vʬű í§ÈIÞ·¿ Ãm“ñ&*m2ãO³rçüèáÑÉ´„)7:¾²lÉáÆÜeË–,k1d§¤/õ*6êuý¹QߨP3H†îFà®X¶ø5® "’ Ÿy2çO=pI7êAÛ…¼?=œ+àìY°K7|$Y˜=þWüÚšJƒš Q£A(Z…pµ3ô£¢u]rîÌZr÷Ë*Ì—‡ˆŸ%;MíCDƒYÇÌDá¼æ–@M¢ D ;YÚ5 7°*ë³ïtB~Òñ: ôú—ÊC¨µ„‹§^α0vX‰†ûBaƒ‰Ü3Íø*Z)Mæ·Dv¸’Ù²¨Û—¶‘¬§óˆvcR$¦Œ¼wËÝQR˜ˆß«JDÇŠ›1Ç“8MdDBtyym±lJ£s%¼æOÓH1PlÐÛy«àcf•³ié4FÅã 5ÄÚÓ±ç–sÑF\ÊiªÍdíƒzݘÁÓxò)s$RíÍ8b‘ý7ÅæL6ADÀýѬh}kÐXj,à(N"Òv.5®X’¡ÖHB×/‘Õ5¶‘Ð òÁý0~ „¤– ‘UùH *“—|1z+Y¹‚?z"(.ày*~ìé_4Hº Ãeì¥b€ ~ÚÊf©ùt†u°ñŸòÅ6^ÔÛð=0Pµ(—4šƒE*ÐÄ wë¥Â?Ü,BÀ²DD•sÄ÷C¼ImM©ª­8Ü ³1 |Rq7…ÿGè)©ú¢B3ag+Ÿ#yd±` d7¨²ÍÙ%tLBçªè„LŒ®…r4‚ô0Óæ,^¸0ÚØ€YòèNhvG§ÄØfaÖ¢l˜1ÉêؤŠÂWÿ,.¢aIÃÔ:©‘l;TSÄÐÅ4B€¯Àáû?è©BQËôK€ájëSå”ÑICuF3ñ>zk‹ŒLvL3ëm×H.׌п.ý”ÀனyÁxóÜt’Ê ”┄:R^°†<‚ò7D/üÎ ô¤Eà>G¹:R“´!õ°>¹zOuñ¿3S‚(|k(Á£´MzKoÁ­ ]¶t™z‹fåŸ(M’îH†+TniŒÜnÄxq0]âv.Q´mùšHÏùÿrl> i)%¹y]aѵBû°&ÅBH  µ:ó]~uo—8V`­^0ÀY™ 8¢·'t´½ ®ÁuSpI•Z“KÐH¼G;\ðo’/!›ö¡¦³Ð„ßȤýàGyŠ›X9Ïù î̯¡Î†Õ¹ÿÑÔݨ³çvˆÒ.€5!8·Tƒމ˜ýìéR+OÏ^Ýf¾_³a ëwTäåRÛ*’S¢±xñRœº&6…‹-™S³ÂÎCú<ñòö²}ùìš8ûÎÿòŠKk¦5Òomsèß©2È! âUÈv|8½l…—9ÃçCT˜X3¼A´t"ŸE¬ÁöfÙ’%5ÀàÇŠÙ†2†m5€2øÌé ¥æºð6‡ŽÒ‡²zKfRcºôûíÝ ¨h²êT *Äþ\c¥Ë%ÐI#U Q>BV>k¦‘ y)Zì $èË¥þ>4±_| §Ià¬$ÿ«ÔD¯[ø…úrfèE‹RÕQ’Ge{àÂH¶Ràw˜Ë°k€Q€LMÃê¦"þêJ²*øëÙÑ7€¸éóL¯ò#!„ú)1yøÒfÑ"Uˆ, fZÇ E‰½‘bÊ^ÿu`ÎÐCúI£4À <‰±5y7ƒünKu‰['hƒ²Š ñAå %Ò@0œ¤a‡Ž;`;l Ÿ‹Ûž°Ë*‚š.ËÛnít*›M´'&Ÿ`“âF¿M±lEl°c ^&7†&7IPûÀ4Žx0šBaL©£I‚Ñ £±"Œf Æ”PÍaLQñ®[VǪØß&ö·™ýÒ¥Ug—:¤¦4ÛÞkߤ×($Ž·ÿSàó1Å«‰×J[§TR˜9Â2´ÏZß“¥E²½° Vq]ó:kÀéÉ ¬Ë;9½,Π[¦ßSäÿ5ð ÏÌhÅú¬níÉ é_²õåÝuÎ@i]£ÓÓäô@‹Ja  ÑJ3ù’³JLÿŒ›Âþp¼éüY²he ŽÅÔjùŸÈë¶Ö"ÆÖå­riY*ém•dXG±•©Ø¬HòX|OKÿºd,%ÞJÐXAN÷êFÑ PO9Ÿ·òr±~3¯U”祯œ-¬sJÞ´äÔiÁÙHÞX¦“/½¦´×4´¯"Z ?ÓôV`¢¯eåÑÐ'a­¾ŠTÚpŽt~µ× ¬©¸¯˜ÿÉÒr>Íÿíwr9ß`åt½×Û¼•FBÛk­\N™x¢Vܸ±ÀÇWØÿ¤ÍZôß<º!VøÀd+óÌ­Dö"ð)öjÒ'rE?%çÂ)Ù?–fïyÿy^Ê{­4ió¢1Ñ&ÊDa=µ™¥¾!¥²ÿ‰ßÑØå¸-ôµe÷öªc!O*´2úq¬!à 4þŒ¬1‘ù$£µ²ÒÆ2²µNYoÅÌjOƼÃ(§>Ôø˜9ux2&)†ŠyÒ‚Ų>Ùþ'cjø–²`à'l¶(±‹öj•€Æ´[ú´$ð€l'ÝcÙ`ƾ,a¢üjÿ“1i|ÛªæLýOÆt¶ðÑpƒqgʰøƒ}à&¦|üOƤU)4Ü`üYLC‹Y2FÅœ¨‹ërV©Ç¦ßô#‹o‰ 8ŠÂÈ>;- YúŽ£ŒÝÿD9ôe*^\àÄød¥qWûZA3Þ r*I õŒEÛk'OÈàO¼Šc’b|LVé 6ýþ'²¤œ±¬¬2¤`L9Ž}*²’O¯„ÍfÁR¹(5Äž„´2ª(+ùW°ÿÉÆÑÈJãÏ”›u¦Lt—ËEµ¤ÿɘ6~Ôº+L™,þß\ûŸŒ cÍ:ÆmwÍ3ÛkSíG•qkñ?YtørK F£Ü&§î–¡ëêX¦}ÌÔ ûòð`ÏÌß+8àóôQ>OÒÔÜÜØ„E¦²ªSš›§M-à³e³ÖK«Ážë'ÖiÌ1[Œr©§þxRS)tkÞXO1“-)0!Ò¤,§â†Ä„^jÁ _¸ºœïÍÞss1£Y|àôÌê³0L„H¸–z™Óm´,R{FÑ…¿³r6¤m(Ë…æfÁöm¡S€rV& _fõÍŒ ~Ò¶›(¬|^¯WMði©ø!àv½È,öóÍAØ5fôâßYØÚ͸eÞÀ¸§®3ö[iá™2†Œ§LtXß0­¾¡ÑhjjišÞÒ|൱ h= &1Z2 xÌrÈ4Å4–6/¶½)ai~ ‰þ 4mXì-n‰÷04Œj:-‹1¼;†”ÓMq‘C\°Ò`vVëÈ»5Óø¤8)¦‚ ¶>é6…¾»¤ýC¥í¸zÂx!’"äžÍí‚ðšÉ‡¥±ñ-úÿý¹¸ý¨»õûÿÚìEσ\zø]1­Ü#|Â).½gÇ%lŠeðêÞíRJ>ðvSÄÃ-!rt¡v÷TòO0ÆWÙO‚Hf[Î4±i`(‰&Lðkæ(ÈÞJnâW׸ˆ‡H¦T^yeîå«P}À4Ò%eƒcñlZWÈ`3ÆzXáÔc¤Ìo…}€ Æt„nPaä Cÿgå¦þ¾:ZÌKÃ匔²–Úe|4ºvWò½š͹fv¢bø¶}; 䯓n‰B)w'&¶RiLÜ4Ú7i]œ=ôoP;nšä=º[[§H¯!9¦›}"Ï=‚®³jHY\ÓÊï4hèÀTÁ\´CÝNK qN!{ †ÉTE(£I¢ % I½±‹Æ=Jç|ÅÓ@Üæ0r;£Ê,}H^ÍcQ ýÙ#ÎTôzp™Å×ɾD}z_ŒƒÞ3(®ˆJ÷’§gÌëÙ+}ˆÑ ºï1ûÁa¸"T§‰XU[z“Ã|Ð ñÒ) ÷Õˆ(ÀÏsõ9Vwž lÝç†Ø^äP)P;tuÏ)³ŸÂÕNö°WuS?5é{4µ–VÕ ëh™)ÔÙ™¹¼ºîfüd4¢…DkÂ&òNt„œÎF¹·ãAÜ– ›eÿ÷÷ Û@°[ŨöˆÑ N§¹éöVÑcÜ%ÈeH¬XŒùIÈ’â¬<1}+g8£dNd…òjÓœ@-<ÁšŸ‹L-¿œc—FëW2ù˜¸,¸y¸6èNwzÜÑ a1p¸ ®£Øô¹÷$P,ÅÙÀ\ßÃñ™=ζzJ8¥oˆ¯‹¦ðA.%CNê·“ñ_z2þûœˆœÜ½œ ÒÇ :*nª£ÔaÙ’ã·3ôšgè/{vþ'™KÈ5u, "ÓNÿɧíuñ׎¾»>¢ïVhñÍ·ÙëŸlîFp±Wƒr^~³˜Œ j wߘ%4.acÞ¨“Ø r|1°›_NÒ‰<³©`*¨èþÝ77äf! g´p0F!/>°%[‰Û ÀËç¢VY á@g8¼–aßnpx¨5¨ ˜ K¦¶ïÊâø9ÍÂ-x%2î!íHy3,&“‰’/t¼‚„Yà9O)†§åh¿ž>«Ï'üJad 9…©å\èm«IÞÿr«êÆ‹ª­™6ÌÝåÞöÍonrƒh›z½jRÇîñ`¶€~9„Øû¸{ãÌòLa—¢éÆœ/2ß$5æ‹“9—v±)N힯`$¼êÅQ[7R?|&å‡95=>m’I«CB=žõ"CÚ1Þ`©¤1Ó±|'\Bm„v+²¸Ö©’^éa—!õZ‡ª¬ uŒü*qø+ÇŸîðÃÔÁ“©“ç®™JmTpëý÷ù“×ß=ÿo›ÌÊ_Lª9Ífrßà!«RɽÉ_´-…4M¦[ó±Þ;Ìîí?¼x¹÷úù÷Ï_î?ù¿æ(ìhZØŒ­´®“Ã{O¼‹a¯Ï~œÞÕ¾Û»‹A\ñPöÐ~ \¯î’AÝ»›!]s<ÂzÞxP«x[YZ N{IW}ÃNÒÆ.¡q ˆsœ¹A§‘mpF6b¬°ãäAhÕƒZj’Ô¼¸4|„ Î6ól+Ïúý~ž½ô¢«J/Ø~¥éÏûò£OR ‚äïð •:`Â1.)GPV‘ø³€ÌÌ×ÇlçŸ Ù8uüm ä[ üV¢Î˜æƒfWuFí²ëvE:ÒÔÛßS`78±šÒ<¢›è)É£¶D¶x{jcÌòäb·}çI,SiXînæôe€M(è盩]ŸJàjÙ W‹fEtèv£Blës1¸ÑË$'³¤Œ}ï’Gt•©ôDlÚe¦ºI…¨ÇÈò\âaïÕ(ûͬ,¿eh®0ų·¥÷OmUJpif+Ùµ‚xŠã×P~ ÇåûrŒ–_æYn™!6§õÂ0QGœKóü´œx(¶‚‹í]n^Qºõ“r‚¢]PéÌz1Ÿ.æ_z `mœV'f‰ò5rºöé¬-†CzVBÈ磺)€€&Ç­éc)ü‚”yŒ»sL4j^€h²œ½/)óY  .ŤjB¥[d?gÈ®¨0é$˜×Àv‡yq  Á´™ÔQÕLÇ®õ£а©Çt"¤¦Í#õPºÏM&æ=hðD4žN’Ä_&¿]!Äà=!`—‘z§Ñ-åô³ Ž*¤t¿â¥År–U}ä\áö1ØÞ›æàV‘HË€W#¸†ðöiqð6òòÇïÏŸ}ûüM"ÃVØ ›î^ILYÃÛjƒt~൲Ü× <#Ìœ-PW½/³û¤èsßGä'غ¯“§ãœé, óú]‰ú]ÜÐ!&«$2ä¯v2!)ED¶4‘\ô¿”{îXjSt6ó.ýê¥i¼on Ãwƒamv â¤Œ  5*é×€V¨ý h?§\C·ax õ¦‘¡áŸ¬¡¶ÕõIF÷åØF ÂöŽÄ£%[ë©[SJ@´Ì*W2N$úDr)ž|O×ùà&ïÓp0{R«ç"SPáDõ2^ôÉõ䪤¤êddcðñV/Úhª¾2ë—yÿ§É#ð– ž’ÿX”‹rç>ÛP˜·þÊ2ôeø|!ËX‚8 3«œŸBnnª/w¨ìòëþ´žvcœÞñl¤E{z&×’Ë$¡XŠkÝYt;ˆû ]‹ØB1ÐÍõ&4OY8í} "—"Ýë.Ì]{yøð,Iúd®®h˜íˆÇæÛ_d[™ôxwÀeÜø£GtìŠÙ‰Ö¾§öÛÀB¨n¤ÅQàYókM‹p*U<¹úÿšFÓ"£ºÆh¾+ç{õÌðõ¿²qµÝWí+„©¨š„±R¨8\ʤ¹ÞR²zb.ï¤¿YŲÝûpÂ.ðU‹ŒÖî÷n:kOÂë/?†ý›çOö|ýü¦"C_£°®`:©"[¯Îjá{Pa¥,þ–¸&Òµ¯%SÖõ×Ýßí¨¯/ ×µ¯/à¿Ö°ƒ{G#¿õà[Ç-­Àú˜Xn%½ôDð-¦×>%â®/††ÓÒ&õd¡Û+e¡-ÃM@δì¦çÚ¦!_Ö·xˆ1=Y˜F—ú¾>€4÷!«#bÞ—Rà-˜¥pW±òù-Õr†þ×5ÂçÝú#Mqû¿®¡®8ä®?Ô4ßü«ôª½4÷­™Óg:»ÛmyQ›¥“ú+ æÕYÙìîÏežy/xä‘™˜_â pm¾n뢻»YSœg®O'ä{\d£ú„¾žú¯~W•ô5¤Üý|½äž¬KãÀ ~¶:v8oÀ$FH¢ž¾ î8jæQwôf^¢K³zN}qWtÇFû$UƒÎ ¶\ÚÆ. ²HïQƺûÁOžkº âßêgrÆh à>lŽR¸ø†ïÜÏÔ (Œ?­´žp^;/K¾w¤ÏȃUtÔ&ÅG1°âÓÇ." üË9å@Ecf¤?×Ãw’ŠŽß±¯üZòÍâ]*ãOiKì!ÓÍlÐðy4¯p¬”–).ÕBnž à‡é%=‡W½Ý][,Ï:/Áô‹Ô³ãÚp„$ àJñ³Ìcr@‰óÐì¨<‰„^OïÂü’ª5 øU½J7 K;‚°ožBíù²;máv>ß\ngû¿>û|ëóÿú~~Áo?ßÞþ|{U¸1â¦ñvôåqÌ›ýÆÓ´’¼Úš›ÝY+tÎõ#óÜul›‹Ùÿüì¿~ŸÁ¸6Ìø¶>Í6?ßùlsçÓÏþ‡ëh=[ææù³wH:dg¤ê?Ò hO͆9.˜vô{Ü|f#Â@ì ÌÒÚp|Þš/гq´Ž8µ©¦ìà0oðIž-&ÕñE®ɽiÿ¿_=WPšòLªë“jØÒä%˜JÌÎ >JS΂«<‹m(°X)lY·wÊö†\ŸU“‚ ¬‰¢Ö °rk¾RYg`?{§vú€våKtŽ›u3¤(*#ÚýP‚Vf;¨Ï³æ~ž½/fUqd¨ëˆÒŸÒ‰tTÎÏËrbÈÉâ°Éj²cò ɬ©F%游ݳ>eßhï2èó•k[*¤²w)­C0¡~Ùϳ‚¸Nò›,f6 N“ôÝ)J?c—dv'DÛ1:ó¬AºÜRl#iÝ—Xsf)Õ%Пês³bgØÈ§C‚¡ÆÐ`@´–@­:5'=F@òZa##€z¡±*FXýüŒeš‹³£zì¶Ævã̃¶ò»Ë7‹£f^Íóò)·ñâ*È#sìÍ´0Kn|jÔüÕs0Ê’^†ú|ϸJÆÈöU¨&‹Pî¬ç².Ícå åö.M{€³Ô4Þ»ê1ëÅÍI³Qêdòn \{w7ŽzN‹@$m7%ÈPx$nfcïÒàç ÉzïÒÀº";À¹¶¡šceæuÙtö —ŽÌ)@—ÙŽ‡³jÊì´30u!$•)2;Yà4§MwÌØl­Y…}Ò7ÈŸ–€fK;B~0W0dÔæÏ R]ŸšÍ¾8/.ÌP»þyiVçüÙƒ÷¹y¥ÀœáÁ??­&ïhÿÙvóGªbX£êÕØóì-°•ÃbÑh0üÙµ¸¢¶×†ÃÝÈÉ)NªEÔÐcCêê Ÿi£2ëÝ3©A¦ßÝL ± Ð|•ÏøÔ™$@‰.®+)ë«¥g”[.ÇsU˜lÆh‡fÊú.ÒþlëY¶¡²ìl;"B ïÙ‰= 4¢Q ÚSÁm)Ø}‚5ÑcXæ‹3™ç铹gÃ>Lœ…¯}Òw”j;Õu¶}ã\r®¶¥ÒÑ7ÔPStþ¸ä6xz‰Š‰­¡kÃâÙ¬9˜Nã&Ïeé…õ·ÙUý ÀÍzv§:¦ŠK\¾" É–ŒMÐ1t.6W¿²m÷!ßs·óÐ\§Îà!8ž#ã—Ê·8]ñÈ%ω<»²æ3<Ó2GSè¦Ç¤×"ßFçç5ï9(‰wf2ÏÊ,Šq_¡}x6µK7îö$i6w¼ßíR!÷ÈÒùÆV8åt—‡N‹“¾ã¥¯°P“è¤×f G$œ,ßåNQ3üÛáï­ ñ{l‰¿Û¦ÂpƒÓ¢9iôA6\; ‘u Áȳ7Ð4}qˆƒ=õ€’nÍÑû¼5]ƒêå>vbo¾wÈðÇ¡«ßÞ¢ß¯Í vÇp&{  ¢q¶ºå:ÆÑ΢<¨5nP¼ BÐV„jûK{65ApFǰë´§LçÒýtÑPS©»v%—”ÀòR÷óc‡.r1Q&@õà·èEòùω^t‹4™>ÉNË1F†¼QHüÐÉï.y}H8Zš‚†oeDN¦ “«2ñMÛm²nñ®Ð´“agÚÖÌÿM•|.ûúñ’<—è„ÕéÇŠf´n8žÀh6ׂræHX £ ÈØq£(AU–ž u”lôß:PÒ:1’ì¬$;F“ck™”?S"s@‡7ÌÀÞ%ˆy¯,;ÁZÀ½K€z¥,B(ì7N‹°ç°`«y+šÇй6)‡º-Bñ/Ltÿ†î ©»yž¼ÔÀ‹(å.ÎñEF•uHdÍ_çhyòR¶›…Â#}œ¼/ŒÀáø!9G„Ø ¡›u£Ð ഇȈi¬ë…@~ÍÓ϶j(äW•íøÍ4Uï jPálNìÍ„‹Ç¼“â/î·{"Új‹N÷5:[À(ò•¥ȹ-þ ÛÿáÙ;ÙÞ’( ÷zVÆ|1õ9ô(dN2œGZ@o˜äF©ÛU òò@±$IÀ“¾¦›µ'·Ô’cºÄ’Ü=’·\¤ò5ªÆŠ-ÒoT¶Ì8ú¿‰nÄÕü"'QœiU(4ÿk¨I Q= ±Øs7@ÿ†õÖII”‰š”¾{^ÉÙ‚ÝT¿‹´¦Á¿¡€ë{ExŠë(OÉûYòÇ"àJ(ϺŒýÔ¼˜õ2çdn}ldO²7—_]™¿ÅxzZ\e²§[æ×‘94®ª·‡ºøÓm[þ.n¦üáÁÛwTˆ•’íÑò¦ž~jZ‹›2”º8³þÁ¦7€qÓðÐõQ)³±ÀO?õ +#ʨHC¨ìf$a@äÅÔÐŒ |¼«pŠ] “Ür`{s°Å!¹É¹š»Ñ‰Æ’¥POŽ–,¬vk5æhËNA-§žk>†“d¡»©>X‘fZÜš> Ö;PŠx®{ȉÎK)¥²"»ˆcUõËÁB-¨Ö“pe`Öµx19fÙ'Yƒ‰Åˆxá¦_§êLªÒƒì*cu@;ÿ<þàs×§9‚mü8ýXÇUÊù9ñìn¸k‘N´–i@†hº>k¨yçÒJ|"ò²å:“åÚÆbTÉÝÌÑÎÁæaŠlBs:S­}Oûêj²(¯³ŒYy¦—­ë×Ç fyŒL+»Œ‰ü¶°§cvö–ÿUá`éâÑžóí¯~¥ôŸ¿4ÒÕª:Óº1%Ì_ýQ´’íB†Þ,•E‡ñwÓŪkm$Ø*ȑϯ|=®‡ k­Ê¥(Yº4£¨áƒ]ÄV¡ý[ãLY*›S°%`à Ø)„,–yÈ]KDà³Fe ½½Î˜ ´Oãm™¢ýg·‰Ìrýè;Ë"Ô.C¥ˆ3·È5bÊÜ(ˆÎšùnï–árn"ç–aqÖ[ÂZ¸5¦ü­BÜÄ¡â*·ˆkCÁleÛBLÜ>‚M¶fE…u‚լѢfEµ¥Ñ>Úã¶Üi05#´\3TÉ’P,¿xïÞš½_]å_1Žä‚½óØ)/÷ž£ZÓaïF² àð ùB$r§%¸¤­DAa§ršŽr"&ñ,È¡H†²ÒÙv¾îäc}VNÇa¿<£wæNð©½-,’†½ÀXój¸ÃT@~pýEz`©TŒFdx£¦Å„r¬»*‹¤N#>ªªÛA—)DýñôÔu ¯ºüÝ¥LÖU‹Iªâà iB I0Œ¡mVÖa1tÓ\;?#Tj‡¯ÅÒ=Õò‘Î89Ïby|ÖöÁ®Ö³ÃWÓ¯+GiÄñrít,\Ñïõ:}»à,hf¥Ùuàü›²ž#Ÿæjî;[¨°W £ùur¹„KÅ‚¸ö’±ƒ†ȱiͺ^€:ÀtwTO`²6¦sPÊKÏ#(ÊvÓ°²8ÿžÖcP#ÍJ‰œoŸ:køÁ¡j÷‚ES.w%R¸†€#9Òçä5»øLU•îhƒG¶•@hk¯Ìîðšb“©îb‰ ƒk¶ë3²öe¿óz0œŽ›¸¸×·%ÄÀ\Ñõ±ŽÔÓ9)i³;*5}úJ­wÖ¤ \²d(¨<#û X‚5y½t úH“ùýݒͪovñ =Dò€œÄ$éÑlû”Xú4”äšçþÃe¢Û}¶„¥Okl ˜osLS&ºv¾aç¦H?‰ÐX×]³öï¹òY9•CÜþ9µÕ¸.I·å$¾YÈÝû?Âî¡`8E¿šu{ˆn³"!–2`_Rœ¬îq5F5•„Ù2÷ÔB³¹ˆ$ºi³i¿éàŒq0üFž“E~08ß™žc—„WÃP6ô‘öõ£Ø– Ü&ŽŒÒ#ŒÐÄjj\•"z¢Sô¿ëjQkŒZa¶þ b¨Ž/ açÂÄC”ôâ´>ÏÎÃSΓ]Ô „ƒ F³ÖÁÿ” oî˜ÝËü³µ“½ú@l0ÞPHd‰3¿¢]XÂS ÌF ÛY—}fz ͺmBH$Q‡'8-ªÿiX+Ϧ㩲9r¦[ !#ÄñbìY³ŒÏÖƒ¯‹ùÄz¦+1˜Ïo †hÏœ‰ãù’âQG/›MØXlĨBè¦^L:~‚E{— t¼â­ýpGUq2©Íæ5Œ]rSÑ/Y<¬)R¨‡büAD&\‹jÑò¿nyñÿ«îÉÞ¢Ò?VU –žÿ³ÇÌtÄÖ-‰Ä—·]õïd¯<«ï*R¥‹ª¸£G©lQÀÁñðø¤ óø&Ë6¾Ê^¾ÊþúêÞ«Wðý•aøÌƒ{/_ñ›W¯îý¿ÿU7s¥Ü/®ð¬œLÇ»üph6‰Å„¿óËûÕò¾üxqÿž)_ }ºgøýÀ0‡‡ø 0U–}0ªOÂGóþ=ìVšë_Mq~ÿ ~ÁJR?ì>„[ëÝ8ìg`8×õÓ{!1)ÃÚD{[ê ÓrL°7 Ê…ýĦ0Ž¥¿Ào³3LRæ KÎû¹d=“àŠIø‚«(@£‹à™¯·3Ps JÊU±8Ådz8m ¹)¾…«#n^#údö°¹Rt·³lÞ¨=r&½ºQàLüÁËc¶˜˜¶jØæ»>%ñSž_üÞ7¥»Ôt¹œN[= ™ Ah0Ð¥7ç[* L°jã«0Kø³ÿþvÛªNûà[>Œª²ëΠ¡€Dht¸8ëôúL[øê‹Í••¡’©‹0âÊK„òmƒ„¶-²àíHOŽÌ¢Ø_Ànöaï(â]}”mSiXè/Õ™¹Þ½+^˜;tgÕÊÚžö'Ge“"ÿ˜ØºB¢‹¢¤n]+JêçRpû‹­/VEI‡ŠÚ õ[*gc$ü&qO)LhqB³?4G_›¦„ËÜY9>‚ð§ýbq§qL׌ÖIç¾!šŒÖ°àÅQ ‘«‹ ßµñ¹’ñÍi 65Íp øÀL‡kà‹¢ È=ö}O•C—~îÃñ¬8Q¾Ú¦K°Q‰ ]cì}Þ˜–ò̳ñ8Çuì4Nʰú&ný-cQJóeá¹¢³ƒ|j{n£Ñ=k9вLˆyAÏG%·aÀ/ÌÄOO¥Ì3ûø[x¬N)Ô‘ƒ>=®&#£³ Q:ôw¹±ÞŽŸè+ªŠ“_ÄïëÉqu2€éW’P|u6¬ ‚WÀV' ¦©š Ë×éó†Z%:ÿzÕ B»\zÉ)àGd™_ý×åIùaºÏ»^…ƒî¬ó¿7þx°¹ñûÃÇÝ>ýßûãÿ<êÞ³N/Ï(`ßl„QŠ9Å·¿EÝýÓòŸfóøç“ÿ|2ùg1é!ˆ'ûµ ¡Tí?13–þóŸ©´÷1UG@ëÕûdå‰!H¬üòe²òÄÜñÑ1æ #.7¾@`¯Ÿ&€Q_@ö”ª)½xÓRs:Æ]û“¨nØFð×§ß&Çafn1¥j–#®ø,ÕòHØ «¶€¶¾ þâ~B¢•®úm…^1´¼ÌÊd™¶'o0mꃜ¶ºN£Â]³Ìvï€_™‘ ß~zÿT©Ôš²˜ OËF"/W³¤Äp\\§HöVIìÎÑnú½Ã?0ãÄü4ìí9¡ZÂŒÛn ¬¼âÐ#$D#}nf ÷ý?6]®Å>˜Ï.‚Û9àk Ò$;9çÖ0e•gÝóz6Ê ;Þà ”¥á¥éº«˜vø¯kóŒ±è4¡>«LÝÎÃæ§ùÿ&5½õ›O·‚°6Ø¿¬3èà…ˆÿàÏÎ&ü):òsÐ lIlwLCá›áØÌZ7`ø ÏFx0id¦è­¿²áÝ9¿i™ÍÔð:H`œ¹² ŠŸyó†äOÕ‚€ÇB‡÷¼ØŠè¯kÆ‘£p¤Õ@°«'ÆÍœ`w$±½jßA8®Ð# äs¼ Ø!´•Ðb6\Üh›õÎ}¨ªOh ÿoëM<½|¨Ø¦á4W-XôÏ :NM†9šR=¼îÈ·t€nw¬»_éjPgF'}°•ƒ»[›”ßc„âO‹—ÃÞ f9Â6GßeLvÑëmÄqø„¢j±¯ô9¨{š±Ú¼q¸Føþqeyz1±!7Ö¥Œ_ ØÖÇj`dú¿ªé7æÇ+C¦¯ÈÆ—o…祵…ÕYS›—ÌX ËÛ4ʼn¾ðDdj†*á³õlu~®pt‚sÃ<¨~íªNú{².sõÃ3“kÀù}èãšêõÁ6 l»âÔÈZœm¡åì±äg_Ìsgù™ãÍ”9\ …ÔDß²lnCPÊ:©kÿ¡šf>2‡Ä®~ùâÕó\¥EL}L­r6ÓµÞì?ûáÇý5êU“°±ž«Ô¥îäÜ@ô Ç>[LÀ-ܲpvâEJJ¢­òUŒ@ç•B!¯€å €Ã½¥}? 8MfaÆA£¿.“®‚³‘(r€E}Ø …=l€ý̶õƒCLgÓ•©Û½GÛê¤ÀÂå‡jNÝ!9R×jILkp•v¶Hâk„ЬàìeѼE›óÀŸ—/_ÉWü³ ÞüøôÏöÉ •¡Ìl…4þRá¯O]]ø³ ^ÿðÃþº ?…?ÿÀ ÏT]øóöòÕó½uAb0p²^¾ôAâ8~ q¯‰z-„‰:À_Õ nk²;A5)çà¯O‰ÎÜ™¹«µ-½à-1%À må%‡mœ©ùT=tQHEß³‹N\cŽ›CdмÜÊ148W'`~Ô¬ iM“œHùéu,R¶äñŸ}±µ2o爛LIX½/,…màV()AÞ*9Æ0I/ 7Êþð~ô'ð#TcÞ• ëúL‰Ó%&+¤wH«§{Kuáõ¶r[`èo†õ¬œ½È6”R8[Ü6À-'û[‰Ø6<%xR¦[÷µŸQGºõL`ì!‰¯)ž,w„c¶žc÷/‘ÚœóSŒéß—å‰P_Éó *¹wiÈè¨8ªàX _j*¦Uzlpg3ä-Çvé•%á CÁ×jNKÃxŒ1á§éƹè&Ðe !™NÙûDÁ‚å Q›z^ÒoÒo€„øñö¦%£êŒ²7›•†wã´‡ PÁ&v¥^Z-—Æ“¼ NQ¹CÒBsIÖ ÜP÷B_(oMÇóhÅöÒÔt»»1•¤®Iû†õ@ýQ·“w…i¾Š#ð™‡Ò2†Nx½tNÉæÆÛrƒ$¡² ž,‘6„×ȸNH•”§¯¨(YÆþ«$™Šb„Öy&Å/ÁHXA íÂI=¦uS;²*?0ÁMË`ÙsAÃÔ–'VTrSªØ’'åI¤†Än53ˆ=3«'p¼æ|@ÑHÐÏ0) #Ĺ:Ș•J:0ç§æ”Þˆ–žÇÖ J»)zˆ}™sÀ =¤E´»Ÿö`ŽKЉ„`Àl½æF1sÍ“û¨Mujn²vÆ•´j²¤ŽER…*Ño0¤!æY€0Ê;Ø:ä=M¹®BBnFŒ“ÉYHÅlÈ>V&Þ°†æ%~ÛêShPøÑi²N1æÚŠûGû1µ™GBÆŸ†g!«ÌˆæÖ!)áÛû :ÖÎ0°u5/0îþÏ(Æ7ÕIÆi!ý¥ß„?™õ þ˘go¦Å"¦^EÎÀÃLËY(OgŒíf†A=øÜüÉ šéïakT* ÅL9(~ðÙÊÒ[›®Îç+KÿOìý=<ŸÌ÷Ô‰†‘U ¯D‚ñÞ„ÐPkSÅ—aĆÙrÞ Á–_¾p3[¾O¦ŒÊÖ¿B 茾#ÒÅòÓ,ìG|D¹_DOMp±]¸µâYuÝSÅŸF7…xXE»iu*¼½îé¶ã–ݱŠ÷ÄÕ[êÛ¸±¶¹–÷tTQôq÷o³A*¦V½i¸ë£›,Ãù††ceݦ7ŸÓÚ>OÂ=§)ÀGúh û L³wÔÃþgÇüAÍ!BGÄ£n=0ôƒoBã-ÓUÁâH×ûãúDƒ0U¯wêôa4w³-ï1Œ4ÎÝ,An1µ)üILwbb‰ÐÙŽV–Óê™ñ>wtwyBô@²sˆÁ‘½ZÌÌÎgˆãnÀ7§pzÑ­ÁJqƒÈ0ÝŒç pêÎjêáΚl9“Ò>3{ý~¿³Þéõ@ï‰N³–(&œ¶u%0kŠ^Ö”¹¤…-wsÇ­'¯¬ž#óT ‚‹»»í®Ò°´é¦^·&¦Ï®¦>EYÇ‘ùì@n!fþës¢ˆûÃd%7ód"ð̹/Bu2MGÒð`òJ<&.“3€¨+õØü^P—é¶mÚâFÙC/zÖq‚oÜiÁÆjÌzQëƒÂ1±ø¶L‡~ÿf?Ž$˜ƒx\1óýðð £îöææçbD#ðœýœ~„mÀ…à™±Á’ÚãŠEžèÿð¡:[œe¬$ ‘ ´qä”c¾Ð¦ëMð²ë½Ç{ÚŸ™1Õ½‡÷«»Ã‰24ÕÏÙLIqd–À)º5U5ŒÞiÜÚÓpjÑz ¼ä}Âc¥z1ZØ<^Bc £õ°4Ê_…Ø h:š¥ä<„­'ªY4/}QŠ6ž%9ÉNcš¤Òw2ôݸídA¾ÆñÙ¨£Éʼc±ÆmîIâÚe.6ª&¹F¤¡1’;&/oÜ=Ê&6ÏÞWEŠJ¡N2_4|kƒ½Âž•íˆußǽ}Qd$ózÉÜY¶Ú› bDån~ |1mJäÞe^ 5üγáÅp\²›×É+çÙ‘|–Íà~ñwD«e„HŽ{Z¼/ÉG´]¢Êפ¬*¨iŸ‘Î6o÷f‹0'øm|ÖᢎãüQ³ZêénŸg'4sÓÙo˜†‡ÄÓySµO« B2Žô½˜&Ó½~‚²î3ÊfÎfHÑmCB¬íªž]jÌ̓ނ`!ŸøŠ[6OÇ{&_,A¶‹b óÜ2"‚ÚaZg-Ên¯ü-íÔ¥Òæ)Yzjî a"êð””2²=Á0ZŸÎÞ&1®È·Š“ QTŠ„ÒNT©êñÁ<¸þÌ\Þ—ž<4€â®gRGnxr‡ª‰KBÌNàS@Û]½.¥ÎU;ªNxLl¾©EÞæ‰0\nJ· &òàb`vÆ6µ@W€‰JE/í­± ž¬ZR¢kg²Ñs {¼teôK­%ÚÕÜî•XE òÅ'ô°d‡H®à°gépv!Š*5)ô N7á&Ý {5«jtx%ZÙù ŒÈÕIâ Òr”)ÄŽxT6%IŸ˜ñâlÒZé JÅ<¢ÞùÅÞ&s˜½õ×'¬:ñÄ+Ð^öU¶Ùžì=ÄLIŠQàÓ,ŽPÓ ÅÂé2}OGð[oÅ%!¶ =Ï:ÙÌ¥n1­Í€=l1ÉLCjדÈpeG(^£énGÍ€€áÝ•ãñ5I“%Hàí²í|Ù`ðÚyr Á•ÛŽb³ZV¯Û‹ùI+;ܺ¿‘Bãò»„¶úèÑåtdùg9•f©ÌžCD ßd>¦‘we9Åp†ïˆe «.ØîyHÜ¿ór™'8AµybeêrC¤2>wxø5ÄZ¦š¹æíÛ¥ —ZkçWåËfÙæo˜¹ÓÒ ÆªãÄGÍbçg’æÖÏa‘EŽ8ç§èMH/ª–á&¶#Í›Éo™EqÉeéÌ]‰nýÓ¢¼+/º¶b[À9ÐôGÞΣàØúKö v'R`ì°Û+yؑ݋A¤·«qÞ4ȧ]¯5çã•Wȯ#©ŠpU1Ÿ„õ’8PÄïÎØ§¤ÝmùžB¶p°u-<µødýåiubÖù|ÃÊË0Ó©ö¯4)F>ç^ADõi=±«XÄ>4ʆ åfÀÌ*ÓÊ3¼u”žÁH í‚óm´­j=îYÌ1ç•z¹=ù(õ Üh”x—Ô¡gÕ‡;Yõñ…%ÉlÊJ[Ÿß´5~˜Œ/”áiÌêNs‡&£_D³©é-lr¥Z¼Qjª@CØÍ6È»Æ;»ÍÁÞ‰’ F(ÔœQÀ‡X8‡zrÛlGÐW®éu©‡d•xÁÙr÷ÜIáueòá²%G´»Å¥WÓkEÞM½˜9 YkzŠÿt—A T9Ÿ\{­Ç™-ÑNöº F¼ô¬cèÁÐ>ÞÖ:XØ \ærÇ¢4B@nC¼§ÉFLË/,®S‡ñ•Ånô<±SÂŒ‡n=áÞ‹ˆ`F_ó› †ÓÐäŠZŠ‚@è óð>Åó×µ{m;ÄŸ´Þ5Wǵ»çÏ‚­³äc˘ëKÞÉÝ肹w«Qσá9R|Dàqà /½s™ŒÛaÀøc1h–aÀ{b¼ñ†a ¤ö$>°-ÝãŽräüãGYa×];ð@ÿ ×T8 ÿAkCa7^¢Óg`¹-ÄÒÑKx:EˆÇDm\à+Rð:VÃЃ’thF?*&.‚dR«ëQÆ xƒ^zp3jœk·¨õG¬—‘®@r½Qè/ù4b =p‹‚Ñ!7d~o¸!à… 7c½PìÌ$tû¬4•F¾Cš¹[š°‚Kõr.´&ôbZšÞ™ô9'IAR+ÿJƒ÷µ‘o( dtœÇeÁ«¥ýY$w% :á/”~®Àw«ã’–.˾Wà ×"›zÞUß21RV©MÚ,…K‡bH"guÂ,·/ùw"N@Ï£F>w°+¾ì(JÈ©ÀFbùK²uÛÁJi£81? >h'È3²^Þs˜Î¬wv:/÷x…Ñ¿X(¿ƒEo]iÓ¸oà;i˜ø ܆`íÌêzîtV-ãîs úÕíeÑ!ñqðãCÿØhJ“ÂQ€’£5†ztØžxà‡øáà‡ ð×ÁÅRéÄJD-­½Ö$kTÞ]ôöJÓ#Kηãä5œTòsÇOi¼)pGÑbò¾j*TÖƒÏÈ»fÏÑò7ÝØÂfà{ùAéÆ(o#>Ål0wYÌMY ©‡ã¾¸ëÛL7‰9{‚ ¾Gå¼ö7’òAá¸ã¨ŽÖÞˆãñ¤ú3pÁ!4WõŠr™r7šþ´žv7}1•¨¥¼Úp󟽂t¥þ©€]3×y îÂûA•졘>Òþ$^»K;‹zAÕÕÈð™:ÊWñ Ôb’ðFt%«^5 ] O‹–u§þÀ/q( âKÂcæAÀ…|´Aº¨"‚œ+fÍíš?N ”›UÌÅA8Q…cL0¿ù„ôFö^3©8+ö”èì?ÿw´ðLÈâ½Nõá*@¯øó‘± ›²§fjºzê!×Qº®9øÑ“¥9ÎÊ!ì„Ç“$½±¦+N#^ùÎ=åî‘;ѱúß¡¿àÞÓ¢®z Îg…Óîiƒ–'ñ¶„|E/{XŸÃG”ë°Iñ™yøáåóµgHm‚¦« —Àà†BpØÙ„kDH6±i_[yXƉä¤{ÊÔ#j`Â<"i‘®XO]×n*êLOVZ¤ðôŸ°ñ´½dÐ:‚]¨¢Å  Æ7Ѥ®'¨À”%B0—OF#&^3kíL@Ôrâ|ãTñ›yH˜a²†  f_ŸQt™Îh M”å£|<СOǃì5Zy2{â3!|™=5Øxj¶*1\B(ёżYȶ²Ô()b¡àÇ S-é´Ãkˆ-—<žtÍ[_Zs¨k†ŒøÒš°“7Ü"|M•±Zqœ ÑÔ+z]Òԃ״ÿ+ÃûPrý j¡ii7¡á ŽÍò›Ò³\Kø«1? ô´OÄoÐß bžÀ’“Ÿa?oY„6X¹ëÝ Åœ åjè÷M;ú.õÑÛʳ`Ø„ú_åñÈàtÆñŒpkQr{"õ«‡R¹–·ŒÑgõ¤[}øÜ3ÌëÅXÜWC¿´^¾Ã°ÚíÀMýr/_è—c²qênžt‚‰æb¿;˜#Ü@c¯ŠÆ¹á"BmÌkŒ$``Rˆ¸D£âxâY}«þ!ˆÉÇlj:Æä’Cš<öÃý™rÃ#p×áe@&#J#1,¦è°Zdï‹YUÎ/ ¶òÀ±Î7$å8i‘ÿõeRÏWE ð|AN¼”pzÖàã»­ì].ü«¾£êØ/Ú1°ø«ˆ'ã¹HzE”#L`T ¯îýpdá. î&:äËÅ—KÄa‚)f6K2‘¡†{.ÈêXâiɈ›ëgâ›æ+⇠¬A”6G†äå´ÑL Ò[ªg2/”$»À¼J,Œ]˜Ì’ƒ$'ŠY4. 4¬^L¨Ÿsôír{7‚¶cÀ0t½ÈÌ„Á9CKLçË “8žtLrH3dêaào •ÇÏ ms²¶‘Dô Oöm>+Ìi×€•—.ÜOƒ¡T'3p êÉ@¨,6ä–ZWB©¤ ǃ~ÅfhòaÝ›­Ò: 8zl)3©0IÕr³ë¨ßI;jøÀÝyÀ¬ãn U¤‡@ýo©Ñ2Ž _°O{XJ}6Ðe©pz†sþײ«ËGÍÐ.ýH90½¾.ö;3Eyµ¿íy’Ž„#öu¼£­NTðùõ¬Eù{l7,ÖyòøZ–™á x©%_èJ‚úG@Sr*É0ìðwZâ‘Þ-V±ä>íûj•mè mt<Ѩãã8(‚oiaNo…láÑš³G?…Æ„xí[þübv_9YAáɸ Œ? ¨AÎH^¥¹½*åO&mµXBù]8ˆÒxQýHbTuÈW†Øz ÉCxõýË+i¯öÚñäØ|Jûw-^ >ª ›%êݩڌSB1ðÛÇ[©˜kÒ@]oiKf©ÌÒ‚@Uw3ËËuƒ>-Æã§g¡éRRAÎB&éBl!ùP+O °F8›L!?×ÊxK=),yp i¹Ð\]ÅFÝÅ¥IÊwÐy¿÷û÷Ûø÷S¸&P*u÷÷P§¼Q¨ã«F½sÏÍ:q¸2ËÉ|v.O r|ÁáÇ7Å¼Øæ”¥Ÿ&( KBÛ`M¢- 漓{Åüòu~.Nà¿Ór‚¿Î‹¢¢¯}Doç%|ÿ+ü1ýÄOFoá?|ñj1v–£<¢!‹cï¶%©éŠ€«3/'§óN&0ÓqqÑÉþ iw'¤]Ãoøn~Zâ«JÞTü eRÎø!ÿ°5è)Á…g'õøÁŒŠáisj®¾øk^sIõŠŸVMG„÷"ªÆ¹MÉà~‰F„ú>ššvΠ³9 7DZå™<ã1åW9u,×ÝÍq95ç‘z³×¡È;4¢¥‹í(a¢o˜ê³ë¤…Úü|›f}º*-ÔtxZÌæ«ò?ùèö  +ßn”êù¹¶ï ZFö‡r4†/_Ì¢š÷‡UÓ_˜iŸôËÑâ+u½™cx¶§•©ü‡æèëaÓ”ýŤ:+ÇGP¶_`ñ;Juëe?mkøN!êÌ‹zX‘© …7¯¬‹6èÉIk°˜‰Å¿þ“†Å͹é[1©~¶bè"›—æÐçžfbgõÈl¬Øk²½Ë§õ|^Ÿý8õ¦ gÊi°Ö(ðWèU˜­{Ev„6Ó )HúˆÙ»|µ÷Í·W ³x®M•5ûÇ¢\”6‹K~-£Ç4’à!Ò.¡Fpn¦—C¡ZÐ*HÓS„w¯jü€ÄþdÈ4•#—h‚š'å&k¡îñ ÖÖ‡)tìŒÒ‰¼+ÇY÷¨<Æg`‡Â/L7aœÏÍîq¯Yº¦ŠlTc•¹Œž‘³*Ú·eîqÄ?ÌkRžPr—=²ÒãDCh½–¹=Ö^±’#wn˜IxÞ±ÞätSžÆƒ…‡³²@)mëcÜŠ(ÀÐpÓÌ2’×>·òÚ ª>[ÑÊ Q Rñ»ÚðºÍüúý£¾a/%ñn§aœ!ò}ô_YMèV§ÚŽ©¾ÆÔ¾ €+f' 4„:*‹³ASý $ø‚tS?—³:'ú¾yVÙñì·­Ûkq–Ýëï 8§¿'Ô,7ÃÉb¶†ãŘ(4š¸>nE†Ý½a6‘f %„lü8Í`qz{øÍ›íôàï?¤á·„£Ò-³ìþ±¨æeFçÃÆx ÙìjGþÔt€£¼ÑBMí¦f9›RdÔ@‚†Òeô£Ø–¼"Ææþ™ÚóÿŽƒ’ãœSì•£o½€Å´ryÿÈ1ÈÏyö]Y?Gã[h¾=á­‹€- `Í?PÉ­ ŒUàuå‹dd•>„Y¦°œrÅÚêo&cºJgºòE Ì'jž=2ôoöÇGÞÃ7u&]”–Ôügßì~1„µlºêß² 8VWˆ=úÝ. Ô¬ŸóÕÙ¼mÓ›Ó.9¿Ý×\4¶S#‡`§Q?ͳ.×![TÙÿ1±Êø´éöVøÂ8ŽÒV›aµŒ}‚î’ØÕô¼þºÁëÞ=¶­j8]Êt=š”œV z7"pFÉË¿<öíó7»›£Åt:¾ùîL"wЉMu¬VÜÉEˆU,Îã²x_š¡°Ücg…ô2"¦öqW&!²3u@A¶£°fS"¡ Ìs&…’ ÐF%„œ_m[ë¢-ÇÙò—Æ ©òÕ0{1¤Ô–á!(‚/.¨¿uÒtƒÑ X¤V¸À@Y"ƒ̈́.½m>ìêK-0Lß®A=¯^ÐIÍÒu¦*1]©"©)üÆ\ 8ЊñšÓ·½þôËã9÷ÉA0ŸàÿôÎp ²íAarêp5,Žt°=–Ôu-Ì>ÈäpºlA/Ük"¸šs ‰ªÒì/¯…¹r Ìk÷2˜'&Q5ö=áÔl/?(ÎÕiÒ4‘JöH·GÏl…Uti6ëåª-s?ØU¶´ªšD¤¯ØÒ &w:²™÷p¶ú`ÕsUÁüêöÀ©D!\ œ6¹ÈL j¦3äº<K\™òf÷ ˆá U-h¦?c4QZŠ”ƒá Us¸Šw”–õ 0wBóÔz»Fª,7pñŸ—RJ ä$²eѽEäÁÞiÙåߘƒsŒ4sËÍb‹º=8v5¸Êw—oCœoé`݃Bkng6VrØtxšlElj7¨^‹_â#Èhïæ1ÒFQK=¦/åpÞÅ%µKÍâR2'µÛ4va¢×d—`‘ÚÍd$Ž~^ûxÖWˆL¢þÛ¾ÞëX HÛº Ò ¢x¨vGÒHÃ…»&ÖW ÷¥;ÁÚvLK°¦®fõØÂfwÔR@>–„±¼0WÝÿ»ô_”½Q‰¾œ„+!Û"‘Ø‚¢‡ì]"dÈDNú b‡P¬´lû›ÅxŽ’œ5äN^†°B¡™Ë-~„~àåÈÛ×E¼@•8¥óQgABB‚8ØÖìCW˜™·q¯0¼›Ó‘ŒÖíæ GÉaŽÄ8iòåR­¤Š¤ Ñ2¸Ü2z¾”¿Z&G9ž(Y°ÍjæBy1Îü®`Úô³¢¬ !ׇ5ÐÄäò0¨X±|Èȇò%ÄqÞÜàtXç šÒ£j²`»…ùa OР[Ö¬˜º ñ6†?¦+¡J’õ@&=3Æ ¶àÆ4Ö5á•Dh’NÄ~,û)ý9ú®#RβgVºéµÑǯ æ¹Â 1g{GýÁ‰3 Çu¥Û­é.Ô`©«/CÙè¬üÇ¢š±%âûiÕH¾ú®%º@ô^ 1õ_‹ñ¢¤ôÓ÷õ K: _Š@¼ŸpÙ`õºÖCÛ÷vÊÑ”¿ pÂÌ{üß1-¢'y‚ÎÈM aˆ›.ºí–E†ëóì>½„»Ã •P€]?%ý›Âl1zÓ¹„5*XM dEFT(Y;Ñׯ¼Jâ55§ðÿ<ÈöxöÃ_ é¬Àl&¨F¦NXÖˆ{jnµ÷Ù‰ž`¨ŽT´ºÉ.t.’¸[ï ¶=@jÈGÆÚ‘‹0¶Ù…E`-ÉlGó *¤aîi•.ÜÕny´À##¼˜FbahÑVXËC•ÿômÍ›µ'çøa1£÷wö!q ž¤–ø™·ˆƒöDr¹áQ·ßpbÁ†&%p‰­öؘ$Ën|¾Ùd›CLÅÉ6S¼aÏü¼ÍOÍn°æõ-£,™I¹)Il«‡2‡Ct¯@ƒ]ÒVÆVÍà϶¬–âk½äùoy1Ä‚ˆ^ôâÕl1)3*W‘[/X@’BÒQ1÷—D¶;¡#,‰–È4·¼GßòÆŠ†|¡`Õ;6ÞŽ¦=aþËÍz0´'@»ÙÓ³ÿ7ûæ5^læýe˜“(ãhAÄž¼É'IŠ+Š ­¢Z€lñ,¸ô(/]‚vyÒOaœ¬#’åúBP îæë–mÈÊÒkÏœ*¨Q –]S]âÀ ƒ¼Nir˜íjõ†ßrªÕ„ŸòÿlÊ9VĹjÎ÷X«æ®ä~ Wkw\œЬÈÌw6íq¯Ì[ªï¡ÌÏ ƒÉ¡ÛHÿ„Ÿ)ýNzê*x+¢{¾‘Â9i¬âs±»Ó¥µ¥Çh%¨ôü]lЩ4 MöQ¶Rq˜ ѯ¸X,HŠÿ”ž3Ö½ÀJÔ E¥]¬©Ã ŠF^Hn ¶¯ó…YQ]S­—<ÿ4 1°B57ôþÇÀOµ\VSmXõh—Ú—EbKá hZæO@Ÿ˜‚ØDD†ÞæßÂ[ËévR¹¡•¿í];reÐ Át$z.»íØØW@ r\ÍÌWȹrRQ:V&YO4D’wíËÀ‚Ýæ(+0ኴ¸/Z ¥äYÇÇĬGÃÁÄ5ŠŸ[iYå_*çZ¦¦RnÔC¹cÄÂH%9(e¦´ ÈY~‰x83"-ü Vh0ÃÛ3Eyÿó@Â1³âfßõ†rUø¢–x†i{àCµt¬d%92uf…6 %·D’_E£Nʸƒ"=¾Ö=….ÃæLƒ`Hõ/Äyv•ð%iÞ©ƒ©‡´j©dϪfwðÔ³A™>Žõ&ƒ¼pp3w&k×ùŒ•CWöž¤ ~ñåÜØvZ‹!L>B<ØÁFZB„ÌÛ³oöž¼~öüòp nÛßÖFæJì5#šÄÈ读j.£HDk(,`Cò¬+›a‰K„¶°L}ZëKæ˜Hhû÷K*{dÙŒ+6eFÓ¼Æ0ógS²a J“fV2§`˜5 Z^ÍITN«ômL.Mžîí…jÜI?xCRFRß_N¯”‰¡9ûÌ£áÁÖáUŽ_¶áK¿ß§_“Ã+Qª¼êBÕÞ#øËË×møúý¥©sÅOL­Þ—³]ºšÂA!šð„“b1®ô3 iÓ”A0TÎ,j°ž©?`Ü“¦³ )D „²©Ï!ÁѰ6èœ"Ñ<añü…/6†kói9«æÚ„¶ÿQN\Vùx”êQ©ÛDÚ©• t„Éÿíhü?  ¾Ó”[y¹½—šrËÞjÊmw­¹÷ {no¾0|ÖÛv3ï…ÚÀ†þÌ8{ÚF½˜Éá^ Þƒz#ÿÞ®¿•“—Ë2•ŒÓÂ8·Gõз{³ªÉÐL×o¦‡n=®•o*Þ¼P,‚á{Ñú úK˜_ËÞjTM(>¥K´{(×hÞ[/ÓTû 4“=ÜÝ [8dp±cèA«›o²Ù8ŸÑºýMøÐÓ½3A¢ ²!¤% í{ÛâZÐÈM%íYñ¡KL ‡ ¦÷öà ¨Œ¼k ç›=Ÿˆ’YÅÿwßåï{.ØC6f”Ø'SÄBEЇ¥Idˆ–5ÛÞ˜YÄÀË×zTu{¯^ÿðôð‘jè@ÌU[Æký Û @M’êéªÃÍ5ªv!‚oúo6ö ‡fý"š_‚õ ]rV³ \ÃWݯÃ\›5€ˆ@ÔZþ3$±ŒϯbfÓ£ƒ­CGÁuäyeo@ÆøèJ9ú…q™c!ìƒÆú`ÎÍÒðbת8:¾ßi9bOÅ3ñ[—!…eHœ(AùøÔÖÆ/ÈŽC ~fä2 ·ñÇÚ‡ƒtR.ÜÜûÊ/žÖ•á Ƚ4+ÎIµØðÜuõ£h¶Ê‘,OCU:‘Üù$L[ÌÙtN< Y"rnz€Š!/º±œÖçæ¤šH>,-1;*)~š½'Mˆ¯˜Z¹…"ža“:-ˆTp&ÛˤPºFV 8s Ç,ãæÍ¸¹IiuFtk½Ää5ÞɪŸKÐÑ^ ¦Ãã“-÷uÛ¯Óg±=Õ$çê{L«ÏPs•ÍÏk캸–ÐÑZ qRa3˜¤MOw;/ γ?×§º¹ž™[µÁ{3¬§àæn»êºdáê ªQŒ°þÓú(3¨/É‚eþl®áò‹ËüÑfºæˆ©(¼÷´/ï8Ò¦ÈÛ}^PúÛ…"ê!OpoˆÐ0•å3?mv²‡ êÕªÇ[9a"µ%C¾ã÷‡3¨`‹nÆ%ïõþæ†Ò}8Úx8êý1ÃV9Š7wÒ!Q´ Öƒ]Cy†1\R¼ yt{À TS³Ã°×Ú|v¡&N£†&¡¯“z[~–SUS‚#G]dÔã‚®k—‹Ù>Ó;‡ˆ¥:4/ñusÜŽ^@ßNŸçvŒ¿•s™Zp¢ y.‚ø„íúLr´X$&ºö<,³P­ESgkáÄIÚZ¶µ»ÊšÎO‘ô2<@½öSSD‰ÀçP0ûz¡ãv7² ÊÆ4 g©:Ó‘ ²LÝ#ð¡³JU)&1^”ÒÓ*"gV9‹b[t~šàbûiÂWaünÍðx"åRÜ‹áሠOæOŸºç êEÿøÓ Ê7eÖ׿4¡I bC´f ØÆÜ•©ŽEÍŠÎD³ŒŠ´F´xìÔ¡T¼.?ÁÈføÓÁBÍ‚qñ¸ìH?¦ÊÉÎ…¼TP°Ò«ù:˜ögeùsÉ_!›WÕe|6¢C(çrrË–¦·AŽ>£ï@˜Ù?³}ƒ4˜ÅaC4Ë ó@cË^QŸ{ o£åó¸íEüq1V¨¢DŽúÝë6ÙÃÏFÙ?nmõ?;6G·~ßßúìi‘kBü½ÁYæÁ /YÖ·µP¤‹°ì*¿&¤P¸©8R€_–hl˜GKönîûïÊ‹†WÆ]QqHÁw=—z–ðÓÔ„“ÔéBÀs"³„á™|RtèPå΂÷µe=×»’ÿ¨Ð-Œ­#¡¢Û]›6DR\@in¦{ñÉŽ{7ݺ»Ù²Ó½‚÷þ6çEÜV·sÑé%†£µ„Í„×^Ì,î°ÇæË¸,ÌÑ{^Ts/׊«Ó}$óo I]®‡Ojvm„¾rá°Rõúq;”jP¾ŸùR[¯ëÆœ>¿V'úkj@¥•œl¨,ŒOÅA\h­ŒÍ£oÌéÏEcø‘—ÅÌp˜x ?úøñõ‰AÍu…`ï(*Ó+C;Ã$Z­A=Tä0¶!J¬‘ÿoÖoä”,ÆqùÖ@lÔ÷œC‚yAC¦œ»‡ï‡4λ ‰h§óÍ´˜Ü]V5ÏÀmKƒƒÎL' H; îÑ”7yiAz—S¤Á~"^?v «"ÇÕ#ðKšõ\•ÓjjnËg†7oæ”w¹O®{óú¬(ì„Ë͉ª¼zB›(b~S“âtÒ#ÄT舒ՌôAèíÅr¨ ØØFÔÑÜb8QÝn60v?0!ý¢Äï“aa¸sì:‹Ù´ætàæˆÃ #Ø_V˜"¥)”ÿò™•ósÔûI¢?„{÷{„rƒC•™æ€“¨FÏ¡þ¡ƒ^òR¾âa ,ƒhÇVÎ(çU“Æôݦp†—žz™Ð­øÝ´jZaZ”P4R²1…ü<³¨§ˆÇò¥~g4oîÔ¯ œžTû=(£æzWµ”ÂÜÅ>…õ‹BNÂÝî:ã"ƒ>€¤Í1ÙÊ’”ïâ E«º¢hé×6 Ë${6Dbã†Ôàix)â MèêVI ~#i/ Ï&@œšwØ"¬çãJ«¨p{}µµ]§ßM˜¤&ÑõÔÎPæóeö'@ÆÉ6Þ_xbËCºÊ£á÷ÂñkÇçUÃ÷—ÄðL÷öÃ7/ ‘}¼ÑÃ'qÛs+)ùA‡ï‚â¡á@6Lr+úËÕ D][)^ð|!ÇùL»—ÿh <•œ³µ±ý[ÕĘFÇÜ, /föؽK„˜Dÿ‘áÐJojä×Uf·JÆ&Útˆ–Ä(A³¼§íuÕ¶hkºgíõôVi+ª‡í5iû´uô4MÊëMÓ1ÄA¸æ<-™¦ÿ±lš0ôÍÀëú²Øfɾo aRC^Á¬jçToÎæÐy…™›¡VãYJV«'Û(#šrŽŒr m»x:ŸVìË™&Å%‘Ø ;í»Eâ‘Ú/ò¬`‘dÓZÏ›8|×ÜdAó…G6‘u ­sæ0wÃø“³Þ^9ßÏ0d+» ðJcoúlwö3ͦå ~ ±÷ß1 ìI=3lëá5`ì] 9Ä@ü­¬ Qâ¦]˜ƒÝ0¢ÕÓ5vÍ­“RÞUÎ+ù¨8“=Ý*ûÀ6¹õrÉäÇ â±ÍÕø=:½Ç ƒ»öÕÙ+™OCOmdBÆuvˆ;fÀT÷C3·1“Ä@.| K€\,紈܀? ÙÒ óì"xÆh@ϼ{p˜p©(F#aÄ >[pñEÙ2êÄ ß.¼ Që#H ÉV†‰ ²R(`´D¥)ÓÓYThÍ롤ã†Ùšæ84ÜMìê[›;‹²:{{†z8:T$ÙG/r|‰™¾;þû–ËFGC‚nÝQB‰Wì Ozï¤hìö{«‹+xÍÃÓkä~²>‰BÍ®‚!;F¢äO¹ ÉÆ©L9FÌ…×xnU_\²[2!Å\OBµà®÷Ԝ΃ÔǬŸ`¯³€šrŠÀÎ[ J•XLóª¸m.O±² # £Dœ•ìöÆQa(R·Dn6Ü‚•[ˆHQL÷í<€Á>ÙA´&6h:b%‘kÂÌÉ S84`ä€Âpä$øÁtÊ(Âñ0—`˜ÌƒdÓk°ÚhæuÔîIœ±žÎ2Â|)ÄϨA©–©;Ë èD(Ð[ñšöÅxŒì;OÊ̱dÚé8'Ö£ð(ÐQø• LeT^/¥ó“tg_Îë¬tšKìKw$…Š“Þ™2µ¹=%ÚFÄÀ Hô‹a‚-¨‚ŽgÙŠàdÜðdŽÕןj\`pýçU3)gYwë÷¿ÿ¢×~˜Köà}Õ J9Š©Cå‹d²áÔaå©„àrà ÔÒ°!b£o¸‹Çæ¸:9¹j`=HY<ÈÜDR½öLA.©5AZ°*}ž»TùÛtìÝjw÷íã–”`îàí!²N¡ÚØÊ«þ’íîAg²0|Þa¯U ´•G#yŽ#}µ±›’¹ ¡è;]t+›.)¥Ä]œÓƒw84aŸ–ç)”ê.f#`æÝºÕ„Ù‘·óz·ÞJdM·h¶…¥ ?á1x/TL”Ç—JLÆV‡J@£©Ë»–ÑR.(Ò…ÀfYy¶ûgÅxî½ ±Òš¼-rР@dø°ùiŽÿFøÿùdÏ i’U~VŠëGÞÅË Ê%|üÏ>q抪‰î„ÿ·wÏü¿A7Ô“D>=ÝsßGEisjL;†ýéŽN*ó“À™×Þ[ÚaíK4^Бi„ ôLàãN9Úí4AóGá*5Nä…‚´È7ŒšäŸ6ècèñVM†ãÅÈÔGµ8æ.òOº®•„@W Z‰Ü³K}«/Ã4ªãžâ’ôÄ£&Õ‹÷Õ°Ü€„ÏEÏ) Go‹!†\Dñ^–ý80™®Ot w¬Ÿ©½­±UµsxKBߨF®Õ§(‘4[Aõ¾†ØžÀdX7]Ü'ÈK¡ Nq_ñ¡ Ü5ÂÒ¤ùéŒãbªî¸óšë'þqÜñÓ7¶âíf }+“#™yO“¶Çè‹¿“}_5fêq#p‡ƒ#:Ë®0Pج>Ÿd%Té¬èÁW+;éË8mÔY?×%_<Œè Ä ¶õíÛŽ~4í+G Í6| ~/+06P‹yEñÐ9Š´“ªF31[%ßZG–å#²ÑX8¹+|xï7éÝoһߤw¿Iï>²ôNãã7áÚ¿jÞ®/‰k·ÿI<á¬.ÕÄ…Ù3,`Ëi·?üoÅRÄŒ¤‰],¤€°˜®%ä±ÐaÝ’ÉΠKsR꣮¸-âß·O.SÉéd¡Ïÿâ \õ¼&Dc?Üá"g‹T p¦§é“E /¼ó¥|Öb^^^KA¦€Ñ ‡ „®G|3˜d ‚×t%3L¼„§sž²ù²kRòÀÁ;<èì4øn8™ 0ͤÂ{Ôupb&ÐÔ§œ”>ÿÙ*ªnÔŸÇqÉ‹&¥˜ °·i¶‰näV/OôüqÖv­ÃÕŽÌÜ Ãû†m} é%ÕÍë–Úà–¢å ¶7K.¡x ±éò)½¸‰Œ"€'Æàò0 “²€kØþ¯:íE7KŠ^ö½Ì€)öäõþ’rZk±êØ#˜?¤’²È§˜ÍŠ ëÑ©«=ŽÈµzè88_í.kÑâ% iå@:\Ny¸ ¶l§Æú~·›m´ˆôä£çoUû°@×–šîµÁ®74ÑãªÙS!æ+\˜·hóŒz×&+„Æcè˜üÞ»›ìaÓ˺æÀnà¸4æøH°×ãŒA˜BÃNwnQ”gª Ë·ø˜VT–¶yà ‡4žœþƆÏVˆ×¨Þ+¢­Ö:Ë÷¶ˆËÆ}·øZ¯ë¶ø]õ\lƒg–†¿n“¹¨äsmϳKq¥aýAŸøÿQ{à]ng ÜøOÝ‘•û¥÷@Ýèo›à¯w ý&–Ý¥»Ššs†ËQCœ‘ ý\™òNI·• ž^NZ…ÚX¶ñ­:$Hx³åû¾­]ªt €®÷Ý C¶u(r(Ih°{Ñõ{t²ƒ1‚ƒŽA¸_òû—m,],»gkoêD˜°„ ˜˜~2ª ïo»Àqytãk÷ ¯Ü·¹nßîª}ËköG½bßæz}ó«õŠËéÓu.¥k\H×½ŒÞú"z­Kè2_wÇx]ïâù.w{áüßó>îït0¬;°Ã‰ÙŒtïVÔw‡}‹$ˆ2Ev¥Ÿĸ»ëߌîäVt³ѿɂ¼«õ¸Þ èno?ÿêKÇG¾pü¬HIlŠÞ‘Yù3ˆ‡Æâ6µs0Î(x¦Â Šñ^ñª€^qþó "ŒÓ/@í,ƒ½s±>nˆ…?èìóÓ ˜½ŸÍÃüwZNs0—|z^=êó•DÙò§Žyy?»ß[£Á˜á•Ma³8mØN{ê( K…À =«GXð`Œ±o˜¿à‹kå/øLòÀƒUù ÌIº"OÁër¸0øÁ©iÐ&’ƒ™Ý YÁóÚR~^uÙÊѾ|mfsTAF«aÕôfú'ýr´øJù ¿¼“ìie*ÿ¡9úzØžö‹r|eû¿£’ဂn-ÉqðÒ%Ý•¼ÃãU^§ÙÇ´ê/ÎΘámw Š£5­\ž‚Û¥5lŸ¬›Bå©—ÁòÚ¦ÿ^D›暇¨PÓ˜hï}ó­øáâ+'@ùaÞ¯:ðF´ü0-(Û8ñ0 ¥pà€¸+:gÑDœrÔgÅ 9sNFˆ‰³ïñí+Ýù+q{´î–0YãzÈîžCX+’Üðï—f¦&󪤛­Ãc#Ï-Žè¬Á„^¢pIIîÐgª1.‹÷Éòbõ.Ù»,àÀmÛsÍvÏ[3‚†ð#ƒ!Ve8,½È‹Ø%‘¶y$(1€‡ð2® o*¦ËÇ_âZ³#nsTΔ•R•â<á÷3ð[Q+ŽpqT/æÎyÆþ± æÌ 渉)oÓ"$ÏtÞQ˜G{écæÁEÂågh.ÎŽê±Ä¥•)Õdl½ )ÆåWè£C+FQ4±±VwÜ÷’Òû‹ñÛžÕ>ôC³“‹ÒÙ‹c¦î'DÆ>›Î/rëä 1¢$[§³Êºá’ø`/U˜GŠzˆAè<–¬ƒ†xýÆ]³¸áéÆ'u”ȵg³’¸?Á¹nÞ#A´£„»‹26´û÷KZxW†JÌTÁÖ‹©al†°ÿ*&Î¥§)F4:YµÒhG…m$Ç#[]:I¨Q™P™¥I;Ž´µOÊ ø—»™çšŸÉȾ ºÐìŸÑÿ÷KÜVœ Xí—Ôèñ}uq hÓymX’÷tÄzÜg^³Xák³û@ŒZ}_yGO"ø¥Í‰é w7× Ðzfä4<9ö.½. É×ÐN6áUó6åÞå°æß˜‘~BùÚZÝSé} uÑP„R¯¿§¥òKŒëhxª1|ÅÐÆ-å4bG%5u~è7ý]¸çëæÅ”•¹„u.0Í&?]Ì¿ô@`*bÃ4R¥Û¡‹$޲ ѾPÂÉÉ¡ o¢‰Éðš„ï±AkéÝìµÚ†é0s 3„|x4ä”ã‡VynÊ{ŽõÍ162²5{—~¿®ð¬À<ÆÅ1£ê Àˆ zcŒýWÓ'†wRx³ko˜}Ë»9,䲦£[ßXfm£]—Φàìº%N¡G¯YS. —¤÷B~ ‡[ùñÍsCº1›@gCw'ÓÁÖÎaîÖ~&Òµ• b$( >b5ú'õdÃKVÏÌRÿÆClã„d|;[éèë9³#ëÜÏs%_¥A7=æo~UôjËŽØ‚ÓÉdű®`¶pžL?;?Q¦I½M¥mïƒ!ä¥Oam5ÕH” Ä/X†ÂúÂßZ¥ÑcQ xð ›Ói˜Zûwâø)’xX‚ü$ÂfÃG¾àÃã°“¨ðÔ‡ÞÙ3;µŒÚ= ¯A퀾¸,&WjÉßv~cˆ~ýzX¤ˆ*+<p§ÏNYÉ—Í«wÝà”#2¹Ÿ‡qÓVj&)Rý™hˆþ™=F 6µì̃”Æ€G‰Åì :¨$¸Þö¬Lß¶3˜×(ËWnÉɰʪh¸Ûâ€í:n2*Õîk} f0¨2…mmCø4­KQÕw«¼g£Ë&Ó}VMÙ ¦¬×‹SV1kðÑ óZ–“ÔýÖÌUŒ¦ÂǾò&d×ðÊa&Ðö¸…¯õT;Z­“ñj«ØÃž6P±¹‘M“Ýx‡ÔÜ›7Ø€‘#„¹A~gç^„Ÿ½Ëƒï/ÍÚ»£áï/˱áù¯²~¿Ï¿&‡W‡W~½×r¤³Ì»r"›qü#åõN¦^ÿóýåäêÊšdh‰3É“Ã1ª « €*g®x sØ/ÿÔFÇeÓ8þ‰$L×)-¹jBÆðe4Ë|†½µÕßš¦úã5h–Å/)(*|FœgSÍ+ÑÇäЄ0í8«¦„ -° RŒÝº¤&)]9Χ‡lÐ&IÅ•¨ÙTɵ•æ’x~MõΦž=nPýìï.é}ûécДÁ #:¥ÛZ4E[Ǿ '`naPE»Ãà–)FhîÒÙù çàŒ@ȶ"œ[ÇW{!1H,#\Ï…¬1°+˜AYý&.-LÑO±”Hõt½N~ûÃϲWO^¿yÞN»æÂ¹Ë’Íî;?Mh6<þôfã{œŸÓ´ÎEžØ‰Á†ÇT³z*mÀT±™ƒð¼5¡BÇÛNlõÎ-‡ÞÌËéVôÇò ’Z<‰Ò#Ç¢vg’t‚_E“]Ô ŒŽfšÌ$ù…ó{¡ ý-š&ÏTêIëÚëΚͫ39›èžÍ–ŸÕφ‡â¬i”FmDí‚ákÈCí=ë–@ÿ7)Ét`¥²ht׌wa#âÛø[=ed„`¡ÇnÖ o`Ú%p•™…W—.w‚™•H!lô#2b‡>4‡i¸47þHÄ ëåÁ¶@!aÊ•—C®a‰p}ìéPbŠ’R21óâðõÝéëê}1ËæÒ6¯g~”únÚFÇÐÙ¬šN ÷ëŽpß{CÑTõ‚bó—m¢A¼$r›ZÖ/Ôc ±š7#ºÿTj4@õ‚™΂SX{zö‰ñƒu¼Ûµ„¢@™{Éà„´QŒ$ìâ—X0–†˜èjiN¼Ÿù>‡²É»ë§¸ÃamCëõ”èÎôsT Ã7W°Í³P0!šâÕ²-wõŽ]Mnâ–à¿C‚ S’Iµ2JVFoÚ¬å¥RŽŸ;A¿:K½Úõ4JÄÊ‚|õ ;øûß³ýçß¿úáõ“×ÿýéÉÞ_²¿=yýòÅËoÇ“§EHèë?rÚçYSà «kR Ñå-H cO+ÄÜ ÌŰïæäØ`ãg}uQÓ0ô$ê*Ê(ª'jLk5D·]óƒ2ªv;t³á)"vúÒçv#YŽçò é[Þ—†Û¢›kx‘¬áâ¨èý¹¹¾—T VH·§o±†±0‡u˜]ŠJÓäv{è« lÿU‹kd;}‘¼¢É£ˆˆ¢œ7þU‘Å Þvã»u‚tÂR¼$M:&M58Ñ´ó¡rèõAçÍO÷øËó—o:‡W-¦Hþª¤Ñº—×ó}ñ–q›çË¿|EË›ë¹Ú¸±åÁrÃ`9øjšey~=È:ä6wU$)Ï >i“ÃídWÝ= ³7Š5´ÍI ³¤]ÛœDK«N½?‚¨‰%N«ÑóÑì(<;‰ÈÆYÖ^;†Ó†-Ú»VÜà…Öâ·é%xyeãÈñõEöUÍA¿8£áÌØÜÝaH<ÁÙöFBÓ!·O¼¹²v²OÉW† DKÃím8Q–”Ú T®iIÆ… [(–eÆØQÃöµ×È–›¯€O<¯À’_n¦\Ö6a'œÔê¶x^42¬rô¥p”ƒ”æ‘STZÛG úü²Ãµ‘¸<ù5S4©4rFnà-Ž~ÕµÙ+Z”uÌauáŒÙº¦†3ƒÕ1éÀm|V£‹XÈÔÁ‚®@ÄN(㣓K$D(åÄç[„TÐI=è 5sÃW¨æ¦XZèõÖ7 ‰KHá´®©1B…“1õÚR_•>L“V«[/¤Q9£â Âî;žüt=3V’’¦íí´T#e?ê1vë„íŠWØ1.3UÛ«Bog4 ­¤Í “¦Õ"vqñ6ÕQq;Ü ›¶^s´'_»>䜊ö—G Wå–H)J5Ñ–ÃMKÓaËþE}Ômï=Õ“&«fÖÚÀb%ŽZßï̽X³U=“G<˜ÈÝ0X` <½lTe˜R¥³t‹öÀ¹ÊùÍ-Gê:™ú^d£jû+Gõñ0 (Íþ„ ôb 4ýöm ·4¯ù„ lÙŽ¸”5Óµ¬Ý¼ÄJp©} ¸Õ£¸W&9“ÒH16¦ݤ>ï'wUJßÜ1Í>W,Æs;×Vt@FÖb©{÷‚¾´0ŒjÏQ] òlÙ½ŠU½êØÑdñ·R(ºú_ðzn%(Øßž©e7GÒGßÌÛü,ˆ@.ÝâÔçbØ€gŸ½8µíiT”¹Ø\±h>ë䯋h£þæìX‰ Ëc°Ö°“h'xå·Y5Z~‰¨¯ßYDää ®½@ ¹bSÅ®ÝÝžz÷û©]n¬±8#1âƒÍðJŠŒ’4ó®y,w9ØØ:4]׳Óê|ì¯`Ÿ1lâÇ Aј$VàŒŸD¯Ã:[›×ÄÌ*y´VÆœ2@|{— ÝÌÃO/¸»Þ.àGÅq#áIŠÙ¶R—+«vò°}­&P‹Oú‚fxg@–'2YñI`¯Ä{Â|¶(ùÖå߸³Ç‹±é,Àž$EDKïIñ󬋕’Ü0máÿŸ½wßoã8ÖE÷ßzŠ ¹µP DP–Т™–­%1>–lç,†k ɉ@ <3 3|­ýçÅN×¥»«/3Iù²VÌüb‘@ß»ººªºê«µ)Ñnnq0/æ¡ùyè ôÁå};‹H®B^áµMÀ†F¢óüT…¥Ò|Šb©¶Õho¦ŒF§agZå>zoùNüéâºÄHgie@²—nS7’°áǓݙ¥ÅyšDkÐÍIŽ!j1à¹çi ƒuƒÞs"r…`x7Ú 9ã¶ôöóјåjvæ•P£̳lÅ}ºrëVÊÛróˆŒfÁ¸"ÛµRö3»ó==›®«•»(¸JÚÁ¿=ˆiMV?Ä¥„‰pT1åÌߤ‹Ûo’ìb€œUç^„7‘1<4ÒÍ­×=î. ÷m™Uh·ö‚H[¯±žg#÷Ò˜;ÁâK'ª{ƒ˜½PÓ\œ€[vÅo:×Ô*»ûxo„fy¬‹ËÄÚ½1¦åBÕV¤µ4#=:²‹í8t÷‘—~ˆ¿Ø—Øî ¥xšp™O¥æˆÒ8ãK‡O• ü>*®–Ü ¼wÓB˜³hA8îE?ûuì<§÷Hse,ä‹ùd³ˆ4˜OÙ%7knÃ¥þ ܯ§Kx£õÈ €4!r¨Ï„ãÃÁ>Mäàd]OLEäãZ»v@ßy)EÄË b_ü¨÷ö¡ñEË•¿X9º…ç¿îêçd÷¼J›‰ ô¾Ãîóül&¯¤Òä¬PŒøG·«¢oþŽÒͤ—^^Zaòê)~ðš?MuNÿÑy•¨öц73q0 NeÂ9:è¬òú|‘=7¾]›årÞe×akIÌïœïã³;åûødçépe¾ª\•ïãíy~Zo“a\Üo©>îšêããçöÀyìmÌ=ò¡Ijq½Œ'E]Û‹yÒ!Ñ!ñ••¾úÉFP 6l”Ø× <¼ôYÉØÙÉÞŒ\Üs“âKÑ88rì(ä_bÆ=ˆ’)…ÞaÆÝš! –…Ý=#˜° Š7¸ÆŸ)Ôé¬E(ù,²ìý`ŒúU ëØg/L­%3;g€Û€¿§†fÚPlmƒ¡¹ásÇ>]$ ©nš±™'œucPWÙó†ˆù?ê~½—tÓß#–ßKº‡{tô”ŽÐýnØ{ï¸e×ÒºL Šž!g†váT+o€¥õÖA€_é`ÌB‰‹“|–‰jÆo ß| 1YDZr2uµbc!Ò Ï÷ŠYAW±ýè¤fÁ_8lâ œFko}íil³O‰nŒ…RQP£shDú¿]ãñQô^`8®Ÿ¢ó¢Pšéîèæ§€e6yÕT¢y:&Žà.8N_°ÛA| ø¨ð¢þõ´F”UziH)磵³3\–1ãC¨Šv £~5£w®q T”ȳÀRúy¶(1ÍÉí2 wžûlHƒØ„¿BÜööÂE«C‹£“¤ãb1­s`˜M¥µË K“¾+ùµx›bó‡Ò»IUkiÇÈè$Îø.šÖl.°‹€ã“•º÷ËT‘>ñyݱÿ†P Þ¢“ŠrÑÙq9è‹)h¹¶¶à¤@„y“Ò@_5=@!iîyñóçþÇOû! k¦ýø-×Ç/ëÃýŽòfhÝZX/ï” $ŒÞXµ¡¿ŒO©)­‡0騰‹Å'|y96®Õ&Œ\m¿üŽU¨Ñûut¸ŒâíÒÜeÙÄ309Ó®‰úDt'‹ óEe€¬é¾,f½Y&3ÈBdýHJ‹»žûn 4 ¼(šÆ/‹‹(œ]à µüˆœÍäË|O›T+Q»$oŸ?Ê-Bë<4ØK~‡Ô .eÜX†V^FÒ—½ºgëì¬(—NËØêÑÁ«Aãí1.«;Äh¾gÙl⼺ÿ.8~<ñØÀ G¯a!£,ä d·j—¬‚T' "DB˜fÀÿñãx‰Ô*|öÍÕdTjT髌‹‰Ï8‰œ…þãFGÀƒùNð‚ûö˵ÁýqtlG½¬¦Ÿupxð ³ÉÙ‘ƒá~5(1ÝY[ý¹Ü«íô[ZÅG€WñãØ¹ÚC#^\‰<àž,öGgΡ鈔κËàq+ðK”QË%APC¿‰6 ¾r'tÃCZ¢üiüýŸ:«Ï],ÄÄ |¬¡+g|JÕ/³j^ÌÒwëyã@„©L\ˆ²ëô˜Î–`Oxœä›v5w =;Š® +%ÉB”«pm+JÐ|ƒ†àõÒá¢vƒ³|^9¸k@ÜD˜¸nzÁ¡†ÊïÖ£¨ cwÜADYaؽßÄ»VÕ“î íÞímNþa„k??W]è_ò[h…©ó #TÄ‹‘+z¾BjLXÁ¹ Ç(»3w95f/ïx;q *oNaXvD‘š¡{›¤š¥€uãX¾"Û¤§|Ó¡óO€àIvp®±Äèçœ@›U =·„%Ř9¥oE4ܲÆpE;jŽvÙ6&K×Óp%Hv+efŸåië„•¬‰y[ÔL•cÿÝuÆ%)ŸGîÃA æv„ klÁè@ëßë‹(üp¬ÝI.Ķ‹¦{ƺì8 pÈ nphîêÎ7ý­á/ømƒ¼³w'I:h^:ÉQb;°Ù$%´(!ˆ•Ý u@?ö](]ÀÈ&$Ýäàp®¡!èfTU¿ÕÙRŸ>rLƾåõB˜êz´Àª|H”w´®"DB4èð(ñNÞ‚[eT|x|Xî‘!/¨acÒ®iOïYJ¹&ÆÎÚm…Ø™1U¬åF»óV¸áô¯Å¯fârC®Ї÷Ý#_ RÐm»TÆûËÞ<ËäÙ6CË2 !uo»çß´ïùðN{î>£µøÜ ä‰{:JKwj!ÓUEñn²§½¨Á/$ÅP¼*nFœY:uÀŸ`/#úmÓñæŸñê§Š^Ê™{°šÖWÄEüÂ"›@öŸ£ù“è0‡t†Ú£«Ä«²¢9|Du¦8°±G‰qù¶((ý[:4ì'+ª0Å4=h9BR•ÝõÊþ£©,X‚TÓG{lpÛía–ø-.BóQúžöqtÃÕqA›a¨ 0÷§pËã¶C÷¼à“BnÄÍ"Ž›Õ8®–¿0…ßÝ¢ÒiÅ$$rOcën‚Fô›ýE>¹L—fQàÝùý wÙ‰* e¯‹ØÎÖí¥+`3¦_'1ˆ[rlðÁ¸¨6bWÌ J%MEžoÇê6Ò‹Û‰û€N|¿i…Ä_j¡Ò¼Œãá ¶¾,®iÂÂã6ÂâR”·ˆû«CmÕlÖÀ#RÆAf4øå°Bׯ5cµºáZøšAx5mPŒQÛhcl Ù°Ìð~œÉ[ÌnõŒbÅÌzdVíÂkżë£cÚ, ``«UƒÅÖ=?0é*?¦¶óóÑã¿RpÓÚVð#Òe:脈ZÃΩ+ûÏkïP6å“ÖŽ'CÒ_‰x8”Cj„Ï!£Z(ì‹Z¼d¡-WË2Ë}{TÃu"ž‘hXuíA5b}hIúŽ5-ÜÝ«ˆlÚþ™hõÜÖ¹~Á}xùX|î’‘Û6“ ¨7á8âÉFÊýPb£‘ÞÖØZ²…@ŽñX›ê‹0¶ÖîWרèÄ­!¬ë½( ¾Üޱ¢Ý¿Þõ«7ÍþõPAÌÓ·Ð…Æï²Sjèd=úˆCòaoÄkŸ[ öÓð}ôé4\çð¯JØö1¨ŒÆ7úN-ê–¥“Vš['û¨Lu*Ïl´ízÏ›PÈ—À “â§C‘bvRFÕ§ŸEJ÷ë[ÿeP¤ÚÝ­ñ£ÈUú~?®Ûˆ„ŽrCeœñ³Àúxoúm©«[l~w~§_ï)]°%ù žx?bH¼ÒO•¼ Ðÿf ­Æï½œJ1wùXÖƒáïd„Ú)¸CÌÁ«ËÆÓ*ýÏÇ„øï‹wÍ6_0w„‚ÀbÿJ0ˆÖÄpx ø…qä¿4~ç;+ñž ?ýl÷“!yª¿ýd8üd~âΧ«h’oUѼ^&_-fÌÛ7%>Ã¥Zäÿ˜fjAžeê÷?å³ÓÉÓñ`ñ^2¬ýð±0±ëÑŸêÑÓÐ׋4£5€ŸÂUý•º-þ¬‰á« Ìþý}3Ê&ü5>!µiåsðà?ü°ƒ…æN©W3pª RwNQ·+ÿâ-Óp8§IZ§Ä„¦E:!J5Ä=Z”Sc±î»~ö BÚ§Yðm‰? ÊŸdéŨÊÄÖ¶¶°Ç´<«<Ö÷Z}FA^n‚¸HNó)§W:YBfªÏ¨¶Z‘0`|±)kîUç$”J§>4­#PÃH~øÂD¥±e%h×¹}R0´Pã=óÀÜfp·ò²9bªÅv"›h®°­.´ŽâÄ%[«›­ÚK^_‡tfB°üœ*€ìu’žäSŒÀNtmݪ§\•éh›–²µãÁ- T²‡ïSü â^æôõ 8¸LVæMLÎRV”HšÚ`536¡.ÆÅ \oùîmD·J)gèNª…w sÁàÉêÿÈP€îeŒß+¦6s =?Fœó=cœïßì8òè$Ð$自yžªõ¹˜†"Ë5¬Œ…à)Õâ°;…"´€¤µßz蜱 ö (ÚIËÚå †|ã Ò’-˜6Ðqq™þñƒ–)!›]bHP†çňJ|ùñW®ò‹ÅÇ«J6O ÙDÝÀ?,²…ÐBÝ…<ÀYdÃq÷’ÿÈ–—€Å‡_d5Д¡[,g<"}ü-\¥×p€Pî;F7îÀáèVØ4—Öu/y#`µ0ɢѼIЧ¹:ßAÔÜ˲,Êî†D<¸g×0Çnp«@?U‘‘¼8l˜ÁÆêñ_7‹3_M&R ¦²îG.3QV­£")þ·/.]ó›FCï¬Ø¸]Iæ.ÃEÙ†8ï Énk·š¿”KÄïÎ@´ÅfòõTÉÝ¢Ü~îiÒq¶à‰ô1~l^'ï Öêí"¸ÖÔ*;%A˼êÝâD~O6üvxµª]ôÀâ\VÓæ{QÁ•¨7góš…Ô2ƒ^üGê ›6PI§ðx¸P†¶ny Gá_ÏhAT_Ò/Þ·4bÄoEÈ5£.fÚ1ö¼¸DÓjÿ+ˆk ° „*Ó§á¡Ø:É?ä€EE‰O. º33´ÍkT#rX÷àšÆÓKdš*Ul1ó ÒTnzâªÆ§oƒWÆwì@Cï³%´sÔ¡ èô“M¥sšqQºãì;´­jGŠÁqôòèeP³†îë³A–Å(2‹þÓ§¡Tz»‡ßâ1«Ñþ-޳±.9"*ÎVõe[t‘½ñ«…Ûß×kÞÜX~ª—^Ú;õE% èƒštÀ—û4Uz޵°v6À‡¿mÖñ¨ ùCÈ&ﶈ϶½=Ã{Î Þ÷ò™Ñõ?Æäô‘ŽMÞÔôÜɉ…ÊÆž>€6^@* ¼ŒÀc-›ünãS”a?#¼B4c©ºäºß‡'yxÁ ]íol>ü|û羦÷”½ý)ò‚¨‰+ëeÜÓi®Ÿù^«ßÈ/½˜ƒŽ.ìè‚õ­•!åW@¥ØTç =2tÃyåðrJcß·œÛðúH MÜ·´¶@ú©ñ eB·ù×á­¦o %eV-0rÈè P”Ì'äÔ€ŠSíÈdx¬ES„–^2'Ÿ1ߠ߬# #Zò¢£ˆ„7Z~6SZËD{¥ê‡¦4iH:lö‘ÀC®æDH4 á›J¦ÔYL¯Q°•Ø ˜NÍ,FeµNãS¤GÒQ›%%ÑZÆågÿ,gÚ‰V¹“•yÔ7 ~¦#Nˆßwþ>ë¸ê€.ªx_'U!m¤ºw‡¨³X©š ðÅ‚;Ëí©»k(ßqöƒÌ]øÂfêíïïz‚5ŒÓ|Ï#;ê èô;5ýƒâ‘úçcW¥é~ESÇAM³œNÝ¡[‘æ^Õîwž#ñÅ«úq®p±ÒNìÚßW·Sd¿ Á<‚ôðÚ+˜FÉÉâÍ{#}¾_ o…?ýô ãO?{²ú ·ä«ž0¾£b𸠌&ÿãѨ›á¤ûîRü4ÓfÕÁ-ó¾¡,¬6r›+८ô5ô~bKO–³ôBÁ¼,ðúcIP¤±¼|È7=¦ù{p p¥u@‰30ݶ¤àA.ºÓœ”r¥ýÛµl} ÏÀy½À¸pPa¥ñ‰IM#C £'&‡ݱ8ÐÙ’Lsðº•ò¯n«:ˆ–`°ØÏU§@&“:ä–²®­¢­~Ù~°4”s¦Oš˜˜ ô‰î î«¿feóÙ¸D÷E¦( ÇAî%ÞÆAbdÌ}EÖ ±q•Šqì‰ÀK¸ævóA6è›ì ‰™%Pû ±dK‚Ö© ¹Yž œB Þ3$êî”ý g&FŠõWVIA,–Óf^§nc`7pp´è°¼ªõðª©ä¢Ò4,*4Š2ž"Ð3>Y(âŸ:‚¢’!}HO–Ou»ˆÿë¡­IÂss 9ñOv —|ðÅÀh?ôØdO8€´›0&P°þËMÆœ‹A¶7ç=Â*Xìu86#cÝr µ|O©ym¢õ„å-C³ ?DáF;,ä+ÊÜA¥©qôØ,µIÿÊŒŸp#÷xü5B/ƒN…=U¼â.P•ÛêAjOwûhR/ÍyÔ1 ~"¡Ô=Lª‚‡x ìxæQÙ‹îAûi³"wëÛF|†j˜$KA—/)‘‚øE2¯²Å¤ØÃÚ@Θ2ŸK¢tzV”ªö…N€…Õ È%F¼†®ž>}#ÅÑГ7×o^3×6ÒÀ›ëË|RŸßÀ,‡ƒÁäõ7׈t*” ]É?Zv[7$…s] vå¦!Å‹+Ìo[¬²(1ƒ†G·O•©þ(_ü² ¿¨. ðåìø5šuŸÙ6|“ß ûÞþ>mÀxu^+:NÅŒ„3%é…ØÓ¿<Ò³è&ÛªÏb:Ío”pOƒ1 ôõš>¢ê}½Xü*+Д¢+jêë.¯EoKüªVƒÿœéo°ÁP#Ù…´÷ÜŒ2f¦Ñ×ÖÆpKb¨´ξD=¸Wzp ýÜgàg_ŽÕÐ:{°só;䮉×ô2"®~¶Lº=¾ë§x8ö½8¨ÅÑ®ënô0ù2=Šø:P‹m¾+à–¤d•þèHúÑ»K‚éßK?ºðXò7LýŸSßÍPóû¥ÆZ÷ôÁæ›J VÃW2䤳vÈ¿)Z»š#Óo‘á]‹´²ÏÀQʵ8ê¢n66Aj„§ÃÂJ8¶©þpJ›,G$—w(ºÀ£¤o9|øž$þ hvCëqË8ÁµÖ‘ oeQDdûw¼Ma£8§œ iàF ·.xm ù$GQ--•¶¥ØžãMX•RBš:ºà~r}RÀ¦Îµ¤³W8ul—óö-)@Wrì¬=6¦¨à'¡nçÕ¬WQ4qÌ,«bß;Z©ÔOÇÓÇ‘¼¬„#—N&ˆ×„Ò¼³ˆòN°biLEy¸ªÜ¯Y÷¡3ðGÈ»£S“BKÃX„IT͔ӱ‡‡\hL-@8ÉC¶+oRç² ç{ò½$ËRHeÛÔd0cÜvXÕý¤‹µûÔÈ£Ø,¨mÉ‘ŒôŒºðKßå:á¬îO„£7˜Äw jpm6É¿Y«ƒI[bovv8’ãÔï݇2Ös4Ò‹’Ä˱¿k¼fÕ•õ ~UÚ$&ñ²o²5¦ºr†=¨T3Ýizq2Q²Š:ë»{ÉøbÞ­wA{>xÀzÈ¿ö‚ >lÂÅbiÛDòG ·²-„6i¶ŒX(åQ>¸†–oú†Àl…$Ðz¬˜Á®ÆhcmÕ|×UgxWÝ ³±È;ÔëÇŽ‹H½Ì‚HÔº\Їù”+È‹.R$V/ÖÑšÁðZä$è`GϔߖåÓ¼D—#üÆåYt‹ÓÃ#§Òt•xb°º®§ rs§â²ÔÏ…±C}<Ñ]ûÝm4u þÁn÷ް?¢{°ù¡Ý–n É©|pÍÊû¬ôÑÎñþ;<>6¦SäÆˆ>"‡f FL­5ƒ@%› ¤eïžW¡a!åÁÿeîÄ{èb²xƃE»‘ñéÔBðÖwÓøDìªEþ­ØüÝ^¨'†âd¿E] Øýè/ù$r‘.Ñ CúË%Ä/»±›t þpmˆž§f”¡/1-Ί¦Àôå\V'Âlðo³˜2±Æß¸’š-¾ßêÏJ—éTPZ•óº[óPd›r_t\bðÖaïp ŽÜï"¡ìxÐáã“ÛCŸVVÓ«œy~´“¯8|"¹q¯ýÄÃÏBUGT͵ƒ«÷Då5!úrW „‡®|PÓ½ôàþs×)"—ëGXéIÝ´Ì–2f¼îÐT=GtÄžañq^öë½y¹UkѱMÜÕÃØFZÎ ê!ߔخ^æ… çk¶± -ÙÃÎOöçéÐú¿R Qwè‰ÍyÞÕyuí䢖­¿R•Ô?c5ÒPu`ƒf%qôƒé¯§O$è,ÅÀ]©ºs¹±ŽVIZ~·ëÛêÿëVáMžÑ[%ýµŽ»u'Ël{æÙ®vû5­}™Wã´œ´µ&ívÉ7×jŒc¯ø˜Ëà˜cV“Zgš\Óå•`õ´(SmÐÐV³¤ Ð;>C·r“Þ%„;/ísò·ë¦ön|ÍGi°à­ë ÃÊÑ£­ªaNV9jóp²×pƒÞ½J[Ç0XG] 4N3hä .Äꃤ¯…$§R  Ìª ÃaêПU™m¢_™BkU]´i -ÊmÐÆZÃ`åöCñŸS.ï¡\¾Í4æÜd1u‰{RŒŽ_e<~(Ð ý Ù ¬³—[Á½"bs²æO „𾉉8ਇà uBŒCUgU:Û»ÜSŒ[Ž6_fÝ•¼ äÅ,:ÐåÉôˆ¸—”KµP¬ëÄ…ŒÙ;|Ùe£I²­Ëõ‚‚ØJ)EcÀFòRµÈcIöx´·?Ù©’‡ÃÝÁpç4ÁlêE¦ÂôØœg0t›^rÆyƧt“¹Q›Ð±ft>ÂEzä¬À6~µ=\1öÜéµ× ;ø†ÎšgŽó.2ƒ‡åsÌCèúqÞ/æk~^ V‰KCJ’8¶¨¬Q@•»˜s>P5g@v!9ZEäûG¿CÖ LBG¡ƒ$Ý`#'A ÄŒß= X‘jEc•UéE†­³§¶P-` ‡à˜v~Álˆ–OÏ›ÃA«%¸oä™ü¢KñïŒi&ÿ1s¿%x]Æ¡ ¢¥ÍäËì“\8öJ ú¸Lô¸jbŸÉÌç—a¸`» ‹2JJÀÉ…b|¶ÏÁ±ÅrЊCßÊÞ$Úä¤Xb;ÿ°‘/Š]ç·-’µ8«±ñ]íÒ³™¼¨ÞcE$IçÙ2¹LgøÐ°ÐÞü$’‡'Û °f;Ÿ(Ñ‘/fàÂGù±Ã­á=Q‡ ›b"ŽÜ@§Æ³Cî>œl?œôþHlŽãxPÙT.›š-.ttû²TµÒÀe–NÈyê2Ÿw{½mû Yá+Œu §BC4Âì `<‚ø"X*!·¨ŽÇ“ôv¼cBeEDâMÄQph-4˜‘¬Q³±L:Žø£Ëª\ĿϠMؤ¿Ï4Z#ün x÷pþéà4θÃäîþ„>ÃÄE@þÓå*oìK7i¨âVߦ]×PhÂØ“’çtð,˜öOöÒG'ÆŸŒêõ“ÞcD¬Ç?©;÷!Ç6HoÓjÇìÊùU]èe9,³ÞGóÁ©}~Ìè½d¨÷š¤@|à×|°þ‘uWÞqãz뢶a\õ¨ –ºxÌ_ÓàzYk{ݧÖÃápðÉ©úïäáðƒá'§tûªôÅ’ôõryl§?xŸ-u2ÌÈö…ï¹·Ý8lbçNf\ùóí̧¾Üv+M†Ÿí}òdïé§ÿ éÕÞœ¨­&ÿûÁƒÑá«Ã¯7E¥»ÛÙfOvv:=‚­? äE²hs>vÓ+f,Rº:ˆcg{zÈëë¯Êì‡/ñ9Á€’êàE%ï€]ïo×§%Å%/AP§ýdëuÓ@RêXiÄ[„£¹¢)6‰£"R,ê1ûÏajE¯9â_ãñ©xÉKŦKkÒÝj¡XigÒ2²žÙêxi!”:Ÿ Œ‚,Ìî?€AxÄ:¶U‚õÍ%$£#êôö!EÐz|ž¨2¼VÿÅç+Ân¢a…É[¡4“‹³¬®u ÐTjuQU¦ÀE:ï«á§“pQêÌS“g°;îg•ùÊÐá—rîa£ÙLŽÞ~h  ŠCçôðˆóG]N:Þ­O;}·¸¢9Ζ•Ô0SšN»ÐX`Š+PÉ\¦ x 89±9$Ú€|¯ñ9¬Ç\I¿”£i™lIp6à{•əŻÄfÆNˆ9¦`„qxDóPÄ’§§Ð›”ôsõzA(:M]Úƒü^¼OêÔ,ŠÜ1]=íEâ4tdOìò!ÓÓ‹„/›ç¿ŠßqÈèˆyNõ+[¹@ÔÇ“%ØÔÈÅL+§Ü¿]}ŽÊ2R&^™ïKÃ#% ´P妄¼vœ1ªqöR» ÚÜ#ȘýÆÅ§‹y(i"`k,NkmGÄÝ¥«šÊàMzÏ®Ÿ?žœB‹Ô®IÏý°‹’"tض2P×sUs Íw²#)•gÔúu¡®V2Xáy¡F÷Ã"ÿNî(])L€ÈïßDôÚäÔåªAËnÖ3¦Ö‚v¸jKÜz )‹Q-®hÎIÁ :îøê$`¨vZcQÛ%z½ˆÁ–g¡O$¶?l8¯4ä³lwö¦S æ‰A'@÷vìØ|ÓÈ-`ïj̉c¤ž„ßbµ÷œ@㜱«-´ž^:Tئô«‹ªm†² ½N° „F¼Û?röaQ¿Ò²g6Aí=É nôÛ¾ëéˤO™;ÔA™ðR^Òˆe X‡Vލcí%¾¼Í;ð„ˆÕå“ ",A=þØ'’VT–uhá碬Îñ‘‡d MG'×5=R—îM ïÔ7mû¿Î†<áÑ~Ò¥Qo›ìWf0íÛ*=f^ÍÔg9ð ºnÉ´óC>‡k%‹Å À¢VªžË´¨åp7w¬8"$èf&‘¬¤9»\s1Ëÿ•î±Üa&]Uã$ŸU=º>p+!P‰&EDmшívð…ºcN¦…8Æ éÐ.ß÷]>1¥¿]änza®jÓ4ÅbNÍæíÒ»‚«$׿ÐYøéÊuÓnƒøì­dx@Ðß÷½;,ªÎækìî°Á‰œUoÛÛ8DzmA¿ü,üÑl_·Lz ËFxp³zM]w?³tšŽ¥ÊoqEqú=×SDêŒÜ3OwJX£ôJâoRf…ÎÓyzuÿB…%)à­·‹Mª;S 2Kc»¾r¸GÀÝè¹~a}íóƒßÃ-9`‡:o²qá°´éÎéóÏA%XGM`í^ä/Þ„0VºñÃ&ý{ÆÚT™¾si"½S‹ÐsÈ%¢ff%`ƒ  ÎŽZ«^ˆ:û!4Ôè6RòFÖŒ°+}íogGEá­IÑ¿E @Ý–º•¤[ÜŸVdvzbQ™É:Ù \yÇz‡e$¦ô]÷T·[Ýñ£á6—s9,ÆÇPL:Ú8+¼¯Êؙ҅!¥½Öö…%2¶…Ô×É3N|«WÄ|al|mrùOr)®Äb݉+`ñ“ÇÔ’ Fh`‰‹¬>/&Ö[WQí4eÚq1û'ÌúŽ˜ÌXsè`‰éVAP8ðåÁ¢Lét2 Õ…oz.‘¸%šsåõàjDuT# ”F‘ †Ðª5„½!¯|Q/1YŸ¾­2³$À¦Eñ>YÌm¦´9êÍê'Ùàl`VmCž¼ÅÜ%j)FZÖɳj}3œ¤SÓT"šÒ¯;6†[u²ÆÎ sk#н‡@F[BÓ ÉèhBÕšʾªI uœ’*ªF!bÉü»µÉIÅT"ûûÄ1>3ÔÀ²ZNÿ·7ñHö)~<Žk@õöhß±_8_/ó @oN­`º™TSpÑP·V1 %Hާ”7VDýSC¤É¤È*³ÕêÎÓ¯{ô<@2‚ÝfOÌ}݆ýÚ5~s;.ë<ÐD9­[Â3 ÁMêq[Ãj$›Õ9!)„À4r;Í[2fÛDÈ ›îo¤¦‰5ï‡à^0 xj¿ v8»é¡p”v;5É áž¸¦@7ß¶ Î+ѵ¹xhQÖ›|›ÃÚ$þj3‘ÚO*m’™È—E¹^ò84Q\¤WmšŸ r=[Í*ÉԠΘÝzCsœÕŲˆ–põÐÑišÑøG°#DhLwÔ׳øMŸëõH£×5¢ò@_"fðعyÀ\o*f &íhœýEÔIÈ×XÃk/XM1]s!,5Œ6©Ý•yè^68ó5Û­á7Ž£]!n‹aÓösÑb¯ÅxÔÔCk¼üEØ—Ä•SØ¥ù´Ðòù¤U‡ëï/1ÁzC<ÜÔ^¸6ñR‹—öD"r-W1 ßሰ!ÙÃç0C¤¼ ¸7Š %ž’õDn ç@:–[˜m­œa8,’.úÉŒ„2Ó T“7àâ„1„ÄÓ÷ÓI^à¥D§ˆFßZŠÿŠ2YÙ :éëØ{ñÅ>¤ ´õºßÀ‹¶Ru“7i u¦ù +ÉÚ)u2èOu^ë— üÕ¾ˆ”éüÜãí\˜’Pú-Ù‘‰ètš¢@®Ç}yžÕçà‚Ë‚v’]¶«Àƒ:fšnêSZú]+†Ãͯ¦Ø­îhÿ)ZŒÞòðT…ý(¤eÇØ­¦ÅeÏ¿ä°CÒŒâÐN” ü°ëÓ|9MOÌk’¼Âïèƽ½4·ÝÎ;½|ƹ¥ÔÛNØlý<¿OÏ2áþüt âšVmÍAU,Êq¦è@Õžeõc.™-ØD¬Kð9˜*Œ½[XñõÉWLrš^¤;A)ûÉHíÑyQ—8R?騾OA`à°´é2tµzf(lîiPùeÚ5{ÀnfSÕÀÆ!™,-„cºÛ#禈ªÇMýylBBß1£Ù&[Épg›„7têñ8!@Kõ·Ö)þ®5’èà¬Ì']à0}L£[îoTùôCVn8ë JÖ¸ócôç´L[âΗ>;%áÉw×믨¸l¼2Wu>~_umÀšÙ—¾ZͺìVä{SÉ%TòLA.–ûØqÆ¿|EŽ›ûÁÏ»zÜTê8 îFwã-õ³áO»Õ¥ü/!cy×#uL7’ž QûV—ï;®jî«®â¤k ^„+®1ßö èm…¯‹ÃµÑÃ7ÌüÛ¹q nŒ`ᇊyYÐë£wÓÚ‰è iršV`Eæ!îWÚHÏ§Ž– ­®ZÚv=WµÜ¤ÑæbX©Þ@æ9¡Ø3-ô%ê&¬ ñœ]#A&Ý«>оöm)—Ã3¬^…-E$XŒëUöñ¿=³d†æÅœ÷‡$HY[¸ “ Å~¡ÕE•€ÕùÍ„¹-y‡9t›’/TáãNäkG90ß éRûŒæ:žÂó#;G ·é¹ßéXovsqªû£®²{buGd Á}£zY':´D¹Ó4X<°¶ù`“ù-=¸ìÈ’/1ºYÖ8:îFˆ%L=QÚ\ÿþ³#ûGoWô°Ä+NeÓ)=‡« í8yÆC£¿à4ª_ô¥3Þú'Ý 3zâ‚O~ÇŸˆAœýt‹F?#ê•]þtóæ.m¢Û8‚aƒ3:\Jbò@tõ,=J‚ë–ª¶Ý´gÆž·àÉy›·˜‰‡2Zf4R­þƒ7Sç!˜•`¦å¦ciêWŸ7OÐÏpž~Òü£Èg¨âVÎ|Ï¢QH yÂ=‡ÚûÁHŒŸh@¸>¢K¢Z=ÐÕq ðÆ&,T:…³ß6…§>.=À ÈôÇçy ­Ãí}‚ö¾ E˜pEû°'Ø £õð8zŒj9"ç¢À+FÂÄ¡ °!Ί™v%v|Åe.ÐÇ>ÕªÈF\¶ÁoìR¦û‚L4`'†åv>%Ý“·MèU[ £ÝÄ.§|4rFù&ôí·oFïþ:úëáKuìáùQ ¿¯v‡"ýj­myŽ¿ÇI­6–=ñ`ÕXA¸Ñ‘¶þìÜ®ú‚9n±wü#ðj[ô+ è‚à1Ú<•a.–l"ü“Go GNÃ’ý-s2ÄkoίåB:#^ÇMѸ'ú½Õ×0ˆ¡l”þ¹¿·alåå-7-Îî´´'m³ ÕS¥V_ؽî’GT,µü¯‹3–i] 7';Ëñèç³S À¥ŸW"ÍkÈ»øj‹¼/'×B  ¬¼0XXB½˜kµƒ¶„6C¸yäasCSñÒ²¸Ê ¨Œâèié0ÃiÊaàà&íµ0É Á ¤ûL–Å¢¸B%(†‰«™¸0MnÀ(˺fëà!ÒØÖëó"šî¼ŸìöÖv\uH¯ÑuUÆ gTgÍŽ©²zÔÕiÇóJmðF ¨ó.Ç4’íFBd­’Óðhy§YuÃ)sfîX ŸÞ˜ð6Ÿx©NîýÚºLc¾‘ìjEÝ|6âj RŒ¹·vÔg0½ñ/0ÈÎ4pì^2Þz<ŽÌÅ‹ ‰h&¶Áx»83>¡F`%èn¤Úìô4J¥w+­fè Ê>£ãJf´(²&i0š’X+ÄV·ìQ—1ðÇó ¢R7 ù bpÚ (ŒHUb“º‚mÀ©a2z]Îñxqøú§9ÀÚÓGÆwH¶Ûø1ü O~&à=ü™lÂÉRÂ?ö <º°yœ·ëîH-%€Z²Î áË,Ù"2ÙR—%¼À3=ì} £Ùá6­ÑîðKwãë&÷a "" åËÏ-!õ6œæÜŸ Ë `SŸ“¼.#šÿrƒè1Üž÷cïáø¼ëc6 ^mHÑûv–F§@»F]_ã#-H𳙦9Y°­ -…T%–)³úsFÏ–éץկä˜ÛWÃ_­‰|µ %hœ`ß™¡½¬›f*fy{&7{­°á: `}š½Ù|¿à½ÜKƒfÅÖ=ÅôâFdªÐïø…&°fòÁHõΣ–¡~:¤J× @T×(_ðY^Æü^GãË(ØÁ>†yÅ`<€È>ØylðM݆F]3Qz-…˼ðÙl‹¦‰=ÇJ„£sÅB7ÆÉþܺÚŒã½Cá&Z\œ¥ T<–a ƒÜÚË¢KÊ,çK•|®ã+":HNƒÃÃ0A³J$Ñ,GäŦ;Öÿ¬+•oUqå·UJ EŒýíƒÂ·fà¾Á”ÿ˜Ñk2©f{›öÓuF; I “ÒNþ|\à­AÕ)ÐâUü¤8C.¡F–õ4†™ƒX£x­Áåøy3N§zwë2•~Šé^ö«ÕR]†q&e.?ep§ßýW)*ª¦§ZM\ȇzJÁ-.¤*¬ Ÿ¸R€˜¡ZC¸pF&"Qÿ…T]oøÂ˯žŸŠ”(…šd

¬¹·\üÛ¬<ñˆ0¾lrpw§¶öå Ö¿YõiNß¹øZ q›Ý­-ŸŒ÷ÍjÛ¿&þ žß “bó×è™CÛX×^&×`E™¡Ëã@8FëŠe6ü©¡±•¦/~’=O«´®5 V¼: wd‹WÝî¼ÿ^»ú­¤Ü“©ü`ØUƤ†Pk `kJ^‚­°äõæõË•"— NÁ§·ü¼(&‰ºîÀ@jC_ZSÄßvÖ{×Á!`+»nÈ0ÛrcÄt&õ1†•pà$BCîYІ4>9E¤>¨:Á§ÿ¨l&¿zÅò>™vÅ‹™‰¢o­më|p­Gåè ¨)êoâŽw¬§ÚB.xDhPð#÷COPâ¶ÀcXÛ$9Ý;$&á_-П¬mžlýš–9€Ã¡¯paÔ¿ÆyA8µ¶« Zu@˜-llkc“ñêPPÔ[‡ƒ™ºÌÀVr0w_ôz‘#ÇM¸c«ÝCÌ;tø“昂ñ™­ó§7'tj!¶ù:ŸTµRãÖâºðú“¹%ñ«ûq̃ë {Ô×ð$&*££¶%žg“7×gÐï†,á¤.ªÝü¯Ó%=.r@è©^4Õ¿EzÃVpf ¥#&ß\)×yÔ½C{s}xc½JÀdøæú‹B#d̵7×Ýñ#œJïq÷ðÑ[ôûÍÀ:æ2|?'·„TLìC¤×@{åz £ªÆhOß›èVâãX=ÆË̦ä­qKOë9ÖrÛôùj¸O}ÿ„$ÚpëÄÁ7þÇÝ:4,\Äæ¬Óüõ çà'!q¾‚éA šDYêŸÛ“§S½Mäh&ÓÆ+ð¾à­|üµ5 ¶‘ÝLÞ(§oN.(ë W5V£r’Н÷¶–³5Ì@¥Ž‹’óJñ˜©¼´"aY^A`!sZhËõ0bÍMä"q{ù.ò?P³zŽÚ•à¬HòB£r®*µÿ„ñIèLˆG{Û¿w¡$b6þ‡UóåW+ö±ÊÂÛšùÕ„¤ßL™WÙN”>~–ÈuûèsýûÌ%d 1qºîjíƒ>¯o¥íG¥ßøþ ¾¯N䯟ó¿¼×CÒ_²éD1²µÞ‘ΩìÇ—á!º'~à(ÑLÆ/-åÐuoŒÈœ8¤ =Øæ·ó à6ÙÀ¸æ¸!ï n`2ý§Æ÷§ÕýÞ´Äðš|ÃÚM†Aóš”;D¶Ÿýíºeèšz¹ÄêØV¶]“ÔµÈÓØÌrpÑ•7œ\‚…Crè€uø’Ãl©è!ù$♸“Õn®y qEº†á¿+ÊcuKÃ?[‡½›>…ÝØ£½­ Ýhcåö0ø0Ã?ubÖróÅg!ê¹LØÖ¡3¬rkC7¢õ†esz‰ô¤¤ÔW /„è¡I†3ÔIÐZ:[pªåŽ’Òn168í"ƒQYrHx?²‘øÌ û˜/@1²Ý`ªQXlæR;\{[!QeÂÄD·÷HŸæ BRB^) r”ž •Ç1ôÝÅ5k'r¶˜³g3ÐGw®Ïò£i#&0ç6ŽFAoÆyC:v\§M‚BÜfˆ Ô2èªó—èš3U®8 ž@$P&±oª†2Û HDn®HÞ>þBz¡E³êؼ ¼GºçFÞ„«ŽÍßÈl%ÞL2@šÐ-`ú+9»-Ýõ¥3:%1‡wJÿ¹®|Ü!?-ÇóƨYðspí :ùmÅV¡_{§÷he&ìÁ@k'^I)J­×ÕoŠÂϪ(˜?ØæohžëÌ^)g“UAçoÈÁ|$à!û¾a»xý!Ûpt$ ã#ɱìöÀ¤˜}‡ùdɘp(pí{£áö£w¥x·¤œ¢—òË)}ÉS¹´Þb†¡ýÐ_÷• œ"[¡+<–å}&Úp/ùÒ_:S+œ¯þª«‡p(U?g]Ö9Àî-X®&=‚vù‰áÍ7§SÙÜ¢½]D'5ålì'[‰·Ã_÷C ]èŒÑ dÿÑ“:WK¦³zM6½’¾+£›g6™®B»Õë惡‡ížÁÃîìJ+m§=øq½ÊiF ÆQW,rꇵr‘‹°d¾"#³KZ0P¥¨û;7}¿Uô HvÆ­úÞˆ/ÝFÁ©‹‚Ç _˜…n™´/ÍwåZ©=¡Ø½i›xûãÚ¢ÑÔ×›Ú!K­<µC9µÅÑÐÚÒyû`›$—CN_âåɼ…bÙ¢Zêò ¾ä&žn¼¸‚$\7Ž!2^”JËf“.ÊÌN &òËyjH€• Ë[æeyãÚ1- â—w‹R”™£ÃÑמf» mgÚÈŠâ™FÄÀu½V„ åû­´9‘ØOÒö»}„§Í:·°38Á²W¸5çeý‹«0ò‚o)I`OÆYU¥e®ôãqô¹h]ŒàõÌAÌ¡Ü{J±Ï¡ä¤ØCu}ý¥¸„ËÄ~ٽȀI•ÞÅ “±º~«KÅy¶À iKkÀâÉ^C¾_Ð^íPDn›j)^éág“Áí×ò´n‰gžlÞ©0í~èãg‚Ó¸0ΪvM*Ž@0OeQUߥÓ|‚ã_ë‰b u¶?˜J·z«06êö'iNŒÙ¦ºS(Jã`n¢£NGØ`ÕÄÇ •MÛÜ`Ͷ¥é¥2 Ñ›@Ehžæe󀫻Z´]±Øn,jÞ˜ý,lDfˆÍO•'I­|®$ÖZÝøRì^8ÆìšZ~³õü*…ÝRéº]9VM©šñ5ƒ èÇ>~¢‘¦'LÎȆú6 Í&¡?=BOˆE:tZñRÈéB»-…àGɹº1Z\ªÕÁ#Rò“©‘>W×Oé±F&©%fýA/t_Ñ#G\J”úl7íR\¤—j\ŠlÌâÑY‹t¸™]]'³“=…hsꢰˆ‚aJY\tN',E2XëŒÀq_‘Tom1ó+©Ÿ€¢§©˜Š]£b³]°9¡a¨¢þFîÓHhe2Ôã'Tí=ÚˆCòòÁwŒ+èâšèg¼J„/·/Ó¥‡ë`HÈoßçu;Ȧӵä6*¾ å÷µæ‡¤Ý;ÁÛ€4ŽQ$Šè]ÀØÀå®J–êfZ̪,›Þ8`‚* ]T2TÑÞ²\Ââ©bÊLH¼ž` Ålœ tLèí-k••ø ÚíT€bîÙ›] ¬ Þõnèc—Š½ÇŸpØ.B»kôCׄåMÍ­XñºÂ˜j›¨Å«›Ã­P0”ahh1RËIëRfiŽð‘Á~Ð62+6gñµÀxÖà_gdƒ~²ÌÕÁTm{+ˆF°ŠÿiÖ±²È„'Ñtá±Xrίq™W¹]ž×;:G’Àùþ{¸<ѳ¨Œï@öÖ.’ÿmIß´;·'ÿæ#ð‘ÁG9 ¡ñ>ÇÁ´æ‹Û ÓJähÜúx¬sD\ u%«3|Æxwc7¼ôêrS@á¸kë™ÖºTwlåàú~%¡GkÉ(wH»Œöæú?›üW WFÓH§…úÈÀkÍÂŒ&DJµÎ¨¢×õ1‡ë%Ü’a¡\h}ý*ŸC¢CHàÃP7F‚¶|muÄ0pô0²ÔA¿Þi™¨N‡çMOÎéwj _Šý?Kc 5®Û…öÞ5?W‚y¿þ¬ä íw(àp.ž0Xr>l:]ØëV<&]ªi\̨C²8¼˜*>ËuÀp3E­`‘U=Å‚C1~“Wƒ`'ýdB5ÔU’—ªÉé4=)J~pš©FàŽùþûW¯À*YSrUÙfÏg¹ r6võ׊å¦oôGÁ°ÌOePHn¦¤ñ „¬T@Úbö~V\ÎøºU­€ílž€„LQs(AÖ¦“@¹Q7Ê9Ôçʃ¤ûj–LÊôú:QÂeeÂÞa‘gr‡è$—6¨I•TÍ芷'ŠÀÇ@`ùl\Ã*¥±ôR?]ÌiK¢6ÌÌ0דì l> ‡¹*¯)ƒKëGp~µö?,Tà"vÛƒíe®ÖN ù.Ù +I¾'•?§S¥ùsVœª¶—É[uPªL?x“Ö*'¸FUNÝSj%à ¯™t|ÞOÞÒ×¢'È(‡TqQ(“6á’—Ô¨[øèQÀû#~»‡†ôµK·Êÿs’tQ0=Iž%ÛC|èÐï—8€y ¶’sÅhÊ“bš¹2›äçóž^Ga—Kq6'pHxvÓ|–¥¥ªx¦f^á빡ãbÆ´kÒDåãÕLÅùí XÓ,½d PTKˆy¥µXœAÎrGjMõˆhÅŒ›‚Z1Ð=èÄÌÑ(˜Ÿka=þà²uˆ¹¿Kâ›É…’gêpÂ05F»Ú8ÅÑ•vùoo•”2Ø>¡1:±äQÌÁ?Im´X‚¬?Õ4¯h„ôIV_‚¶[2÷)·(gÝ>%\s ¬ƒÏŠ MÔL²yJ¾ü˜|Tí?iPûôÁ&'DARòØuÿg©d¶rëŸÉsêg+©~(ën·TÛ0ìýŸ]¥½•†ê2?,ÿÏn/éÕçæ“ž!ª<.l®|Ô©”Zƒby –Ù§ $ ÈU H7šŒKÌͬgr¤øùmàiЍ^¶¬vg8øô)Çí †xäb G` #œ6:lkY#l`= £’àÏYÇ7"kIl-,0pÌ»A<¼Èªˆ"©!ÒÍýµ¸`M làXa· $ýoˆz”³&â5ªAÀ¼uãÄÆ[ê<"v̰—ê_ïJ©&ûÔ›¡S 9Èç¿jð%Y2n EsWµÇÍìk‚c»+ŒæÑ4^ð!<ìæ½ˆMG(¬dlÔf'\;WÊä÷RŒ÷==;ã—ª ¶:±ÎŠkÃ¥¼3â§Èþ}Q¦§ÕûeòoÉp¦œ%»Šô_N8ûÛ|¸#Ü^z»—…ð§´0ÀÏoV†ß¬ ?ƒ•A›Á§k)½Úô¸B¥J2Ÿk*iÍ)éRcÀ¥z ëôN%ׂ;VËäûv$ÈþíiÐsm-Ø Ê—Ý¡EäŠE¸_¹V1«Løä޽ÊGQø‰'OÀ¢s°ftå0Í:½pg¡f‡ËÕ¥fÍÅðމm]ú¡È' à•M²í“å6Þ'˜KÜ™*EÙà[œÖеÕÎvÍèv£ÙªFcÇN8 þ†ÖG Xý³RK§ú 7ØO³ñËÚ?š ^³¡äR¦³J ­ k˼gí2§Ì¼„8g/ꣿ:gâmÏÏm>ѽúÕ™pB‘ö&×6ïÔ¨7”кPûW’ $¨ /*9GÍâÿY@êîšR¶¿V--иUõ“Åtì&ó¹úïð³íÝ'Ÿõ“áþðÔNEi•ì¬FÿEQ¼O6Þγ t|5Ãשj‡Ôæ÷·ê%ÌB¹«ä¬ótž|2x "÷“¤ –„ñbó‰0-ŒCî>é)Mळ¼²ª­a$MÃ;ó¼®ç{_^^ÎÊŠJ fYýø&¶Áy}1 ùÉŸÑgœƒÑé¯{•ƒkB¯¯µ`šW¹â=JHËlê$?ݼløžÂۼ̒KºPjÆc|眓R «%ǹ;l.ùc)€7®–£qñA•½˜kº¹¡ðe¬H‹‹.¤)˜6ýaŠ/ãÅtÊÿ(*èL˜KLøc>§û<o‹x¨Šçt¯ÔªÑàÀÒ]ª?©sw%iN^…­­]ÿžAÖ±¯;xÌõÔÓ/`Zcb‹ã³—}=ÿm§Ù-îW\O|¯­æ„ÃAA]TJOVòUtÉMü‡Êä!┽µú"‡iIPÖ—G‡åq\ÏG Q#ÁndJÃ𬮿œÔ¼DCVÖ(8 åQ§B­•ª¯> / MxþÁŸ JU~÷¤ø«2#O…3¥MA H©`”Š—£ðšSüï½ëKúÖH"ú êÐmœ>VbCUãèÒ€¶Þªë~ô­=ø«K`‹™_• u¶/ÌcüíØYUOt–a|¥¢b4«•õ©V_7Ó .üô¤êzÃØvþêAªcýÒZOÂ}¹í²" ›1 *†ÿ$&Ai’–ˆC?³ŸÎœGCñÅHØd`k:Ø kŒ5`YÕßÈ`í%æcG"ââêsG>3:äí2¢tpnBúã£i'›Áû==áγ²9µúÄF˜‰’Ö­¹LÞgÙ\¿¥úÊÁTx²cV q ?ðúø/f;-²MÖ+íô!/FKêö¬l¤Ô¶Àù  —žöAè)aà-ù,$_ƒËŠˆ H0¾’üÐ*:#}Wø;èÔfaרnñ v¨pŽq~j*Åü'õWÖî´¶ƒÖ݈0™f£InÓwˆ‘)^‘Ë5ŒØ¬¹3¶Jz¡_˜µuÆ·”A4¯ƒ1¶yWÛˆq÷9ÏçÒõöå]ý}D,Õ·ûý4øûøLyqv5ïú×û#ïV7¢U¹®ÇlÄôCeö ®ùƒ ”¤]Ò%[ìüïL„rÇN¹åÃÇšoœsSÄ9 žæä<]†ù³oÿº¸þã"¼-Þái1lZÚò‚O8sqÛ8+BÔ@ë²_˜¦7]ü.úR8Hè;>ßç‰ËÙA+;<§¸ý¼¤«½©¬!!ڨΪm[Æ=z2_KOÑ’’;ÑÐy‚}I²ÆËÒ§.ßþD8>ÏÆï};ÒJ…÷jo¬ÄîôExëa ÎšV Ý!È‘ñë0·P·âéjº_ ùâíâbo£oº°Œ˜Mœ¦ïßáö“ )À6õ€q~gYY™uŒQTÒðʼî Å âÔMÇÏÈUøˆ|›äÈ㨤UAG«Ü,ÌŒÎwy§ã»¿S;ÍüêÞª›žlã/ÖÅ=…LŸß‡ß,jH)·òYø…0¨¨…¥°Oo.æÈE Q¬‰:£„95Aô±e'íq1Ï1†4ɮЙýÌ´­„ºÐ‘ÎA`{¤áĶô¨0 ˜Ò׈ªò!ŸPpB²˜O@P#S‹Ê#HÀyC6À£ e¦ád#%5UûïÊŪd|ýó—ËñIå¸ÝŽNM[µ@ÌЧk (oƒ ±,3ݤ„iÔÝ•œ‘`ÌD”ÆšžºXeu‡i†¶g¢ , ° óª§Ä×Y¶‡Æ“ EãšàâfµÇ^¥ð‡ÚB~–Y l«¦JEViòBž-ƒ¦Íì “«xAGÈ™V•dô KÑÔIQL£TQ—KO€¼˜êºQ ÓÜ.»Biý~‡AQþň÷}“|=E4%Ö ûbSŸÓ]«>*còo[ñð>›ÖïEyöØõPǯžÏ4Íí«-¿a®}<~]Æú¨Ðîã˜xùšKÈ—kjŸöi"0ÌWNù>ƒâÿ§Ÿ¸–ئ>B±ÝnWƒ1 †âƾ¥QЕ’ Ì.‚Ey‹ö£*x6÷ÅX×€¯ ¿õn®¥ù­jÙ>î rœ4Žû¢V?¹*|É5Âx|_xð»[[]gI×Ùgâ\¯EøÖÎŒJz5«c6ÿ×¾@ñI®\c pê÷“Ý6-…ë’ɳ³ÏN;B²£ëÖY1ºTñy¤íþü–.jÿÎרttiêÄä°¡dqÊ‘Gg/2õ¹F´$0yÍÖÌmž+vT°+@æP£l©% ­æÞ.¦„Ï@ìí Ã™H rð_€I‘, Ó†V4™gÄaͽˆ.t£åF]¢™êQÇÑÜ •k2ö)‚ØÓq)r[Ò©¥&KøþLàìQ“Xéà.ܸ%hýSÇšŸ9y¡‰ ‰—>êLгŽín—Æó½Nù Xæâ¢Iºt>fbC,­bRð ׋u’©ÂŒÏup9ð7ìbV1„âÚDö5¥SìocôÐF¬¢fÀª@ëâè#ϧaŽ ïÔ·ê ÍØ@ÐQŠ>æÁXXÖA“K1ÂAÍ*˜e~ˆ£ýÕþךPÅ+§;·jàtÜÅ D­“Œrލú“Å8c«Û'wJ}¬Z'ÀK5“z4R)}ç(-‹dVtÚÉvªZ1E£¯®´wï9EQú…fÀ—°ìó³âŠÀ}° [qëƒÄ™gBîrÐD/,EÂ>¨íá²Úzʉ©‰Ë¢cÆDãî ïD°Cæè«?Ü síš±õq–½õæË£k €)i‹A+ø¢¦tçh­¦Lˆc –^.U‘¨ûCV..Ig„>3}X`>µ `@ µ"9ùê\Q†L½ôî}™º1) Ô;èi)noo…M²k­- I½pTÄ‚6ôòÛµÕyòS·w¤à£_ÛNòYKyÝÒ1‘°Ö–y(% Üh Á©¬ã"²«/ýãfðÕ¬Îò íÈ3N+ú¢ÖæËæ½Í·ÞÍ9›ÄÚh¿š(¬#@¸Fþj¼ŸÛo&y+Ù%nN£"(?'o’ww]²æ¦¤mÝug4wç alöCBʘ»rcÖ©éÈ'k!ªl3çÄÜèÁƒ!ËBx¸ƒ~ñÂm.ȵBÝĶrbÁ]]Ši ßcRËÀæz*a‚Ÿ˜P¨|Òw¼2ö8µ)@‰z¶N+h™+0DMÜcÛœy,ºÒzߊR O]¹}VñÕ{7©ëpÕƒnÔqmõƶmã"3 ÜÕ¼ äoœ˜ÖáÚòy+ïÍMLÉM öQ'“‰Æ&Lðö€^5ýœO }³•–gJRÜÚz ¿5,Æ×Ó¢ ÆÖsb¹Ä´õ) ‹ pg§AMŸ“b,Åh×¶õºß€ÄWªnߤ5Ô™æ'ħí²ÉÀs§Àõž?=)ZŠši«ðüpSê¾+.üjÍée:÷….ŒŽÍ·Hå]#fhI—óƒ ;ö¼æe mǽéÉt¾œ¦ÖƺêÉ4´Ý!DŒS¸ßð6᛽Ý*%%¿OϲAÇiÑùé¼µ¯«¶æ *å.|Uâ›;ÂKÐÑ~²Ô ¹KäÜO:¶@§OY‚ìŠån¨l t8 ”¸ÆÄ‹+‘A¼ üNUï€} ŽÕào;ýæ•ÂF4ë®»Ç*ŸÉ9Ág4?8Cì3úÈ‚c9f[K]+“PdŽÊÚïÛË|RŸo@c4L—.è³#Q-êNóÕZgäK½Ý½P´FK8Õ.KR_Ùš#TgŸ²ƒæà dàܽq`iêµþ0ÔšeJìŸ:Ó™","Z¨]êŽ?êyƒÖ޵~bZòqÛˆ×æb>s3™ƒkóŒRõœ&[Épg‡®~§:&0}HÑk™½6×ÿ1F ^(ÁÌÑ)‡¼g€—vÞF‰=èT˜&ÔUÚ·sëy…ÎÊ2W.²>AyîoTùTé¶~Á+¥¿¯ºG‡¾Úœºt¬ùzúIYÐÁÙÿÃŽóÚL¬>2Eü¢‹ÿ †€‹ÔÝxK­C¤=îÒ?þ—Õyqó ¬ÓŒ»ÕÿŽ+}¬{þ· ØžøuÜTÿ=n‡~L»£ lIûè ‹§UÐq¯8)èåÕQÿÛòZ²“ôÏxÔOìá'Õ¸‚"[è»í»w[Û¦ÛÂÆºÞˆž’»þ·¿o{ÿ­uO­¼ŸUÎÞ‘  ½ªXˆÓ~¸JRÿÌ~0ö1Ìãá†OxÐX¢³ëöö´ödóEÂ4bÈsQØšûià" Š7èÕ7ÕàwCá©TGBŒ‡e/ :abfYãðàa°„é½ç(ð?Ïràª3ÍA^U‹ø.\Ë5óÙz‹)'[ÿ\ÔbgBmÀï?ã9ûYŸÆñŒudï?Ëjpï¶s1‚uâÖóúwhű¥D‚šƒ âyäÂÆèéáDSiÇV1¡þÌsnµµÍY¿¾¹†±¶q߽ÙEüBŒûÏzŽ!MN„Ñ5Äwªkv±91jÀ¤’sŸLèÖº£OšÂ°§F¯Ê¨[ˆ3b/oƒu A™5æ‚møOÅ™­Ôâ)Ò¶ª&‚}]_lÁ÷qr–¶úЏÕÝ„ÑÁslà•öUE´n)8Fͨ€á¢ÎÊÓtœu>Úk^ã†G¦¼þ+\„7˜¹¬ÿ®uy)šFè ÅÆô錪wxòŠmÈOñ"‡Þð]ëV¢B£}ëEÓl´oÎG}»ÇŒîö0ÖJ¶-Ock=SE× ³fÅeßx| ·:Z¨œ<˜G9rˆ?çL.O‰ÔŠ •©¸D¼v”¨ÄP–Œ¸ÈU-ȸ²ËNշ˚ݘºÙàlÐG|›EUå)äé‘¥{}5ïÚ$Ü8I1qB^'ØõéÒI.äµ0h¾»±åˆ\ÍkL 1ĦˆÄ0§Š/ð4ÊÄ¥ªÓ¼¯åi8à‘hR-rIª Z¬7åÂ* ®j¨V$»v®LÁ¸´¹'¶‘[|qoãÆ¤noòªXá‹8º ÇBÈGî y(+žü˜yÙyŸ1](:˜äÍ9Dp±í€‚T³ô"Sš%¦»±Í\¿yýÒVS=¸ö’ªßôôí¯ €ùL#ä€Áý˜í,Jéç™—ÍYž¡°•há4hñ}ÄîÔ1â É÷s“Êæ9Õ½¡HˆÍè}ç‡\„¥nG`: 8]×—u&Úæ,ȲÌ$³5€4â­ö£Ñà  Ù™j÷¥ÜQô‡;„jê¹ÎÍC×9Ã^ȯ®ŸˆÆúª×{¬?ÔÎQGÉ2cg×¥:‡‡•UÐåJJßÚߟ´Ë}ýDRÇx7#†Ï-¹Ñ`E׿¿µ˜Ó}¾ujß„4‹tÍ¢”uÖl¡µJÒˆ¥ÿ‰a§2ü:ÊíÑK¿CòòX>ß21¹úþÚ¾¬èoÈ È2˜éhN§r+"?ÑŒ,IxG%Ä•oÖ`²èåXWÑX-ÿÜ ·{÷Ÿë«wÒswy¹Ç]¢õ9W‰p·¾ƒ‘êÖeâÆêï³%ª¦¢¿E²Ã½¤«Dúž¿n‘—Wu™ 6{Š;éoØëEEAúoVË%*ï‚Q Uuà™”A’Åx?­M5 °\cº…ç­j…í>g€‹BUcL#;k5­/ñŠ3¥hÍ‚½X½v¡ç«_˜ãÙäG^ ã,M¿xßÊÍà>ñgCIž•(ÌŸ<ð‡qÒµ,äÿ$ÍÀÞk ÚŸ]W;˜÷=þòÿØOx ¸ °:>Ð@¤úÊ´Qµ t:[é,!Û¾­á¡Ù tqÝï]‚L—v€ ÝÀÇqú®Ÿm±Ij‰}A&G\ã¶ \QGw°.λ^ˆ-—*½Ó"c'T;ºÙqïZßUµç!Ý®¹ózÿ£äÜFÒVPƒØæ†òëX "-™wL&›ä\»IÒíø¥¹«.©n­©ŒgÚ£&´²S>M°¹,Á˜JøÎ¢‹WŽrŒäÇG•¡é­®Öƒ¦A2þÚúœ¢{îù8K|b\hGñ^¶Y…ˆ¸ùzu„€ êŠH¶Ÿ›¯Z˜;ã‡8Ü–Q¬aØ´G•Á0æ œµPÇ€Y‘Iºð jt·$ö[s`œº> ÷ ¥}1Að#LǘŸÁ=‚h7uh„ÈÀªVô$?;³yVóŠ#þØÎ`À*\a-F/¾ürôú¯~;zóâo£/_}õ• ‡fÛOv0⃘ÓÉ£ï[û ±#–Þa¥Ü0ð€°Bá›ý7ðo÷ªwÃZTÖŸ.{Jý0ì\ö‘úœ`€0‚dzª^e^C°ªÖx©6„ãsß½GæåMúËFáa¼¶gªÃF‘_ r‚_'kãˆÐþ h—¥Ô•®)îË„ÆÃP’g¸hÉ£$\÷OUEeõ%U¿Z¿ú~†Y^ö!}ŽÜ¸¢(–xd÷Ö ŠnÃ=õ °¤,ET‹ CZ“ePoüÄëܤŒ ¤`“WiEh†)²!-ñ÷ê vÂQDíI`öØ1—Å¢Lªe¥XèW€eáíãÀ_Ñá«Ã¯ðC×_E-fOÙÕDŽËT ÷Žù·{GÓÄÈ/•¼É¯¶!Åú=éFU›¯fMϥءÖZ1o1Fœ]ÏJzDz¼ݺÌy­\ [V=¿ gšmî`ØØ×Œ2L°EÃjÏÅŽ?d6»Ÿ£2À1à+s»]¨Aü¿Å‚n“YFŠœÛ“5˜™8“B£ã—Öìj>ÍÇy ¡Â˜¶ïY]5»µ4Æ"ª®v|1¦urÍq¾Õ çò"Œiˆ˜É€1Fˆ«{Õ_º‘®‘ÖÅyÑ}Ñ,W$ª¶a`­B”,tð‹ÔCºÄÆ-Ù:¯®Ó-øôãE'/k£ƒ‡Mb¥××bמéNžß0 ¨ÔNzRè»ÀhÄ9‰G+R±Õ (Œí¾ô fƒtZ^I1O l€À+H{¡('€só¹¶#Ÿ§æ³óÑ6j…[î+E«8*ñÖ™!PE°3ÏÕ4s’is j/NˆÜäÑ€›Í7á:‰DYuh“§½„ça§=am|)!Žü§nÃÇ„-Œ¦³Æ tûŒ-F]Êwßë̛͑{6-·f««³Å:MP€›j,?ôì©Tcgó³{hëVŽÌõ`LÕŽ &-‘5üÓA€`€½Î: ‘_eõHà·i_œ‘€C$ê•Smhß…ˆ3áܘӋ?jöÌÂõØ7~Z¼@ŒHeN¢7DjH–Õ«IO¸Áø0 BòÏÀ`¾B…OOÖ±½¬1sw•ù¯ó‡3µþ˜ê(»Ór(Ž6È}]gEMreYœÅ‡+ìpXdoÔw:ÚÁ‚òo ›á.·YçÛzµ¬ï-±ü üP5Ó‚‹h¦©‹iÛAt gm1%u âX!Ž ¹j‰–ºóžyñTí&ŠxgÖÎ4®á]Ñ_ †u¼Õ"ZÚò0$±¸Ì]LMЬQ¼º¸° ÔžjÑ(N®æ‡~ðg?é×ý ×XÉh¤~÷Ѩ…ó|Ôn@OûK6…@7óÔF™ktÞÞð …TáH<Œ*ÙO8üÇ*ÇêÓØ}¦þR;Ç—•*cãõ Ñžÿ¹{sQ]îI*öê ‡Rî§¿~™] €FBíý°Gd÷•Jì%âîlqaà½Õï::ÞÓcä{A“ó Thº0cô€ŠMè &xÔv*T=‡t=ØÜXµó‘qˆ«h°µÖ®>½E+©‘ôÉstWÕï¡w’±Q05 ?Ñïebâ•WW ~–˜w ÛÀ?ù¬îûIwøÈN¬÷x·—žkF¢·=g{Á@4÷v×ÛÐo¨2°_%ûßÊ‹•âz-]é;^÷¸ÞâF–ì‘LIÅ—¶¸·\NQwÑÀ°)—M8B"×&급‰ìê‹ý§;;‰g"OhZ"Cò7{³kÐmqlðôˬÍUŠŒH}FÑ'w· |`§°¯TNæ«V•rNŸ8»/SÌKϱ[qÁ‰mB/Ó A2ôû¿7òÐ…_[‡fÆ)€\5«üªùÅ ?à_HjjC¶iÐH£¬²°c6 PŸº¶P˶ÜAHµKç8³\+¿ušE8íAì©Ø{Kîñ%¸Ýëeoèß~ƒE1x(Ú«G\E3bí¬© k‰Î ­9j„¤-p´àÑÙ ³}›3…¿l&ß ©N—–x«º+ÞV\MÀDWDk–É@ê [Üt¶âî]+>¹eEžã·¸€¥¤~˜o¦y¢rí`qd–9àgCú:Ÿ¨Õ›e^‰~²3x*9¨¨â¹…š´P·©´Ë•† •Ê¢ª¾Hrdw¦ò‘ß%-õqC3aª˜pš’Êܺd?ùLn½w©Ø±Þsű&Ñ>š»@Ñé´²yápÎä5i{p.=¨¥Ó#˜^÷h¦×Ÿ`Mg½c_q~$0òŒ±ñêÖàÈÕqÏPð×èßIޏ˜“”ÈÔ‹N÷ û3ÑbÙnÇæíQWýöÃIïsM3ŒÏùf±ž†ZjPj„»Ģǭ'ºïìw¶þ°…‘èì3ñˆ5´¯þúÍ›ïÞ¾ûP7¨u?×y¤ô™ßWIÇ©¹=„WºÎ?• €FGôc[{H †ÝÿÂZÿu4ÜûÃqlUö¶‡ÇÎP·×êÃ'õÑÃß>=UãÆâ¯ñ¡â—CRRL'T#S˜ù3P߇;ŽI%A§‹‹YÒeC4@öŸd˜ð«þÈ ©?·àwü‹™¦e°*¿ NÓ‹“Iš\õ—{J|ë'SðþåæT sWuUk=9ì o¿œïðzl\Èp©õȆÃW“/«"³ù4]²wR@ˆSŠó jÊ^´ìÀ 9å%›‹³óÖr¡íÿ8ægÿÕKž%Ÿíø[ÛÑå^‡Ð ¸xC±]§ØnS±'N±'bêä*ü™T:ÅS÷,zgAÈû¾Ö@ÜIë…Ý ½ŽÐwzÁîîTÉà ŒÜBh†8ÚÛÝ93èí=}zìLˆõ¬¡ŽÀLUXÈ(¤õÉqQÎdÍ..Ò˜-*„ÓÏg‹:›)Ñël€_t;é¢RŸlCÙA}Uw´X!'m¢oÏ`I¢—,Ðr‡Q±÷iýc{9¸çäæ=*.œ0y8ÜU¿ ?á_;@­ñ v£Ÿl謵ð»íþò{…ÏþC h{¼¨‹ÓÓÄê‡l¼9°SP@Ï<¬ Ž%ƒ! ?¡v³ŽV F‡&¢Ó vúj1é²Sõú°tòOX"û·:®9; Á€ŸEg4íѨ£-rŠ4>µ±ø·’±,yï˜~<“„v’£NÄ5¿eu^¢Ñ®X3.Vyµ[=¡[@~*âD¼åàC’‚O›È ¾óH¯í)T~*:Îd{¾àë5øFNCý馄œ%uë´÷-dßçJš}‘MUÕþ.^}7÷Ô¾>ð;§³è?Øù ÿ§v?}²óÉÎgŸ=Ýñ~vùßáð“Ý'Ãݧ;Cøóɧ¢Ìàq}1 l[C2–øçã*»ø_AOŸ¨>ýô¿'jzzòÙg;O?ãOõÔ¿ŸµöôX¿°æËÿµ™¦JPñkÅÔéY–¼+Šéû¼é‹tJà+ˆåž+‰-£6“µ<•–tz‰ês¸½«F–¾~÷ðn‰y‘6“‹úÒǽ¼LgÉL3Å[žeê÷?å³ÓA6¤ãÁâýsUðÛo^ï%Ïb©ãžs8ë4g3ÔÞ{Cóͽ~uðòðíËÁ»¿½{ðôZ4ì0ž¢v=­´ åG×þ `à¬ô4ém/å³ÐÉBUA+<èoR)¾&«¢è²ƒ8áétA¶î`j©ª¬ÞV2D¡ÖMÕÀh:xõGÍï2%vLõ]¶EWª÷3î#ó>¦¸ŸMÊÚû¤Ì¦ì1ö¿P ~ýûû¡d‘,)½ÆæiYe%]³üèküYVYÛ?¾€±ó1ã^òò3³ü°rìBM~§‹@/ª*?›í¹¬üòíŒà¼•ô±Zá]þÒ%.ÝÎayAåsz«ÕjŒpJP ÿÊ5u›OôŠs{“’6›Zô§S>òë•EQy)rS…h\lfdf Ð„÷¹ý7§û¾˜fùÐ>Ï1tñí@–ª,Š9±²”¾¤–a¤†:ÁON vgv†V™æóôVÌç®útç³§»íÌ)|çy¥Á#ÀPƒt¥˜€æÕ‡Ù ­Þòz°¨–ÅFƒtñˆüñX‘×øñeþ>§Žž#³Ò\èKÅ„þœ–jÏÔiz69?ãßÿt¦$‹é@é°ÏoÃÜ>ÏÂó8.€÷¨ÈglÆ»FŸÏüiQéߪŠ[稅b^#ÁèúEos:ŸºŽjk~šO³úò#YÔ bŒÅ|épäe/Ö‡Ù.Åç¾­Ô–‚¨¦#²Ù‡¼,ðpc¶ÌV{ðâð˃¿üõÍKº¬¨“ý Œm Oص¯Èظ¿‚‘•øª¨bá5rzu¡ºpm)¨í¸2¢TðxžÖçë‚ÉÈ jLÉ¿w>Aj3ŸÐ ‡%û§‡h\©¸7+^aTJ¤°¡íMD®—xÌ×Cí )œK‘H¿üæ-m~ȧµÂ‚_–ф׾IÝB&yuo~PlßyË3f„&˜zvÆ™Ž`#½w,‹·êkZÁlˆ,fF`¸/¿’ç³ù¢îSj—F•ì#Wß,Ö(Ÿ`F‘~¢æuRThÊ Yñr!0X‘_‰®d3¬'ŽKÃk*¾¦þ2#ªÌžúµÄ4tâAÅÒo’·DÛÀÊó1¾·œd:û†HP8ÍýöʱÀ¹€tJ“$ ð¬dç})Ét³ÍÝØqHg㇢XÅàÅI…‡"ºv´vØzï^íáâ;»êì§ÙÊÞÑα»@#~{‚Rª_+©X˜T„fù$1ùµ(hÔÇ[m×bqì›[npñ(æ]Õãi~µßAδ ƳÅ)~0ÈAСÒÄ dnÛÐEÀ«ôå7ßüõ›="§ YhV$ãñÙã]5ðò¢ ã©ÛžmO&:};CJƒ»©ð¥Žbëv¨Àc–î%tJÁõÙK6V`ÄA(!¦Ó˜”•ŽÂãJsëb¤¿éš15ŸýÀÙëHׯ¼«ù£a¯hVpÚ¿^OÔÉíuE¸5mÜà¬ñ-‚áC,Ä@ßmN4­‡)£îÏôpí‰5™;^­¬&ŽðV5†¢“3ž{özø…=«}Ñzt;øuÇòð¦^¤(°²7WnP'~Ì”ô ¿tõÿ(rÐkFy´·ýT]6Áà1µÒÖmÇ?êloó—ý¤±11зP/îõgÐð££èX*;n|òìvþ>ëPwLÁ}©û ùæ¹ÜÃ8±"ð;66²‚°¡Ê]HÓ²­»Q¦>[hEJ¨ʤ‹P¦ U”éSX±N|oµa µnÅ"¸áªi†¨î—…Ò½;R!î0VDIÅô6Ôe/”Mó‹ì‡ö2¸ëÒ$ JÞº¶¡\i¢ÃvÔøÍ ¢,7ƒ‡wÙ Õw&¢tÍ‘Ò1«ý£ŽÑ0;Çîdåt¿³¶MÁ[)êŸ'T'4¤ãGŠ()Çï,*¸gS^;peö¹ZËý£ã[žÓ[OÓ³ÀܽÔKµ•! Ruôj„zýÿÙ{÷þ6Ž#QôþÍO1K®w‘”_aL'%'ÚH––’“=Kóà!8€1IXæýì·^ýœ8çwXfº«»«««««ë¡\ŒV¨^Ý#4¾É­(R“㸘àƒjëg¸;&OÇHpÍØ‚à,g>E\Ñõ¨Âu¬Ð1ö+åYˆ½§ hüE›âј¦Røö;žîÆÈÐ_vxJ¯ËþFécQ·gzƒ Þqžq»çÐd8d„tïM5) ªu¹GöËožÓób>÷Ÿ·ùq/ÒbpU‘|˜üœ|¢¯Š#Tïâ¥ó«ôž_ã±ÿ :ÃPuÐwuƒ“å”N-~J‘ èìTEªH³“.þvŠã.Fmªšòó‡iÚÕÝB) R ºjWߪ|÷ªA»–}Ïí:Ëÿ–¼OçŠääøåËßýéçî‡é©††?Le>ã“è]uÍ›Û`¯áåm˜ex^`ιɩÁŒÙ næºV¯žËÉX¦CÖaÅ΃ ?÷ô¯›+ ZW¬:ü^Îçþʰ¤FFu} PÌ¡[ýƒ4d—Š9²û`郠”Aþs¹*µÐÿi·,î žk˜Øë;.h ù=§8IqÏvý5Ó1#BˆÊËFן}ÙdX™¾¹·ü½È (È›r¥µÜ^IT ô9ÆK*¥‰ÂKFçKÓ÷Yú*ëÑcãy„à²+„žO¬Ø$_Ϫú›þ×h&÷ÍÙYÿk}yV¿9ïM£d 8ü$3S÷mÕµ.ZqíèD°§áé›ã(N5 …æ ˆÖâ9«$,æeµœ/$õ”®Ñ™ vät‘Jüõ ;ÜÊuê/úOúæ¡É¢lÌ-|M?G9-ð˜ãåb!<âÍ1áˆceâ6סªò7rÛàü7»n°‰U¯7X‹uÀh õŠæ—[Ì1ýË-öã&«?¸Êi=˶S´É¤~øa[¥·;w•耋*¼z5¹¨ÆMAÏÒ •;ü§‹ÿœá?çðý0µÔÝW0çc•qQ½ïòÿvB¤sy’ ´§¾@„É z7ü;:+q¢Ð|´º)dÅ _‡¸ÓŠ˜Æ¢1³Ü#§0ù4Ce£X_{¹cUL¾ÂFbémŽ/è1]&”ªÛ²W‹ÓÕ Gæ)†ï°EsÓx·°Çlß›UK¢Ã:ލE:budT áð oWÕ'Å|T´èOò÷…èçúfŠ1 &®Ä)‰(æÅÁ×¾Ÿªœ‰Š“ÖŸ©f m)õñHÌIyò@*´03­Z,êÁd6µÂÒÏüP6Šfy’-a‡vô{ÁöÃä(œ‹Ž¥Gv¦»šÇ§ú¬u®¡‘×sK¬<‡ƒØ¡¿˜ÌÖƒ/~l….TWü˜)k€3›WíýHX΂Õ¬n¬®ëÊÙ£‚ë ¡¹æ° Ø:Hè‹zÛÆ"•ËVìÀäó†ˆåÖ"ÿæj#’€b>(Âí©iÚøvèmÿOO~÷Ù8ùV£n2ÕþZø¹SàèÑ=n[ñtÙë ¹P£k-½pª>´å?NWk2Ë| ¨ŽÛ'ti?²·%šêlW°¢ç°›ì{¸U3_T“ŒAґع7‹!”/,Ê`“Ô(zu4xºxuÈrž«@çéîévÖ9;üÇ¿ïuÿðèßþ_ŒÑ¸ä8Åšü'ýŸë JÓÜ*óSC™=«Ìïâeìõ9Øã Am“®šØaK+‰_ßîu“¿WW°ˆf˜ßóÿ®¤âJJ§ýOê¾Ü#¨õóàecm(4€/‘™þ˜©bªùˆ’?>œö?~ÎÁ¿ Iý ”0PBS˜¯(%ìÓ¿‚`± ‘$øuÿOû_=98€-ïýÎΓGÝþ¤ÿ 8WŸíïí}?÷>KÕóôøûxÇ´¸¡#ŽÑÍ[ãµå[Z&™e¡Š£ÖnŠ‘ì†ØÝ¹F79Žß×ö9Ýu¼{êú¨»Îat0XÎã«“caðî3AÎâÙ§µSœùËÆ°O4h˜[ÂCtB7›K]^¡çìܤÜî’n­?6Ñ«hdÕØ›þ³} æ`Ï,‚á?ŠÍ‡ ØáõúiFÕMawLýë w÷À£ûð\Ø›cgUäóõè¡Rÿã Ir‡"ðOo^‹o–¢ÁHa{ A¨Qã÷n*›~RMÉâsÍè¹ÿžóHUÕ˜èǯ6“Ã|µ~(XèAŠjðµÓ ŒûZØz|éÓXíû_Á"ßÿʬö Ⱥ¿B°¾˜ ×BÖT¸Àï¿ÚŒNŒ©idøöa4YN5E–Ó_­÷u1hé=¾}Pï¡¢ê=|ÝœI Ó(¹Ñ[Ü>ò9H‹_u“QñOÉÐ¥÷Árqב¼üíï—p‘ AÔõŸ°©c—û‰kÆñ™Ô-ê)x sðùï>ûYfd½^ïŸ+[æ*kßÀ¥kèbpÊbÁêS/‹k1ÁeÐQÆ7 ôµKªÒi§ŠÆa>›/T .ÔP:£Š -´ö{Œj‡‘`æTîÎè¡£ªj4ŽpÇGbË£aŽÜݳepkè+‘yÔè:ÜùaŽúš,l2* ªU)~chi,…XQkéé,É*Âqó.úå Že£ò°¯òž{5ဢ£UÇ.ËwÞ½~öc•¡!Æ(EûÉr¼(1P¥ô/™ù”31¢.¾[ÆhZaÀYAŠk%_'TB`! ~R&žÒ!¼É¥}²¶¹ZW¨»A—ül‚ )ˆþ¬6PVÒF`°/§Áæ™’êþÿãp\fj­yèš~¨™lm-©‡çûà·ß…Îmì6C1¯…¯žY|•Mj­{’1Î}•47¤„ ´wuTY»Ø»`Ñ»pñP×h™m@ÉN}“cÞq,ºµ¦Äyºñ˜ýD†Dõ<˜ yÿøìŒ&áüu—ç]Ûw>ô¦§áÒèÜ)ý°Mú>s‹Æ›.eÜs¶=ÇѨȪŠˆŸz?p(å7L$Ý":rÔú•0Í’~q»Ð¨öMxý £9"ס½1Û³e±¿o†f¼‰Z‰žüè.¨bÿ#I«6ñ¿žl 9+ö»ÐVäÔPüØxf¸7râ‚Ø=Öù½1÷«·£À¨Yr#‚Ѩàc`x6££ o˜|Z÷1Þøæœ'ÁS{òM²‡×ÀqÞäD&ˆr3¿÷Œâ/ÅÚ*§RhAS-#ÍÅ›„²ÓE¤Åø™) ~ltÞjGž3ßñ „Ã/ÃÃ5 –{èž>ü5x=l[ÿh&9ü5¶—!FüÇ}øì¼\YY¿I¸MÑßQÙÐaz¿ƒ ¬K"—à¨^Ptq0\ß‚4>é†Ïö»ÃƒN 8È LsðO'¸þŸ|??¹§GÙGôãìàðÜï¤nH⿈Gs°ýMZ›Añ­µ!v«¥>°ÉeÛKŠë–|2›W£äÝóÿ~—œ±#A}¾ÝÑÕzw•ŸgÛ»»â‹î»×ðïU1žm%·¼¥<Äð2§8¸GÛ5ÈEÃlëKôÀv™äñQ&t!Q¹ldû–•Ö œÛÙv9Tsʯm2…ho÷mEcÆ1ëôLH+ÊšµgEÎõœ8x[4¯C£svÖÍ.…J  Þ%Rîù‡Ucû‚–âsŒ¸B©Vám\ò‹µÁ%ŸìñåÁgO$¨$¾x²·ÿy{pIjº.ºä ”Úýj/ùËÓä´È‡\ÓðZ‘ožPÇw§ë›‹Ùný¾ªwašŠÝѲ?~Ü®O’#.VËù Þ5*zÓbqÿP¸¯®ËâfëH>[[jô7”¼k*X+,qÎqña¢½­à,Ëš\p¶ˆø`ís–2qåz?­nÆÅêS®u‰¼8¬P¾°j>nŠj4ÏgW«^òbAןš‘û‰ÌçãU’~µW£ûÑß0@ç”4hoŠyQÎóßã8·¾ÿ XÊãÞx„Ý}ŠQ;ÿçãÿüêÿÛê9è͆—œ9Ç…<˜—3΋< 8ჲ¶€ü3níq5– =\-&ã;¢†ge ££ [y»MªáÒŽ´¥²ëQœ&—R]“«ÅÑá‰ØšcIú\R¦ŽTüÉÆCª€ÅèAâ¤u LÊßÄŽr’d‹<¿@bä"…!ð+Nç&ˆ†§beV—[/?舞*hí]bvP4Ø–“B²—½-œ‡]ïòë¼SOi…h_·£mn½3NþFc{&c£“#%Ù‘(!ÀR€êÐI:Ç7¬mŽ‚ ÕÍȯev׿P¾œ;hVÔÏ ŠËGoÉ£L^yRqQÂãò²J LöÖÐý¸ÃC/”-ŒÉIAKd=Pùä*¿†QÀ\U N>ì©¥ —‚³Aûdƒº% ÷t/9! öB2nqÑ“zؔҵÂsà` Ü!åøV¹Pà0*@˜ æqJyÈùP抽xr1 !+Ä·j–¾Eäk¾á| Q´Ëä€Õ9d§Q¯Á(R&š‡eQ+oH}„Êïcé´­[)l8ôÌ|_¬ÒßÓ©—5åÑ2U¸s—èj‰y/q¼µ a‹³æ$/¦*w7ÞL"„)·³ùE1¦²¤¨1Æ1”ãå&¬–Üm*Õ׺gGŸín4B·N>hr¾c?Í`] ‡+/‹Ê®2œeHFó¢Ýý'_|Õé¥[[.Öx)¿OuXJˀغ)9wÌÏGäR™'“B‰!ˆ ¥çùú {·º“%í S0…]6ð|×Ùš×Mê–žTˆ\5£ÖjœuE…`H×T—ÒÄ_¥/øü:ÇôëDßÐè²îª—ùEu]l)ôs߯ʹ,góÿÝzõaVÍ–wG“ „%Û³Î+'Ï’KZX2ëQZÃPv1ËÇù´Ì…àÖð5—Qmìeùå°Õ`6…|ÜçŸD˜CÙ€Ø× N —2ˆÊrÂ.!&ÈΟ„‚qÁӬƇ §Jç„­³¶YƒPÑju"PöówÌÏ~Å0-<¼ôî1|—aÀš3,úcçãN¬‚ãºBµ!òcIÚI[<”øB]ì¦Luk“b‘^,Q[W§["fÁŠy³1C_K`šbœÕÏ“‹cÒ¤4¼ô¤,Œ«z9G­ž…í7õઘäø\ª%¬”™ÓWCÚn³TŃ“æ˜G¥w@¼D>ùÂ3å:›6qf*Ÿß1)WѾ¶¬­‚’: rR¡”—©°êÔ^Hç¥zVQ{ÙÔˆøeóEZêÈ‚4…ݦq©1êÎyqPq»Œ#»¼`í\$% _ r,ÂÈF»çÚÁ²®ð5ò@¦Å€·N˜¾ÍîêU1ä³g pò½ JZ—( $Ñ Ô;ÎÞü˜Ú X_rœ˜·*Q<çîÕGέ|Á™° æ’pœ]”èÄœF‡t_"º'ÑŒîNÅ;@ ŠE^—<˜àå¤7Gƒ1ÐÖ gö’„¼Ç®$ZµyËÚ*†‡ µ!¬µ„õLÒ2„‰¼7Ò¬^€¨§€0`ao’lÈà-Øæ6d8¡¬4£zúÆ=Gf±¥(Bäjþ…#€Ã® {}®Ðéïm>à"Ý„òŒ¨bxr*)ò>ÉÚ6‰DCà]sêPrÒy9îL6ø7(×d¦aK†, èÏËp¤è—“S ÿWùå¥XÜ íi¾e…07&̥șju¡¥H‚ ÇqŠÖG,çn‘œ R·p¢ ÄÚ.Æ/Z2†€LÑV•¸8¥P]b¦<êcq xù´˜ÔI\ìÕÃ=ÆÜ‰ 8ѶlÎìÂþ/áÉbÆtÞÑ|K„ƒR÷„ïÐ`ßÂèäÓÂäûË•{ªf9:V™pÌ\q—ƒgàÀD’!PȸQ5džÌE‚B€%­ü4®`¸ã! µP¤§ N?(á@ÑLÊÂ&Â_:w´À5oã0ϧ#`ûW‰ÌPKV¢|ðŽ®º¢.Ël9= äF^E"ÏÀv1ûî XàQÖdÅäèëÞô²7 õ½óÍy²ûM’þ „W•LŸî`ùÜÌ)甈Úï®Ð·ŠBƒö—£;uÖ`£$' §ƒPùÑ Úã ÓFR~¬j'qÇÖÖÎÇû JË;Ö ìdÅÐNíÂÇìÍ–›$å@‡úJòç^B/[ØI5K÷°ڂϼIšÙ3¼¢¶°c$i”v´ôâ÷L‹¾^°X·sf‹ðƒÄÆ»§v¿°#i uP ‡Nç\ù°I:|lØ,JÕªdã$6ϜՃ%ìÔb9¤èÇãj:’ÑëDôQVâšÏÊEN ¥Ä‚pÕF¶¢‰ñ>Œ 8öÇtKìO€õ¶±{-8ˆMP/’ñ\Vûö_뛼ÎúÚz߆݇vßÀ|pÿ+ŒÆ…ü5¶,éßáÖκ§ ¥ûät¦œöãýÙì¸æ²…HŸš;’sö‡žÚiì‡Ý ®å¼ml·.ò ÕWßV{‹v{XÞzÓØì¥$ÀaŒv½OÆLßC¦æ,#…ºhwØ ‘Gv®{!ù‰+ X–ÑCÊl _úzkÜ8Ú”|ëºx¡¾ª78–CRè²<ªCÑ‚ëçαŠõ;,ƒƒu Á«7Œî}W ïh±_ä)ãæ0¡¿ò ÑtòTN?i¹PÎj5=rA¡ U8l÷Y9dM-Nz×ÙüΓ+Éźœœâ¹ºŽÕk>Œw“qUÕŘc‰ãaeÒ|òö/¯ß&™sç´æ’ñ®ÓsÚ‰„hÔÇr±Ü†?ã=àxñjaÑ} <16ë4»’`Vò¡RÌ‚lŽ'2£üfU ÜÔF« »² ]Çy‡ž2XúªNïqXR“Bëï‹…LÔƒÚ"õ@F‚k1ìp7k–)j§ Â×aì¸nt¢âu#–d¤Éâ…/Z‹G·pòЂ'—ë‚~9ð®ãuÔôƒ€†Õ8tû‡v7kÅ©UH!JlÆ£æ‘]uø»gÅ3gY“gº¯¶ñµúîQã„ê«.°ó¾(fIßÒ¨Kò‡ñ*ÉE¹îB³Ê™1€³åÅs·P…„Ç7©O(΢kx gGÏPɆJ¶Ïúm„þaB¼§O–£ÈOµ^;‘-ÂmÐ3 Úqý•¤v—7™ˆÃÒŽ‚-c¨í䓺CŽ òܵ ³»úèH÷íQ‚,¶a@æÇ¹'ûvJÛŠzÒOêô‡é± ÆnÁdŸ„€2—»Åu½©qfƒ³†S¡º¤x9v©³­¼ yk@ùr„PA«a?j`™ÇÃ!+ªi;]À‰V·¥µÅ3€hÃHªXïµ±ÅM¢³Ó:ö.”k\ R»T—JnJϱ8¯îæåmf­¦2áÒ÷ê58•šµç[È‘ù‡`´)Üâ‰9x×g*¿e>ü{>À“Fq;®ß3 VP×IÍÄýŽÜHþ¸ ³Ûn²êÐ* |oññ(\[£³ÛseTºÚÄ+ ì² ŒùF’hO@V¨lGH"¥&_]êDo[ &T1gmÉm´ÅQ„8‰›n¼ûijpH®ckÛMqlv¯ 8†DµaÆ €±é·©JGš¶`®T\³x'íOÃTFf‡ M‘×ÎŽp¹^ä€!1౩<>!¼szDÈMu6™&,A·(#Ϧ”‘CUìD–ö"£¡áö–óQeH¸ CX/7ëæFfzî©¢©2ç4Å”tj hKRÄ$`_QׇÉrŠ*ë\4ã"®âëÅά²#s–r]F{þêHdQ6Š!öŒò’c‚[r’çû’…š‹¥¹M­£ØYo=·èÙuG§?¼ü­e9%“óÕÄ¥¥]3L´Ÿ9R-y»H‰ø‘YÔõ,Æ ðܲôÎ áH‹œxbÛ¸Š¼FU†¯ê=÷[û³µÅK._ÖÅHüuf²û)ÅKWî´s¡w˜5›/]f‹6¯¶a¤+¸UT⃔:ÓŸ×Uä|}qÆÕHÎñÚÕh#-½‚-(¦d…1Á§'¹é_ÖêÂQ]óø& lU_9Û [‘Š5™™ŽœÔüC ÓˆazáHEú³«©ùUäaØÆú—vš¶zÉ÷Ã$Dx šª`íóµãZØ8&yT`ßpÆT¢æe:VEÏà‹ÊI)]}ÿPN;ÚVHÕ#LüËèË <®ÈÏÈúè(ÈxÉéÛ"¹F'"ä ¡¨›J©^CÑ(–޽_ªu[)ÛÎUÊ”’lj„7 Ü„Ðë c¼*H†ƒª Wž^÷åi6#Ã:œÏ®Â“pҾ…¾[VÓ]ÍÏW5WFtS™<ËíȆgR²JQ‘NcoÅÁVu—Jö¥z^¹Ý—YWn©ljÃ<’¦é€ì£ rxIF‰‡M`NÖÅûòKm¸À§Åd€,¶¼dîWÄ–7)ɰ*êiºPfó‹¶ ISÈx¬•Á"‰÷:e‡ÕÈ ¼gÇ{[vÞ13bocƺ³æð·7¼0yQ)kžBeÐKþ%x?O²Õ+”¾ ™žDY½¤fa¯4uGÿã¸\O,ñycöc­‚)‹‘Ã{ò4ãî[¥)8š©Óƒ žøAº`’¼ð\Ë9f*À12KÓôäôùñ»çÉ»ã§/ŸÓBŸˆÁ©’ ytòùf—½Ñ÷¨Hñ€g…ó"@*í¶í9é°"èié)çáuõIÍj’:ÉþÐ…ÿ‘zË´ÖM<WSšFü°\·Íà‘@²4t˜lû°#óEy"Ë…‹û–ÌÛ'T›[ΆdñW‰:Œ§Ùžå˯ ›…“DtÏÑ•, Ä›|>uÔBêA¿dÛïªd¾œ²¥±²ÞêŠu­8W%³“_=Ñ…ÂÏþËb¾Ê¯¤_[|γ…à·ÿõ’‹$Õ5¨´'ÀÇ]“ˆš:´Z]Çî³–%­8õè‚ Ó‡ ²uù" SvGmTÇoO^¼0Ƭo´®K{ZT†k®Û]ˆ©&ÐQsRþT ¿Ÿ–NnÑõ,Bm—ÖJãyýg›î&ÅaÒkæŸõ¡+‹#NŠ bÊv¡"ùEï¼[Û2¤¾{K”iÉÌq:Ø3JqC|ÌëXu´£å#JkI6¶‹«e®u½„ýT™f€ê#‰1çPlyïÍÓ(ïü‡/;mÅÓ:?ôþ}]iÉ‘¹ï*ýòZeLõó"²”Ç:¶L†¯yF]at,Z~GugöΣnÚÄÈÜâ„`äLz¬º_Z–çZzçk˜‰‘'ºÊ´%½géåo^Èn(Ÿ—uzNÖÏÅœ,„å%9Ù q³Ió&½"0Ð3÷„<¾ß-+s²±¾]…fœs- ïМè´í0 ;<ß9N76%0êÒUK_¼7[¸“G•BNŠ#úªN§r€µU—r¼ï`M ÷ äwâÒ Õ>Ú/¹õ¢S31=uÕïÎpôçɘ^½ãˆ4Þz‰Ò¯wžj `dWù´2^k¿]’îŠ[€¹KðŽÒ8†¡ý<›Öõ«KË„ý‰¹#º)×+<‹ûú}(e­±N>KÉŽï‚N(¦÷„Wôý‚ò×o‡†ž¦wª·0QwæÝ>{—E·Äªô扼åÈ©“!#48,l]5f E³'X¦zß¹“ãZk ³#:eX«ö~¼"•~eEÕ¯À¬tj* åzèÕZàUö?€ßlÂïDÑÃóŒa:Ä`™ˆÎ}£l… s“wò÷Q’"Á¶³ÑKAlxÛ/ã¬kUÜñ¬B>ÛQú<º²H̹úÉ]3‡VË©k>2pNJ†…¢>[´£ðä3é? ¯1“Þ` ß%§'òÛ±”ƒCûñ‘y“ÒBuUòSßw“#Ãl¹Ô‡vJÜÂЋ >%vRA׈3G–&qH{¨’Ö]›vmÀe³\¹3½Œ™XÆ¥¨£‰©ã3c,jÄOËXˆŠh«Q+Ä(kò¥€ò×3Zu¸”Æ%Ñè*¤ÏåÌ’î«b„a|ˆê‰ò²>½ÐŠ&Û˜QföЅܱà³T^Ѓ^Ë­Ÿ==gÔÀ¹^«®!HSi{‰©OÜôÁ«™¨Ì]Š–Ö'Ý¿i—’ÌÐ"F:P YrVÔ®’U øÄöt¯ ç3Wl¼Gÿž@© Éã•ñ=:‰zÆaiµ­JÍÈè-ï•úeãíZþ9:9bçQµJƒû°’ʵs'Â^H™Ý?‡äC•¦ùÎ[¹Âæä%èÕVX½ÎÇÖX/f›º­åŒ¥õòÀ¶á²¨²££Étò@ޱzG7+*v×®»l·`ôóìƒeb ïz¬l˰›¶.o;Ør”g\Çj2êÓû¡Š”¿f mé%b=”@¿DúÊm’6%¼ŒˆŸßLBÈ{ \ÉÜ¯Êø¤tFŸO: c6Ö`V¦ýìz>k!Ÿ¼ååey›¤x5¡ãYäËE…‘l.J„X {VǰQëøÙ²b‚‚±HZ§Õ£5l‹nGX0¸èsŽFöñcýÏðS¥2ç"2L/-Ž”²Ù0ÿ£( }c³€†^V¤‹ÑÐZŠ&Š*ºS…Ã]?Oõ•dµW‘qŒ×¬%Z´M+]1 ÙÅ£m€¹­ƒ®¡D\÷!3_­¥†'§ýױ㡮0dq¹È¶Oò©¥ƒã:tTð³nu]@>gtq&ÑÚøƦ¬—YöŠ_ôȸ< M³·lmíÑêxv•g&3ÂŽÂ×Î÷‹rL·7Žq^ÿ@‡xƒKu. 4:ÕNÄ2ñ;ê·˜¢Uû°¤K ULÊð ‹Xµ£öÜq¼¾8ºöŠÚ=6Od\´»s Ñ@#ª O½³Ã:Žå£Mͽ¾z£ØDȱÎÒ?XÆÌB-‚ÿ_y×ÒÚŽ˜EûŒ)j•6丮ËÑ4êè­3ÖNOs÷t™uH1<ˆcLQù3×NWå§]†¿ètCâÄûáðõ>YÏQ-.ÿ¨fxnu#‡¡€`¯ ’ZjŒÇxW‘ÜÑòý8–W6üÈ´Ú³Þ» IÊ’’Œ²ô^hÑ5ûD™g,å”Y#ÿ[ê\Çuâ½QYÖeÀ9 ŽJ*{nïÜ—[Ìi»¸U{|͸¸!lâÛžøv—³¥7i§‡;]ºõ:êhLœ‰½ÆÚõ¼n!ø’­Ã²×3œB*Y;Ê‚¥>a™WyÛŸ|û'q}9×3ˆQ'Ç 8Lt¤ÞŠ}ol[ë¥øøÏÛ‘øè÷\홌ÉVßD— ‡&†÷Ú`Ú`V8œÈÆ…CÿÚ£HaiAæ+ 4‰×ßðö#8¦íèˆ<#®ÀÊÓÜçË)\¤::f²{›ö>B/PnµF«~+ÓìX3Šd#oŒ·ðI­‚-›È¶{Ä`-¶à^/%Ô…’]˜º„²SbÄý¬'jg DJÐf*ºMmô3%HïïU ò@°öY¤¼#3˜O’ $»™{íkaË;ëÞ}ô` hÔ3¿Ì&ô Ešpä ¾6ÀÉ% e Ü}ÔȨٮJ©G”ß+»ººÜä)Œ0‹Q‹ÚQÓ:GÕ$qñêe×$+žJI²'ˆPhqZkZªª>ÙŽ4Z=†àìðJ&h—À³ÅWϤÉb‰ƒ Ž^ˆS0JÎ$@)òó®%׋ƒì™ã!{6?g"ˆªˆ*K)œ&ù¬á(QÁ`iËçÖ:S|LÂQ[O©ï½­è&´è—ÕlÁÁb䥄šÅ+œë‘8!rŸ·[UÖyÂÔ ©:QÁ´ £îÅËÚrªö‹¸Éà}>*Œ·®ÒÁ³­>Yß­ðÕu±èKj/;;à!Jýu] ò ‘5M]'ôuMq¯wÉpf[E±‰NJñ ›Ê;7ŒùÙS”Q€ä®óùÑ6üjj”ƒmST&Ý&ì¹íMŽùD*ŸLÃ4w7=à¦SiîÝÒ 6ÈÒL mRPê"ù[wª±ù97¯V¿=É”›·Ëb+|OŸ¿L2ö¥R¶@J9E¥ÒÝëT4”IP¯±›?r7\–Å"@Ñ%ÒÓ¶[½­ïà SyxšÄ˜ðsr®^V¸*)n›C°î°@Kë$+[–·JN,8(‹(¨ØJ‘ö†.Ç;ÃCc箹s´ÂÚ»'E¢ô§Wˆ£:UϾgQ³Šì •såÌo÷–šÙ$ö<–³b·Ó’'Ðê .Èx(zæUu"lƒjñjfÃôåbIâ%ÆÇËDümËÈ3Ú°uJOxºœc œèt5 `ÔŒ’‡wä¶* !»œ˜[>[Nf¬ªtúÀêªkëfle¬Ó€å¶£”H{15ÑNB²•RÐ;íŠÖ­mÈ8UÊ8Ü Ý­³‹GÆ~X0¬tcê¿þ±µ¤Z/iwÙÖÀߤ^FœÉùÏ%BlœùbpµÕÜ'#Ås £> H^ñšÍ(CaÉmœÂÊ6­|n4}c‰}: M6P¾ïÖ…J'¼…9ƒè¡v$’ćÜð˜A*Y2~mÈã²-šÉgÏÇL}…~ ¢ŠNŒ…‘ð^ÉîtÃÕSØÕA󻦖Ùf&¨çìÕHgN…óMˆˆ¯`(Û¨Qª­PwëuqÇÃ:.Žqûos6KGúµ0õv´fãõ‡RŒšõxÀÏCîVâC¹Pg¾(‹öa1©2ï¼úF¯ßâ Éç@¹ú<¡½a}ö´çzP`QW¢MôSm‘Œ>ª§'ÏöÛç/ŸŸ¼K>M¾=}ýÊrûØrFcç Þå>¢=xAcÅ hõ= éZ“¯ˆÏ”ôÕL*œ7ƒ4_®M@sðågŸï!‰gÔë/¿|òåš4j%û(>á‰p]"*HišÂÿZâ¡ò#(O.ÊÑÙëGÏ6ã'›ÁhTt[…ñ¨î—e†ˆŽÔå@ÍüË|r1ÌMò¯nMRmrmw“—X_ÎöæêÌ9ÿ£ÔÀiHƒ"½ËQ\ ®Ð>RÊ¿`õ.†W†vê|gÂy!ÞߢŒÛÒK éÐòPAÙqYþh¦'WÓCa„Ü©\¸ {Ìçía¸L¹CºDu‚;|’»Óa r`:ßÉj0/?ðÙŒ˜­tXÍ@Z1áOÄåÎjc… ¿ßÒï;>j ‚)z…AÎÙ~–N wN„ÃÎÈuÝ@6n~¸…æœyÄ eàpP]›w@¥'¯OŸ§çaQ>øeß¾s #ûK±ò¸˜Å?Ñþ™þ4' 0CŽáðÌôC­ :ÕðôëÆyWJÇc’Á@Îò#Ç)ºô (¢Ï*àœ¦Ntø·»§\9'ox\n$AÍÑ|DÙK^\O¼gÍžµ€!â]…ÊUËU””x¯µ{G?gæ—|?º²‚¢<Çk9‹N´5­†¬ìA ¥ízˆzh³AêRáØs£‚ªÕL-nX÷’¿B°Øûyµ¹±V¢Ø(¼Îv%…S)áÚîŒW¬Ã%êŠÈNïv» ùKaİ$MDã­žÁ9fU¡xáJ²ƒ]½RÂë¢?Oƒ8Éy(MŠSÇ]e?¼é¡8sÛË /ÙmoºÞÀßN÷'+7½i&ÃB½~w—‹» ¿ó^3 7ú»¾±H…OöB}´ïŠÔˆdÊd |1íî µ*§®ÅN8pÀ§Ó±ÎÏb²4qÄ(ÀéÍœ³ïˆÕqzQ¥žÝ¾ý!*ìÂÖ8§ W¹ÊÝ鑚,Ò TæC¤n6á†íó¢Ö)\4õiLéçÂ#ØŽ¡R[BÀ‹ °`2*e2_„Êhp½k-0!N8°æÙ¶fH4u ÿôj£Æåå*–ßXmØ:/öF™×‡ò† óßňd“I>?B]–â­{žìõ=Å…’’¼šÕÊ{­WžN%ë$®Ì%:/cð¡àÂáA¾Ö/ªê}_ÿR"Üåàr$LHêĽÐÈ ™”W£ » ‰ }’2b¨2è YWÌìîS\ŽE$ùvÛ¹­ãô¦E½àîd\OÍ}šZ¢¹ÇÜYÌ÷U³x]ϧÞQ9ã°8©Ü°‰Q]2‡E4%-,s¹+ðìJõu £o:<žæãUIrŒ†“‘Øå¶á˜úù>‘#vÁ‘C•¼è™1:Ð|Åd¼ÊCv~ô l Ð>!`± Ù >>•Z)ƒ]’t3Ø&·ý"»8’ýÏ­­ÂØ9P'˜—º[‰žZ£>8ÁŠM-z¥1‡ŸÇ¡Z¢MËp=7Ÿ=K¤â™põ5C²6#kËÁUɺÉfho‹Ô¦bPQ%Æ6s­®|´¯!¹Œn®A” ¾já˽µª…/¿ÜûüK)ðÙçòò xÖ®ZÎëô ÏJΖÈYNiŽèÿž=;}×IH ôÏ`áþ ³¬.@nûzx5’ïÁhÇÎâ›û¨>–VAe>‘5S3|º…60üýÝ{ ¼ËѶ9'Q»r3•nés%”š èI>½ÎëÆ×ïÞ›W‹÷ßVS­nÀï¿].ʱŽ8í—Ch|«IsÓZ雦Ó\©Ì’+ÊãÈÑæù çî#Ù®¸,…—-„S{³¢ã™$X¯3þ#ëáÙé[ÜáOª~÷O^w‚ñ£7§¯¿{ýýwø¾ò³×ož×zz|ò—çïðÅ?>yùúísûù¹•ðÍ÷ßàÃ3ÕF×Óuk[Î%oÿ׫§¯_b?¹ë=õàÃ4%ß½þ Ž)(¿Q›çnyÁ˱ÈzÏæu\¤¢2i×[%®Þ%F0³-ày¿Ã÷Â*D/ÃÚ3!sm”•6Éf+ Y ³—íZ¬nš£ÆÈð»8=`yµÎ*èFæ²]¼ÀRŠ0K`ªy#Sǵ¦]{ŽƒwÀëù†ˆ->zÛ0à ÿ _^nÔøbÖ/~Dk»L—’¯,7(4Þà a×oMÁ#ù®Ü”²Ò[àÆRLj/±¶ÿÊßšSu¾ª1 YI›Ê¡7°  É@LÉ®;n¼HŒöJœ¾±à!ÚN>Xغ£[c2˜!¨›ÍG˜.cáCà^’i=ñUØŽŠ9 Üù¬ì½¡×qÍØýæÍH-ûêølNV½Ëjœu~oÊàe>Bk)$”p¹ß33|y &Ö§ÀQ±è“¡Ï hñ«¤¼n·Qü¶DN«Dç…LR„CÒyôt©?¬šàCP¿ßSB”Ø£fòHÇÓÍt­îø“¦Q /G”¦¾ð©öm|c <'N‰ËÂÖѲ¶°ÓóâR-­yñ”àøÓª(<-ôí®Dƒjy‚9ÚV t çS­nG9rùô.9ÆÔx€¨ ^½¼ ÿ‡?xJËì8æ©û˜»:‰`ct—u&xZ-§}eš‰¼¨yíÛÎÛ md·í?xÚhBµæñM;c‡øIPÀ«ß3q¨,_……µÐZ7Óc›œ##{õ”,µQŸ”:Œúd²TFzåâœîÈŸ Û 6ºXíJÞì¨@©êVÚr› ;$M)DD[‰ÔÒåÝñÀëj|]€’Ïऒ7ˆ2ñb¦¯(/ûUA?ƒÇÅœ_Lm©¹´ð ,Àâé]K¢3Òâqó¡ë­ºêµ\"éâmã°)‚^û—¾#í¹Ó¾2V„AøíPW¼†b0±\i ÝÕš¶Nÿ¤(¼äp —~âl…m©†õ–5é³q®W·QwÖ­±”éSÜžÃ=åde¤Öi™¼äšÕŠ–|ó\Ý1Yé%–KQQ×í¹ î(Ñ£0—Bº99G[‡³ÜEÁÙb©C§r©$Ä—ÄÇuW‡.ÉÚM¡Î×ÊÉá¼ãû¯¦âjç£^œ–çÎ ¢4›;dÊ”xí×l Ÿ3œçžšÝø¼rYq*š˜ðH!–έÅ”&@cÄA×¼¥º*§"°ç•?s|#1›ù\û‰YT3YÖ Þ•¡Û¡ò+,Ç×U9dÿøKØ­ÜJÓPÌ^äw’ÿ°Zë!Fƒ~L‹›kRi/§åK´”âîg¤Î3§»’8Gæ‘Û…}bAAÚÕb”Ä  ñ@¥®‚¢šVt-I©;•Ò Nò;üÄMãh6YΚnYã¨43UðëˆìõǺð<\×uÖ±E‚b6Ž å3¥”ëÁ©¾ÛŽú³ ØXmZ¦`&Ÿv«åÝÄ]® @Ë‚7 I¨Ð¯¦ãk™ÛÇÌ *.±û—`ZýxÈ ©ý`Ð:|ð˜•c]qÆOáVÔÃ*ö¬¯·s1p¶ZƒÆnÓbõbμ…i'–¢ö™¾˜M!0#õ«¹rZ y«ä^æ 0¨&ô†…n䇵nåœNä¸"x²A6`éL“lgs{ÌŽTÇÜ… U J?‡E /2j†v6iV[5öá« ºŽ7tj|NÛ6¹ØˆGr¼Ce4‰cÓÊb²¸k~ûú¥ 8 –C~ÇSK{Ù¥µ‡Š’(Y¹¹d³µ¶& së%‘Æžßb ks¤ý‡Þ«—žúЇâÇf½Ð̳âýŠ./*;vqP}CŒŸ—hFŠ2 -Œ¸ å·½W 1W½ïD½æ)Ã@~EE¾;»}¼:ïyK?TiÁÜ„ö$ãbjË(pÔÅ'¬¶¢GÑ`, õYÇ'Z® ù"ÌñJ~*g6Íz`šZÃ’p¨vÓu¾ ñî†-‹4r †'"‰´ÉàêÐBãSWÅ?1+|1W*±œ2oŒŽïáÜ5ÉEMÄ_ŸŸþ®™UÒêQ;TÝ’»›Xlš}L ÿáì’i7eú3„3ïe=•çV‰O06ªÀؽŠ/r‰P¿FÍ¿èÉN}R,®ª!‹þ¥1áPFw8fÊ>º¼àØ¡—Øž: 8vm??¶k ý³¥×0ížPcñö]dÒÓ®5f…ýfŠQ€ü:Fè $›“®éíºãîñx¼æ¬ë´á|-rqLTÈÅ.³‡†/÷ àÈõ¦{·¼ß¨O ®ë;¦@ÿŸÍÊ“Š-˜ª‚lc€4Ã'H ‘Ú—¨ÿj÷„ì×ÜÂìšÝêzì‹D}£uQ¡_&Á%Ÿ)ӠΑ›|Ä#g*±°¢õαP°„$¾œŠ‘ ÞS±²:B.þ…wÝ2 Òw‚Nu ]›Þ)ÆYW3fÝù‘m#*m ©!]z;OyéRcäòõ^hqïˆX÷Ñl ‘6a( (EºKkqô/€¢XÜçöWEŽ@£6Ò'‘šRwÄ®ƒÃ¹©êºñŽ7vU‰ G9É×M*Ï&ܺµÎZ¦öM« hÍ̳ŒÒ5Ú®aÂ= æcÛìr¿ŒÁûÄŠhö`(Ðí-)Áä$C)i'Ô_¼5pŸÜÄ!5©Õ ö #E–.q6@Ú(â—Ð+)«ƒxË&cQJ?f KsB±Ï“ ýŠ­ÂÖË[Qâ´iÓ¡ÏÈ££j.m‘Õ×׳ ¯<ŒNXÞ+wyþ‹ßÈùµ·„pXêšÞÝ8\ %hª _B5”´.üúá…_ÿ#]øy÷}}^­Màç·xË'|>„f„õÀØ™¿žâ:!Oò n ñ³Á©ÕQ}Ü÷$wEr/P¢6Ù›œ§›ýœŒ¥µt²MaE«²¬þ žÁÕÐâÇpÝñ‡ÄßHôŽãé¢C4üÍÐÀ×ñZ‰X8ßËÀê-{5„«Í¾zÑ báÛiÿ«è¡úrŒâ\ ³®¹ †RT0‚3½<:Ò= ù“Z0ýÞ$œ ]Û Vxžøº±ºÿ?K1­¤ºôŸä3LæÂL¤ƒ4Œþ ÚñRÌôOÑlÉË\éoÎÍÙVÔÀ& ßr|tv®‚‰;Û¦]°-IMëTsbñýÉeî¡_zò×’„" "ÎEÅ—J8„ȉ×Hت´Ågâ["öã–h¤qcŒÙîjbð?;GþÄá%Ï(z4,Öê†gx©âñ– 2!ÆÐW´5£¥Eýé1lÀ ‘§quZÈÔÌØhÝÒÑÔ-“I‰j˜GüÄèËäU¿,nÃé‹Én10$ÆíÇ|ô<EÛ?Û;߀õ¬=#ù4Ñ®GصZkÝõ:¦K FŽb¹…ѺDOróÖ7Oí«n¬jz»ÐætgFѵ—ÔWÅÙÜÏ=,ÌûÊÇ ¹g›€ZÇŒ:ûr0†Ìʆõcô4å¥keŸ”µwÀÉt:)Ÿ£Øª´P[F°­pW[nxÖUí‰r%ÂÖÖîÎ)êý$ÀåjNêŠ,®åŒ‘- ÆÅe) ?˜rÑlõ$Îö­¦¼w^ˆTÐlh3t(…ÒæøPîJvýf®'®É-’¶ÚÌ(^P2¬ ŽÏMl¶…^É)gº"—¬º4ÛøkSæð²ÑL¸ï*YÚ®õO·ËR¶-ßµFiÑö7ƒÝdu´nHmüŽ£òEùÀÌ*fLðKz#K8Úu¦¨mpÕZˆî&~ûüi‰Y¸ÿqƒ_wŽ~àè7kƒ×=V’9(>œµjCy;až~ÿí·ÏOA>¡~í¼á ó¢XܘœƒýàY6}÷úÍÛ7Ç'Ï¡ðþž.š_` Õ›« ØÕ3ñ@|ýý»ç§ªìª¤ÄÔÀ½ÆƒÁ–8æEµX`F$Þo®,@–ZÓq+âi¾.*úŠh$)„äGKð•éuN>Àvp. Æ y Š^ÓS¿žÕ¤£€õfz“§÷TÖJˆub‡^ÈhŸY>›ÊCYƒçhMwÔ1Š™)t’b% éh§šv]²»îš ƒhŠÇzñЬ‹úÄ=›;™I^/H‘èÛ%‘¿ wÛYnºÂ»÷Þ ‘ßôåb\dÛÏNßm{æ=ø¹¬HìÄØSÙe>)Ç«£”ÒbÀ” àhÿ e?C?kÊB=.cõHceÜ­nrSWG{Ýäª@:=Ú O"‚Oéãe5 6ÞWM Lì-û“ÉJàUÐQÚÞ>û]1¡ã³¦äZŽkIø¡áP¢[nîQr/2F=TäÎùµî‰NúW£”ÿtv`䣣ˆzQ¤›h}ÚÛx²g&íIlH[!¬&8óè32»îbišVzɦ}ð¯ûX/þª´9ZLq»èó"ž­*XÈ–©¡ô]˜O• Ôlº¯²Ô›`ÀÂAmS"VäÍÛ‘‹4Z¢›ÜÙ¤²:Ò¤çtí™ÅæÒ§óbÄk¨º­€ 8™ä¸ÓÞt÷ØFnÈj$Úœ¯"²Šö0–ḪfÙÆ÷‹Æ_’»Ç] §Èö«´7{*¿62/¤]Z„“ ³èT µ¶b¶NA…h¡l‘WœÃÑí~d·0]8Ô…qs nú…o¹Ì¢š©ýGVíå°eøµV‡zßN8vô&uíì’ñQGqå̉Ќfk²¢dCsd•Rí^Z'IS/;-å@ËOÆÕ !ÍŠ•¦ÓìÇo{ââ€+Ò2êLÎuõ±ù ‹Ä»2D2JvSÒéMÚ%zäó¡.1¬#WuïËÓÜ8½Ì;øæ£Œ+mytÌ/ª[§x¸Ï:ëªa@s”óéˆGåqjÃhËŸúÈRû+€¾R×[SÇKf4"|št’rò#ßç Ž£r)yq\ʆÞbuì¨ÝÝgûá&Ö@ÎˬõÓÄï»â~M¶®ÅU†¸ÙZ”™Ënµ/½³ßçõrΕáDÜB™Y‡*x™šCÖ–ïq£Œ½Ò1Òý±7ìq®·œ¢¸ ®`'¦¤Ð’n^œ­/d\Rþפ5œ££ØR#:ÝÀi—îªT€^}²”YGô A)³rW’ÎÖ»º ‚FQ3ù5° NÉñ²[`]ÉÙÃÖ‘¦ú| y”ܵD™¯&Bó£$Lé§P ƒ—ÐLnPÉÈP*‘Q ´y-‘šP›"ã]h[Y‘)Õ’3@†¹ èvXýh`ÎõGk„?ÏF]\Á1+r…ÚÈ š/^ZàÛºŽV€7ÒÍZªmÚ^‹¦®¥9¿ÖF­µ(@[šòkm:°u6-MF«nÔîéqoýåÉCˆ08À¹±WºQ}ªŽÐ—yB¹Pz–Åw¬PÂÏ­íÄ,}ÓëFöu$,n×bc-\Kia ü*>”æoÈͱ-›Gh®•¬g«ËÅ´F‰YA7ânÝ›õb'ù¶ät5¢Ü’-uŠ0q§¯W“‹jlz-(Œ‰¶–ºóõ»3kçÒ,ÚX-ÿ™ºï¦˜iÒâ'XQÖëqI½U'Ò‚62ƒ-ä†ïWa~Ï?Šdµ;¨ªùc/ñgÃAùo€qnçŰBM¶êZDÖm”­Î¶ï›Áœ?Óq‡[fÙP4‰LG‰–jåÀ´ƒîê’_IKUº’V-Õ`´‘U¬0o=õ‰ºfT¡§X§‰&CZöN„óv5•ÄñQÈ&ùm? ;5ÿ|Ëw-¾0#ztC#TxFtQ±ŸØÜÒ‚FÒ¾xøNÓH|L꣑"ên Iô'¬a°a!£e:¼òÑã½ )ÄèSK5aúÛÐßFf=Ôµíà÷`²ÆóŒ¹25¼‚Ó‹íÚ#÷@¶×Άñ§½É‹EªòQÎTV4'Xÿö›ì¶»êlwHŒ(d›4kØ¯Ò =kYFE ¡œ¢2T;(¬‚}ãJ!óbÝd¢×‘\žj˜Òº4SM÷5³²µÐq¼ÿìŸ;ô¢ë Ÿy 0#È™ zoà°iæœÒ áH›¬–HM¤«ÑFáCSwª:”†£U"  Z6!«y½Ð—ˆ,»gw¶á=ÔôëJÇ6ú TÍ JJfeÆÃþbw9aåý†³Võ¢Fnå˜ZF‹ aÖŒƒ²Q¤›iA :nš)J+xè/‹‡ÍTlž¸ÆÉ#¢õ…$¯ßVÓùÇ“GûÉ×d"¡x†ÀU ‡–iŽ^,Ø|>ä˜ÆÐ‰FåD8G'¯_½:F³pŒîöЩŒ+=\škis Ý»1Or3k<²“Dãàü2I]ﲜNW{*Qa\ªÈadaÛ>áåñ«§ÏŽñw_¸"F4¸1[îsžX¼ÑQA÷â_ødàŽá×>¤äk2îEJj³½\ŽÇÖ¸Ú^» gH“—"øÚ×&(Fokö.Ú“íÁÿ­WâѬ«®¬Æ‚úÁžšÙýéºðc:†ßê†tûG³HÖ¼2KM“äOŒ¿™ ë¯_n2ÍþzÛT(±zÙ‰-QM€˜ß¯ “iòI`TQ«_Æ)Ø(Rºp2:‰ùhö©9 àµˆë1­VÄQí®‹¶…ñÏÙ63›â|´Ø0)³>øÖ%ަGŒ×.j¼Që›Á+ÈW¦œÒÈs´œkóCJŽÖ ¥É”QÂQI&¿´šä˜%_Õb©úÍÎZtUEmzÂ6jl$… ŒOz`i‚I?c“ȯ%™Uö ÉãäÀ2e]¼á,Å/1c&·sÖˆl1ÈǃåxYÛ‡GI.ܘ²ÆÂ·¼ç– ©‚H™\!úÕeŸ×=ªLÐ ¬,²@€ô>gp¢Ýg Ñ-¢ØqÎUÁG-ÞÕ¬ƒ(¬ï^¿;· n+=~ó&í>霷ܰ_ŸEûõü¿©>ÿ¯óÍ`…ºŸG:¹a¿¾ˆöëõ©pCX_Fa½xõæÞ¸¿í~‡õí·÷†E9É×ñÞ#O‚EÙg9<–KƒBHKÔè‚ÂÅ KÀ||ór8,¦‘m:+‰M ,k–D1À÷á<c? ç>5  ¨A1ÔQŠi…€Á|¥|Õ팃pMYxkf¤^{ËKÖ›Ü×h1`˜KíÝÃãüõ×l(ò±ºÁȜᄇ.à‚˜[¬m„dCñfœ@ë(÷¾ÞÔ@9=ͧ£"ÛëHÜ6 Š•ðIC%ÆyXÆ»ƒæ79j`PzÞ¹½å ¦:óz‰XÂqâÌï ó¤ä欣s{ÃØ5KÇ]4 @íÒ>dêný=܆)vcîûÞRµ.€–ëH/¶¥À¸UTd;ª]z¾Ÿ7W叨€DÿÍžh'y³o}øhs_[ëäÖ÷Å!ÅB:»MVçݦúÝ¥ßþ¢¡ž®Y)¤RjˆÀ5:Пú†]ÍÙÏÔWZQN+që£ŠÈø§xÞ#†RÒ—˜à²,†©%l¯™S‘uöÀƒ&û¤¬ ,4°$#]E—þùØ‘v¬ü2*‹¸ÿÈ,‰Ò÷õ@hŽ)Õ$‘»oñ µè)umRËBDÀÃ_v~J-Þ;Ù¨'h@·®1­’íU;襎÷@"†Ej÷EcFÒБ´¬# ‚Š:.tÝ#Ï)r€ Zm_Y¿ÄD³_c„âCgdªˆåï¾$îG7??~ØqéõiŠ¢| ì› 8~¾ëÀ|ûm#;4´¦Á¡…Üó\b£Ñ -ól¯Í «¨¢-2$5ÔvçìÜ'ûÁAÌhã "•#Í7š65æg‰ÔP2ªk¦á>ÄR­£2?Ú^PÕ¯Ä3Gq†Å¤R^N³9ªwÒ£ôÓƒ=¼^z÷üí»äÍñéÛ矈žsAZt|M"j’ŽA ô'›§"˜œÕEQÓ­úy'm/…'ù4Cµ$lï¡Öš:?Üö~Xõ²³Í[TÜÀÏþ^]MÛ{Æ]3UP¡t†C›/§u†M®«ïVßý&¡GTývmuj³{ãú€qþ't¹»9.Bw½N¬« …3‚Õm„¡Ä‰¨¨®®ãÑÆ´"±ù›ÁÛOiÿB!D4tÖ®¦v:¯#Å>F؉“Ç›ôŠ}®u€íýž›JE¯ëô'%ÚJ­E2:hRàŒG_P,†ÏîO0’a5¢ù®”ªÖOð )ê'eç^ e0lE+M+¾R$r¯-È:Ó´OËÄ:»[”ŸÓ33³…YŸÝM,½M»Þjú>¿m{¡¼ß¤ÜÃáu~AEM"¬0:Ü€!=òùQP³uÞÒÔî Å©yn¶HÝÓšb³ ÝmIÈÉŠõ8TÙê·JT̳á6®Ç´ßGŸÞ~?U‘qkÜÚû’þ§>_îïïí}ñÅg{þg_þì?y²ÿÅ—Ÿ}v€??—ªŸ=ùbï ÷x1™=žŽïwÉ„m<.æôóq]Lç‹þh¼,úØro¶úv’ïrØÔóqòÎyË|T$ï@Êz_.“?}ÿ"yå‚ZÉÛb‚ÇêAÍéÖNÒü9-‰ ë°Qó»«Mæ³g§ï:(vO@¤CÇ¡±êÀÖ€;^‚t>‡ó!HÕÊç z,ŠäëáÕH¾ÿq˜÷àèö •?©f+¾%Ëà xÙß=|%ü!y3¯0ZûþôåaòõÕb1;|üøææ¦‡¨éUóÑcC¡¶AjAgcZå qØ-’—/Nž÷öyïÝ¿ÛÚ"wÆÅ{Œê”:Û~Kþõô‚€’ÿ‘¼ûÔz¾ÈGêñi1 {—FÅÜ*AÕ›äc û|gzãb|2‘—jGàW0Åê…Ž–Òˈ\éÁÔXàD[wTø‰À ¸‰fRÜæ“€Ž £.gbZë馜«’™/Où¢ß ¯¡_Ht¨…i;“:wÕÒÈûbE)À€–j¿ê©z™9µ_À«6íŸä:¿Âø}\«@ß(?RˆÊfº«/ºB_¡„.¦ù«Wb^䨥ò»€=[bîÃå™qUÇH£©,QBFw¶+tÇ#ïy>™äsôõUuÓ—w˜ÛˆrìÙýn.ÞÃ0°ûº±±a¾Èñò!‚Áž<€£dw?>hOéÄï8èD$¼ø[9„Ó~üµ3U±¹B*w'þ„Ëgb^Ãrº„!qÒ“Úò¢ gL`ËÅvŠ5¥d>_–õ⢺]SXpµYav—äð Ù3€á"¼÷gŒ=mq‰dé×'Õ”ïÞ¿I•Ã@= ;›~¤tbo.ƳB‚‹Ü˜á@ff½¿±T ›Ãº÷×gYú¿³gùu9üùU>_ýŒοc2Ûï¾{“úǰ0É?£Èó3PNýóà*¯áûE1FGÐúgŒ°ïÇE1Ó/qˆË|ßÐ]”jOêŸÉÚ»æÖþú4ÚØ¨ú™n⾆ݱ¾úùfægJO ¼¢µfÎo‹Û>)Á£e(å —[NËë–’5tnÊÁÕϰóÿ RøÏ³ò§ŸòŸ¡Ö šOYÆD‰Óò¢`.ÊÑÏÀRV?ã¦ZHÙÿüÏhÙ«rò3üGSJåÞœ¾±¢êØ—¿L 3­-0ûdÆ$pÄüåB{Þ‘Úß2 âHÃ`e“ Ã%+rŠZ¢Y›SK‡f7€ºUµð6•;òz1¯z7yÑ kôŠáòquSLŸ¾~÷—oAWó¢wµ˜Œ¿ñùóªv‚JaXЧÄz²ÎÙ6þܶ,â±+=¾eèçÃa¶ý)ÖÚîºÀ¢Iïþv•ÓÅrÎa¬ˆƒd’Ùþ¨¦Äu€ÈXXï6„‘H‰žNŸ£ºÒ—ÏR|é¸ÐE5® ©uá“R,›n–Äš¢pY½Â.øªýþšv?FCêŠÉz™|ì‚d3ÒÆiæÝî«Hm(öèÀÇb9úµ ms±¦-¯Ënö*q„¦oq3†þaüKùNÏ2©×Rca]–ã1vqEQÖ†ÅQŠ^W)¶<¼=:h€€ìsàêÐâKü›…-ØÁ`4Inšî€Ƥ" ÕéÚ~ø¡½‚bØQG@p» Ï@rŸTˆºÄ80›Ì緵ˣt+÷j$(5À ë/‘Î6€ì¾P•v~·‡ÿs"éКÚÁ^øß&àÊvÿw{—{ƒ½´IdGt2Ú™pèd‰·i  ØléØÝÝc@ÄÜÂiÎë¨àK‹),8Ø “ç~¢ìÈ®ËâY&Aã&å4S aLûnrðyG™ûlÏÉO¤Ì+”牾±Æ|9À;ÞM€>ôl¦i-ùJ»cÆÅÌMɾUmňv“•TDt”¢ZxÖØ‹¼×ˆ¤wA±º…»Q§¡ËªºÔ; ᯮË⦱ºEN‡˜šVîiö!~%D›ä ß]ú’” ÿ„bwAø¯…¼% ß1Þf.ÜBÀ¬#GŽFf-eïˬíjcÖV'£ÌÚná#0ëS9²Ì:èGœY[Åfívõÿ2ëÍ™µ…Õ{0kã#1¿VžæÌíº±·ð³p¼÷åeëø˜ ÛãcqæbyÎάš-Ç~Îa(6Ãi*ãçw’7RP¼¤¨+¡ÑvTøQJñ ¤E¶‡§bØ[½Ì?¸QÏÉÈÉlbJEåg? a7mÚYùh¿lÙ‘ŒnÙº}gËþünvëë6 ›±5lZŸÌÖ_,U,­¡<UúOb(ÿˆ’×èàT<ÞýQ÷cXà9yåã1Rçöuž×ƒ|V¤xc½©ª0u nðö›Í ÏT±Ù¼¸nùVÞ[.WðçÄRAFvmG#×w2µ‹»¿â{ºUÆÞÌ1Ž·ÞÌÙ‘HmçOèïÊÞÖEaÁ’Èšégn×Îñ;؆¼Y™Mçb EÂ-Çá{ˆÄn'Í6éZëýjýB¢‰öËÌ·ÖȪ ˜ë˜„–/U½(ð‚eO¿Ên÷»É þ»=€¿ÆímÀ³ÈÜzŒ¦=+2HÀfzڿ鋨"ù,µA¤çùå“a¢þO ú]€¬"Òº@u©ÛRp7ФE¬ZÛ&Ù¨X´‘¸# øàs˜ûàó½N)äÎWL«åèêh³*¤ù=:èj¹®^NßÓ@ X‹N 8žÜÃăÎlášÓá¿í©óiAp»3Ô$'Qädyø`GÅåá32|½¸ÁÂ7t±½Â×OÖªì…}7¡ yÊaø¦™—‘JŠp²T¤<®ò|^]^ÚaóUÙ^>jwzûQŸS";Ò*æØc˜‰ö¹vÖ»lA°w°Û’éQú£5ãÒ/n:‡ lX¨¦¿-ñ”`5 ’?Rýµ†›“ýá¦6¥cCh·»Ž¼C&çŽxÚMh—K7íîeµ¬I?hpû1ë&¸ãÖ^_Z&á˜ÃÅĦÁŒÀšVÉo:¦4·ŠyORíŸÒuEòL_WÜ#øQVœGæ–¤\ƒlÆ1ÝÓÅ¢ÁI¶ÂÚ”31óÍfã5áËÇO‡Á¥5xäÙt2TYjxrråNÅ»rºJ»>6Q;¾ÁxþRFÜ6\|Îé.wó½³þ¨=:ø…=zU Ëåä£vé³_Ø¥—ù|T|Ô}õ {ôçåÇíÐÁ:µ,¨¿B_£«I ÂZKWÅx¶éZRec¼ýø¢ZÞoƒqF—cõ‡¶ÑÁ©ÞE´F,H¨S<•–¼Â÷·”x…¦Rd>6›Wƒb2þ=!IDiübš±g¤:á8 ã!z‘2Å58q,Î%@íYê” ·˜Eì Æ@¶ˆik­J—øò=sFcr³ÔIìbÀ´nŸ5œÆ‹06ö½›gäÍ;pjá*wذç¾5šTD9úÓb³£¤FñÁ„Ód‹§HD›¡‰h:ë-ìŠe ½)T ĈZ R¥€ÕŒUä-&„U‘f‘¥¾ciƒ•fÎF€û›d/Èòq™ iŸ”DDIÜü±RàyÁÍEr‡²†ƒÅ|gåtaJŸíµerÕ]bÔi’‰^¨£èX1‚Ô"Ò/nñë#4KˆË‚²QQoú3‘ÐÕÝ“—¤ ?q·wÉQF‚XôÍSovíKØfˆ;¢+Ű1wxÐüpçjéÿXf2ƒ¹ì˜Ymç$ß%‘ÕÔÆd)çÿ¯ÈUÅn ›!Lî²iy WiÀFÀ@œr÷gÑeã_ÇŒBv÷)?y´)?‰s¹ÍEÛ8:GÄØÇñÓ×ß¿ƒÕ’m“—‰o½ÙãÅs¤Aw†¦ÛB:ÝþÛ¼\,Šir±B/˜²'îi_+Ùá»ï^bŽZ>/²Ë‹ë/¡ËymÄ‘åUQ×ù¨xZÝj—~â–gpÚ¡¿G„8Ö¡‹Æu£ÓCï;a!åµpÛ| EÞ)–L²_—Ñe”F¸ ɃÝC—³&^Þ$£ºÊrt „6û¨µ¯t¶EŽáváϧïo¼À»¶oÿs (Gäöî}IߘL †ÐQ»'ËzAAèÐ'khG±[`9ôÈ"VNn Cž˜³áî`T"^x"D׌$+zÖMª® BOç÷Àব‹.ûj ð›r<–è‚hµ[¡kmu¢H»”Ó}ÖÝ ŸÌK'¯lõ5>}L†¬‰õ‚x²"õ²K \çD;ªŒÚÞš%¬×O¶76iØôn–_Ô -¸¿WÖC÷©£ÍšïÑ ›ç6Wyt³-sd€ÞFõ«ÏþÛÔ¾€LD}[‘6cJÍ[û|Ø1œM1zÔlQ…óª ­2ýFÚ}—¢.a^™F&s­i¦&4Èk¾ÎõdUÇ´1.«†‚ß¿‘à]´mb všÆÙ¾»,è…Wµ6ª©ˆµm¨çîlQ±ó_Fi”ùƒ*¸ûÒ@&¤"Õ¬]ãá»Pt“JQ¡.-µ µåS•–¢Q*Ô¨ÛLìõVXs#agâ-DSæE›µx‡U‚ÝX¼€3eìüÐÑ ÚóçÏm˜¯OÒãÙÙ¸>&.ׄjI9_’ªøï6—¿ß©Eª°Ãy0Þ­’— Œ¯|)¹)C¡ÙµàļŽ×ø†uÄlŽê(³¹äÓ¸µ{;¤‡Y¾Û0#gƒõ;MCÅ8#BñDЩÙ/ÌKgW7ÿˆnß·ÑÛxïØ@Ïq¯½£ý,ݲw´UŒïJíá¯å†­ÁQËl²5H…u†Ÿ÷›]©Ù«át5./WY§7ÅðcRõÂX,ëÍ’%”¯¾º‡X穯.((Cü§Ÿæ” ´vgîø0°¬ÕwGÞ­‘•¬×qM”ç¨"ϾH²í=J4Àfu)¬Ó+Ú¥N_’|%OÜN] 90Ä%-°.6"`aB ,!ÓÃW¨:²Þ‘ëšß·õ€ ?ÚWðíwÓ¦½¨èÝ”·[7!Ö]Ûd¼¶­8ÚöÞ¶\1·¬g)ú8s`—ˆ%~JÎÐ ú7'èÝ\½Ÿqó×àXAr0†³ð4aGh(>¬FP2bUžÔ\8Yå7hÁ©Bã wë„{ÔÞ*.ýÇâf°Úc[@ÃîHæx˜È ]Ÿ›k’‡7–UB‡n€)ŽÔÍÈcœr“#x´ cJ+Wñ”£¯(œÛ8÷XÑ\·7+‰ 7)'있ïx[Y ¥Ð©ùb]!îISAÌà#   ½¦ø^”˜‘úÐ {v|-kÅGk Æà?kƒÿ|ùÙçû_ìqðŸ~üù“Ï÷×ÿ)Ð øÊº¸?¯*èj:0“u—é_Åç‘à?üì>±wTHŸç7°Öÿ2.à(þußÿXN/{Ű—zË÷ßtý(=§®–óÞPŽŠÞ´XÜ+Xvñß_  ÉWŸ}ù»{¹ ½Ý’ì}~øÙÞá“Ïþ'y»@ñéi9î'ÿ¾µµóîõ³×ÌKwv!· æwRZ}yT.Ø>m—S÷¨Å‡ ‘¢Íƒ8VæãÚâP¡ÆaH«árLz=à6€\ «ÂÿróSÔ’oSôÉ(÷Xs²5¡©ê18RÎ8ô™(qgŒ–-ù D1ùŸd{;ÏgvТØG…—Bú‰UŠ’S)DÐp+ èS-¤Ðù&Ó§«Î!§—·ßƒl±fTÞÊk ¿X/2W1+{>€q‘ˆº>y˜I¢‹¡JbñS9ËäÈ¢œR$ X¬ÃˆþYJ¥ÝÄSCnÒ½‡@ªAÒä›:óNø<Ûp¶9úN8Ûp¾Ñwa*(©­µ”wœdd€q¦d3¥„=AëÏäæª (µ¨ö]A. •øæ!“,ŽpÀ\¬:=–Aþ‡î|’Ô‡R#SBÚ:ù°X»«ñ$xòÈðNe€dâýãb5+°*¼,wülŽñ†Á½ tÆ;˜Ö‹æÒÙr2[¬tA¼ï=ìF¦ç€NtE¹gKý„­wó¥ØB%ÀÝ ônT©ûµ:ÇR„ 7Z\±0D1è26x†5Y/8æY¬ýÜôð¼Ë01ˆì VŽ ì&&Îê9:¢%ö;Œ—ªûìêM8ï÷_Ñ2Ƚ›l£sÎ's5!5ôòÇe¡™hÃ<"¢&Y0œƒ(¥pÐÈp¢Ñ9 øÍ×pÆÙ‚(Ät9¹S)¢AÕ m¶Äœg‚á^r<]YØV ò1žtVˆLNÛ´ iÁeŒ©*®æè¡ÀH6ã7lñ[ã?¯µËs•bLŠô"…i€¥D2|ÖAíÁüÕwÔRá œù¿tJ?‘Ò_ÚÅ7 k|Ä ‚W‡Y²§ÅMrd‘²CYÎ]F@bËã -3.`…‚œõ œúÊE €I ¤íæT•Ç¢›Aá°MDgÄG2 ´(G§”IŒ!8’ûBn$ÿR¬˜Ý+¶ãHÒ_*Hz’X  Lt—d{²ˆ®¡Ü*Î0·A†ÒkÔ€Qª«U{œ aº‹ÃR³)Œ>MzÎ ¼á”j3>ž&j)ãÛ—4îøÂ–·‰ÇLÛ„¨¨‰®pïäóîßùr™vY<±nw:ÌçÃÄš„޶º.’ŸÛANMŒ{¹h…ÉFk\5ÔwίÔs&9¢aBq¸áÂ\ˆK ñž|@ØDäCcõì èÆo q@MÙÌ(0€"8AáYñºÊ1Æ ÐŽ‹$rmƒmžA+¨p‡rÎ{ŸC3ü"vûcƒQ{øµŸ $~Ý2©QªDøª…²m¦³ ¶‡ B“T/ÙàÄ>ÛßO+V£iùS!K…ð*ynÓOêôð‡)˳×JõÅ¡î>xàK?ŒÇÖ cžAʧhê}±r 0Þ§ÜæF‘Äà´lƒÜ/}é20'­½N 8z€fw¨(LÁ'ý‘³˜«©|V¡VÀ¯7¤ôèÖ½N Zßa3óÏ ÄRÀW)N"4W^2Øl= , E„eÄjb|Ø[ì´- É‹Ky`Œ:ì¸u*Ô"š¼„( ¦>ÇT ‹ôyŠ¿a58JA ¶¶©-‡ÂÞ.yø±¼b–IêÍÏÛx^9ÕE8ã’&è¡ÎÔ‡°K¿»ÍWTR‡1®ktã“í;nvI½Hkê²lÉÇ@£#ôo[<`OF› UÑ"û$é%Ńٹ¨(•Ä0´ÕŪ­ª‘.wF½Ô|‡Rÿýe•¼úß!9¨ãB¥dxóê.å=ToåŒm_õêCy×¥yãdû¿sV¶À\k,r—iìlèu)ç¬M<,?ˆù™n]æ%L§Õ4ÞˆŽT´&à”`.*ðêÃè®GÿÚø«-¼yÄʘmmý“‚ŽlЬ™^~x¡«…7Îwê|öòKÕ¬‰êÑôè¦Ôêä~¡`#ûÁÅŠz›½%¨°9…}]³Ð;#±¬˜LºÒšo‘#¸Â!¹éå=T–˜¦8‰Õª´uʳ`q£(•(yÓšL)äƒÈ$P¤”,ïy°Um‘hî-·ûÐü¿ØÛÀw˜É:×èY§dX)šìíÝCpݘus÷8K4(>U–’l4°pß7 cè)ö¶ÓÒæßæª8i§¤á-JÖ¡ÎÍ ðâR¼ð…31‰¸§‹ì7)plW-Kü"æs %CbPÍVþ.Åj kSˆNÿ—³!0-. ln¶œ”Ù&ô°m½Š{:ZâᬿÀ­’ù˜h‹5wëˆ=«¡³'1÷åžÒ:&9ˆ¦ó®·å/ixsH;»034¢ú"”ât’”rUXðM^”3q¸–Ž ­9ïpË ß4ð+ïGç¡K¦Þd¸~r b§Ýÿ7h3¾butÉ ÌsÝK>`¾’$]¦w¨C€þPF´tt¶||{žFaŽø$ 8Ø™§Â’è7ïX¢* Ž‚ð@œ}R?þ¤>7låÚv¤•qKkè–Z†/¡ÞsŽå6#dì2ˆ*TNÙAר£øy‚¹º  áÆ/à@‹&øòlÿœ3¤Î5]‰åXã ­Dzk' TçéÅÉ{D?ÿ…Zƒ»¼ÉS—Å>,¾ Ž×¢ÝÒ=ö!¼5‰í>ë7Ÿ 7_¶ÙÜc“ñ& KH~4ÖèÂÕ5Û1Ò:‹±|˲pÉ„‹WžÝ9MMnI·¯>üUMƱS‹À€ÔVáד|Æ9¤•à#z,;õ©‘D\•‰¢~òâyçf¹¾ª–ã!jå ýå…hF±O®|¸NHÔnZõ(°ã=ªÐöË„è;³ªï%ª·ÇJ—‹êÆZsÚLj$ª˜‹…Ó°É¡£ w3]L`õ™"›káÙó£–´¢hΡüïåP‹z±,ÇdR—Sáª.9'­Y1LxH'Ãjš¢xò㲜Þ`ivYß›W~ô2Õ5³NäÊVÁXëAr1oÔßí½yˆœ@˺^¢‰O±ÈtQÅ@b‚_ÍÅÖíwΨ¾'AåêcÓ·ž ´¶ÊnÇ 3ssú¦¨Þ‘X‘îÕMÛÙ'Àó?™wXIgqZiMXÛÏÔ$À˜þÕºQnÔŽ2İrtw“‘ºSi‘"OÔ¥ïË/Qfä\DtdI h¶tÒ wÓõÅÊS½R&ï9±ý—jèa}¹2+Ž˜ÞŸ¼G®9)pmä#gŹÇ\$•`ùa_QYròÁ>€“®Ÿß>7!0òzÀཫ¦iPXp½;sª;—îkþ1úÔYH÷LT ý€v¦¢ÁhÝ2,SˆÝ® œsò ­é§ÝŠ¥˜„çZ×"Ãy*XEk¤K‘‚‚¼$¯º"F$Sõ zá6,”O/Ï>úõÁ}-¹{µ©£ñƦӬ7…k^nÆ[mhŽŽ‡Mj7’›HˆZŽs$kÇf éÅO‘§Ö¡‚ˆ®-npK:ùÀÝcÈ|QMàl(‚¿‚s±ÔÑeru+Áò[d!ØÇBF×!Œ ÿ¢ˆ‚’_„Ó¯òÒéáÕì!yË¢:tÖ1UŸÆÑ jm`Ž’†“Jd§:ÄC,]é?” Iتµg+jŠ&<ö¶Ô0ýq­£{ðn9Ÿ¯²`é[Ç5oÄ&r;±óc tS ÉUÑFd¶ý†RXncÊÛ ¯ÿqía_JÔ¾8—çÞ`Œ @³3ÓÃQD6]çt!@7 Ù™SÞb-^Qó©óTëç!0s)O0Ë©ôÈ)Ø©àÅÂ’Q‚4¶ ‰B®î Ç/“¹jª!Gk*›9júŒáØú•8i~GÊøa#‰(—roõz„ ­Þ4µs<]ÛFWœŽ|ÄVQÝh¬§/ߨcvø‡ô ò×è–“X>Þ¯¬iRtç~½ÞÙùꂵ££_¥_Ï%Çôo²sÇãqc¿à ÞGuá¨Gjjwu#Ó[z:¡q2S“®é=ž¯i–!Ëã_óÂ&˜ ž¹‚J²òŠáX›v¶NZ€×¿=´ü"”8#oDUJ^Ò–Óˆ’Á%àãÃóìšb8ÇÆ÷„5o?Âì ™¤5]F®IwéÜ„ç‹^ò7ô!ãAÂa‚¬rJò ×dÏü¶’xoªÛÏUÈEYîÊ ™‘+tÍ—ä¯u[èt,œ16¯H]ô,q½P݆¢¸@2jh©…—ã'9êŸÅyÒX&²Ž„á»i£ÿ(‹yí±è žâMn5ªòöxËŸŠ)º{‰?‹ºàHZ–—y‹ì2H[Õ¬˜*}Iëê´ zëQ•zi/` ޝ æÅØ*ì%&bˆ¬L¤ ͉Âë tò!fID:m™ïñßð†µ4ñÃ"Œ)¼ƒrµ‘î;j ²ôC"…FK†ÖÐtŸ*ï%™|ûT‚ã‡>,‡ɺnÐy…æ—,Ìè¸T®ÍH„ËS1w‹¶ÆB0JÔ‡™Ž˜†>@7â´ýÚ¢Ndn¤Àd“r³ È­€Þ¹,Ü6ÉǺ€ØZ%^|òM²mÇÕ —8â’Vô'õi®cUÞ ´j¾©ÿ] `ãéæyÄx€2]ÃL\µb«k^èZˆZŽÂˆHJÃIJ¢è§ÑBoÁÿë¤ [jÄ{Û>3k@¢¬½dšã~°±µãe6#j@r?Îså¢ïTa†²éˆPæ}а ¼U'ôÅ&Œ1ðÞf]/Ç qdÐà5#10v˜çÔ´%óÎâïÁ›ìéÆÐCÑ&j=3T+ÀŠÈÜ[ ¨fÞní<þ(Ÿ­ŠŸÙë},€¦ƒâjIn5Ì_W0È­ cÿÉÞ–4Ž1wÞ8â.ÝÂm…¡Ü9hz5£ÈÁÕl†*Ú™»»"4Û8¦£quA"ÂxŸ®Ÿá_üoÄ\Ó¦ü¿Î²ô ˆÞ¨ 7Ëÿ²œÂ>®‰&á ‘É#zÀ¶0Ü6Á¯Fû® #¾b0;/ôÓv*Ä¢õpÛEà·¼mqôÕíu•ÓÌ­mù–I¬N€4]ŽÇhQ¯¯êNíß6{øÃt1ÚÜ<¹’‘û/ôº’üGò_‰rÂ?OõÏÝäùzj¾:Ͻ:ðógûç)ü|ãþ´Þ$÷þܵ+¼Iv¿q{¿øðÛ)ÿµûà”œú%ô90?RAÑ Ê9)Eøi(æLö{–5];ìk …*21™KebøpYkëhkDdq&ß¾~Édï.i§=ZÛ—Õ˜–iöã²,¬þˆÜáú ݾ5ËÜ[â°Üh™Ã¿øß耋\cÒ?ôµærÁÞOÙç(ñÁH\–à ñ·¥[ÎÒQ9SÖJôã†âXé =ï0œj¥ß]л ýŽBÉ;‹Om¸cÎu,îÝ\Yõ€p3ô‹U]¨_ñ\q³‡›]h>uÀú¯ƒže{D5¯: ^%5F¯‚÷ÐÁ[»+Õ½s+´›èªiz›¢ND9…E’½Åp6ëݧÛm•`PÌ“¶w±ÜþgÝmfQ~ñ¿†§:46DC‹‰vJ“Mµš¶Tâð$ŒzõSjÎT-', ôu Ææ„oOI† ·‘#ZÄC)–¦`wÁG£[Z\ ôòav0ER e²É­=ðtÌí&÷lYCiÒÔäæ¿æUÏS&«Ÿ¥š6Åø©Lªá«Ô{A,pâ±2¾!ùår*Zè6ÜêÞ³£/¨ ÙšFàq‡pTÀ†¸_Ï»õà-×ó.èëû’ÁAË·@":ÐëêVõþi!Öš3÷6ݽE¶¢'´mÅ7 *(›P£|I_ôH1LJŽÑ0¢.²19€¼íŽdÉûÈ|¸¯n"é)q‚ jQ 7É`» mvF"Á‡HWe®ŸýðÃm/É`¹d·‰p˃oîk~KµíÉ =`®‹ì¶›¬š_p½ÌíJ¼”ßa)¥º÷Vy·‡¬ª^[VŠý´ÏcúißskHv3 HÝo-ˆÆj0«nb¡ß´âWç(cš±`­U0¬·óœÜA-zY«ZP¥·¶£ÜÚŠZ³:M°wÞ)­¿ÈúÊÂkòí𸠼Œ@ÃﹸiJP³àÏ´n‘€iËÒõ ~9A% ÖZÜytÐc/1'ÆjD4f«åd‡¬ö# â .Ö@Ç Ùj±Â·¤ùáÔ­ëó:_4òÄ·úî¤qW­oñÁ$* oÆé¬6Öó¹S¥D¹ õI™,Û)-ïø›¦+ôm…ˆü˜·E[Räi3Wû½Åƒ"oC;80¼~´uøT¤XûÒ½G‡®©à1mÍŽZP¾–Å< î&íZlFU_ßk‹»33kà –´àÔDZ¾álúL‘ Ñïs,L¨:×y§:YÑ¿ çvöÊpð½unR‚.<õ²h+ý3·T´•s‡/$3s@jeïG®ÞGl9Ÿš/ψ)[—³39 ÎoñgÚxºœ`†ÖF ôt9Uô…åUÀ Øö fBAÌ=ûò?ƲnÁƒÃˆûåÒÉ(Xƒ¾Rb¯ªŽ[û‰[Ûhó†>;\ñ~ƒØ²· · t¤ü>¼ã&s¯rŒ¡;q™òu9µßÅ+[õ뀦–ÜbAl¸ÀÜ@vxSª ¢M–ž Á!¾0Ëñ%û‹yøäPÌ3/>»3Dæâ¨Å3ÀÎy¸…È‘HEÄ2•.9‚!äÄú®¨ÐÒÝϽˆ½Û*bïö¡ÆD¦Èñ{Ÿ¬ßûdÿ‹/0‚/|>û‚ñÙ_î·Çïå„í±{½ìbûVÅÝ}‡J–|=¼É÷?Ž`„ãÞ š|sŸp¾NÞ›››…†­æ£Ç÷ŠÎ«‚Ñ"ÕÊWÔmö¤Zâ#«ˆ8©p³ /^åãóV«Ä šÏ–ôżº™ZoùÈ€•/Ä-Ôá\‡ÓÅÌÑùœßn½}óü¤ÿöù«wÿëÍó·¸rÒ<;Äý¤ÿãæ(­6°iºYA’ôS.¸œ–×-E5¤ &ðÑ~>—›V¦íiÕ€ä T¦N'ï¶¶^¿y÷âõwÇ/û§Ï_ã×·~ñæ-©§ÀPt-gh—ž«°EHºr\_›Â@²ÅÀNA^[hÐŒ5’5Œ½Çé­g Á€´þ­Ê–%‡n0´å ;}yþoäòäìã²ÓJ™f#jÝÃh´§òZ”o-‡$¾r…mqQNT°Ùô•@‹×dZ”´£üÿì½}cÛÆ‘8üü­OJ?`E1–“4W5rO¶åÖ=[Rl¥w=YåA$$!¦ %+±¿û3/û2ûTœ4w-Û$°;»;;;;;;/*"…´ÑÎЧ7oسMÕ—ÁpHÓ7¶¤¬p‡ÈóÕ‚Ä 6’ëhð’¾Ç°yáÜfû¨äv$¤nlª¦/?¿åðH,w5$r ô­9=²ª¾‰¸`tV©µ€zIÇNêc  Téd¨®á´ŸdÍüì»d«JÎ=m3ª2±øwý^}ú º.ó5l’Œbù^ß¾Ac¼Ø)΀~ ê_;&%.‡ËDwôƒÃcô›ÿ®òïêý-äáøŸ™ìMìWIŠc¶žÆßƒ„¬±3Øy‡«öÆ‹™ëƒ×Ù½×û-%D íf>H ˆy´Ø Nd]D½í2CÊÔyñÍ´e†Ã"Ô ¸Œ­„?ÖiÜfÆá.ÛßCðBg æÊÎ3XÃ烔ËGNDÿwyýJÜ£ŠS›S¤|øæ®™Wâ•E—"—E §:›M0æì#°{bîx)§)L]æ ¦"Ä?)*?Óˆ*"ã|€oƒóÉ˪z7¿V§2ïà·`ÊE‰S.„ÂaYØ’n. uI…~‰ÞâœWM!çüœteS²Z¢â;~¹:?J’NaUò!ïëÛª?æcÞÍVUÇî"–ùpJ46Fùd”lýMo–JI¾õ70u›&<ö^g½Àœ-õ¦åù"l&X˧Üþ8Qn¨”²¦œãô 5ïÊk•Jˆ0æƒÁ‡'O)çÚFÚGÍꊓ6Tó™çÀ»Îk T£„e–‘Þ«Ÿ<ê¹ðNRÔ¢ã ç%1çï„®îÓ|pÊoN{È¡‡ò*öÔïN1% ð5!»¥!fõm{‡ƒÍéwÒTY,.Ëëh6ÚÁ0G„—^ÜŸ†Ì·JÈr«˜Î¯ÈeŽëœlŸ¶x—cRmš´,m÷„`/HƒLüâ¼'6VbÈ<ªó dkXBCçâp‘Xnn; ‰·5:›•›µÜÿˆ^Â&zá‡4W<æ^ǘ¨­6PøY 7@_3lwYÔèó©»vÕÊ€Ù<8Û„vJ-¦îZèfMDü~4A`7·wÊSgP)°½zÈzYiwœ9¤¾mý´§ c¢lD•8Bi]'«üˆ9í¯BÇÛms•'¼n›ïôLœIfLÊ+rYÂÖ ôÚZ–°Åà³F?.gEozbœº²ìv´ü*NÂŒÿ)&–#÷#u³Mù|F³ÉºrGbƒËOT«ç¸¹›Îò÷¬ÆQPÙ¶x Âeh¶/ù9ƒ½ÿ]GÛª„"¥)G!ÀN¦û¨ ÷Œír.9»ê¼P–)&oãã…÷èt@¤¥'io3ä;PaN8I¤ÂiÚ Š“NN7±«j·q·¤óºú¯gÐv¼v('µ‘X(Ǧ2o !œèNîpOõ¶ß'ƒýŲ%qÂS˜†’s;iô<4èÉ"è)é®Ô‹TjŠ+QòdÇoûT3¬9§¬$ײ¥¿:0[·[®4ÂrN¯–êÂB¡aªxÐa~nó)”OÉ‹ãÃr – ¶¾C«‡â]£ò\?CGPþ=r×ÐQЇ Ü1Ð#Xy¥JçJÜ-QG¨Ñ§fN‹ÉFXé¿‘¸$¬µ×p‹«£9 ÓŽÂ:9«|X¡’Ù¸{bO¾PáXPÆd»dû •™Ó­À'%H+ Äv–2µ£Ã+øiGQ@ßË ËÁ¿­Ý2¶ÅäÚO£À#^¯TŒÓuÃëF)o~jõIÖ«á´fÍÊ—´Œ«&Ήô*-,Ρlçeå±úŹSõ„ÁµLJ‰»»-A ìà‚Z4€GZ¹$..·¯Tš¤¿É(U¯‚ÙÛŒ¸Ô­_œÇ )–„ùb1»gÔ ÆßÁÓ]%ŠgØW[Эôí4މÒ=µ9ºKC3³Š”܊ꆔ`TÅ©/®/êüú²«o¬cËÈ ƒ¾¶¹ "YÑ&4.̶ä ZŠjè,ˆ­Â.ëé«j-õORxܤ§PÇ«$ß±‘Ù1!ø¾½Š0c‰˜Ò(¾˜F-ÃNIL)ô¸éáõ°“rËëÎûÖ}q¬¨Êî0e$3Z0føfºh¦)Ð-¡¨«›È–qK-õ2E2E4X»(fCõ¢!ÜE°­ÞûòÙFšXQv"FF”ŠÊu <­N7X”þÐÙFR³®ezá::ˆi{Î>×*õ˜Qô+þÍ~ßW #“‚ á7êÎñ¨kˆÊŽí!ÔÂV1¼*›uòÎÞ#ÑÂybõ ù§ž¦aEæ™úéÏ|ÜðÌ(A T4/Á=¥L°£)’dƒ÷ Á#–âÐÖ(s(3x­Œ$¢;=í«å 0ìÌ |¦ä4HÖrÑ ÑÓ`†¢p¡¡Ê£üÃ,»Öy-DçÞ AÅ€eÝ:—®rƨ4l%h+ìi|.‹|LÌ<Æ>I=NLÚb‡YçÓòïó"KñL£&üH^©Õ]X²ÇxßÌ É@mô‹Øj£bmmçí(o>õ°»”€0Ý‘ƒ\ªnyß$îÕF3‰yqÃ×凩,!ôzÒ­)Ûj÷ŽŸ ŽŸÃÚ¿¢ „:¤¬ˆ1Eô¦ y(ûž.DÁC=8)¾§d:AÞøRôíeE'ݳ0*ã°¤ðªjf"O6¹ÁÝ¡øÜ”D €Bn’~‰Ò§xàtš®Ñ #ØéÉt á†<Âvőӑó¦7Ó¼º·EdËyi£g,•|ÞB@½>´íwõvÛþÚ]:qA×—S?ØÄ«™(í=w­4°D‹‚?]º7›”L>èÝ6r˜‡qaËÏÈòð–POrÔ Wœ–Š™–ãM–Ïk µI”¨á"VØi§²nU±coÇI±ÊÇÖ û îm¦ot¦LlÏAÕTHqÆá8"D.¯•Xä;ÁÊÞ~ŸæxuãpêÅÅ.CŸ<_O’×0ajv%RŒÔi×M9s¢ij‘üÁ¤¦¥DÐ2R¼wô9ô}a/”ž/«€« )_\7 Ïùu枊2 ¾¹x¹8WVínK™iG_©NZû~’ x‘C˧rtyq>P´Œÿ,AM¶Ê‚‰1´¶¹í©á/Œ¿2@*ge>)¿/†“ü šË.´itˬ±ašU• ufGŸøbøe2ºÀ²åHa9κtU.Šyç´I™vW~hÛb§¾´ ëh¸³Îw¯l|ºC)úÔfJØç nu oT„ueÅôT…,àÂNCñ@l9œêÚA[8uHæÏ ÅC¶Ds4(ÂlŽ«:ôß»=(?³pe4sOˆ³0 K¨¹ò‚NÅÎ×t¥Ó̠꘢^ï}¢Ã í“ñÓɤ%´§*†]+´VÛn:ÛÆo[Ö#¡ÑÙ/@b«Eotc{tA}þl[Ëñ¥i/ðǤ‰#’Q÷xƒÔc4gÈ?Š¡-y²ßÝ=,?‡Å,‹À×ÍíSŸÀ$,”PÁ1C'pÔ¦©þ¢CërdßI˜‹z9Û!gÑÅRç( öVDNR“³‰Ü‰©Ož²M!šùÄînz® !ô9ó³0©eEoÜ¢°ÑÝà>×U¥ªÔ‹ª¨9´,<˜SêOè|Î}±=Q»4ôe ¸Y7XX¹;*¯‹1½,çïò4˜ðö&ÎîÕÄÙâ&Ь”;›ˆqÉÜʇ›£rôŽò.L®/¡¦PÀ²ègƒé½“ù*^b×:»1EN¿‹¤éÅ'ä " pŽNê@VÞŠ"|éfí¥¶ÍlK‰'KÙ]B‡Æf 4š©ˆš•“< 'ñyÚO/àÏ%ü)áÏwðçü™ÀŸ+ø3…?ü¹†?‡?5üibnÌI:ƒWsøsnáÏ{øs¾‡?@k@Býtà´’éé uwkÛö8Àð2°œ|–<ú­£Ì…“‡ñÍ”ÇÄ—UPp™ý—ªø× šº˜fà°Ô¡¢“ØgÃæ$}WÜ¥ƒ„5É6?3©“&w ) T~s• œ³AÒ"~"F½ÎX\ã¶¼oi¥ÛéŸñýx–À†zlé"ÙW®Ó ø*ÈÈLIÚ²©À …õatÕÒÐÏ7Ù6˜ËÆÛT³@qF.Ñ*'aµf[(»—…ˉ7㞢¤ôÅX賯Óhh¯®?Ù&|8ž³¿hÑ蘶áJ…N“‰ß¢›SÎm¢B™ìêbîë YR¦yÏÜ*¦yÖâó÷ºLà4⨨õï\`×ô ©·-w a—uùÚêÒèg Φì3ÅÝ«p3™h·?š¡{WܱРæ]¤΀à¤é"ßXMÖÔ7Î't’éÖ¥l$ŒÍã c2I{=ÏGˆº8œUúøo–‡rUŽ©×=3Ú®áÀ<ÎG•©}ë²4 Ñ¹×Æ%®_d 9¼·Öܱ8U•g<X4 .6Öw1Cõ1ò@UÕ–¡s8VD‹±¦Õ´õÝí%.ƒ"Ol¾ê £9.3.8¸®|DšHô„BPÔ÷ôÅ}/¾Õì·ˆ{ÊI\CænN·xXáœÃ6L!yàq­zÔÞ4ÞKÝrqì€Ëüœ¡CO°A”~ÖZ±Û’È+{ø„?sgÝÿ˜9®ú ÁÔûx;Zfœ`Úbý*/°„T*P¯kÁyœµ«©-¢:7Ag0êD4pDìÓ9ßò£È\i)±oÆŸzIdè~lXˆ¶Ï5H Ñ‚‘¿¢í²­D!·¢Ì2•“õœ äEŒ³»®_—m9$@±õ xa„RùIž±˜_ØFÛ͆M·Âj_H|.Ã1p?ÛÁà.øÅ ?‹™×ÈêŒ?v²Wâ@ø‰s;Ö_C±ÒlWÉ/§´4º_im¡{Á²ÿÜ$„éðÿãÈl¨Ñ&MX#cÝ•)óáõÂQÀs³#Ûµ[†hR«’Ù¢ò‚t¹·6œN·±4ÛoÁÃþá] ÐΤÐy8OÝÚûCžÞK+t¨­^8ù—u‡à³KgåÔi'DC+—çºæ`v=,0y#ïf½p|Øí2ÜvƦږ²+ý}ÛZÁ¦ZX¾ë7U©±UöùSFúš5n ¶q•ß)+N¾†[à˜²¢œÝ±w&œL¯ÆÕ43æÙe]Ý’ç9'êdÌF˪ˆÞÄ/P%;MF—Åè"N'!.TrxJ+6 ÿK̈‡(9\y.ç"z¨$Z1÷àáœGì cçVyÞM1&ra{•×w2îÃÅù„ΔúèOJ‚¡2ÒÎÆìº7¾°I$‹ë!Ç4§ãS•24íô ~÷w!lZHi|Øw‡À:°ä€ßä7E#ǰñ2ÿþnËÞPª0½¾I¯a«iŠÿÚ(Ô4›åE½kü×U3äg™Ø§µƒ›«Ma}†Ÿ^„mâ=ÕOÂëÄæÎ€UÖIå\/D®;»®†©%8¶Ü²† Ó~ßÓѸUÛuÏôkGmæó¨I±u )¡û“aW'ÞŒnU}ïz2¦Ôå…©Ý Îux§Ìi³×¦g´³âÂV ç©G>‚]¦l`oq5…”(’û(º²a†‰%e7ÂÔ€gMÐu0mÅûkõPøš~sY§+'Yþmë'·~wº™ øßÞþêŸ>K)®å¬èØðUNÖèIƒ";¾,>À‚þ°÷!ÿ°7ýO{bïXƒÈëY9šøîîPuðÜ,¨ôŸÿœÊ@šüAï5Ô¨—7ÑÊS8@Q僃hå)Ì9¹›^¡+ê¥;ÁMîØë'`ܤXMÝ‹7-5¯'«šú©»øË“?FÇ3Ûk¬f1VŸÅZÆ„µ°Œfº­ïa¿#1SæŒvùˆ+rŠÍÜŽ~ ˆþÆCd€M†‚ØE…ο»é´¸m„eQéléõ[~ V€†³d>zWŸï:$.ºáAz²Û¸…4«=PÇõBXgqPv<„¨Ë1‡¬(GÀ›k2û²“BQÖ—\¸™·ÚLjßLËRŠŸ´CØ›L>ä“ ×!³ÅÌÚA¹ˆhwa=Ù øfìY=“q­eÆÖ‹²OÜZVºWPëq=ÀPîÐëë8’ÏjßVÔkì_ñ¬ÿÉâYß'h«¢ÈŸ'j«jlù°­Îâ'RË~µkw—“¬¹MøôÕ»9X´_[»×΀ÖaüêùLJ­ƒˆô¿oû)|‘W’öå|Eþ5›¦¹¬n‡*ßîÖ¶¥pÒÓgùM9§ÚðRY)ògšŸŽoËÑe¬ /ñ”<ºÌÔ÷&”·«ä°âF—MšI¶5‘*ưï(&Ü¡î¾PqÕËò ñ”žºî¼¶Ü5ÈÌó|l@Ï*åW“'×å÷ßçÐ@KMŽ–å/ª% ‘k[ŽòÔ¨ª§í`:®¨Â 0ûæ2ZЀ±¥óëëºÊGñòŒr€äù¿½ ˆwË•DË”ÎYʧ Cù cIê²4{ê·ë|’gû¯÷YjOZ ä°“,ý[F3úiûâ¶§ŽG©›¤ùo®Hå˜l>hÒü@T÷IS¿4$óf÷MÂRO üAÕ¢«<¯>èYk­•wHøž¦!”ýá©ÅòVÝXjˆ²?("ú“÷AMLÏž×\0Óp¦?ðDöì¡Ð-ëíƒZs\æèõ‘’]O ¯ëR IQV‰´}X2QÅç-qèÐŽFÃæFxÔ\ÐsÆØÝ•L… ‡‰êNNNt2óÍééi¢eÙ.-7gM íq¢¶3¶a­ˆm±Q½SKÆO•ê¤Q©Þ’aψ¤Šúbaª¨¯¾zøåWª€Nõù£/¾úª;UÔe5Y˜*Š„P'A~ŽÈì÷?¡4¦xº“‘çÙÝÏ—=jµäQ-™ž&U>^û©žÆu~;˜Õ…)††ø áRóY9Ñïšw€Ã«òûBÀP N¨54vÝ›âà ©ÚMÎcTòþ ˆd\œ “¡Mª[¤Î'8iž‘3*l_Oª¦¿öº@áÒÂç/¦çE´J”ÌgöôÍË}Dô—½ÁÚ¥ÝSM¢—€Æ×Ägór2ÓWÊä-!z9_ @I]Cß•½kˆ*Ñbq„a `?ÄÅÁ*wÒ;Ê3`¯Öòð>Z¸{xÓ«F· ½ÝC'ªHg°Y{çU> }/£r©òŋնæUÑ~äd–7wºëƒ$ùÏ"™Cgódýz2¿ÀpÔëI>A5Íìò Ê­ÁÐo0ö:Áð»2%‘Ã&¯æÐÈÆO¨Èø šªØè #. ðú謠̬%r7vhï°õÅ@Í@^.“æÝÿèšIルÐ;¬Ñƒ©Sˆö&k\#བྷ/Q̇˜Oîÿ׋7ǘ ,åoüÓýÀ#ø‡ï<£ZÏø÷áküyøš½xu„?áõûùsúýü9ÿ>z½OÕñ_~òrÿ|ÿðï?¾ÜÇø¯*±÷dŸzJ_RÞH_íaC?@çwT@ìä¦_ì褓‰<Š …«ƒâ‘Æ@ìS†Êå rtµƒây²­ç¬™.NÛ ¾Öeë–"€_]Ʊit =n Ÿ·‰0]Š™}ÔT„ìÒpK—’´O1'. `Úô³4²_Ñjü]±–žoÀ–{}MŠÌsÕ«­ÇÁwÁÈ¥®ÚÁØÖ9½=µ/ÝZC2S"CÊp¥¸ogÕõ#â Eÿ±9Àz/ýaákbA%|šI'úfˆ¦Öëû–Õúš_°– <¤½çi¡™ÎÌÄ!èža4ÓÊ3¹0ãS©lÂ/““÷A̲LÎ#0¸ˆãì3÷9øbÞMr½fÆÏš©vÊTŠíD{äªî¡çýØ–SÛ{”3 ™­·¸\SÀÀÆíײK˜ážÃFYø:>üž×uÄ%K±eÀQv­<8& !è.-Œ½—ÙÕ„™ãÐA{€¹""¾‹F^×Ò2 µ©ºxâœ-ðÕÚÑ@Ì"™—Ûn¢ªïU.(cºcèz[Þð]qÇ 64×–>¡RhFšÙY>ÙÞ9•Ä¿'²þòDsìo—¬aEøB†fò Måõ˶+£êÚpcbàŠÁb|kr´ \oØ«”™?`€s¬«÷1³c9µ±ŸŸl»¨'‘š4IX$jcªÞ›±´øíèaÆeƒ†%™“`T±HS*†Y¹3xW1®Î†âf»kÐo ÅQ•ó”—x|'ãReM@Qcó‹¸Wù”“Íû u€â80D'<ÊRŠêz‹Â½©}2 \3?# ŠáÅõF5Ù¹wxû%2µÞ¢¤H»âr˜¢m•ºåÇ7ÅþÇ{ª[Œì颻j¯3èåuvʦ>™)cÚ™ 8«$WóFY™¢‹wŒ`˜£±›Á‚>´w’ÊÓ|‚Æ Pr¸Oã§ÅDG'ɔՒšU–ê:¡K ].1’%Óo™ì‡ÆoŒJì†BãIæC}(Ü;íG¥«>H}5b®݈6˜}¾ñfûºšá%Ÿ¨T2 ì– x¼ÒÂ3)°þ‡@¦*تæXüÊo&Ø;ôýVnÁJFD±†C%vqät}´ËUÈG¨A,êQÑÌ”p'D! ­‘Ñ$ÇKÏgVô'쬊ØX˜úÓif¸²ò›¼œ4¬(OS™Ó„Brª:fbAM묲<ú©¼ÊÇ…:"X(¾ æàD5…ÞTÚÆh³T¾Ù<Ϭ—Žt¡×†éˆ]Ì< öÖ§°›ÐaÁf°˜T+xÎ׃"qC+-^èˆ kÞÏYß*o5`®œÏ B…/ÖóM A©°/c˜ãS!%…;Ù… ÓÁÉõ™NÖ'’+g¡õºXÂÄ’½N©…†bÈR‹m qʈKbHB#Ò5$*6ÀÍ# •3­yf, *"í¢–;‡Þ9ɤE sµU†8S‰'¾a7O}!?»ùf–;³ÍÙK”ôb³}ªÓaÞ ì– oÞwŠÚ¤I㬑¥ŒVòÞN{>;6[xßèÔà¶òç‡Q~ÝLJ ˜ö1¦H‚enÔ’²¬²;*²9øgÂ4`gî °8-ühÓ2“©Ë,ö–Mp#ÙSy+4Û"áä¶HQ/Tß©{^m~(ƒ [­¢¢[$[™¯`ÙS˜tQNt1Tà†d¯o¥$ ¦ðl³¶”ç’Ò,ùóåIòÉ ©q¿; ÿàs.¦Ù›²BQ¦ 1#›Èåò ÒËø¨H3iªñO(²œÂ×nâFäòljàü¡GÁz…”cW6ñ‚Æé”`^ñX`ÆëAÅ CoÞ’ås%j"¹š}޹nipgÈuç°©7ƒÍÝûrO~Ä0Ž 飛ÄF´Ã3{XPü÷Šn—•ëç=+Fù¼1WkX Ú]͘Y:¨èkt›%3¢@g„bZÒ´Rˆaø6°ÈgwCrÉšAÓamáèSÇpˆˆV[I](d¶m Ñ&2Ë{C±8ÄœO»péªË쨰!?jX Q;>öWosuÑÙOb'‰&’j‘í|‡ÄG£*Æ–¯+Q,¤*ä¶Ð/dn§Hh6Åî‘nòh>£«ØÕ½¨Ù -VM™ÇÍQ:@ÍÛxv¹ÅÊ– ÉÇI©e~…¸€'½ä”±c¥ (ùbÌCzÏ9gs;²’’=4òz™Üo«úžóo èìc#uÒ¥kgdñ•sŽ@h>À˜=«ªwdð[/ì2ÇÛåNñE Æê&H+{^R´°B”ã«HÍX=Îû#€c¥Ç¡ýUM“Q>UL†äÀ1+„§E®Nn<£t¸5à”lˆ§ ÿ1M§†?Ñ1²TÇýC1Y—4böÑÚ`—ÈáÊȶÈŽ¿GŒ“Ë÷XT…5Ô—M ÄYµE™¢fï];?q†Å¤êHu‹fê•f¢Ì´s¥V0œdÚpغ£¼êg]Í/,+'é…D~€®ݵ ÷ke*®Manv™ªÓ_Ƽ^g~qÅ7’#mkŒe™ögh³Æ,G=æ Ó<),Œïª¶SJÃ~‚\±×6œ?òà…Þ9]î©Äèd2OWLXuÄ•a¸‘6²‘žr7D$‹äÅÌ5ÒÕiÍ‚¸0³†ª±Z1Z2=ãеoö_¥§ÒÊ^´vDC¢£e~Ûf<«:˜ÜüVêNɃL‚£-¼“ü6g•yxPÄlT‰ìM&ÆD -rGh†Ç·p¶UŽE)l-(ÿ òöô“+Ü:@÷A‹#Ý/Ôçš7¿Á´×xm~ɱ?áäó¡%¢'?Q%Åñpm­+(2©\ÕøÆâxcÂ5µwˆâU~wF1ªoU •Y^N{£³<‘xRáîO$‚[’ѽcÙ–y´ó9z)#»+N)…¡ ÈÒ£½7ÇH ü$?ßCmfƒ½‹`G±¬ö«h ¼ã ³®•ã÷œ¸ ¾ø‰ÛBׂ…Å4Ðö® tQaµú „ʵ.ý¾£)µZT‰.>'éÕ? ýŸËÆø¯$‹Ñ$‹ .È0½±É­ßNUlyºžÕæ°‰SÒç]iƒóшÆ>hvNpW¶Ævúþ ¿Š¥²5NÒEÍlB)âo¡¹·©½éSÒ~>·¥I )ÿ˸§ÅûÙ‘Í¡'áË&ʾZÄÚüœ~ZÔÆ3šX$½bÖ[­(M“ô7ƒí¹­¯ä|5ã¢~¿ºXôELYGGÔöÖ;$Ü ñ4»šÎ¨ _ƒG½^ó‘jê/÷¨JgƒÞLOÓ5áWGƒô6%­S¼ØŽh`€×¾]ÛošJ988¢©ßo¾}²ÖäåXýüË“gôïCõ-kÈêÕçÙ1ÿû¥÷êðÙÚ^–dá*À;ïa šŒuóüïoe󳼯 _¢yÝ»¿`ueB·ƒ~õÃ'^;žÍ8¿“­ÿVV (ËGm¢XmØÁËêÛQˆŒF«ˆ ÞÖpîáÕ4|´ÿt £jJ¤}n{õçµÛ|ò®¡n%a·DÓ_´5Íw-MÓUE[Óê #Þ´?"M ¯Pb` ºÔ楒s’@x±Ý‡™ƒ?ŸÃŸ/¬Í'áˆ$ýä!Äy‡@™4d‰8zþjcÑ.¸©ùíJ¡õ¾T75ŸõèÑ‚Ðz2ó¢*ÊöþOÝѰ]¢z#‚Y¯­EÊêîE„º rë”bÂôIý3l‰®8s4œE/oÇp8ÂûÈ6W$è. ›«eåÃôëÅOÇäl`}3}Q‰§TŸfP%¶‘2NLŒ%”¨!Àš¸­~¤ø8tÌ0@tpØ7Ø%˜ÌX1…»»2”Y´sÊNXf¤ÁaàuSˆð«LœŽúú§ÇŽ0áÜQ„NèÞkùÓmŒ.µMφÎØ5/Γ”*#FÎÈ‹êì.IuÕ´oœþ1 ;gs„³£³œ™Z1æC$×Û²)ú5·3M?q*EúÃèRRòÅsMé¥LÊÄyC Í€-Dn!ù?}–ª6ªÄ]Åô)vVÓ/É·ËÞ] ?œ-•0sˈÚ{«K¸°Áןs ö–I|ŽòÅx\%/‹›´IŽ.Ÿ%ÏJ"`¶©ºÞ~øo[Û×:Nˈ=ÅJy-±š‰¢ïÕ »‰Däx2‚]”„ÄiqË‘€µ!J8{ÙÓÄÄõ7³=ÁTv ÞÕæ€Ó—Oïþ䎛ƒ"ñ–‚ _íËP+9yŠNNÅùZÏà´¸ø× þošAgÚÑÀieøaÜX ªÛUõ:R ;É6“’a ;gfë µ¬0½f#Ùr>éšU´±ü¢bú?´ªÈžéçª ÖÇý ” <D`Ñz“qxå°ÜZ»÷òp"]/uĆý¨xlíÓ[j¿;OضŽGØF³%üº¶¶º¶Ä’!‡#‹°†m"^ƒµ%,Æ´ d\‡Šô|Õ³tT`˼þXg#C}¿;‘2žà¸:s|R©¸5Ú+F!¡Š2b§Ôތҡá_6¯ô$æ—Ê,”vðË·ù”ÇDǹî åè:‚e¹ò,÷ÒíÿÄL2X”ÿR™ü2Üñmµzúø(HNjEO…9‚ Í)HRNü 0õÉßÝfóþ¨§úÔÏmþK©sŠw øJv+’ÈRd·")ŸŠ.‡ÚFީȇ¦äÈQ›h£iQ_øO·Ã•`Õm8KoØÚAÔDõ){À‰x(Úë¥1òV†H¢¦¤¢sE™˜•°¦®Ó&—uUù ”hoƒC LꊞÈ“º\—XÙ¢ŠÓŽÈÓ~px´ nçDÃ=su§Šbr¢ÍpîõžèQÏV}úòðÍþj:¹Ío”eUÒ‰îõE¿úB':Ð3‡ŸøEê2‡ såØ7¡Öí·¶ÛÛȹHƒi;Ùè÷ÁyJ5ÕzšRïÛêé.Ò­g»°TáGäxúÓ¶r¨¼ e!Kä|v숴ztR”Åç’ïÖRÝBªÂ^¥~÷Ó–(a îO4¨)W¿c‰ HUVÿކgÅe§©V±9r•ÍÆ”æ‹®wÅA¤dLél?Nøã7+× ,FUnKvQܧ%3Ñ–b}"4†§˭LJؑ ŸDCø. ì~’>eZ&Ó ´ÃJ~Ð's`¼úî£øÙÀµrWÍ)&[=ŸNM$GÈ«{½kú꣔*ý!vCj‘à r#Ùsï±€d{ÉV•dOðï§½ž¹ƒÈ1Y×5æàŸ©è>pŸD8“Ç,k7ç'ùzAoW™–grTóéù|¢‚¸R¶¿|6èª:{#Ícʤ™ žÐ…“¤YaÜ»»?ÕÀ‰2ÃyÓlÿ'Ã…+k鲂7{ò›Þ[víŒ žh˜Äªš3¹e¼¡˜ ¦_xƒÆNB! ‡„1¢"…s­)6‹ŠjÅå $fÞÉD¼DÚ9íDaKëÙF%¥Ò*ºãº,v‚;<“ó͵žÜ~›–%¬Yh#¯ˆIs&1ƒ ÃÝÛðñ‚îO,¡[*ÖÕÌ#[B62¼OÏKJæF”ÖA:×U?—¬í/¶¥ìØBzⵚa °„-‹;æxÙÓÂû¿ý˜qV‹ôÉÝ÷ÍB‰¶ÎV”moM®§;íP¡_O˜xµäO"Ï kqæªáÃ06‡ªÙ¾C¿OJæbÐ0§IÌ‚&QnÖ Æ‘`‚VÈw|»Ùǵª /k8¥KËv­\KƒÀYŒÌÏ6Íà`ZHÁ?a½Âü¶„©– N}öO4­*fÏf²[ö0s¦¢°Ùçj  vZ#ýõœcE¢\U4ýþÕnp…Û~² È.HIŠ—O4aŽÈÒõwŸ) ð¤õ·S–¦``¨úqFv' ‡Õží->Ç EéW(ô´­Ó…‘£ñKݺ£1òC»˜BñÕ*ä. ]mûIèÊ"Õ…WŸĠÝÓ¬¢ŠÒK¡Ä˧§¹tÐÍ ’Ï„¦ZBæxŒ¨g]ô2eFNi•,"Û“¹¨tä°cÍo‰û*½J¥¯]“ÝîÛG‚¢…KC™Q©³èw†7±6 ËÊÜÍ:ä6Vªü ¸BýÍ„bÓ¸£ˆ HÂ\ ¶é8ðƒ»ç‘4Þ65Á†Ú„ű…PT1Œ¢]Öçy«\Hvp=÷(†rn)Pû¬Îi¦i(êOêƒn^b8.ŒBØ×Ç6IÄq¿ÝˆkǨÛE__ö_g=@nIy¬ƒùÔÃp‹¹«TuÃ.@»FbÓŽkŠàCIA0ž‡ÐPÙúleÎæžïg:œ·4k®«U唃Ö4Ê|ݹ4+O‹ˆQ¡œ$ÊØë Y#U‡N^3‘w)ÒÐÀST)Rk†×uYÕåì.£Ã’¬QN_çÓ‹"{سjB>é¢püˆÒ½d£¹…eÍAA¹.‚ZBJˆ× ÅLSÇ[Ò`³ü¢žeR! :`çe·å~T”½è!-Oö¨·8"VÙL­äoáF«‚''Oe3¿¾Fw–èŒd× 3°¶¥qêPN:Ønr2ÁÂp§‹’¿¡ ÏQV§çj^Â7ÙÞ¨œ÷Ú p‰?öøïÖrTê’€-l,»8þEh¤øŸV+îö4Ó­­ÐÖŸZk-Œ—Ìsúõ’?ü\uû«/>úíø¥<á±Ð?Ä~5xäŠ{j›=9gã1‰ãYàÉt^NèîqMd=À@KãdB‘ÿ“¥×ž7‚¼¦<æëBc©§ª×VŒ}Üêžß逓7wpX~¯î'_>jwÎ ª~l³Hûúð/û¯—ÀƒGvÓ_/÷^=y¶G£Þ¾Åãù[pߨü ô`ÿ¿^¼9~Cµšê ¤¥&9òMÚ{ù’Êâ+÷˜[ü&›ôÅûg‡$”§:³á_‰*¿@ðzøê0íwtô–ùëWºûϸûÓ1>þµùëoº¯é}UãÃüש8dœ¤Ä BÌÖcû÷îc]îùs.w~ŽÏ¿æ×úSlÿ*µKuí_»êõzÿ+zlþ–¨}Z+` ¥:W"½yòâàð§G»‰CÚLØ6 úF%¾ùvï€&^QÀ&Ϋ{l‚‰:Aê;í'âÕçyÝ0§)Õê&Žl“ú¿©ÛÙÔô¹iÎ\0Sb(o0‘VÎaµÞüõՓ×D•ïéÐNqîT3%ÙÆg”þ ¨{õú[¿úõß><Þý:µ‰ÑÞ‡y•y =cAÄy“AKbJF6<¡qÞ"ÇǃVr²>-.ˆâÖûÉ:ˆgß))ŽËFþ—¶ø³øû¼¼É'˜mfÝn¹ÎKŠ‘ÿ}yM›=ˆ¨úø82_y&Zõ+4\&ýµ_ߣÕ}j=îÔ:boÉë¶¶¿l0MÅzò !Ùã0Î#ùœbž8Ü‚©ÜWoí–Ñ:™M×ÔüZjzö¿¹Çà¼ZK ÞŒ=˜ [ÝßÅ[ÝR,$lbóœýÑ1Ÿ–¸óÞP—Ä ñ”ûPîË—n­e*1g¹Aù^™ o‚½ó@kô:•¢'zåÞÆEkv…V¼‹àiøæàø§KG|z!ú¿Øì Ý“··Ð¯^44]ÜR,sš€0ºº^Šd(ß±Ö©O²žŠÔÃû8|oúÛ½ ¡¡Ñ¬œÍgÅPk6=Ûœ°]ý†ìÕ¹0«èD÷Wóá·õ:í,L,Q¡[GmˆãnŠÙÛ„íE)ÞÙ[Â*Ë®Q{SOU´æòbZÕ…4vЫîs¼ëékŸ “(‘U´@ywþ‘­Žð‚ô)*VçX塯Û#sÿkò Ÿ©äm8œnÙÞ®í@_”Ý0y^˜ÆnMݤ«KpÆœH”'PtnìzÖ,.®;HD O¯MR²JEAÙ ± Gcvà1fMÅy‰Né÷Ö%N„b—–®ÿ<¬_Ü'_@ñPBðµŠGQž@àIêò࢘RF˜E:F£É…,}A?C¶ªß­x‘©ùЮ®x‰D%þ‰[Ñý¿³M¯Ø€ZJ7¼R8 ÚPSH†»â°ÁfÚ¹>¸šU!V7Eíl”qÊdí=Ô ÄݘzÉŒ.}Îw7Ñ•ç¶E¹£´'¶„Ÿ-O·Ï_²H7n\ rA{ûCÜ:Üc— íëo8KðÛ™“hÓÄMÁ•ï©3øW‡_E‹NœWC&Ù¹$ó7m|ˆ[6•é€ÁÍ,Â…ÜŸU$»^ôŠ‹“¦ÖÔû‘Â@_/n(;eŒég{qÝQ]>Öß«|6º,š–Gc’ÈζßðPç¼èp˽^{5åÝ~:ã áˆ÷þ:<þëÑ~p`Õ‡ÕUMnŠö³Áµ8îšvx!ÂÎMvf–î“Ä–Ë-ty…$·‚$5s–8[ jäÚ=ï7ný΀ŽY—û¥g®ÕÈ’7n-2ݲm"ÛÒ– £ôëMÿAóØ 7|Ë`_Š¿š)®Þ1ýr€0–é¦-Þzìæ-fò!Nù‘éŸäM9Šóéûp.NkL´Ï]ŽÛ‹+ÚÍs$‘HìáQÄ9ç¸@®gB‹ÃUڹײĩBÀRÆ©EÇÎ"ÊÕóY¤î@"´¤‹¾8xæP=®ç³Ë¿ Iâ}Ÿ¥Dllt_:-ï£|ßFo–ktÿ/ûÇnÀØ©‡åoí² 1Áªè/½Ç³³Ìì.S÷Xi'±ò‘Ùðü‘kI ÝÚKÁÀ¹ÏúðT Ý«ÄáÏËÎæ–›MXÆj.׎_{ü'^Ó»> ÷Ö`Ò_ÿU¿k ^!E˜7†{k†GìÑÙÛù&tø×Õ=ªƒ¡šH”xÅSLômÙÔ gÆH6êâ Ž%Isk÷8}LõNr¶¯E¶ÕYjk›‹=N ×Å”"æ:‹ˆ™ræb’7±`8hý)9§JÐXK›ú²Lh·7Õ/¨¾åWã•å‹…­ôã鋌@Ca!üœanpŸ*¥è'”“í$Ù)“Óîdr-uËÍí˜Bµ³‘T@¸f͇KG¼\Ç®¬30ïA1rRѪݽ‘-]É^ÂÚ,®;-Þ_ÃÑ´Ûü=;˜ú%àÉËm¿çj£–m§Ê«FRw«…&“Ê‹©5kÆ6ÿT¢©G—w¶q§+í°÷Ö"&’˜Í¾t©:«yÝq~áü'ÉmމE•›ÉÕ|2+dâ:ÊÂt\žŸÔj=pÀ®|çÜÑê{ä…Üks#U¡ÃRAùXPv|ôЃȺzêÄOÐ-~d¶È®ªp@h¬WéÞ¤¤ô@d‘¬œõ43$ÙU~wVü¡':‰³²“dùTÄÐ’Ã`ñå š½@‰ ^‹1ù¡eBÍcðçr¢Ù3© ‘¼œ ºcdÅÜtv'Ì<¡ K:ÇØXÙÐ=É`-É¡0ŠbzºtÚr2Ì•@~zø±´2ýä7ù˜C‘ä2{·ø=’dF/°Ña!x7d ý›å-!ßñéc$åNpÉÐ:ؽ$˜ŠW¸Ï¸ºËeÓõêiqÑz¢>@K(˜n’‚Ì¥Š’ý>Ox®JNOÙÈŸm®¢™UÈ-·eòäžn]™p÷VÕÇO…ÿ©­6¼WõÏÝ·ÃzÉ®]Ì~!+»ú6ùùûv~¾dߺ4m\4X÷²´q Ì®‡dêÖ¸ òºÆkê® GÈÎ=/pnéë­Çª)í *X ê KW;òÒ›¤Éï•>‰âÏR+Ùq¥Ž/ŒÄ sçF]0¡o´_’_ ãu—Ž;Åv%ÊØ›Î„£v‡rpD¯ãg>5…Éó¸ÂTtMð# ÓU_µa:w÷wNqjW¿—9g¥R ‡T-Ü ´i•}lxjÒ韌¡®h.…ºs¯+¯$"ªÍÝÙÍo”ö^¬°êU¹x!üÅŠÀO’ 0=ö.§¡†0²‹ëjÌÄ"$È% yKz­t†¹#„+¯oY™Ý g0‡F3XÆ­µšn‰ŸFaŸ”daÎË3ÉNò V¡)&ýH¶Ù qа‘šTDñY^_3–³-ÓbdFݳM¾fý8ÍøígK4¥H‹HH+ס2jØ&Áz R~G÷ ® c'pü8Éyp™óé”Ãy‘f*I^ÓtZ­ÑÏZ1¬çµˆ^ÿ)ÏÔ<Æ®s5µ¨ÄÔu»°ãLæ E?¶‚5«K ×4ëÑñÊÿg|s2ʘ,ù¥œË*1êÑ»âŽg‰¢肃¬h#àLX -°¾¸jެþˆ…ÓÍ™Ž#‹=fçTŠþ(Ï€aŸ@Ou˜eªìÇÃèN Ã’%|‹ø¦+2s…àºAgIÒ;9ˆ¯y‰s?Ù9,Vcí»0BÍóR¥Ñú:ŠKá )çZÌ ðrút+4"iTrÆúÔ {! ¯­#€õ£ì}/ùuòMö¾×[Oî0·s“¬]ôgóðXD©IœÄ;$ÂÄÅ“˜$+Ìу]} ²$[ySÌ<:èA& ìq±d«¹Uû®áA·•P¹q3aÛaD ® ×`X 9Ö×ÀŠ8]ú×{JW.ÐÇ?ËBsQ+© =‡£O#=N®Æç˜Ù/«ôpæFï íW/}]fHz¥]2²mÂÄ[çªÖ,…Tú6 µ ¤ËQ¡saØW½ò+ËM¼3¬™IHAƒåçtF0•‹ÖÇä ‚Fê6«”"3n¶ÿCËÚY&Ì”ob©Ÿ£8l„å.-Ýû.èšJ8rúæ`Ì•¸š ?°“LŠi&Þãïa«Õóp“ˆ'ww’#ð"×Ò-ðúbÛ¦[Q]Þß¨â  o¦ R€ú )5-Z½Î ‹@Žóï¦èsŒ¡#j¨×ùGäóYµ¥MÚǾkC#É‹6§ÌßÀÝm¾èwHø%2¶ðÊêôoß¿oþ¿Ï6úhT#_Ý÷-’¼Æ4ì¿ÅiìŒÐ&6Èš~Ž=¬"Þ¢ 5¡¿åŒñœêb<ǃ¯r6Tõ°ŸÐ»°OZ6V¤qÞo›Hüšj@ê‹êÊþkRÒÀ¸“p˜«Ãd/6oÖ]⦠ÉÓâö/hÞÎ*@R¹¹½Œ ª­ù}[E3&þbWFÙ+3Áx #“rãhQŽ6VÁ¹„ÛsW φ2‚7qÚåÔ(É»u›ÍëvÐÉÃÂÃdp±‡ŸHêçê;klpŸÆ¸/xãñaØî…PT;\x¼&©€%í¨MF²½¨ ª,³–}Xù~faÂzØE£ÞØmpÔœo¡?[ïÚÉ^æ¦ÈºµæšÈuf¡ƒ#ÝqÝäåD b¯¶RßòLG“ù*ž×E‘d¨``>/`‚¸~[@Kð¯uä„s÷ÿŠONÇ~çÞtÊîØz4ÃE®]HUR熵<Ó4|О‹c6´8¬¦“»]ZÞ«M_îu˜¼ÄF¡ ÝÐÍ¿œ~QÍì8óEg9Ø™ãò¦cBaÓ—?ü¢gœfC í§›w'”ü¢+ݳb–oª)œPœ!U¼C]Ì?¯PÚBSŠÔR0-H yD’13Ùv*¢èh,%‚û®–H¨ÖóM옫–6ð¾QéQ”Ϻ˜TÌ*@ ¢ÈüÀîDxd“uºÛ=2{ö…xßÌaO‚*Ô 0&¸ñC€ÜÈl™§.±ƒÈÃÄ.‹Î B 0MœTë#¶ËPº9ÊÂfÌ ö…F@%`6æ›;XewO£õBÖaêÍ©5Yi¶H HëÙÛ÷ƒqu‘½ïõ²ór\RÜvê"oB ¡Ì€g*}\îÐS¬d>i2@˜hÒ¶À‹’“D÷¯ò;¹¯sçXm¢BOÇf9O^Ò“ïX;pK§ü+ ½B}R½+Ä(m8ô|Ã`THÍ¢¸|.îD”4è.Š“[¼xÄ4«w¶Yy´$`o*8å¿}ŸÜ `ýH»ÙwÕå´•ר$ØŒ ^A¤jäxQ°ÑÙÉÎFF@`Âïz þaHµ6W)‹F¥ÅÒ±§8M‡Æ̾ÖNZ>šA“ä-"!ºê“2Ї0€é1œ7sê9GÒÀ. ÊãhÀEì@¢Ë[¸rïçE ”‚¸RnõïÌKCâ,zæU£NÐ¥¼. E0.êÉÒƒ—9EqvõàÇžÆ4acW†Róõj~†¶(šÚõ«pÈÆ²$ìTò”·Åƒåpë4Ò’¬­dkë81µ$në™Û’¥ÕlvKëÄ‚¤;í ÊV€ÏÔó£rý,”Ãt§µ®ToÆî,’êM—mKœæ3Ô¸“ éFË0zóÔªÞ áÅúÐéÅ,¤¯‡\úóï°švÄ̲°Ýób§:Nôïý¤}Íw¸ƒ›©$û%±apH>tx“··ëß÷\ÈûžVx·/³ÄÎÐýŽs uìQ?vޜԤöI4ð•?ö" éE’‹Ø[ÂLG™Ë›Ž™Î)ä 3î݈Ž´òn¿šŽ3ÑÕÈP;’ž»f¡H1·¥Àó"ÚÊÐâ°âi¼\‡è‰¼P ›$oŠÒX ðo²i2p®Ç`ëÊÑ¡E~Ÿ$gœÔ ‰-"í§ýÖ#Ë‹c'HõÎ2×Þ>¥›J ýGÞƒ9ìÝ„x€‡(C»[–gŽt¦¼ñb–Ú,1ÁO‘„«dRä7wL ¨¸¶íÙ)¤†uâLÞi¸xÙ B‹|3Wl0^_ ´}¢:ÝDè`ê®  Šês!rKÆå3$Å£5¨ 4~ãCùÒUü¡ÖBº%‰`Þró¤?®ßªþ´ß;qã&Í·—DCtÕA\8¦ RÛ^„¥NŠÄOKïWfÞóþ#VN/¢º5XÊ!ÊžmÙK£‘§îéÃ͆û#’ ¶%oM;˜„îø°)ò®ç(³‘(^ãžô©ùsmDž#Áê^u} ¶µËÒ£1#ñ™^çpÌc•Þù !‹Ðä0â.]BêÆKÍ!Ú~ ›T=óN/¶gži²²RM{<ŠT"Oy˜Ìß‹.‘uÌ=Å«Ñiù$Ñí[Ÿ´lo:™&–&®Eø1q4ô'fì8His©öü R«åø^zHø‘$­¸%jEznªR祗¤4Ú¦w²vë) z…´%=ÕŒ§ã.ç[ÃêʦEA.2ëÒ7›£î*Cn¸å0¿-÷@ËVߺøÈG*6ã}I`àoI‰Ö„Z´@P@3Eï¡Ý,í>ÙÆÔìéÜ›šVW‡Ñˆ3ª|{l»œlŸ‚”Pv×ùb4ê׫{û²ïk´{$×4ÊeÜi¤Ùšè 'w· ¼ž¢ø4:ȶ]ÖƒVg¸b¬ãyá+<=ü][`¡²N†Ð2…?‰µf j¨?öh6ý¹m5d’Ì"Úb:*ZG`tªÌþ™Õ@µÑ®èÔç ȾÒWáÄ™2Y;õÑ+àZψѾÂ0"]Ø×‘‹wÒ÷,‘Æ(Ãî‰>ºõï‹ Ç-ÂŽë‡ÆeåµG¥ Z|þ%œ| Ö{KƯÇlq”±Ì8tž³Û¢˜Ãø† „:nþ=ÎÏTxŸ°Ï‘³s»¼ÞåVÎ!Ÿ„ƒé^õ@ìEI¶X´,ÕÒ–Uicáù£Uúð,g˜´î1¶™iò.&˜³é¶¨G¹:½™[ £kS„I<Ñr¼Œ°ñ}QW(»\¡óƸ¼(g›UóÿòV!!§°M•\<óèX,%U}·íÓ±k†äˆÎ¢ù¾R#%·[ùütK‰þ"t»,™ª‘‡¼öß‚²é¥°°Œo³ÌA_aÔZ͉FG-#óÑÅd~"N<ãþ³bÃt®™ððAÆOFdãŠf©ud?ŽÂdðÅh›mg½ÁGaF WAæC¬µ@n¼¥ äi?Ð¥Y>íU"¢½Æ–ØðNG@ŽY¼ €ö‡‘é ¤.þ¯³ÄÅÄâ³Äÿ6®Îá»,‹ÝÈnÇ&d“J ­m9”sÏ’=@æ¶°¸’°‘á nÁf d…Ý’ëÍíÚÔ}Òƒ—“ÀÇ!‡Pô]qÜOí³r‡Ü‡k}غ·íÞ>T'hnk‚baN«ÒMª†QÞŠzÅ&Z¥êpo ƒî¶¦wÒ0™ˆ £x8zO²i%`b=ùŽFþ¥ӧɈ:–FÒKhIû”~êÓêJÿ]æµj1ÊÄ—Ñ6§g¾/ZZ•ètµUUëÀ*·kšøe«eÿQZÙ’Mó]ÿx>qv‰¦Ðn¢µ{(•ÊóÄsÐvðîvÏýÖÀÁzÛ!´ FâªÂšßNm[G¼!Ñžë´Û/åI½\&$>×(Ú䵉Œ$»@ÔÊ™M¤ga¡]Ýäúïj÷0tI¹©Ê1EãžO0Ûµ¬ŒbV‰.¡pÇ!è”69TMfmù}U ÑЉ*1 Y£‡]ANÝ)Fí0–™v7`Ðcºcg66-nJ<:=ðÚ°ÐZL¹] H]¡ã  éz8à;g´ï†”SýïOàÖ?>šü¼e¿’—Ü‚Þr¼“˪Mwò4ÀÓN™;ëË®xû‰ÈKj ïL1C FQ±•ë¦pLÁnG.Ê%[ñCÂOrl :´/ìÎ/ûFFî›Î–34ìv`”&¥¿¬Kß<‘dá•M¡ég xœ¹Ä¹Î6©˜ež³rÄØÆð›7ô¶ GGôÍÝ(toß^±;îÛ»ÁÚ½˜ŸÀJ_Q‘ƒ“÷ŸÝ¼ã¶ç>¡ã\öC>ߎØÞ ¹ +4=[pÔíçÌò±®lxqcÛ òû`l¹HÂà]Åðð²p¯¿fî†NÖ€Oÿù¹¶›âúKçJø­(™Ñ„º‰'ý D:Æ{%âè`;’JqÁ­½ÐvsfÇ3eÔ±× ÛÁ,Gݰ‚ôúð/û¯玂-ïeš÷˜íÉ<‰†µäÞi=©#:„ XÐÝžêÜMÜÇ—{¯ž<Û;ᾟbòñDY0#Önz6é°é@J'BîÙá±€EK«a½à­2ç-z‘¥êŸu)¶_>GSx)¬r÷,QlºYþ¶œ³îÿµ»V#µwQḬ̀Ž´$cKî']©†¼tb ºF,ÖØ éž¼¡,m£^îÿ׋7ÇotÿŒîdò³õ`ïåKÛ¼j?Ì¢mÙÓC{ÚçŸ@Y+u±fÞˆê–e™¦Æ¿Øæ/•m.>iòô2‡]ŸÿàCoÛÙ¶5±MШ*Ù©%ñ˽\úN¢R]·œWÝ;íö·Û½ý¹<ïÀÙ\7KÒ©m3z3uÌi= F6.^€È¡¾[‘/›ìë”ò¢k©’ËGJÁòðô©hi€^döøQ™¦ÝÆè•I.­êýˆ½à—¬ß37œª¾ƒ9/R•¾£(¹l£H¹áÛM“´k™ o]µ³B³xAü¢¬½9ê¯ET‹æž.ÎÙêÚ;ž›E*¼.›nµ^ŒC4ý4LW/ýZ®œÕ­¼#¬“iÔáJ®×¢ÿáµyÝ)¬¨Øeº£ô=H3ŸuJ~ù/9ðç™2 ‚NE‹¥Gupr’Ùµ¹·ZÇB[ß©åïÜ(¢]ùhuðÌ?Ù9íVî߸l>eÿ_ûÝs³Ú­Ü?òldÚOÑ¿¯Ž‚:IÀVî §ýR¡X?IŸ?NÏûêjg1óií§¹²×äM2)ßÉz†×ÀïŸÆëä—ËÁ7‘¥YX[­K‹ûߤ("ŒY‘÷tèçQ>Í1ë•°É \MõÀKaëª{`,CºÌìÌ£-Mi'Ò¶Š£J2;½e1‹¹ÛPÛLµ¬Uj±hc“i·Ÿ`«mW;C6n™½˜Žé"ñ¡÷úl~~N ED$s¥Ã  fÊüX“ý³ÙŒ)ÙÏü}^Í ë[¡ƒ’~±ÅQœà–²»±Á(MÝ>[ÔPî5÷Q3ʯ ù4§«ä ï´×ìrÞ´˜ÒC~N²÷ýmVȓ߂s×vªKEtó1@B@‡Ç§¢ÔR€Ò½££´ÿyï´£Ô’=ú"ìÑþ7›ºoûßœ. èËÐ7ßî¿YG¿ ”¹2ŽÞ÷¿ ¾vP¶$  Üvý.èùó•¡tÑß~Ø;õ¶áº¼¸œ aƒ¨FèÂ1v©‡ÙçhïS›Ü8ŸåK¦ª£mÖ³mØm@탲EÌêÆŒ±œ^Ïgj=Sëc¿ZKš9øëñâ™b½SšÌ ™Lƒ¹…¹a!FG‰ýGîÿ jwý¸-µ¯kjÕÕu]azÐ ›.ÇAœÑZ&]prI!rÓL9´¥S9}O/ŠìaÄ>˜åÄo§0… ,c"DWVtǵ¹­”3Äçf°”è€è'žì{ÕÐÈguVªäí4zªQßY"MÒß(lž´.x¿µM¶säò0ÝžñÊÁþ¸ŽÞž”›z­RÆzò,’‰ì:SmWš»²ÞÀa’wR9%í%,;©ÏªùÌÝÜ,9ý`sbS½ãºÄÆZø#Ó*©!HÃæî꬚42ÃoÞ»Iš: `XŽ]‚¦rCËÔ;]Ìá xSûkŠ!é ?@\CÑF?´±‡Ôj_öH. ¡,•éB ÑþÂeÃU¡\-oããª0^7wvƒRÚÜ/xáqþÍüº¸T<Ñqaó„»Ó‹žª9bÎ">\ z? ßCšŒ¢)·Í”o™9{LdKd½`›¢JØN{xÓ0*ôÉ®†ÁËý½çmƒÞà)#7ËÙb êCl¤¤¢/Èi»>ùDëAYcƒ‚ÅCÛüDVlWí*ZS˜Š˜9J3┋O È%KÄzš¼kßI6ð¸4‡‰k™ïÔƒÐâ¶Ñ.ƒfoð¨ÆOºÛép1;1 »yjØ“ÍÎ9kÛrŸ‹tgU¶ã-ÉpƒÖR›ÛN¹MÛ_µ¥B!#?Ñ;su7»ÓŠ­À92™´ŒÍÈ9åbÜôñlÝW'Ú>i†JŸ ¹‰ãi`’>¢«I]=\Ç~gÑ¥;óúc÷ç“ò­é ?ȵh_ƒNòÛÉx™Þ½VðÛÆÇ Ú4Dƒ},w´of]¨¬x·ƒßÙ¥Iq}Jy¯øôRcFÌb<ÑmÑ!>ëûž²#9yМª0æpžôZ‚ìûŸj±©ˆmgXKƒè˜—pNZ›úÉpn>ëûê|³CX'·Õé8ÄßJK­ 1-HY¼ tà!Ý¿ºžÝ)ñQuH' áŠ=Šs*Ù¹t´îÎ1 ´¢ZQçjçßüõՓ×ol{úÊLyRñ½™ï~É ^»¯n9ä„.Ni«¬®’ÔmF]îÁՀࣈ<“‹»¹qè-È‹™rÈ|?Knó’tÉZ)û ÉÓ”;T01Z›‹L Þa0ûòñM>é­?²üïÑ£ú§Hl±>TïòË"†ßÎòÑ»øF~Vâ]›ãyhpùêö£wXT«z§a “'!âÃÑ–+&Μ–jÐ ÎmÙ._cAµ(ApìM³ŽW°0mwëG1•q¯VwHÕ»ð: ÅQfôÚ9>üýƒ7ž€ã+|"ž]ê´ª9¢Os~^”QïV€]\‰H1@>ݤ!&WK ê@Ù§À0ˆ€0Šü¢ØME`­Â¤B“FsãIp ƒ„©€N[¼™’R4&5¬ëí Ê2šðzë$ÁšÍ àùÃ|L&MF7”¡†e(¬…($7§¨,3ØùÎÊaé$ÓRvÑF¤ƒÄϼÁÈg¸ê0u„N™KuâÞÞsèd¤]îðGÍcÊÒ]sE¶yHÎÊBðd ŒS¶:x¾êYØ_5·À_Ô_•,lãïè±R»Nàý… À ·¡ÈÀ§à >©2)ÊÀ !E¢¾Œ¹¸·˜ÇJ¥^þj7²Y€pÏ¥t{œõh×Í£a[a¸{]–»î⋘¶ÄFµ$´x^à0´úë îñ»8:Š$J¦úÐÄï)„U¸0ò8™œvÀ•Ôö*¬#J‡íøL§sàœ\¦(ñp¹“l÷ZòÌ`:•BÁV•³š<êù‰Šeí£ì}[ÅϱͦšT"ºfiH_'–°¯-Ý52ãY›û"á» n{:YÈÆ±“Ðù2oZÒê¸~þjc["r©üDK±-x¹“bú6}мMUÆÚ’ §Ä`ð‚DûPí‡f#ü1o³J«§3D, Ït¤¤è*&ç ?RK6š[²ü ùî˜8¢É»op'.’ÑÉ)Sœ—©©\íp.‡° ²nðÂ1÷_…VŸ¾zµÒP;Ö@d¾Êƒò?VCodv|І«Y,YŒÜŸúò@Ôàa5§>/ͨ.¯½û§=7Å‹êηŸ¤še8rµÉÊÚÅû‘‹QóÊÅÌg1ÄùŽc×°ÛIu§>ó€ùKqrºø‹У‚’i»wG*’.Ï”Ñe$Jœ Ñf£p¯â£eBŠÉ> ­2Bf9­©ˆÐ¨dQãÒ§ ÁºÅè2@3·í±…•êP›Å8íùù›ÐÞ.ÐÇD…G¯Ìezöñìð˜ÆÔÛîµî§+cÚâ.v¸]ïµÂÕX{’°ká=îFòf~}]Õ³À`|'¡$ð¯ùs*«WNMœx¹ùYQHræ`'dØÑ-g\y™Îñãì8ÁŽàˆL97ÁíR¸iQy)×Õu¼Š/»k8'ÑÃßmÜWGl®®Šä½˜5äx×8T c†ª#åç`2«îï÷á2IµÊšì Ëñ‹ëñšOÉk˜X5¿Q¤KÿÜŸïȹú¹øÎOËop¹ú1Ôjí'«ðGZíd1u»§™ïH(Mße’+ê=9i?•O+A KÌßj• v}úzOW¥ÇqO¢7?K—fpïå˸g±  "øw(â¸ÀGâÖŽÚQ(–['±+§0[ûR«Iñʵn'¤wìÞMÜͤœr ÒfI:vme?ÙªCk¸ŒFgêäùx$ˆ¸Æñ£¹Þ±–jâvÑÆ[Tªº~ÄA÷’[8xàqÇG7ìÒ!.‰[`ØìBöÙÚ/•¢šÿ–=78*> Ø··‚¾±¾ÐðÃp¿Ë¼^×eUòzq¦:Ê×w×qƒ[ÿÕnhZ*`ie,P·³Ð¨Ýw‹±ÿMÜF ¥/Ëi<(€ÛF¥–{KXŸ‚­FˆžF‹&rÙé²ÛÙµ`‰†šóû®PãµÄú4e Du†Žü±µé-ImɵÕŸ­¶<#ByÐsW"÷È]øÄËXãsÙ5ÒÙÕ$)Ü[“·4ˆÛ;DMyÚ–o÷$ü\‘&qs=Ü)í<‹ 42°A—Dtø:Z_ÆèªþâÕQ´¾ ÀóçqÒq¿C"Ó7VúÔeÆmlÊiSö$/: ußNÞ_p‰ßl)ŸUËéöÖÊmÆsØ\DXéN1IyM!×5rX«5ÐùÆ9u†œ[—™Z“,Û¡°¡¨œã°/vÉla`?úšM!“¡xÊ_ÁÓ>?P–ÉoèíÓÞÆU!˜iL×1Hz‰Qm®.µÁ§=RóBiŽ–½Œ#h9,±Šr~äw+A3I -aF ;[·=â¶j•µ^îágÙ›­O²).½…ŸOvÛ׉–¿UpócoýVGÍ 'Wý‰ž`CÁÂ݆º¶¶LSW¸ ÅëGjF¶À–÷ý$žY-RÜÜlQXBœ[X¤×ˆéчPnˆ‚0.Ù‹,l"! NÌ×ÓäëöR îi4ðZf '•.·ñø&²Luàå®ÊSšGÒF!Mï/òZª¦Y‰ÏÇ>£µ7½wõFøqï§uÁ>‰B¡ð Ì…uÑOà×­Tma/C‘Uõ]†~Âö=b„à–Üù—Âãpˆ·T8#/¦l—*€öÓ#±ïØ€…n ¥®®<°XæO}.—~m`š¨ð 9öpˆýÜ(ƒÓ5§ ýI8º‹¨Zj8äì»›¬ ªóÇëIkœ'3Âwú^ Û®{_À\t°Ã Eù!ÏãHü¨i([òvýNtÙSòqñŸkÐ'üæäáé©ÛÊÉöŽŸ¹Ö©¼q•×ï´ß ¢1îa+ž¯õ©oìK#R“çÌ·ö8¿.ç£TÈã(¥cîkÀús¨¡èî´.9·Ÿ {77pvÙ“Ëû©÷ Ú`mMŽ•å¼ºÖmoÍÅ}QÑS_ë&•ÙFã<Ìé×*]A趦ÏÂu@ Øœ‘6U¡…¼fálê¸sé“„¦¯æ¢ã6]bÁŠDÒN¸AE‘£"k}eùª0[m©ãgIž…hv†,W/ 3|ó…]œt–f<=x™ª÷¦"™»5jÃŒ˜”3ÝúÀ¬¨Gy#½c+– ÏåEÊ]lÁI¿/ê …ZÚUÆåE9SÑÃè/Fû¤£ñsÞoœ˜|h¸ÆÂÈÏTйQ»ê‘Q_%ΰâùV‰ ûq®™Kê4©TÛD‡á(ŽYþí$ߟo}úvü›ÿ—²¶­·¦Ñ«sUÇðkõ#íÈ__Ȉ$@ÿ§¹·õßqLšìÜqR¥×KQ)ä? 2 ‘„ÉqqUé\³×À€dÐÍ+/ø40KÒÝô7¢Ôv\Bù  ¿%¯¡AôcPš/ûð*Ÿfï{Áã­–ç?O~ÌòÉ„¿Ýæ“w | K+Çæ÷ƒÕª½}?hiÞ¼Ép0±÷ÉÝÀ8‡,xåý³È¡ Õï»ê#îźcÆGqt;†Ïqv½é#âýîü}”Å:|”|3°`T…o¢XS(þzë±Fr*äÚ(Á4dùü®•dØÊÓbŒ°“Yc_ºz™¦ ÆYP 3KZ阊N8ÙÛ£ÁÛ6¼õ긺Àïøõ „î8Àv¼ÉÓ¤ãF!Rå$—&ú¦ÝCo±)%2KG>mì¯]<‚‚Ŷ—\Ú¨ÜÒïµÞRÕz䀠€"üȰ–ažØá0í\$á’^׆•ÄR‰X5¿„*â“­WŒ á¶¾m}ÑoÓÒ£½ëLIüõÛ·mÏÉP±}lê~­;„ÈÑ ÆÌ¦ÐUètRgÙ;  SÖÂp‚L°«í$:¾íT}¢:–ºxïPXñžò.pú¥â ë¦J:Y¢ô®@¶épx•—Óá0Ý1HÉ,~ ¯=üŠþÓŸÏ>Ü~øð·¿ýâ¡ÿÙVÿlþè«/¾Üþ-ÿüRüüËG·ŸÍ®®?›Nfï¶h‡ŸLŠš~~ÖWŸÕŤxO6ëƒë»ÿo#9ÀÀ„ x¿Ì§s<,ƒ`ñ®œíPÊYuuEåQ忱†Yj¯ïHœdO{É#èÌ´ú0AÍ䨮0¢6Û›Ï.+_öoA:ÿIQN“¯ øþïåô|‡|4˜¿{ ¿}ýr'ùúr6»Þùì³ÛÛÛöuPÕŸáÛçèh^ŽàÜ‚!AQšQÊp ÎþòÅÓýƒ7ûƒãÿ:^[Cùæi5.H}¢ÆˆÚ‡Zã×%…š6 Ê# sÝÉÆò§ëªÎkkì;ÜÌ@T“K| °&whš]—5K¡)|‹r¬­áÝäì¶B“‰øç÷ŒÍj>ƒãÃ|ñCM‘­?R^üd9oŠbÙ[(oÚdÄq°•ˆG¢ÝÓ²f~öÞòãD÷“êì»ÞG}ÃtKÑ@‚„"ÜÈÓ*úZ£•^Y“¼x;Nö§3Êì .º¯Š3\j_¨t ÖmU½½$Õ5Œ©|v[À‘šZuÍ(mñ´b$‚$ÅöÀk+´Ö C7e5Q!™õ>Âá••õh~Eb8Æð`óâRMT‰’)n_y>;ßú·¤˜Ž* a Ôh…Z‹me‰¶=ÌÂt6wW„_ø—P ÿúØÅ šÆ€,çRž†ÍÑåÙøå Q2ʧմ!1è@àtj0}#"ªŸUãju`ÎéˆpQL \¦:õ-Vv¡©=Dý<²&ÙÚÂ[fv%MÞ_MðÚý†¿=ûöµ~êèIÒg{ÇûXàéÞëg/ö^ªjÀ\Žñë«Ãƒý¿Ò—ý½7ß¾ÞOOYÕ®¡;¡*ªn˜rù¨øGt¾>ß{úâå‹czüÇ#Óó8 {t¯ŸÕÅ Ý㩜”i¼IÂÏZ}öhRQ$Y †#Bõ’Œ ŸxÃnâS5ÈÞÂxDà$*œì*DJ "zÿÏú¸9i5ǃ6q´>îîÓ\ᚘØa’Ô ÿòÎÙÁ# ΑLôõ«AË Ö;•¢Éw^Ô m@%­ÈÁ ÏEl{f–~Ò‘Øi¹×HÌp†”švÂöd×»i’¢–{–_8i1tOþ …µUE4Öš‡~‚ Pج6•(‚ƒçšm¢å;TÄÐŽ4jsÕ#O@E °îêJPF,ä´³£ZSn ©0 D vï^HuqÙ•¯¸x%:vÜYþÚ  âäá)KŸ_ã¾ œÚœe«¿XžcW\™úíö‰yÁ·^GƒZïCQ‡¼dW”œ®Ùu÷‡ýêQÂqO› Ry5ÅL$‘ÌÐ\ªI^¼9)uF)Ž'sEö/¦ÍuY³¶·xŸã%³8??«f(F2U:Íl ;\z5º€Et™O‡¿†Š¿6°9æå„µâùÄ–AJÅð‹.ÉëcãÍ#k4® ÚNK8Õ®ÿ:{{»ù‡Þï•ÏÙÆ´¸ÅÀ¥@EPªPAq©+ À„‡ÌÅ X•ãˆe§•Ô¥'Wƒ‹ºš_gÛÊo©E¬+=ì%$.ä¤$ÔáÄÚ~73oÒžò^Øeê…©³T”²`q)gž| •P Wˆqç…¶«qÎåÝ M†CJ‡†!Kè‰Rð÷€4¸êk÷¤r5(Ÿº»+:H³èx–ŽÔAXo#wWŒë«wC’ͳY]ø¼ò8KˆÓËùôbf>¢$Q ÕzŒloÄYJ•±’÷%0âø(ÔC/оqÌù^ÎóÿÃUÈšä)7C§—ó$Sµû†Ikpú|…6¸%Û–ã‚}ßÕ¥Û˪)Ø$ŽWgE­B`dzëªiÊ38[¡¡ð]PÔW˜4†É„Åg¥’7JXª?îðA~$Á¦o ¹Ã"@§dª1k¦F/þò‰ÒÅ0c•7±–0‰º±ÒÓø4g¦tÍãËP[3æcÝ8+ ¨9Ôâ¯`Áx#(V:²èñ¬¦|D8¹­x–¼cÌ—ql‰L:l/ÚŠuÞ÷ssˆyÓgElÏêàñvŽàu8@µR¨®gÞ¡Ž>‚H ”Ûr:®nw¿„i¯aáí>Œ3¯Fù­!’ù¬5ãMååz5~ÔkLóS$‡±=É_]*2…3[#õ f#ë¢txB„3prÙÆ8E¸Í1]1îu–¾ 0 Ä✡°6FÙÈ!™Á^{§§Ý¯ÕðéX ¬ 9Řá¥r0ÊUðT%ýñoTDVá€s¢ÀÌT¼#?¡>¦{”0¸7dv º¬&c“ïe:§Œ÷˳⊢ȪÔZ:³V|¼b/ÑàŸþ3åí&©>¢§®Ò†™È»âŽuéÀÒ³D2¶þméïÀ©ñ+ÏTJŠ]¶²%Y]R—Óá¶³qìv·/“ÑE-|¶tFÚ^ò8y$7s¢L/e`#ׯ*rÂ#=5{#ƒEÿo)LžFªYÄœ’ä¢jmŸ¦°÷ÆËF"MA¥I‘ßN~§&"ðTŠMKÖUS㷹˯Â1mwŒ©jÒö¢nU­#ZXµŽM×#š®ž.104Dvç±uÖ…g3{¼ñAÓ#xlnû1äÈnQŽžUQûZïVjl;÷Ö9oc+=-²í&3ÍÛç‡jÔ'¥Ð¼Ù%EJ_‹ËÊ:JqømŸ³?'m{ü‘ÎW±s½G|Dw{8.3gÓ£X#VíD‹7×Q’mÁ‡WÞlô‡'Œ”™¼YX½¢è¨n.:FE™¸µÖÅŒ¸ÀÅ=–•¯DêFÈ@ë• »á &‹ð½öÓÞ¼ÞñO§y½ór# % ÒMXDçOI÷jó_r嶺gáÞc#]S|Ñ!»™_ÊÌÛª™Oá§Ž­¦^oQ%üØVv#:Dk6íþ ’ ÛûÉú]5¯™ˆŒè¤W!#jr`·Yí¨º˜âý€¾þ4Íj|TÕý°Q­‚Œ*ÄEõÉPQ-‰ÊC„Zºxm‹Û ²ñH¨úƒ]Μ×}(ÝÞÄÈäڃƔò!柔: aE"ÅîÇ!v4+BT ,ÜèŠðÊâN¹‡(¡&€¼ÄQ€aÉ÷;IöÞ wÝUæ4ç}P’}oåµAƒ î@J¾Þ5‡—é8é¡we6HÀz‹ïJ~»IHˆRpá¡g FF¢ò,Ð_»Ö9ü¡“¾ú ŠÛ¬ý†jæ»'³AK³;%¨©‹Eèsº£d˜Ž†[`Ư.ìí™T0XS°ƒGòÉ ÎÎuDL<ìõc~?ÀûIVeëS­Ê€IE]í pÔ§ð'ÑßS½rµ‚/éAx'ÌùHïLI ~0<¨{hÏÁÏ4ä:„ìɼZ0ï…PÕ—Í$ÍÔÐ÷—æ6¯/°‹L"ÖѪšlaA\|¹k ‹o©Bd¼÷¨j>!øŸžŠ,,è©ÊPßfX³ìB£¢‘‹=‹—ñÈQ.ÝÐ#‹ë@<sj:æðܯ­mÜÖÅUE2©ÁQaLcø¾:”½š{··RÅ5ŒÈ×d¡ŸÁ™ ŸÀ!¡·v’¡’R˜õ“æïV•»4ö¦˜à^kTÍU}‘O•™ ‹:ÉŠV³hÛ¶a”åLÒ$„mIP¥ÅjÀ&ФýJÖËéº:•¨:Ð?Ö̦ (&#ùT%ƒ§ë$î/Êß“*çÄíʶ„R–o]ÁØë;ÊÜt–#·VrbSü} &a¼mLBî¼á\ìÉ›o^&ëoö_î?=fì@áZR4¤ÍíÍÚ0¤#žþ}ÒẪ*@! æÏWÀ…¦“ân¢K ÔÓl}‡‡µãE@´µHþŠ˜= Âáõ¬¼B©AÝÆ¸Õç(  ð ©ê¬ç—ï‹Ñ|Vd€‹¥·‡‡^jqjd‡õžöìTßžU£Ïè{@¼/Q‘|= ƒ·9Ý+»­ê‡ü’­?ͧ$Kœÿžˆí|’_˜ðIåÅ-ÃüØ’®ñâÀ½HªÓÁoÞž•Ó·gÙ~õöl°‰>x³ý–¬¿Øß½ã¬l4ï¡{ou^F“ŸuYgw=ùMòÅ—kz3E †Ô©HqüQŽ­.b§)Âù¯‡ð¨É°xÄC6T3Ù  ¦UË{îàö—Îkì0Xö¹Ú!e¢¬Cµ‰×V/ôâ DÝŸº;¨Þ=wÓ‘`þz”Ÿ`mÊ jjû $$!çô-ŽÂæõÇ[?ìŒÉ·fù,RB)²?ôü—N_u¶£y±¼««+Ï£eÕÒÃEYxòs­]þ§• úeLß\Ô4¼%iîA 7Îaä‡íø&2áÞl’ƒð•:ì…zo–¦ó} ‰\957µ»ÂŒ³oõ’}¢»°Õ¶à„âÛ<‚S® %|Ø2„û#\`ŒŠºv´ÿúÿgïm—Û8’DÑó›OÑC€ ’<´©±,ËíȲB”Ç;—ârš@“lèÆt7HÁÝØØ=±±OpÏ Ü³ï¯}“ñß}’[ùQUYÕÕ(QsfO,¬¬ª¬¬¬üxÇä R:¢/Û•û$Ö/NÞXÛý;íÀ¸n¡]aQXƒ7ùx²(«?áÑ…Ú×yç×Ó8ãG0ÅQtK ýd®VÜ„~ŒSp«ÊùÜ=çª8ƒKÆ©}…³2½JþdðÑl81CJ­LWÃ>}›Ä× ÝüD=5ßÓÓ¬]³NÿN‡)Xñ¹~úbƒ7ˆ xÿ®æãùȾÌϠΟXd.“¸]è×å,¿LJ—2טtÒ÷î«2?{UªçׯÊ;@êÍ¿í¢u€zÔ‰~¿é>/¾Šf›rÌÑP@=ùÝã_™04Ëö>wâ©õ„; :k3kØü$åo{„“%ô‹þà‰þãoõ†AÐ]oÆ©Ë÷P²åÛ¨«å` üà ø‡cKÜðÅ£CÕ†bÍ1¤Râ`˜ÑoX%vÓ“•™tÈQÝ=ÑÚ53Öc?kÙkÈ™û š» w,îËg*^5€ Cª—ÌßÕÓÖèS"B™lCÊ£ýáŽPϺ;›uy•±ïŒ™¯]¬¯Ö¬µÂ¸Ùp¨ÒÑ×ójt=úþÙÓ§ YÝÇ;““øµæø:XéøzÿþÎÝû\``wïv÷–;¾ª³Ød•Ëë!œÌªt=m´Ú«Ø¸½®ëϺ¾‡ìm:¾þPa¾vë…üOáF´§Žz#¸ŒPƒ¶ï,÷Ñ=žt×ã q¡y4%ÀýÅX¾.ò\M*°Íç¹\¨RoP0ì:[{j§xði¶_ë‘9úTµ1é“[$ÏV†–Ô½q– ÏFí«[ZçFÕdÈo8 ¡O¡Ë¹Ô÷¾#Yx@´* „7̿Ζi`}Æ$yï #gÛÑ?!_y´@Rgíó"žNãb…%´g ÌN8¶#1<7ỖéæÚu)o8F¨4Þû邎åŽõY³åýèéÛoX¶É·ôä˜ghë3zՃ뚱®C Œ‰¸¦´¶üÓx6ƒ± U¯ßŽ?›7—ÅÜX’†¼8«øWßE[æ¶žémÉ£ >ºˆ‹Šƒ0 aûÑX®²h¸‹¶7ÎøoG«‡Ê¤a¯ã11-p°°, Í2´o½MJŠ_ÛOM>·Žfýì4)+ž€JZ‡&Ó×sîn©ÈÆÆ—vɶ[?¨¦åĆãYY©Ý"VÀ¬Ç+,zwnÍ»Ñ;—×ð /kû%6X)™WjoŸ*Ƥ1–;½L­ÃÇßµšŽ‘M,-®,Ç„õ¸€Âë‡Û##{à<ëj岓ä¹ÏwíjÎÂø˜+÷³›1cчٺWËðwâõõô-rоsìÞæãVq ;h­ªë±¨àU` ¢Ò38‘óN‡%u8€pD$Zá¿J‚¶è+=ÈvL›s wMVMs¶¨Ã¦hò‚–dÀ©–#êLXΩ&–üp¼|n©áNbuĵܙæa5`XÀåm˹·pãÓ¼ðöÙr˜;Ìw‚c¬§v­g$¤ç¤ô¯Bþ8IŽŽ`uwý宇¥C*5õ#°2‡²Y óÔ’››A öhÍ[z}ÒnÀüÖV4TUó·X õ‘‰/s—ð:ÖÔ¤GS=Bªv[BðãØÉI³>°´k«Ú†‰pW³v«6OGû:A…}³ˆ iõõô­Vú¼³µ½³Ìïôs/’„DðfÊjé²$ƒ¬>š_Á/t€n—DfÖ~Ê; (z€ ¤`:6U`ÑNÏí:Újanmx~pÐjí#ÒL\¡•”EÑzÖ.z”Ôl&­÷È*Ï›Öo’‡¢z‰Ý¡‹-¸'vHÒÑ|¢€òŽÜÿ'gg–©‚rËÀóe[íáø…U2z}šáäRÓ„nß¿G}â7j?˜ObšDË‹oÅÉ5‰90ùØ‹nɤ£ªß}í/ZR¤ á Ê7ps‡ ŠXÞvËáRhފؘµ©n#U€ú ·BèzßÛNŒ îéÛ¦ã;ÌHÑ0ì@y1å[/Ú³)ôŸd¹l´¶ dW9ix9¹AI?Æ’ÒüØàè’¿ ÛÈe®øxú“›ºož¥W'å(Ÿ%ìH SÑä˜/CkëP"{zë`Ê!4qáAdÂ<{"ñ¸žSv9‰Úž}Óñg–Å£>¯Ä;Ú”:¦–ƒl ¦û^×ÖÕ›ÒÊé2‡TR`Ÿ!ûðNRóOÐÒ‘( ±O'­%E¥«=EYS͘ðA*ÝPZ3…Õç‰i°M¶ê.ŽpZl"ŒÌµH@" 'µ”ñ»ß‰~YË<„D1~–‚ZÙ™íµÞ*?egí÷ìGcB¿÷íÍêî8Ég×èOﯾC2î‚æîGŸD½Û&z=+zòž² \-ü€–Ñ»è òx§Ü²åE•oæÉHqŸ/÷¸Z+‡Y¯.⻌’Q?ö…X³²ËØh%QÛB\+¿7WÇ0l1ž"ߟÑX4 òÜÙ4h û…l³¶¢uðV,Áˆk/–¥jØBDŸÞ«3NòþÝXÝ‹¦#Ø¿Þ^4lŬB54oËÛ]K÷ Aþ€º¸æ²C€¬wi›_§?˜šâ¶˜¦-åý¶žõ8ó‡ÊüK)³Nr÷eƒá"ó´>)_µ 0Åi±T·8é7ºZ’iÅ›ÞÞë„¶DJXûð¶–Pó~,áঈ»²ÁÒ£äŠYÕ$vÊ®ÈL¢;DbºÌb‰16(1dj ¢qZ¾ÖÉônªæÔàVÏÔ'dÙ}à`Ùwq¥9®W\3ûZyÂÇüÒœ†r3\u«„óJ\Mñ›âêL £Jþ‰!Šlt'x¾£ ÄÏ'ù©¯éN7:§œP[O²´ÂD-‰s³CØÜ¬§Õ²z¢—}=¸à¬sÔ¦´tݨu:hu¬=Q»éÂàù¹÷¼œÿ”&øbè¾8KÇ9<{ª„¢‘޽òx£ /2÷9\l¢5¹â`Ð:5¥—,tš/L™S,sZ+3ÎÏM™ñ€QðáÄÅåÊBûÌkí|È]õŠâõ«.ÛÖ…"ÐØPþ¢®õ¯¡¦‘±LVÀCºx8Œmx¨àY všù 3 ™Œjƒvº 5 @p~šŽ›Ýˆoo_$ñX›ûYoôGo¯H¯ßxsÅÁæ¿J1vå8Ÿ¾ÃX† ýŠÓì¸Ïc\Ù®B@ÕkºÛíSÁÀ¢"ë t´r‚fb;.ÀÑ ÅtGâøTm«‚xïØ ®XNÊií[¼¦¸%Zÿô‡¦ 4¡³ŠŠŠìç!|K}y1é|ë@7@pE™Lz\naŸ"Åŧ“8»ŒÈwÕ…dÐ3ÎÍ=ð¾|O¿´0lzH. [-'Ü*|p¢Ä»Ñâ„-pÚþ®‡×þ˜”'`“F‰¼Õ\:#q š¶ç)©æœ=Úb™µL¦-mX%nؾNÎস›ÚãFgç-¾#ó ð"Оƒ¢çŽô ÔlëwŸDxíÒ­Xƒ|q‘(XŽ1=~oix¡(Ú„¥]ÆPŒ¬<Ïe™;à[Å©É'Šê žŽ¸­cµÏX£O …LFÎfVhµƒ ´u†mÔû¹IžûÑ&ÕDúÔR¯êØ5 ᥚÏ*ºaâ—ߣE·u´“r¤¦5‡zåPMs ×8¯¨Ì§‰o g<_Uc0iemÿ@|nqP«öc¬lëÖØÏð4©bþŠ&˜¨ªIšLM(+ ÎA‹Á¶ŒÚA‹Ý®°˜Á"Ç'9É™›½±¢÷f¯p6\YlÒ`ëjðg›£‹Â_A¹èëÏu š´4ÅŒ–èÂ%/"Ø«ÏvZ›¼Š;Øüš'l¹sB:eã.ůàíB:~+›û¶±¹)5‡\Ý´Ç¿Ô"Ï=Ú¾‹ÚH’V¶hEåüì,}Ó‘(|׈BI(˜¡3hˆ'+PÑ·æ”5Æœ‡•ÃFTB%Ë{0YÕò¨R¨@6€“3ðó5èéù¼tXÐÓsŒéŒu,…/IËF4N °HzE2¦ÔMÛ'$€ªFÈŒó¬U©VQ,0ò™½ ŠˆW©ÀªTv¼L%C/é‰0\\ö¹b›œ§s¡ÕƒÐ­³¥8½Ô“fº'S`NÚÂÙ¢´É¤M­BÊ0¢Di½–Úavw´3²8¾l¸Òþj}£Ž à98”DO[ówxð £'T@>MÔwA(oCuöåk€gÞÒ™BÀÄtÆÉ A`:‘P±ë‹œJµD’à4¸ÜT_µìði‰1†Nìïd:$‘ÀÐ_Ÿ¦™ª}ëÐh7S îóëë]ÞÔˆyeüª8 ¶"Äû‰4_û¤„ˆ­Ä !Ô°¬²!ûÁœo½ ‡WH ¦[<¶¢ÃD3P Ý`DX’žñÅ’¢´t¾³á÷A³¡}+^XûóÒìþúãYU#J]C2ã9p®íÊ-ù§Â Í=·“óES[Vª5æ5ÁüÃ37B³ô®qrFEhp&®'OÆj½Êyâh3œ2©H¡Ek.§®AL´™½q PÄÐv¿ˆ±Ž£ô¸~;èªk°^C·b÷ÀGZx’¹.ä>i®™ŸŒ÷#mƒ”i ›J[“PùÉ¢mIÜšs%A:tØÁ‰¤ÂÍpõð1hx¾ô›Ú—~“ÜòÓ7û'©º7ûpµ7ûÞ`çîJÜÝ¥§ê÷ÝeÞìù¼%c2F^éÒN…u‚Ýfÿô_5fp_ÇÅ8z §þè‹dº{¬v9ë`›¾Qà Á‡úa<lNÁö‡÷ŽÀžËeJ˜¦'§0@b;-)*ÆÆÉ‰Aà Zà"RÝLÊ•GŠIsÐ?pãöÊÀë*=Ÿçó²þ–¡Êq€*ÎõoOk_$çÉ›™‘‚ Uô(WT½ò^ê|µ¨€|Å€Í[Ýk´[ß{éA@Ç[ŸÞÊG±‚̺¼5À”Ë@ŽY›RMø.‘Žö„ÍÄ BŸO[2$Å;Çœ<îÑ[ÙÌ;¼pо„‚…™ÌŠ…š¨˜’Ø xÁ¼s§ÉyJ–º dú8;S"@iü,!5¸šêèL‰@l²÷>¦9BŸLªƒ~7‹\‡@ªÆ=„N•-‘“´lò ªý,ÇÄ’1†'‹æNŽOµè$R"‚m±q­'¢‰a¡!ÀXŸ…cŸ†œâ×sn”—úv7y#%d’èbØ<µ‚<{œ€ñ7í8"9*Á!O“Ži‚-"ÎL‰CÛ§ Ô¤™qšŒ÷uм^ôôí¦ñÎT¡Ì€ Ð&X5" 5h¨TRÎù‘mèÖN3‘UŠÈmeqÞì®?u†áso¤8^šnÒÀà:œAPˆwÜÙø}Å䆰ê&zÂÙ‘O[©š§@dÒ;ìЫ¡¯_¦W1ÙJ•<ú0KUI¶sµØ ›j›@ØìÐbÐÙÇdG!q’YðÐE%Ž–renˆöÕ¢ñüÐ=cm¢Nþ$ùQ¢ÙŽ»^õÝ‘åO/ñ(}'=0õნ¦„È„dA#L”ðþN³ QÀÔ·Çï¸YNgÉ™2ï8cx ɢŢ˃Z5æ´‘L ‹¨]‰äЛ–gRJO $2vÜxl®0ꤑ)u+D`yOúIßê0OÕ1~Dpi]£>rG`GË¢ ‰­M®ãE%`”'¦ˆ&äÎq½ŠúköÆ zðM^f ·ÑÅlÝ&©Û·I,»ö¹‚ñ©“Y KÃZ±'É hh§ÓÔR¨ÑÊŽF€f½&šÞ×1©ý‰.ONÚ£ ¥%LÇÝ(ì}Ÿ-BN(›L9G/*ØAawÁ ÒÕ’U|q”ÁĨ¥Üœæãd‚ÆZê»åAzj‚J%Ùt-r?à0iFË“”¾½yˆ¼cd÷y)Fà]Åf Hí³i™f\& 䜥b]¡×†Z°ˆ¶”O5Z{ÆÀ[:k½«÷m„qþ–l¸N 4…ik4ЕÚó1,‚¦d@;c°kØ!=äB¦â ƒ·dìô±Î¶acE󪼺èˆÛBšâ´IØì„ä‹h~,–QgC,®4K+õ¸L&gvyÕו^‘ðyD™DðYcÝQô£Îüâ½r¤€e«1­¢x^å J“¬«×,>+ØÕY)YÒ®Ð}7Eu8£·Ì»ÉÜYêtòQNX{²BPTØ•“q?Ô¨Ù2Ó²Þ”oE¼\NklJ(òžd8I"¸ùÇtð‰]êÑ®µNE—AbŽ` Øâm˜r:ûÚäÌ!v9Ii:'d|23*ÍÆÉ›†yô‚&(x‡eÒšàdöI+:›ÎØíñ:.Y„‘²eŠï˜”Š3tÆÚî¶ #óñв“d]Ä)ÒÞCïqÎÈ}I<ä  _p˜@X%-J£[ÄÙyR'?|\…4Vì’^Åãyh¢Ñ…\éËNj^œsê“–ÉY‡˜ª´°e沩îÊÑÎ>Ý>@[€YÛ„LáÃÛÀ¢x¶Ôk;š"!‰ØZ³ƒLHü³=`Æù¥ÑP9óÞÉ(Ió˜þ)‹fñ_ïôG’ìEä0F+!²ª1gŠ.'bU;¯ÜøÌEVE‚T˜Æ Ndjä|y$€¹¢ö‘~áÝ% Z fkB ¯iA¬N¼È$,0ù§Ad{4‡o}ùIyôIyŒ5Xš÷{[³»4¬f$ƒ›#nŒ¦3³DrŽê&èmU¦¿w"q Ò(~Ôþ¯&œn\ÿ„ö),= +­ÉÙqظàFp°ý»ˆË‹úH)QMø17îgƒr0ÍÔ»¸Âha@›BjùÌB9@`m‡ü+¥I „³¬«Rü(ÚéD5r‹¯Öå.§ÚòGMÁëóW{V^‘ÒÊÕ‰6+éêü²n ’¦ÐA¢L)µ‘ JV…ÁMÁ®iœŽ„$ÚcÜŽÒc³ZÍ:l›«Ñ%…·\œ“~Xà®’dÓ‰–GÌ(}LûBá3?Z3dBKµ­‚›Tàj ¥ÆK`’5µ¹½E£“21?âvþe"«ÍÕÞj°óôÙu­Çç‘層7öo(]€uex@Ã:ñâHŒ‡ŠX«BÔâbJR€lÊ^ßUË?òqP­õTíDµ˜‚ý'…8åÕ[ÄCä Úƒ½ak8 ”ÈEz·xCE÷G/&IY’bLjý˜‚'Z±,K±1üë¾"ˆÜ¼û €c™A[Å›ÁäQö9°v¬¢ü ¶C{•’ñv”?$ss%YuM‰L¦”´ñ¦½Ó¦iÖ6‚.×V",¶CaW«  ×øð2®[ß`j{ù¶F]>‘ÍÝ=œàÛî2œÂÎŽ5¹- àcêð@Sü±º©k°°5d#Ù‹~ˆ(’dp…Þ8»œ &TF¤à›Bæg­3|Ž(f¶áMÃ_f¾Éí‘ܫņدÝ+ÕV)73ÍŸ¬š=l²äMåT¢IÕÖÙÁí¦ íÖhvÖÙ•MÜ0¬ÿè-.iƒíÁ;{Yv‹[²P1:þú&Z?«Ç•|ñã—ß¿8yô›Ç~{xòý7ß>~yè$YjÞfõež]º¯:n£.!Ù G?‡±ØYµI©i:I0“V–øSœ¢ÝMsÿÜÑâ3êàÞ"ñ è+꛺½©ã½=´µ»Ý[¼&™îoì¾~Ÿ›Nì ´gÚì ŽXÞ³Íi®º”gÐnõc˜87Øóí¨k+„† —3Mg+¶WoMU1ïAsE"ºæÞ¯w\]·ÓüïýM¬~ùÞ¡ÉbÕ6ŠÓý§Úóí'Ö:1+òïFÎh˜-¼‰ò:›Û-YSmE_Î#€_yFªý[·ÛrìÁú‡¶ššT!h3ÔdbMLiÓòÍLÓ U¡5ÓÈô[™ÒIòƆ¦‘khŠPÞËØÏòP¼×9Øy‘‰Séâ qr¶†ïJÒýíÊ(/’3ðøG[PàÄ“D¨ˆ“¶ÏÍõ)\@ìpÌB]Þ‘JL¸ÔPeq¥U$šªH©"Äáç~”lqÇJÚ2áŽBÞÕm’Ù(LÞèá®R®¶½1Cøûà î>ìt\ÝÑBøuU=õJÕÑ#·Š{d´ ' »Ñù6ÅΤ… T«g¾‡äIfK1)¢qzv¦¸Kmg`!—j“Ø'FVÎíò¨!4‚ ç—&†¬O¨º£ÿ¯H-k`DT\NÈ9FÞ:éB‰‡@—²œE³I¬„oÅŸœE‹|®à–$R2™ÄF9zdÉë› 47”†åÆQÏdÅ=ai,È[š®JÒ‚§6SýU… ‘·Ë ô˜.ÙC£ÎPk¯|Ç×#ÖµÓºi™®¿­FgW ±-È+ìDꬬ5|®ÓÉ„òÅ|$t±ÌRÑ7g2¸Æ-Áw\uø7ù5Ov‚ÕǾÕ¯êzµà5ñ‘Qdôð[gÿØm¾x¿æëÕÂÍïS«^£+Û”z$î)5¢æ&ifÿø›'/£çr0¡Ôv«c[¦$B¤=Jfœ,`¿Á>[AHœ‘>Ž ¹´œ‚c·tØ×ΓiúkcÉU³3ÛVCPP¬;N ©µ[ÀnÑna Œê"!<ÀSzÏa^ ñ‡Ð\ÔfÆ?~œ‰]‡Š®*Ç•¤Mú‰’Ggpå7sHj m[¦{§oGÁÓ5–“½s÷?çh+‡yQ„‡T!8Sr4窥!nTO÷ˆ¡à ¤cÌE¥À“qL=X¸Fl_‚?G=󸨝€r¼*c†ñm‚<Ê€ž|O‰? ÌcJmÒîñYýÿ Ë*âYPÿO°M*^€àA×Kí5—Úæ|ƒÅ0[¿Vb:b½ÐøAPSÐÌÙÇA,€î}wâ7ÐÈ¥f5;¡»ú°•§š.JÊ5Ëïµ+øÀâRgW½ðšÍ;]ÛFðŽCWE‹ÓS%]®9Pk›Ç®³o9®·";ÉC˜HóŽØŒyÚóc"¨*òƒYˆ7"êŒûÑþ¾sǺ!Ê¥9“˜¬ê4Ã+_‰A“Ðæ`|7zÍã :]ûÕØßG¹7p FÇÐý×úëk=¥-Wý`üƒþ28yöøÇ§OžŒå€“†ý;¯²_û²%j¢n\&è8[à ¬{+£ß-Ù¸Y“0°ÿ5W¶ýrvÏ"Ó~øÈê #ûr0id ãS:š"|C{ÒC´r½ÉiÆX{»‡xTTéx®"Ž »¢Ó¨_ƯՖ›Uâ)©áF6)ÎA¡+OíFhˆ_ûNÖ¤ ™ª“ê9Ä–Ææ¥ë^éèd¨D«Ô¦xAç[÷"¤-R·aÿä ´÷ èF%-ÑÃWaÃê­è›I\)DÕ‰¸\µ¦¥®* £˜)- XÐHiÒÚñçp¶GÌ_mnbÒLÓ]TNGÔb©™²'U~BÒ’~ÒåÖœ³¿°ŽŒ#›Cc!ƒ ¾Ël .P¯6;§2´f# bÛ驹€+ÌÈ]úñ2=UÜ»ìâ½AòÝ6JÈé|²ìzE_4ÑÚ߬ڭÖrÌ¢Ô[!šúÈx0¶@\ùQÒmY/]¿*é_Ÿ£ú^ワCØ/y¦Oþx’¿ê¸ˆ˜å:!:ùssòÔÏѤ %AªòW2U‹®qd‘ÊL|U[õÌ„Ha©¦c"ù!g <¬òÙ“Š/6ÖÚ@‡Š"aŸÐaQB‰‹˜o_Ýc3fuŸTdO(úȇ†Ÿ&âT ±\¶í­Áh®4l‡¿I&× P~ú–WRçݾâ²àŸi ˜8ØdÉç¨ÔMÒy»ó©ž?zKí¾£Û=Rãl\6 ìü:sáÑ%]¤¤Šà wÝÀmÛ×tÆ“kžõx ÔÿÀÊáå2“€ sõ¸îNήÔoÚŸà³æe悜ä.m´T ‡ÙiÌKÚ¡poöÌèDw—usêÝ-}MðÇÜò¼áÂ(jè›ïxH·™¡¢I>Œ©£X7 È6€?{C׫lÛ3¦«ftozs6Ú“µÈþ*´A ?´°~ æ Ñβ]›Q’6v¼7šÆ79ho¦8ƒ°Š‚ëβf’l ò9¬^‰ê$è)…·*¾žÎ÷t»I»ö–ÔjŒÑx4lâæM4¹€Õ{ÑtªqØxÓú¾N£öè­‚÷Î!°â×ðˆÃÖiS¥Ðj#aœ7§´ 3L¬O5åÝ9‘ˆgº$Yf[ /T¤¦—[,Ö&Šœ^kÂðüè¦|i¸Œú¢±k3Bñ„],B®Ÿ_ ¹‘Àa-ËÑ•(°=ãvQ{t4ŠÊ˜Ý®]cUÂߨU—žq‚¤§C@·Õ~ž×鸺èBTª Ü´¢V]~në×Ñ¨ÝÆ*=£Ví|:ìDÑ6ÊuÑvm`–UÝ@e!‹O€ùÜ3jÔiÄGÏÝ\QowÓV–çžÛ>ü¥%ô†óaÁí·vZg^½ Û"´Kýh³j+:zóæ˜LH[ e†Ö%YÉA´Îš`^_,øv;†˜òŽýØVЂLI°_Í)``–_£ýà8OJU»/·Ž|åÁ¹¦x Õ×elÅo'Š›dÉ5 ̳ê`çãÝ”yMÝ쮌nÕ^vû VôÎG£yA‰À. Ðn‡@aÿŸB|@UÚVð‘_¨eèºdÇ9" PØ›4{)ªëú"(¾è3㑽 ÌTCéŒÍ ]œ¨*^Nù=-ûsÇW9(Ê)ÊTñ©VvÃ×ô§äà3W½bX©³äèYjÁ­ E|geuÑj½ª^eÇÓd)¹Ù“ȧ8ÂÅÕ‡ ÔŒ›è=d zUµ÷^p h}õ"v Š>Ñ­ßDm‘Qµ>´Äú­RØÝ[ÙØ ýa!X·=TǧÂY­îÎÕñ–Rö Œ$!ÇÛÆcç;»¾ÞÇ}b§Á¦Ê4ˆ@kï]*g­ý&ð¦ ]€#Ó`ízR<÷ÕÌʪˆ³rB!îyR’}Ò$©²iµÆÑFöA´íÛºòÓyî„ÑÜbÛq ¯¯Fè,%Í*D(là–žIW[®µÀšW[¨]%E©ãí Ta‹ÀS«Îÿ´âªÌ0è¥ÅÃæ Çé êak¹G¤¯VíQw¤Þ¨qRt × ÷ºò&÷x™$Í\kwàŽ{x÷^…³bKSª1¹š67S¤9,8 Þ6‰ŽòbÜuŽoF¡‘É'†ñøãˆT?ˆà·,Fýท@Î2aViw ŒÉë ¨·lÒ“ÀªµUFUËÝ’tYkmdž@Œt˜aœ“J¿hw–®êºA¬RHFiwúŒ±ªÑtô*±Œœ]`Ø NQ•N“€ØáÝj¥d"ç'ü¨‰P腴Е<£ …,q-µI¨A³òªŒ>¤?kð~u”*ÞU·Jý׋¦¾jÂ|Àlµ¨|5!VÛ®¥Ákù[3GE»}e'>¬×ñÊŠÿ<ÅþÖBx‡sƒuðz`¬—®‡¯ê028ÒN‘“Sˆðáç'´…¦vVÁ¶1€(d½A.!Ï1ÝÔzµ\ÄB°£èg†ïãÁ•`wf‡¥ëýyÊø,™,`Χ^¤ß­Z”ú’¹û‰‹€)ö¹jEÃsµ€P$`J¦p`Dj·ÎÝPZoˆô:)@Ϩþxp$9¿‡óåuJf›=ê]Šf×u¹]W{Û>kù5îºìÝ¥G}ÄV°H¸$Vå$æ‘a'¡ 5*-Ø2I–mäó.»€˜ÍÕ¶Þ«[¥µ½wd뮣Cü{dð¨UpaÈo¹ï Òm–ç4§ÏÐÆA|~͈AŒVÔу¢®ó †ÓšáؼÞØSA»Ææ äãb§Ö,‚{Èe{¤œ¥F‘ë‚XvÌÓ@%˜ðè<„46…aÎ'[îxÙ`Û†8Ç÷ ƒ…äÊråšÙ´=p™} ‚~CQÿ!îöJ¸×á¢íX mövW&ghÑ–!™xhb1Uç)µ ÷¥sÐ:â€!j <ç*¼Æ‘#§Â=™,•ÖªGoí•ÇN0ßÕcÐ;ˆÔ"žz í­Q#LK4ôÖNKr–ÖnÈb¿¤¼Ý¯ oß^¶[û®E>àö½Æ¦m „6ïú–EÑ=iƒæ…¸" ¼ÇîÖ ÎÃÃGOžè^`«óê¬÷Y ì+Τ ×ˆIw–Ä!×ÖmÅ<Ñ2Ú„PÕÎ6Ɔ7ù;d "hD1¢Œ$Ž˜Q‚§‡7`ØuTo0½”QÊ‹|>ÃÙ]arEú9ð$å†OÊd‚c®Ài¢Öû"kGihZ}'«Q…ÒïÉéÜç„¡@¼ýž¾­i­; ’q¶8Ñ(4‹ÚNy¡wT“ª#ô¨ÑÁlLµœnÛòº¥¸ªf“ 6Þƒ=cf›¦è˜ÊFªò½Ålùô}BhÈš-ôþÄ&·L ‡"'”ç„Ës nfœIe&5™ E·þ䓟ù½& ]ŸâÑÌÚÔd­ù ø©©n]ºu$3éiB¡(ì´ô®ÞÉ3ŒÙÜEñ®ìœ ‘‰µûóÓ&S©ïâK0R†“ ‘ö?Í…-,,ÄŽGØCW£áôÂuª;•(„W]/£[S¯aBÒ7³ó{AGÏriÖS !¤W)$šTŒ´X¸|ƶ@“Ù&•Pœ×}¨$O+8ܲúïë´TçðÅí«ÿÐ#“rae%/ µññìàþÎòÝÈdšã‡–äÒ­=„Ž.õØ-nص伡ãKÔˆŒ0;ì{¼ëÐÚå$¸Ù|'g2`LE ¢\(ɇ3ƒª±¿IœX£N½EúôÇ…$꺳7PníIŠ|B¦’ß#K>¯fs\éPa–¸‡5¯œ]´'”Õ¹æ8ð“]bLôýaØ ðKå\F­tî=ƒõÛp·‡'ÛÖL ©ºÇàXÁ§8Ž´«aÁQ×ò…ÛGoA/ö®¬L¹Æl ן­£ÇhoIríqKV1sOÁ®º…Ɖ.¢6“--õö@üÛ¹:ßœŒÓ󔌰 Å4 jü¯™È¨«S u£Îš‡QYÒAÕŽ{Ñqý{=*­‰PdÚã(wq»i£+îʘÊ&€@4¡JR! ¼óV]-¤–ÀÕ‰¹š„*в/§HÀRÒßЂNtÄÆ¾[YU|&[|x“Æ»°Æ%vB³I!ÒjƒU³í¡@ï²óòFŠ}ÖÓ˜±Â¿k$o½©k¼:4Ä 5t¯½:øØÔò´ô„ª’Œ¡³ñh¦ißCßý«ò-¤xšÏ•\Ÿœ»‘.xÆ ä~q ¨. H-ž–£ãNÔÃÇvëÚ1ÂÛî*ÅWÚ´É¢J.æÅܧÛó9èEGj¦³‡Ž&nm&DFñáC§×áuèõÑdå)t ‡Ó5ZdM Öûæõák1Àæh=ÂGž¼Z¶ú2윚P…'ïš5d[¡º±žË\tðVÍdR,}lC³]'­Õâ®îFš…y+^@äÔ†y]Ó);ZebÏ"PdCf7ÔhRÐTÅЪ‚=ã>¸oƒ ú²fˆêçÙRŸz£"æ.NLÝL Ú5 ]_O5¿Q+X>LÈ ‡IdøcãÑ$Yc‘°û&`v@RCɃ%1¬U7öñ7‘ 2Ë æD‘¯@ éC×f`,hûë"¾,jý5²IW -[[Ö'IÃZ9 ˜è Yçf3;½\ë>ZTX…¼CAÎ×íÁ‹ø…²TêPHÞáÄ‘b–ï5ÁM2‹þ¬%»ø•üC*Ø‚QÈ@çÂõ8¾ëÉ8ë‰o}rðÿQRœ»I7òD¶¨ð™gˆÁ2Ç3’\*­í|¶[«ÖTkÿ“r"9¯½Ú¾£±Ï>{Ü ØXÅVslW,ÎðÅÛÍ„¡QŸè“22IÇ TòØSs6η+¨Óó%@"¯í?}RþiuÃëGxMª½õŽEïfØ­MC¸ ¾MV~ËyáÉóç/ž<{yòè7_œ¼xüüärxKF´D­ÐWý(ZËxVëULÅb¬SѪõÎj%㥠Ùq ñ= Á`y†° 똢Nœ®–¡4V?Ú'?’á±hþ­ÐnKìgjeÖ± ¾fŒÖho¨†õÍ';Ã7@bÓ!w(ªÍ?ÙÙk¨œ$x`²càI¸ŽnÖ¹ifÕ®v2¥}{îœôƒZɾãe¨'Ct°”‹Õ.pöÝ>°0ÄÇp_ÂP·¤îäEìIâžðZ½ÖØ ÜÆ»Œa·&Lû /~Jð£üÚØðÁƒoðÇ{OfÒ´ÄÅåû Rª“ù¼bÙ¨´þÖ•i"oû¾@ ˜j}Òͨk$î ÝA.J —H«²[±­s„ÕAëö2F…¬—)âÖóDÔÍpÛ!£\‘0"]–Ñ »2ƒ‚ñ²Hèsèš9$Þ1!‡«¼p–ßF æ:V…k@PÅù¡ášëüWÐæÿ Úðïõ}-™Þ’]?Ò³r¦¨>¯º Õ`÷1À¸tµ†\IƒæL(†³ß(Áâ&ÉRËjG®½›<#~ùµ!™6èìl®ÝÎÈ¥‰Bž5Æx§SÄÑñ÷ÁÜ üóèíλ@ÚWÌ-€;½b¸56Ô]†î%:%´ü<˜PÛº±K®§rx?z«Ûn°Oª_íé` Nt…÷® áìdhà Þ"'‚¶÷4ECw‡2ÝSÓOƨýɸC.^äªk ±‚Ÿlšù 6áx#ó{à-U$ ¯†.ôEÜf||Ê;å;6øYhVTKOŸju¶Äü›­Á$5©·[*}y¾·à\x)÷mžÃÖ“B)ц¯Ÿôݰ…vÞÛ(bÂz2púÙ²›Ý¤eQüC[.¯ã˜×­ß¸[ãCÛWtrƒÆEñ÷hÙò–ºc‘ãwPs/ ¶áuòöÔdÚGû#+ÊÂá‘W¨Ê<]Q(Ö¸ˆbêdíD,¹tù¤xƒÔ¬›•m˜ÑO„Ky+¬X *ÕJ]±FâåʳJñL`2å‰V)‰!±L\Óë†Àªàút•‹¦÷r§*’?ÎÓ‚Îñhdm\‰0Ã¥Á3®rDuýLH¦jª^©íB¦“±¯Úá‚c¿*\0*½Ñ)Aþ…µŠA\9¥f”Ɖøbß{ËW©¹f*ªŠp+*çÓö`Y0öмÆ(»Z'Y3uí7é+=bÜTcÒ TÃøÕ<Œ›bijα4y6µ6Ñq+Ø2.SP2‰zIsù9Ïc?ûA_,?·³ÅwÒbòþgU6©DWf‹ð•¡MÜGó³S´‰’¯Íá匞¬‹é»a¥µû"œzNäÏŠæ ŸëhÙ]\×nÅwLsSÿŒ\—7ÈÅP® ÄÊsñ¥¯‰Û'5­ZºìDÑÑðÝÊZ¶Æ;ácš-œUö¨)£ör· ŒÂe•:1ÌÆy3~¯«t‰î&µ8‹(NœCèH©5"‘¯êâƒB–‡‰"æn£y¥Gi-7ú†ÝV^ÒŽ.M Bžâ»'dªúòÖ­å‡r:q½`Ä `oÄ› ƶ—ââõ;0w»Qbi……zlzD]ç_Éëu¥–$à¨ÜO<'7(j€;€Ù (蜋îàá¢ÒÒ» y ëËB5œo˜*\?œ¨ªVv­œUK¢çœÔdUºÔe™«¤BuÉ·žÖtiCTgý6ô£KÛ :ë·Ñ¬ ]ÚŒ©¶~K :Ï¥ÍP¥møï>t>£u>ë }c¥l]œmŠº>·äPva)`NûtYµO۔˱jY€ßO&^欆^@L¾yÔ³Ô÷´—ý¼M]x6RMúF£Ñ#Ò ~TãQ«ìjdfiŠ(äH²Þ spaˆ® ¢ •ß…•÷Õàasd—é › ®ƒ°Yòkh­P› Ž£BBBå7ÂFÖÒ.ö‰Ž»9 †² DÜ-ÖÏMI™(O³m¨S0:p×ÃãéØo&Í­š_»Kè渽t¢ï_ÔÉô„Ó ;¹†Ýª“œ*a~Z{“™W™Yš’b„{›ÔµÞ-ÕË„87Æ ”‚ŠkQy}qƒá¤@v| ú7›H fï8QI8t )§Üûg ¹Ý.¨%ÏK'׈»c˜‚Â]Ýkº QˆÎm·%qºýÏÖåoîq |% Êð£.(ÈhM}Ó–µ{:P¡@¦‘®±’¨gZñÀRqw¯ôäåâ n®á|#5ß_?§ |²Ú'Ó‰ìµÍy&Ñòö`U®·Тöº9é‰í¶Ÿû Ô§¤uø#”Ã>ˆ´Ÿ“Elº‘õ|YžÃ[§G`b¨¾È)´æ rLæš Ã‘‹´¦ÁDu3 '¬¢i>žƒÄ¾>ùN Ág 6çSt„<“Tq¬Wà87)ÈܰpTÑRI£9è<Ñe6ÐŒñ]—~¢‹ ý3{g¯H_‘.þ|>‰ H]£†ƒ—↑Vsmø•Ýu¼+p¾ÖàÛX¼|¯1•õµ•¼ìù/¼M‡¢<›,p…5e\,öÁ}–Ã=Å;ùøì ˜¯…;™è|‹DGæËÀ•ë},F¸,£mòÁUÓÇ™Ï}‹v€ô S5Ønv› 7-S¹o7×À¦( ÒkñôÞeÎËlÙR™gk.°ªËñkhµ¨é‰ÉqyƒRÂn¸­„£\ß¾^çÅ%M 0 S Uðîóqy×i2¬Ä mœujù¢H€f5ieã#ŠiêOOoý #'5±Zš‚¨Áñ¸‡K½Ä:B’"–«½é]Ô¦lc‡¦‹R!K=}ëÝ>áàô•„è«Mˆìœ\düôm•`´Y `Çñ¬!öäènæE‘ÎÀTÌ‚tľ¢òlôIÌ9“AR±+p½wš`J_2½1M¯öˆ"ìdΰ9ÑÏ1ð£“ö.¢ãW—³˜²#“b²`]²*` ö‚LÆ=ô$µT‘‡Œ<1¶”¾œÄð˜¼Â¥¡×ùÄ”Hë@éH$9ºQK„#WØ,’ê­ÛÊñC`7ŠÇzòÝd3þ8Ûyì!eÆq-êÕãZlyɨ£T·Å\èÓ*ò.~nV3»u\TÁÛ÷&èÞ¨EþA8z x—ñ‹Tm©´¼.“tZåÁËbÞä ˆS€dÅï[¥ð ”:mò£ÊA£I!,|¬ÔÉLž!Ú(FÛ(±o½©üeº p„’ïÈ!­Ñ#û·=9ÏÔ6*üE¸„î2ŽœQ(YTˆYg°{ÎýM~&4Ë(Q!‘Xƒô‡ M+]‡qÒ1…1Ú}V¼KƒáÈLšAr:Â3‡œh^<»ðvò°ÊgO*4ìØH©‡õÛ~‹=¼h ŽóÆï(4ý…q—ëÃ¥‰KfWF› ¾‚æä|6–BTDA%;vHΙ]Í aúi3眖©&­Ù¶Âï¬@¾ž+¤ÙªºÎ&s“Ô!‘»?m7øT:{ضçO¹T“ýñòCó†ô\톽ê¢ÈççFÁôˈzÒäÖv)ƒÿŒ&9ˆ2ºE{OOÇ1Ri_ !}*ŠÌã)r"Ô5šõbE˜kL/p•[«I(h €ÈMMÉwuSP0Ü‚X£)¼o…®xÁ£IÎÚÄål¯FïömO±C%¯’MxÂFµ·%ô4žç%„¼©1aãÔ—pLy¤ª`jeTþÖ¤€²K5L ÍóÕ!e@2J|ý$= ظš[ q “&tk fbÄ&á‚¶«²9Ôc4RÐ$5¤ƒî¬lo­Ü¹~VW:0™,£RËÓa‹ÎZkdt…c¿ E¯Žþtðë·¤ éŒ•o‹áãö¥à¥HFîŽFËqjx 2Ac[ ¬ÔˆK=ðº½}djZÙZnØ¯â±Æ—æ“ìýè‘àzHwéñÒÔ2O,¡ ÄÙÀ8 gs/JÔ<ØØ¹ÿéÏ®úooçþý»;Þg°Ë{ÃÝÁpçîÎËßeúŸVÓÙ§Ù¤ºì±7)ðç§e•Lÿ[­©=âÞ½=¿)‚¬›ìÝ܃Ÿwwu޽M}ªÕ ýÙâ¿mEÏbE¸x=³óy|®æMžO.Su28T…§IQnláýølQ¤çUÔ~Ô‰† ÞPá={úò·Ñó"Ž*öp®$b_íäÉ„$É/²è‹*©¿_ŽÊþtÞÏêÇóª¨þ<_ƒêî)l«ÑÉx_¾D¦Zx'jËúó´,| e–_ŸÂ……ûˆ›AH8ƒr"Úß89QONÀPIKhÔÛ½ âçLŠVwÃ-H}‹ôûü B`>Ç^‰OuÌ3®yøô¹(ö£BûYR=Mpnþdª¾x5Ÿ¾x"j2ô#,tmllÝÊg2ò@¬KšŠ·Ù \­¸RhŒ…[Õ±'m=]QÀêÅáò ÁébÚW }‚Ã έ€æ” €¤Ñn;ƒ¾¤S6ÒÌ—¶?sVö‹`ó|jód[nmf:€P—î®Ú–w‡÷qSÂϽ{úµzºb[VfÍ92ûÐmͰ5Û‹$|½ôv.¸1f —`÷¸ÃÂãæMAà§·E†;Ï¥âUèhÿömÉû8_âÔnrà= «ü2ÉTɇ¨bu¤ØÚ°ŠT ëý®f‹²C9Ö¢hB&«#mqc?¬W£˜raÍ.ŠöÏòê ˜Âu}2&_IÒµÕrïf«…«îªj»+V ì´\zwz©Í÷)['>Á•Ѽ’€0°åD‹ø4éɳÖzJ^ÓzÒ¢Ú~ôÛø'%t¼ŒÏ/®ânô"ÎÒ8z<¹TÔ.$ÓüMrvV$ ;Ÿ*¡¨­ ÞíˆEù›¼T£¦@Æe ¡¾¸€'ÓäËE|‘çp»|ËÒl¨û@#ƒ²Ÿ FFcî iýÊQ‘žbP´}U;ýÛ~ºL=~Ñgëìm7ú›>ñnßi.嫘ÂC ­J~Ó…i\,úJÖ¶AG)!ÅC\(™ðIFf(I?ú!KѦ¼tÏ’«x¬Ðyª–ãïEÍnôÃáÃ>ul=ÿñ÷ÿS P§£ñÕÝ/Õ™#d‹è,É¥$QÔ7ÀJüíEþ:ÖeûjH¯Á{oMT•4C):)°Ù”¯q4x“:-SA6AÔä(ó.(…˜X,óbŠ® Èh22è/¸^àIaê¤ÊÛ RÚ˜I̳)Ø!AjL<ll<S>AP€!´Ò¡XÓxL–SůH1i&šƒ„‡†Fîí ±Ñ¡y)õØr°ýçûùþü¯þ÷ŸÿåçÄ[|ì˜~iÄv{;3™ECèΠog/èt4èl €§?Å}báftZŽÔ6Šú³ÚHÁ<.9ðžÕÄñCgÊι€i†¦O§EýŠ$p‚à=KH½Fä=¡Åæ„r™êñãÍ1XXJßdi!ØÛ]^´Åž%×àÔ†±„a é€*hY}š¨mHOì•ê†ÂØôƒH*"ÖâȨÌÕH±aO8X1ÖÆmwYÚ_y)”šG·^X«›máЬ$öG§¼äjaqîÒn$ÌÍä=‹ù2›ž=ùúéc2FäÇ4ç×%–"ÃóÜ\ÃM'ÆsDPtaÌÕZƒáÝ{-Þïúf¢„MEê& `ÿÑRåà$ŸåYÒòì1f»pzœ·^Íwîíí¿Ãûø}OÊèñð³àã½ÏèAð±)}LÇM“=¼H0›&)5\L†“zc{ax×y´è—ì‹90_wwúökl«Ù¯{÷ìÓûˆŽÁãQjl¼2›ÞÅè®ßÇûÁÇTš)rÏ{¼{i ãñËùY€\¥ >¬ì§Gƒ254†Mxù½%î1M] òÁ}ï&¤ßŠTŒ¤žžöÁÝ`+ è±?ÓH'fÚðWLh"íí†æ”lý^}N™v½)^ìA¾“}=n ån4Ø?²³³ ÅÏÐ…v÷-þÇï°ùó"ŸÏ0<5#¢¤ŽBïÊþZ+îî:˜8Cä#¢ÊgM¨Ê…w×AUŽzì5·(Doî†É¡ÞÜ ½¹«ÉñmpÓ£ÉA™øä*ƒ]Ç/°èT¢ur¥äD $ÌtçEë›üªî[Zu”eÂõ†üÙäð^¨rŠÖÒ 0þŽ€ ñ_Úî;Ég'¤öµÜ" ÏçÎ×XmmÓñùÅ/–Î}ûXóA…äK»¿òª1b»­á Ø2#ç·ìvÈŸÄ’ 5fõÙÊÎ —Q[ÖäÎ}æ¯YÄéÅ„ò§©‹lmäÄö4¼ï!·'Þ÷jò–voɘQݡ쨇ÖÚEx¶6¼»fAnñG‹¥/å\Üû•?Š / ,Qq¬ÙðRv‹Úxh9=Ø9¼†"F”ŠË•#²+&ZmGþÌ/¾ѹø¸¾0–­—š„XïÞJa2Èžú¨¯Ã5îÛ—²ë{Ë^2EeûB¦µ£³>¿ð¥/^u>§.Ã9Ð[%VllÜ\'ª¯Hé4NjM@O*@\£ÔÐ!k»/@UÐtøÑV¤o^ÇiÎ>žš[ÿðçƒÑ£†¿×›ø9§n2ñÞ»Þƒ%xïÝÕx+Ä™3àRÀ…ñOv¥Øßk-ŽÁþ®èÉÞûudÇëÈÐëÈž€_™ŽH&÷çÿE¨Gh|Â̘¬·¾û·„ÿ®‡¿Üq†±Åßö€'ö@ÑÝUÿ¦ÿEf•ß™½e¹/;ò¯¦#Æ è/ƒ¶¿ˆï®5‡þg;‰ôjX‡'¿ïæ°„â÷<Ôrßë> ÏÿA3<2‹÷¬¹ñßhÇu3Öà–ˆ-­E ì_†¯ìy„sðô;Ñqå`Š´«'À{ºl~/^FCŸa÷ãÏ®Ÿµšg§¤£][ÿJ«ëfÂà‡cí3ªæ5õ™¤°ƒûÏÿü‚ììçaº6°Ý-Í­þüÿÞT”½…EŒt½É ‰ª÷„5X«{6¸ à/³Fšj6õó¿|¤ à£Éç+Ÿßšx¾Îw•xŽ'à t¾L64 FËzjïëAh±ð¶O¿ÍËÀìºáu„‡MêÆi[›µ7=Ó®«Øàë¢ú¸«Â"™EmÔÔ¸vUeÚºƒOw“ÛŸ½À:º…›µ wWAu×ȽÕK¤Lß|ð¶/ôºŽQÈ®‘àsIyÇB ÊãœB~ýOþÿô2‹ôë_í³àL[¢_sŠïË“+û&+g¿ûîîqxQF‘<å®u|o¹)Ô‰¢Ãc)à¹ïöŽËÛŠûx\Oˆv¶¦mA¥ÛîŽ%MÕ»*úæô‡™ö?ßø˜¸÷ÁóªFöá’þ‰!)Âáî¿!¦¯ŒýQGh¹ ñ!’Dómömkúî­!J¬â“ëIÞ˜ª^šñ¶Cj4FMKÌÎfÐ}à²jbÆâØÇ|øßÖiVëÝ =´K-ÛÐÕP|¼ý¼yë½=–hÉ4· Þ[{¯ÇåV§apÑIÂáEÞÙÀÞãyæ á»»³P¢º Œç_,í.ëÕ Å]ëÅÌöb¶¤b®«*köÃÐCݨy-ß¿‘×ò].8¼7Vx-›H 3ö{߿̧£ø  ~QVôýËI|ízìßftœ‡6¤ã¤ˆ™/L”ÒØqZ¿¿ç±j¤=º(Ò2úºm>ÌrôSe2ô7£‡¾‹Ÿ|ûä 5ŸFýþnÔüêW;ŠÑݽ׻7àh-kx]úA%¤ë%Ζ-t„4uE!ºŽâ_è½[¡¯.$•%'Ðè4©®Á¿Q‡k##&€pB Ý¢¶]qzgØßìFÔV/£(cêÄ v&QöÎ@…²ºh°ìé©Söô ŸF"¤Õæ(­»%•REÐ)UhQÊ"à ˆ\(Ù z`;a‘¬’EÆc—ñŠŒ(ã$Ý]0h(Á2 ·H2–„¯*2J ŒúÑÃýÝ)7MT’ÈN¯¨€‹Nž_ìYt.òœÊØÉÀ€@uœ%édpeÈ;Iw|€¯D!Àg5TγtWЖ %bE»¾¥]|N´“%œVâóz+çÎc‰sYâ¢r&*˜ºU-ÓùùÝQÅØ<-±´WÆéVyáw+ug{Z›í¢è`ñÀ/ºpú÷:Œå2{ ER˜ƒ½^„<,ËÓrïõƒè—ÑUZî¾~ êª}¹´ô=›cå³y)‹8à©Ä|,È9–ÈÉÅ °Í\$XäB˜Ko¾.’«Ae¦ÔUR`õWãu™N‡¶Ð4-±Ð4•e2› Kd6¦ËX ”I'§qz–ÞÓëQ}O'PÌ-²·0¨è §„œ\Àm&Ý•#(‘ž'v±ž¦Xât"JŒœ©4‚#·3J¨’˜žÍk˜ÎÏ$¦\À…1”­è²DìNú¸#žËóP w-Z™8½`‰‰,1»4U›¨×Êtî¬v] ´ø¦þJ®›:M±ÀT–àÅ§É fT™×N‰7{ÄR Ä.!QED¨„ÓŒOÿ@ —ºYºY’ìXÊ(¿R"WL& ¹;LLæÐ$Ù,1K/J±•éÌÛƒf3Æ ßÏ$ŒÂã¨áQÄ Ý‰ YÈ™ÙqLî4”JÌó@‰ÂéN% §D50*,Q¹öäˆÂß K¿DY+‘–rÄüd w×.k})Ë$ƒ—ˆâ^YîˆÞZ—b[)繜¯ù¼ôçké­Ó²y–wâé’Á¢5SÔeþª|:™Ç„©‡‘qúM²”»šªª,™ :A4Nª„¤.à”p÷@‰YZ O w/Ò)ÒSYFÍa[&/¨L^È2£¹eA…tÜð¯„3ëù´›O ú" )Y6½²²,•%© «ù$Z2ŸÌ £‰,£fçŽÁHÌ¢´¬œR.‹®S¨rÖ^UÕVÖüîžñÇym0çç¹”¼ós,’ŸÛ$ZYž–^!›d‰~$)så­¿«*‹Éé@ôøt²ðe‚ÅÄåTbáÀ˜Qq¦øbâ BP\T|ª,|ª,.f²ÄìKÌ.D‰© cJ0ÄŒZÌÜ3*1%*wT ,*w©…JL$»™P‰‰,Q¨Év× ReU,\¡pQ¸Ëµ¨·R¸l/PâÌŒgõ£lX ¶{×Ó”øÎ¬ú;„߯æ3€(f³ m…ò¤pF‡ëT‰‚§ 䤙`̶’eyÞ˜¨§u¢·ï„v/.ÊäÄçÕ)ZÃíÔïâI éA(~S‚Ùj0®/EKÖ.Kûu0N°3NTŠÍ¿;Š{?o¿ºóëWcüzçèÁ«þñ¯ÿûfGå1Òâ³å“17nâ'"TÀë49ƒðQ1Ń;—<ó¹4Ó>7ÀAâ_CŽ`²_Û£>æÕlÃ×P6O/hz7Ú4Z¨O0Ðdš!¬Íè|ê@ÀxQ'“n9€rêýÑξ§ÄU9å´ÚÕëkƒjÕ+u$Aë¬ßØ9§~8˜ÿjˆT ¸]0³R¸ƒÚщ ÐÓ~ÖE|í¥õ4¿NŠž*›˜èo]Výa0–óÂMLaë8Š Pü<|ßÁSÔÒ…€_q4†ÍËÞsPIF;àÌ+î g)UÏ.bTKB|ÿ (Ô`)plí”Tô‹!¾°‰ od:L¤°·¼Áתö›°w2uŒsíÖß&"‹w$#혻Šð<)@]MáGÕœ kXåÀ¯Á|ÚÄ}:mÅÔ?ã=¸£þ"êü÷MÑI¼Ä\8ÑÆ>¿¾€‘üvßÍ<°}“B.†ÈÙíôܘ(²F¼dE„vôZ7¡\`Pƒ×щv ©~žTOU§ø¾-hmqùžR/&SÛyU]P.2Ü‹ÆÄ¦¿ÑjSb¿(äs¡ ~_D;`j… Sîôuy–̇¸±SB˜×qyóê Ö% }‹ý˜^)‰!*¦¥ÔŽŠ "pd 2ÂÁÒÐ>r/ ²ÅfЇñe•>hØ‚õÒ3QµÙǯM7¼œalI^-^ž*|I)òä;©ò*ž,)FÌêF!³6Ùá}ºÓ§fí@Âý›¶ ôª”pn>=¥©BJ‘‹ášÒàyœ¤. LtȜĽ*9L‚1(OQ[>ZŠýsžñìůæ_Åë ÔPOq9Ó«Fh ÃÞ;c~´Ä+Ü@ŽGcyY.OæÎƧÌʺðaî––Gp¶ŽOç¦mÂ¡à ¬õ‡÷\ aûA\ BÓgÍù¾ÆÇYá%]k¼ÎâpZ§.æxçŃÔê·Ö£%||v¼Nzöxÿc}%Wüj\?ΘßÎxßp¬ßwœß{Œo:¾á±Ea6˵x #е"ˆ¾tÏ–l1 p#'ÖÛ™CÆ/Ü©¦N°ÔЏJ]Aªé0òmBÊOI‘÷È*BIzÉG¼‹'³‹ø4ëx›Óž˜ _fÕß…X °î œª­à” v èÜŽvjgU”2ŒdÑOKD©X6n›ºÊò•;æ:᪅æ¨Näj–2ÚVDù:Q0¦î5R2$02l~?Ù§(TR­ŸˆJþt0G.>S€a’É;ñRº *ë¼t '0ÆCÀ r©$dy=,äȰ'i>_lÖ†ÎkÔs%ˆQ(ZC°XÖEÅ J%™fU¸›»KºÙV‡èÉ|L™§"V§¡ód븬k~t M®Ár­×}Ó>¾/MN-sä¿aÒ£–à½æ=Lklµž•Õ¤hÓ%ÃkUÍ \(¥˜ã8ž&ÝOÉä''Oæä„Esâ õôKƒ•†Ìkio°C9DùñÝáàÞªde”笘…yòÍ“‡ÑïÒ+5Ï£oçjc‹ÚWøëáË|š¥å(Á¾¸Ó¢‡“I„!÷n™Wj+ÚØ2)¶ ­%,±3ÌUŸU×q‘|ŽI3F±’i“1Ü5¦§ó rÕ\ø4/T}L?²€G˜$›6%–š~ûì‡èÛ$KÀúúùüt¢Äî§lè— À ž•œ XUø08d ÃTp‘?%)òUÌ’¢ø›P@bèÔ¼^€ŸI>ƒj<¾àUœ®ÙG[o¿ç¶ƒcêæ"Ÿ±¦QõOßïÍKÈêÑUTÙèÇ'/óý/£‡Ï~ýøðÅ‹‡Ï^þþs“ÍÃR:¥C‘ê“:`Šà»Ç/ýFÕxøÕ“§O^þ°ÿæÉËg£o¾=Œž?|ñòÉ£ž>|=ÿáÅóï÷MÞU mMæ–q¢û¤¤>ÿ^ '§&ÃT(¸+½‚#žsEÒ WÁˆQ¼ä\L–„Ÿó-œ’™Š´2"3šhè®Ç³=ÉFýnt÷WÑËöÖè9ðÝnt8‡ú»»;Ý諼¬ äw£á`0è©utŒéÚ1Ô³V+07šN¼8ÂËùY®}P’Œ«}N­'ÌôeOÿo‚ôé 3Í|ê$cTKq9œÜÜeÖ_ Š^)^VŸнa¬&+¦,an׈s•ÆQÈ{û æiš]–óø HÙ‚T _ ÚÆÉ¼_q¹L÷ñB1[þ‰]!щÊÂU,°˜ø’¢Qòé)¢¬¸õ¥ôz0Ð!ßÔ¼ì .*Í-ØòhPÖaK#%}#»¾È' 7Ë¹Ôøb>ÛY2ûÒDw–Ú­jŽaoôH‰ `«38A—8>j>pßœÂHP`€¨ –Yš,8EÔºÏGhÙ¼IFs: ñj„3Ëy\Œ1>²l¹’=ãŠ4AYå¯Ù¿ þ·²Jp›€ˆA˜ å@ƒG&<ºÈQ|vø$z¤÷þ«Tß uØdi»œ4wžN_„…ÚðÔå¥S¤€¤¨Ñ›Í@µ ×68Q|þú Î_jKQÛÊ}Õ}ÌÐM©¡O”9º9™îŠSÈŧ¸‘j½×ûú1lÎ?¼xÜëá|T,øñ½ÞÆ©b¢× xtÅ´ŸÅ )°h÷F›‰(hWê°å‚Ó‚(ó’6 ) öîÎó鯯S”obÞzJºñþ›¹b(à9·Áç7ØZ“–n¿ X[Qï~kñÕ¨ÚÑçUdr-Â+Är\Q„Ý$ÕãR-8µ½¶Î“¼µTaõ'©FÄžé$¹‹ñõì"Ð{ʇ³I²b¨su®*õ9ü+%&-¢Ó‰âü%ð²˜óå•Õ—0¡òU JÈó‹ò²Âƒo’Ób6DØkx‚\ùj¤GIéèpy9N ¾-îÁ»Ž&y~©P9CIqÄÃ0l<ÓüŒm]"jmvAÄëFÈârø yè.“df@«ÉLl±,FÈy”P*µ{FYwÝÞà„ZpòDd 8m®c¤¢­›ÔÁƒQ§æç‰ øçÀÈ¢'…j­àÆ4*øm¦¹@WßüõHpQµá ކ_•ES›Äˆàj5b¦H@\-Sxl“*é¸â·êyÑ¥D‡×Ñ×Éä"%äŸdWq‘Æ°ÒøŠ  }>¦N.˜ s`7“æJ), Ka(â¬-ƒ½2BØ+-…€ue°ŠÅ6)‡±¶a¥0äIÚ‹¤ìjfŸ'ý,©oæÖV¤Ñï_'£ ,0h©O>šÃ^@ê/r:ío€ Ј~`ŠûÖlˆªiØ2>Ÿå¢îŸ~kOèMÅ$èk•ÞÝ(¹Ç8QK„7LÖ[“n»Ñ¥Ôá|uR ÿV3ÁîóªkBåhSV7ûÓ6ît"ñ4|Ä^ôjÍè•Uþ®Ø•Áõ~"jx1é'V·8ß(ë'Ö­gþ|i©²^òÎzÒNc ¶Âx÷¥Nè;SÛžÞ¶êC¡oMÔÑ!±¢S’ðNç°¡£@î$µ‘ɹ±OFÏ…ùN£Ó£Ëc{‹ ?·ÇQ¿ßׯª«êé1rø3ð¾ÜQëeÇ\&‚¶H Çýè2J­VOñTÌï›ÀUëuÉßKHlîô«B š²ý_I‘GtéFËUcc-DÁÚÿ&rªš²ÏòŠU(í Éd„ö—¥CHNK0”Qr̈jla«…3yÎÇ ds_TÂ)Bu`àp'óŸË{  Nõ67A×ÇcfÔÍÎhyµ ×ÄŽ÷h§þì5 MóâœOùÙhrå3ù “{ò”íÄ#- Ç»ÔÜL uæš+>‹¬´D)V•ø#ª3Pj"Á^I9“‰„U$g“dTÑè'`‘\²)%È£E|ç…ïPò³Ë¬d$0] 3'/VâO´Pœ1Ó|ü ñR–{LGqÕ±'Go“µ¸^–ZzêëìwZîËBÑ[ç׿l°íGG›å`°ÙUÿñßÝÍ.®#±¼¨ôK±ôKJ«G^íŒkgX;ÃÚ™©í~×ÐÉCbUeúšvèÙŒŽ2ŠÿÄãô|J˜¥|>Öì©f±µ¥óGñ‰Iñ“Å,†tR}-ãÇLšœ4`üî\^m–— è|ŽðGž¥I¹y울lŽÓÄ–ƒ¿Vd"‹L‚E*Y¤ É’ë’ËÑ¿‰ËX戔€8?÷‹çóʧXœŸûÅG±¤`yý¯p¡—A¨N‘úÇŠ,‚RKú‡¤RÅ· ÿð˰Ë”×+¹:µ„™€çù„¼#ôc`—ɾ7‘úêqé›”@ñ+ÅòêÅTñ€‰«iöHU?UÌnÜp~á{òÔÝ·ñMŠìÿå‹G_[É)t‚NÍaà›±ÛìÓïmFr£Á08œ%rb™TU¦>¿ê@™2j¿Þtˆô¬¦Ä¬IÒ£Ð5Š+øy¹Ö Õ‚zþz¾•ð¿ ÐëmC cÂ¥†û÷Þt¶w´à9†ìèl^ mèÛ‚ØY}o5ÁœTÚ³A|Ø©õ›NtÔ-Räp³ „Ÿ¤›kˆˆ§Äšë‡ÓÕeË£˜TxÓ¿äûYžApˆÞÃ/ùV ½Ä·ê—|©Þñ7xiEmÞ†ö1dÞdvê8éŠuœ”Ð_ZÀ£¸ öì ü>L)ø%߯翇·øK¾§%èÆô𛙯ÎR*9+ï%½Ó/FÀ*ATU¿œ·éd"ÞJeÖ-KYWP(ÐDh"|(f†L¹¬ŸKuœÝ² Á‘Êͦ¨­—’‰8g$ááEpÒ0]èuÀS½¹©A­Ü)É'`õBF¥ëŸ/ïÅíõ³£u6Qx¶zàK õ«­1?Qñ¹V-|"¿ôˆ˜¦6*M„¨ ®&÷Ø'÷*R¯"ó’€ }üAQƒQÎT/€ôꋌ±3;—I3‚¬yDon@Á7â=;ßì‰Ì9gv» -Y€qµ‰FËbïPÏ’M?Z‡[ëtµdÅÙVÔIòë¤?5ב§±þåZ¢¾l¥,É<)Ä"X|œHY²”?~jðvôqðÎbTaV€¨-ê+ӻưÕD§dÓßÍGM›ùH5ÒU©ïß&Ñø„·ŽdÒAÆ‹t˜2’S2µ¼Ù_ñúNftu‹WºÚÞÌYo¾^uÑ¢÷Ànên—à±Z !%æEûÇ™ V&ÞÚլⰜËs·:Öö÷<Ö¾3Bê°Q'ú=ìfOl‰ÃMà‹…HÛiÔ¤uÙÏbS°Nˆa»ãm- 1ÓÚ¡àŽ Ïw—xpVk(I9=ïÀ­]©uFhw1B“Y=!•p#É!òhüz!ˆ¾’ÖKˆ§ˆî®Êé ˜¯Ádç;t6#HtFâ+p\Vdôî1 tŽxZW° ‹eâ^LfNbË)ÖÙnD-] ‰ZM² 4ZŠR’ÒM~ECíÿ œb[ÏtÄ­µ §d)NjôŠNîn¹ ød)ðÓ‰í¯ŽÛ7Þ, [QÈVô2§!daj\a„”U°;ùZ\`ª‹ØT›%CÌÕ…b^é2©_Kbþ{F#t‹ª?Ž84Ù GV B´’—" FŽ©;éÕ#&x‰š°™U~uõhqKSSiE•|^úí¨GkNº|ÅŒŽiÅJàËä?Á/nÊ-â*/ü•Y­»v–Wc5ç§>Ykó£&ù^%`ÁjÄß­¨Äf¯²ΙU#èWZ«U]¯j3j®›Zëwý45m9üi­^œ·ö—ò1·)°*oÔ?¼–j<Ò›µ»ï:ƒ0ô?wBQ®f¢Â‚y¨+N„’ØÓóÔe}t/õºd^Ýå Ó(”é4³/ô$OÎñp€,¬Á¾AªrVlZ#\eîIG«WøUâN•\ù†Ã&K'‡6½XŽ}`z;Ø7Á]¾Û‚¿X}+_E\é..k`²œwiÆàCu&W(o=ï “:80a`í.à‘” >x𼩄}ÃÒ)µ¶Èˆ”s´ººˆ¯nA $Ñ-ØNM\l*xC„–¯&?¼•U³þvZY¾H”[ƒ¸isÁ"”­íj½Áž&k–\»`s¹öp…¼bpÛ²þëc¡Ü ¼À+ûσ]Øbw- /€$ÖÇå E±'å Á§ZsòÒþñáèø¶5ïvš¹ZNÜ«[Z‰¾î´vÒ½I3þWÀ=ø j¡wö½P jzÞá/ Ò3.xéëGØú…ò½•˜aÔ/U€â’aMŠ+h^Öº²\‹uÅ©w…ÀFÐ(™°šgk‹a´Ï½A£þ¹A½Þ$Y…š§½·/Ö™KˆW'gŠOøÖ|¦dƒnôúàYžy‘æŸq{{ÖM»¯;ª,›‡^€wAŽþZÖÞÆ_ EXã¨Vh”&õ`|…~¥ZýèåbÆR( 1üÈ<•-è vØ`cŒégù‘5E®Úªm^3¯vZôó ;r^"ƒ†Ò7}@v4°æ(ÆÍ²{m¬tR¤­itNžU8ˆ¸‰‘V86@•Îtäá ˜œÎ1ý¯Fµ¸§á0ô¨“ÓŒwŒ=ûÆøÚõŽx|4%­Üˆ;ô"á+V.ŠqœŸ¬YCÖœãé¯à p¹õA¤ É ²•„$»j”vÜ,úĉóßà•¨YË,L`Û;õlÍslب|èTI#øsº;¾öŽ5+…¸¤·)/û¾Q,CÓµM!Rñ»%NêÊ ŒàIE=‹b‘¦Ñ‹\ˆiœžEo£ò£èx§cz­‚>”\ÊÍÔÕgÌ5pÑÑ+«ÄÜ<@ ct®v ÝWlï¼<Ü·fBÈɪ„«/ìòÁ G“,¶wâVH¨û›Ü •R*C=¤¥¼ÎíJÇUZy€ÎEnœç1ÞDÄNH*³ä¸h4~--\ £bÞìVM·ó­íñ»Ý;7ßïü GlóÑÏ“Ÿ!P*güL‚Ùç×b»=íÉÔˆ_„ø%åX÷æµÐƒg;þÛäTÎfì~Àña‘Ž*§a3º¡bäÞg )äÛwnJ ´ ¿-!JAðéÈzržÈŠ,( ð3ªí2paì§£OïÜôÇøÁÁDŒaL‹uájæI.fåâbÜíTE:­:ô:Ý»spkrûæžèT¼Õè4'âÍŠ~—à/÷ˆTñ‰ álq–”‡jV-0ì õÆCÜØ“˜Pòìµl'Dìèb‡ „Örð>ÈNÓèË2žÉï¯gÅxY³·ã¸úÊ¥æ’#z qx¢/Åò|}AdßgI1®ä MÇræ²ËIô äç7‰,üe1•à 1®Òd)SÈËÕüøìé¡ãã„Sü cdæ:”~ÃEOŸÍ`·wÆœƒ¿D+‰îÛjª–•æ¶&wuÈçÊ~Êr*fˆB±*cJ»çÞM×n¥ÓêþG fNÌ¥t:×ל†_FÞâïa üà=6§î¿Fû‚Îý¢±EÔfÏ ”™#ð›I©Éèa'[yʤ>ÇH9’8Ëj©VðÙ±es\<«ØøÌÙi\Äe™o+šŽVèÁ˜ŽjF $¡sk=óÂGVNHxo%²òÕ2I]»d³VÌÚi¼_Ö'}›}ÛÁ'“ш«yÞ[‡Ñ'RÓ>qA3ëêUMŸÁÕjòVìZšÍuÿ®/š8âU1S7)D®I–øïÿ«øïÿKü÷ê:Üj–ËÂÿò<."A§d¤(«å’Gvº‡Ùyå6·ÚÚ‘ä ¢!/ÏlŸìÝ”Õ|}RŒü€Ý®R¤Ë³YÀÔ†™ëÜÞ  ʧ7û&¸bÁÓÛOá¨-Ä:ŒªòøÆðäÚµ®yïÙó§?¨Éo£¹/‰O2˜ ˆƒ%§?N}}"é²€‘|•ó¯ï0¸XR¸ÕŒ+»Ì“í¶0ÝnÝ×Vsh€GMEd«N*ˆ0¨è´ÅÑž“X.οÍr‘äqô}.å`Æ€1Nb)ßGÿZ]ÌÎTº šQV©b‹‹è‰”‚°Ÿƒ×Ø0ÛQd0ÕŽXâ ¦Öo—LOE¶áÙ¤ ^¢•w+èöŒÅŠsäTaQý\Lg -½‘ø·¾K×ó'o¿Ÿª¨¼ê "ÅZq¬m`²§ÆÕq~RŒ§ùçóY&§rãçxG^.fGȧår1Ž€‹„ãèl®æ‹ ˜î;”^†RÑ™±[2FxÏW(p¬`=bAaæÁz‡±Â—sŽbb¢}®°CbR°sŽ‹ˆÁkq—‡"µŠTE£WšÒðÊXö.h—8r$ßW f„ÑñTß"á@¡›)äæI1« –{=z$kŠfÿøæ‰d³ü'²yŒñx“/eJRøE^Bîx\2a ‹&Ï¢_* ös,É[´zFé?þÉ= ^Š›­õ“9˜æçÿ.GßC´lÓ¤j‰çF€Û¡$(<v¡« 2 Ÿ€h0±9"ñÿóÿ¯ÀèÝI*U‹ÝhËÀœ=är;ˆë!ºœ…bdYÊ ûe•¥ÿø¿%ƒH˜2çeØhÇr¥aœb9ÍÅ)$[AÈQ‹ÅÒB®¬ëdŽa¼iG©ZHù"i¤G6–IIO¸G|þÿ Ñ¥ÖüâBU‹dÓ. ¸yˆ O%9F5Èÿøßy’íšT)¦à,b!Ùà·ìNºì&<€˜ø‚U]¸’ ­;’ÿñI¼^ÄËé?þs ã&É39cÉ‘ò6ÎÓÇKŒM£Á!›MÍh"ªAs!ËM¥èë• Î7󸌻4r* ^ÎxB uEù:ë˜R¶.Œ•¨é­2×°ÿŸ”xì6ú:ܯʭÈWû N6-¸·iÁýM lZðÖ¦o«‚ºu†ˆ`ºõÏÖÚ1vþ=ÃcãEÏ·Gðûù¥ Á'„]ŽŒ{o”Çç£ñ\À„»½%·/,Ò³¥{p µ!ô3§©ez‰šÇ¨O¥m”ÞÀ:üé^Tmm½ŠH©æ ‘¦ 0 bÆ¡!Œ„¹u`j€j®½æì¢÷ý‹G‡f½'઩Ƴ3Úƒ=Srp%9…BáÛøÔÕèçrÔ‚£6omý\þ\BоŸ¡+k˜;÷‘aEÞEKª7_šMÈoáG·¿tw-õr±Sý¦N‚8Y¯Ó ‰Ý[0 9k@n8+Ѝ_¿fkªVöËË Xñ£Ó-¿TRí*šªW |Ü$*à<øÑX°3q¡WÛ«†›Ž¹úÉ+5üÚÊ0绯A{ƒ¤/&àûåº9¹½–—X WÊR†·|©EäËýWªw·v^57Ž€)=Ú¶©ÚuªRs‹·o Ž °_XWïé={ Ý_an*“ £¡{Y‰sñšQ #·1Uf?> ߟxƒ¼¶ ïîDû¾]„IÒ”ÖÔ7•ëD'®£s,€#S€P<¡IBqu±Ê×MõuÂÛ¯Q;žK…j:Î^ NšUi©ã®¬N°ã“H§azlÜÒ‚Îú{q¸UóyìÁƒqÁ3ï|°¹ö¾³ÆÛÍcK£­‡—UÒöÁ‹‡ñšß†¹ÞÒ(^šŒî”A×^ðhö¼(ñ¯{Ç&d“r»‘N/o8%‘÷¹€:2y}¹øh0]_#˜cïüꞇÇgr‚Ѭp{‡â¬ëéAÎ 5vorÀp„²ÿªv–óº² ãÕB,¡Z‹Í?tÛÿJ¡ºW¡>vØÄFè7œjÛ¶oö´ ;‘2QžŸ&¥\ЭðP-›\¥(Ë)éàòV9óý™ç?¼ARÇd8NrpWë¹f\289ß?Ê“Yý$r¸Îi™ŠhëÛü…\ÔEq=…¸[¶µ‚ȃE ùãHþáÝVÑIòLrq4ϳ©\šÂ–é*N \çÂê3+ä’»J³Í!Ïo•Äy4¾ö#,ÔãÛ°ºÄryù:NÅ"Žva©=Kdž9­èß$ ™qý $`X×É5ÛB®Ê¯mGY Îz—n9YB¼¹¸˜‰*ºAÐ; XÂÿ¾ŒV–KQ,q5(R{àªÌl–]ƒ¨°Î•‹âLêãè¡XeršßEÓ‚€¥ð‘†¢³,—+×]¨¿ZÀ²È’±bšÇ×d;ä•ì@X@C.[“·pRJ‚ÿ®Zƒ:7‡ßB“fÿøÏ d—Ds׿Ο²Šìø«æsx1“ªÇ kÌã$ôä²zA<]ÂtŸdÔKËX"9“yÒxŽ% V‚ÞQi¶Ž[×§z KMGÀÇfEÄî@¶ÕsxBSÝq9¹]ÈN‚/ï’KÄÁ3Šo)£ø–s»eÍ$¾ßg?ØÛ—À({¾ûøô`2¹}«Ï$a§òa—Yüz§]ü9C¸Œmüp޾Màª"Ÿ.¢/WR·) éZȵ©®8ŒænPþèKºékûʦ«4«£àÐ-VKVŠÕ‰º*øDâ­Æ«ç…u±_Âü(8y­RƒxMÝAƒ‘c~|ç7T5¥ñjª¯Þe¥*&„]œlw"²îSî5½;@÷'ÀÝ£Óºë‚ÚK€,´×nxå^¸eQ…8˜©²ìÌÜñ@†¤ïTË™y$æ×¾ŽÛ¹âÚ×”v&ù®=¬·?¾S’Ëb¸¾U¾þ\yäEÝÂÕæU”Ù Š®)#˜‡¹oÃ?UÈý6nç;ת¨è5÷K¾œ¤ý.¯ý-èà)]oŽ‚ QTf"9…%Ù‚‡-Œ™8Màæ§‡•Ô|v£Çp#ü~,{üEÈŽK) Í¿Vé ^µ+Y!^`â»,?'˜4f½ÝèY&KáÓg` Wž¯°:!ÏÏáÛSëÓPë›õ1çìÏ£’-é&Mñ]Yá,÷æSäpÜ Â²±º5ûCQÏ D2gØ—-•¡<šz1Òñ6:ô¦`–øQ‹èßÄ´ê’j±–×NÓ¡Éš¨Î&Ú¸?ˆó”"JòMM_}õqŠ‚©xÝãlÙ{>ÖL%•0¸Ôˆ6ôÌS¼Å¦ZÑu„e{4Gv¿ª9ðFJ"Ä$ò ¤N !uªxPI%ÄŠð2UÌ?Vš·ÀU”ÞäÄ’B±ÒhÄ”ìäÍ4:¬?Ä·+ qòà4ËÈüÎíõË“ªPmÉ9.›Æ§© r‹¯@"1Š9Ⱥ¥Süa†Nèæèƒ”{-XKBÜÊ ¸ï¨£šãαÞáÔC-ŸÈGÆ‚óàÂáZ’5h`1 µàFŸ[‰~¯÷ᤸ’3ØJ×>R8ü˜&¸Q¬.R3XYõ>xõ¾7BZÉhWPÂQ-âV°‘"·B‚¹“È­b¾)Í­[»u4·˜s!£æ\øápî–â\Lç6Bc.Æûhˆ‹m›)í­H‰tt´=[»ºi»Ñ§ŸžÇù‰«·²=¶¾_‹¨°LàæDßËA?³]Ü$”ŒSs0#2Âõµæ" ù/í3Õgâ§wõ¦æ8¦à-¼©F¹žÀ!Ž1PåB”[Ñgý¯¶Øoˆ\Ôͱò:,ÂÁT#¿±# k5B²ŽÆµ`­ñÐ Ü‹^â>­åçqã vQ`å~ܬfÑa0= _uƒ&’^’ú.GugÐ’o¶:öÜ|ГG±¥òµiÄ6dƒ[_~R|vg`\Be¬ìÍ`‹ÖÑóY ÷êÆk#é·==¿Ñº…N­|Kí$^Õ‚îŸStÉMõ8I•ÅQ7ÿšýŒ#ÉЭxTƒ¯äVö*Ëîd“Œ”Óh9ë9ì›|rTXMs6/qß´u%ôLèèQ-(m,mÒi›“%ze1 ¢¢àÐ-÷fW+ÍÒ´‡kŽÃóEbfí—å JZ-TÍ÷…F‰£ìkx“º®m}Cì˜2ŠúVdæÜà¬Ap„åÄZ¸ËŒ ; ;8óMÎõ¹·¯qޡǸn3¤Å;›)È{}ÙFe{&êOª‚Ò CÊó:&>3drYÁÇPUþà]=µÔ{ð.Ÿ¼ßõXªÁÚá^ÅeVyuÈà$®Ú‹8ðó˜³,€Ì¦öJ Qœ1;ö @ѳiD Xï;ë.‰ÿÓw¾’ü¦èé;W{¯ne†ùŠü›ü½…?õȺ íàê~h†‰u:¿d·É“Wj žyD‡}™€¿Œ~Þ†Yy˜@ˆ™èËhËûÏïËÐ-Àœû‡õ}yR}}X÷¢ý>`‰sg“×vÕJÓi-õ!§~É'F²Ò5sHtçó+š5ÊóŒY³hš8¨.u„¬Ž×¬B­ÝèÙÍÏ~ê™$ò½#{³)Bc†'`ËÛBiDŽðÚû=ÌŠ{›£h´äûf#D ²bµ!Ú?åX}€»¿›¹ÇaÓ°9h;Ÿìæ{;ÎDdS£>!ÕÑl›¡,«?¾ø0ó“eÂÃéɵ¾½¯ÐeÛ†e#>qmLMPjæ,çbhn„Éбw¼¯-TÃçF&+Ä‘*lkZGÛ€ïN³sb ˜£»Ö¯‘7ø³*…4Xðõ®Qónc­/Ä›²øä'žßy?n^׿í|ï·œÈûæMolÁ-¯Å™”˜å“f¼àyV@È‚“ºßTŽeYÐÚûñ§¶õ¯3±5Îk8Ïj OœËkiYàÝo.gèŸÚf šé„×. ¦PxÝœZÇÐ5 Æy˃nî9޾‡ç\s#áTkx{uî…Ô¹^ç~@û­uö©ÒÆm̺§_ƒ2mèѫà]èsrø\ @V±õ0t,×-½QakŸt7&ùø›²i›Ö¾ÇFñPÃ'’X$Yuñó›G·å¿[?¿yüÅg2Ã3Ngóã“Ó×g‹eºúEªdëó7oUÖÚ€+Ydº%õ™­Ù ¿æsü:>Ư“ü:=ůׯ›œ Ì§Ú:;ÃŒ‹~-—ø•¦øµZá×/¿àWž÷Á* ÌX–øµ^ã×ù9~½yƒ_oßòæ–µD.‹ Ù*/×onµ^2Ñ3[îF?—Ozºì<Âè«‚`‡æ'1œA G2‡®„=ŽéL^¨>u;u` ¹[Di"º˜‹”ŸJ< _Z †ír:•® @já S¥Nõ! ð@ŽJä ¡>,,$ ª0·Eƒïƒ¥ (*ò·ÂоÔË`˜«0üæHPêÈBpNøžÐ×Yé”Ù÷;‘Ð[À¨.¨Œz&‡?~''¾[aû1[gÉ®®<á’‡œÞ®ç…#íâƒÃf»ÏŸ·nRÛo\xàJI-E^´Vß±ÜhT·Õ™¾$õ¶ûc£éÄõèÑZä ¨ˆVUI2€‚CaÔ[Sðl]ð½ƒå¦ÓÍìû¼@M µöú¥’ÊÉ\Ûý@hœHÖ†ÛÓìxîõ•“³È2 1…£®y›­| ßñ¬â ÷‚¯pŽä\Ñrô¬ÊíÁøÛ.•ŠVìl÷aˆ7²8m ‚ϼvij™Ä‚ñyQ ŠÚ9Œsr¹o`ÈëÑó~°î>@:Iªº¿¦SÕ­CxM œ%ئL-·}Ü{÷pèw/LnÐ’G! ”ÈV‘â´¯O°cóIs¦ö­%>Ʀ–6×djéÌZ_,ù=³×ß3{—ì™^Òu’Ì[¸2Úû¶0±±é[”þ]Ž. ÞÑ^S½÷¢Ÿ4ûWË´jR a‘üMÙ¯mÄ9´hb¬+Bp«{[Þ?£SÒ¹~·Ôp«óë«Ý&‘iµ•ÇÙú†Mï†}êÞVó°ðín‚ºyÃÚóÙö÷ Û-Pj°a m`×ø­ÖëûÓ‹çiÈêyz¹å3ð·X=C½M‹gð8ëèæõ¯^÷6-¤ÌÊG¯ègѤ½[Ôä…pç¬~¿¦uì4ÖË'ÒÃÓ“«ÑÇ‘•ÿPÇ;Ôq%Î)é½Y›ÔÁŸÐÍ‚•‚EÔh'r6|Xö97ˆè\Õ–d«ìÖxË{¨Çêq8¨Ç·±Læz¼¯õËT>¨ûê±»îïø¤LÀÐz1‚Kò‹·ëj>3dS‘h”Œ é9œEv–-άZ­xaf¯LîóáëöÞñü1ðD‰Îúx ×·7ÃGM¯´Ða½ï¢Eé“#š®eyþºµË;7‡¯ì–o²ÎûÒnƒÍfú+Ré.”¹bì·ö ºD£ˆÛ ¡HÞ˜ÔÑÜ‚g²Õ䋨©Ch×ó››õ¤µY´o­†m¯²Æµ°*i±Q¯+®qÁµJÕq÷#îexCïzµQ½©S>×IÉmÀz í'¿½€TŒÎ%Ï®7n½"óe2ßÚi´Ê/‡Ëᚣ¿4aÏ^¨rKåî£êÙ«ÕÓÜŸÛ†´ÛŽrµÓ4Æù ë®Y4†¥È–b-oLÚy?ƾ»®¤Ø¢'bÙF¬_Çx¢» ß›µ¶æÛÖ,¼KS–[mûåõVlºÒ­c>iX˜7ð‰Ñº?xO¸‹_´<ÄV³H ¾ƒ]›æ«òôi8÷×aÑš´Y´ü­L±1÷^κ׌2òÅM=á Ð"ë-æ¡YsÔNÆï Pþ ×APÛ^÷¦"û£–ÇN!×i¿tÒôR7f¯A\Jn]Þ&é"Ø’¡ÆÓÃè¡È“5_Ù 7„û5 Û*SS1ö©j~5Ö† *í÷a¿Ã’ºçšº}ÎC&òÏÕ·£Òä²ÝNÌh\n¢«ãÑ:EÙÊœlí€ÆÖÚ_’B ôñ¹/4,Ò]zjè¤b[ÆßªÏÐHHÒh9±€3=n8æm ø‡q<ÿЃæu`—rdr ?i ’³ÈëEÙkqLÚAÆ?$[—9Ygê4+׌7i[‚°êÅÈÁ¦)CÍ&iUˆ®ý×. N·`¹.×Î×SSÓŽrX-{CjiØO։A—m64²o½D¨¤DoíDYÞIhµ ›‘•,‚Tšè[þßC½RoËÔO4Ô<‡ÑM;¹¡–6œzº|Qv-Ï>@k]šôÎߊâý”muîÐbÑ&‹Ty§&dØ uÈoÛ2ê›ã;ç÷þIo_ª‹q´«veÓuÕi¬ÎDòwÂßÓ˜êckÌ(×zÖñím›¡ ^Ë:ÀÞq=òË!Aˆ! ’FÉY#ˆl¢q”~™ä‚>h†¡à±ø«ª»êã/ZÐõ¢S±ÐM˜G•W;/º¯Q…îpÆtˆât³ÕL~Áôý 5põi¤~SÆî=jðìLÒ–¢ýó ˜›úf–Û°ii‘ÝÞû¤wË>2|Z=SœL¶—J[¦«¦hÀPˆµm¸w[r¯Ë95æmÛa+ûó+úеeÚT^”¬Ú–e¡Ì<ò;—–´üÕÎ8º˜·dÃ>†Q•p[ÇU§§—¿*‹~èãÆ.B½Yƒ$S›‹è w€¶zpÅ.`Ëí-W1ëÔrɯ–q€¶JÆê^YpëP›{VÙÇ!>bVëç [‡ÑxM½áäØcOuè"Ÿ ÇÏDŧÔvÏäu§çi—‹®~CnÙ|]G{å{ ê!£‚"ªÜÒØ5̯gã7ò$¶_Ôõ{}Z˜V©äV‚uP£÷úÇGï¸è>÷Ü>Œ^$ó ð ¨V6!ýÃ&ˆ x8Åç‰z—[{ç>ÏØÌƒÜIwÜ„P´ì¿ì›ì5y ¼®­ô.¾ÚzmŽ´ýÂêÕ'Vm¨‰n1kžDV …J¥ÞŠ<‹Ô½¾*¦*]PW”¦4ÞþæŸ}Ýà°Ù$ä(YエYhÃ}< G&=³Prcâ0wOL„à.jǬVý ïlâpjÆýD;5ª“Š~É¡ØÓЛ=ÀÑ‹ ö3¤R¿£À {ñ·8ÄÇ-ªv@¼‡½x{íñ>Z?7d›Ã?”›V|/·=ùï&à‡Óúòß}7Çíå¿G?¿y {ÏòÝã»!(FIQ¼”\Sø9Å€ MVºÓÝ$.E•«Ü$‹œu²“„aT?íR™’ZñR#%T² “T¡üò£JKÕ)Õ³Rôàƒ2m2M2-¢¿í%4V†Ä¢*ø™ÕØÝ Z¥¥Û¤›Äþe½PÀêr—)±V¨ÅÕ™F¬§a\„*xô}ÌmRYŽ]»¸ÎñEø‘–ª¯òÚ‹nÔ™4[k¶Jr“”Y’NP™ìzõ@1ÉD¼Uɧ–$ºM r®¡åX+³2cø­òg{´ìæpÎeQ9Q õÄ´­“¡ ‹¢ëgì˜rÕˆK¹Ìâ…¿ ‹%b«‡…éáXuSWm±é@ º+„î(ÁÕE莹ÁDu…iÚ#tm&¥Å=#TÏꙞ®ÝSbÒª›iôP"à®ÐÈp·R‹ûG‘bt5~˜N)¨}$~kZÂõr/kHìÍÙé¥IŒ9ŸXi%ÊA\ú¤‰Žªõ›K:R’¾ú‡eå¤Õ2_sôÊ#RÑü÷z®i.®¥Ëe¼4UˆQsè Ï|U*xÐè—Qo0 Mƒð Éëk{YÅ´©†ÆÞù±5HMGûàL[2²rªPHÁ1A_Œ¢U.f‚[[í®©c2aì›E‘mNߦøA†ÂlšbµŸdìê™KôŽú˜-LX£ *jUÔOµ3´!ašÂEõâÛ2jsçi¼>C9¬h½ÖšXùY+‡Ú&A;W¼pXq¨Ãµë¼ê8 *ýeö}Ѻ» +:=‹œŽMܾƒ=)ú1oÀ>o1üYxÔ¬f6FÔê7mqÂqk *¬Çˆ¬¸dd›¿…ûúJ6¡Âi×°?¼¦©ˆRƒúµÉç±½¢¸TAª§¢ðÊŒ½±¯ª’LܱÍ(óS–áHtl7"Ü®´àÖ8s‚üø¦Õ‰Úk.;°Lé<`òË]ž¢ˆ 䥆ƒð§2"=RâCÒá{‘ä4DÏ'ö£ÍhKÎã¡®ãh»Ú6`kUÞÆ*åBª58ÛÚur_‡òUè@?q…W„ŸÖKKÌ6Ù._Ûa6Yjv”‘—Eò$‹7ÞÙ ¾’‰‚½ ·±²nØÇ AøTkFm¼‡¼hëÞÓ¡Û¹ÊbMÂJ³ëÕ!C£©8`é‰cÇ—­V~¸¦ åÂÙ[F¸¸·‡ˆ[ ›\U q“s[ Oë7r^Cx‹);‘µ­ÓhþVÏ 5L:ûQ­'’Zzpªþ×v?úöbÕ{¦ªz+Úû«-J¬U$_Xq~{M‰Ú2bú*'4øÔ¶õÚ…|jƒã``9'ÞQ«>MÎÆe`˜ºoï0œÙSoÞܬuOŸwî‚ ŠÅ4îàsi®°]Mb㤼H´²î¢]?†ÑªÃÉc:ÚM¹|(–íó~iÕáª1ЯĬfêÓÌ8]%< ‰'à‡*ßî…¬ÍÝÞÌ£žL›';Š/ í¿†ûð¶Câë%–º<1‡éï <ó.nRញ"™WMÑ늖:@‚手]Ñ|uH­.H-•"J~Ѹ×YëÕ0­©íœžÕ;áJS¿y^Å›¨kÆß¦ãWîGŽ->Xúø>l'> ëz¾›ýºhOHô„ªt>j'¨wx†Z/ÚùMè=ÐlUÛÂ; ñ~7û8ù¡æë“6”ÖjÛ26 ¢[ ׉6ak4ãoÁÑCG$ÉbÑnʯý6Ô~ƒnÙÓï6<[5<‰>»M‡kd>WžgúX›„)ÁY• »Ÿ/ÊÓ$ŸóµjcZ¸'…Ø’²©,>8NòÂ0.ù¸ò•nfÅžwa>‡rxÙÏíáG7 ®²"¡‹Ìgq `Õé–q´ýèÍL¬èR‘Åèl«8·ôÈj4Ë#Ü@•ñjÔˆQÌŸì”WÆgpa{¦ÈNMR´‹öÓUëIåÉÉi9¶„hÍÕÕöš=?Í »“e½s!)¹”• õ¥zZÏ“Aø~ä ‰°®©M>: ècÓr" 3(WÜïòõû]‘¹¯sQ¬Ä¬LÖI‚¼uù€Ô‹SI¯S±É¥(O3ì¸$]ggfŒÀàPo³c¿©˜£šÒa¿§ïœã~ïÇÑäsà3:æ¥`Ï“\¢¾¸ø“†§ö@V 6»j[]­C"XÛöRú¬ør\É¿v$^IIÔÉäéM”ɾ¿çÄzÚ· ‘ÅœþmƒÍ³Á€9ÉjVX¨´Oå!œBgqËùÚÊÔÉ‘<§ƒOtR±¡':¹Ô&':ÙTy=¥[-iü.ã Í|Q¼Z ™™¯ÇdÑ%å\[Œ¼‡^ëR¯Æ¡–~e•ÜËpå÷2„œ%¶´Ô:;z‚_ú†¼™3)ÆÆlS¸î#ÈKsv´1ƒ|¯3Ôï79«ôt©€P—“ÓW±¥´Uÿ¦ó=NQ:%sì¹ÕB+)ftÂ"á„Ô,Î&:?æN³ÊÜ Ž_g—»H%Žø?Îú8Ê©¿Á¼éòó‡tŸx#³Éü-ÑÏ“ ²“ÓWJ_%}½5–ù »ˆgªŠúÄ­ŠHDSß ãñš¢%»÷/;¼Ÿ¸{¥Ü¾Å8ϕݘ¾Þ@b_¤’©Á7¿}Ë_Úݼÿ³Ó¢kwo:¾»Zñ=¼YŠ¿•o·ùÕ®Àb§ Ã3i~>í¹2yʤñ} ú^Ç úô]¯-:AA–[”;çÜœä§Ó~S“û4#/õSöVÇë”:]Êérß ‡äÝ…öLÏ™LYO|·)ùÁOÉš½âU°¸rgæ^ꉙ)oèÇ·)U)l?`Ú¶Ý@Ö:¾æöóÖÀ{"òEÏ]Oƒš=QܤüÐ&©JÝM·ºBëÅEÉ-pëžj€ 7Èᙫ¥ÚßjÀZãè xÛC1;ÓdŠ™_1';PvÙG÷üè¾r8eîr5N{K'NIôÐnð¯Á€ÛëêLg~ Ô<Óì—žuóL•R^Â)¬Ò]y†Å }U¦nJ‰eÏàYRÿÐW6§BôÏuOλ¡ðÅׯõ•ÉR§æš8=¨@ÊL… ˆÐë0iî¶/¼n{'eÆ$IÉ¢X>MõCNqÆ.¾Õå¡~ 9Ê‚¼ì"+\ªœs…w†A¥L}"Ñ[¸…ð“©mÉÕ3‡htzy!Q80×™#“~,&‰áŸdn’i˜É”¼Ë%,¿@D;{–r[4é•Ãewäæ™œ*%Å"™TÎ:œ$¤ZC¹búºœcDjÙþpŽæao³[! 'Gz¡Ñ´Å®ªBcÃghÔ“K"UZ'^ ¹În“³ÅÕ´õëYŽaßÄZK »¼Âìp¢ÅÒ*±ƒ÷ÜÞ»gÃæ> /¾Øot“Ðí½Tíº£¼Û®¡v½ß䱟–íIÝIǹø¥’]„t¹ ÚÏ…îoZD¿ä._{ŸØçìPÜàZs:YwÅmlê9«X»ù¿†áÝÍ1›aØX¬uó“¹ ¼Ëõôdu~énÏM‰-!ZiÔDŸŽ"½ûÖ½§ÁZù© “ ÂÍ}v û,ÃØ=Öuàf=†‹½vk8©nu »¦ƒpjy¹fƒØ©£È‡vƒh=›§&˜ÎŽ8“z+¯Üº$¯\jêçŠh†ž4LÀ“ð ¸­³÷µ °×ªè_ ø4ñÏ>«{mª|š{ö¶穘÷wéí+þú#YF´õ#C2î—ûòÀ´tð$kLP@@•{h£jQÎ.33|ÄâåŽÑ’ô¤ZÄ9rc>À‡êÎUN›˜8Õ c°Ÿéô2è`9åëR›èzÁz|‚u‰v µ6< Øjx¦žŠ,uá¹eš½<+¿Á®Ì]ÆÁfÓAô…D+¼™wðúâ7B‰æIbÆBˆŒU#(±F!.ͳßå2¹k(4ëhšÜµh‚ö[óãJhò_e|ÝõÇWøÈºû,½À:ë¿ò¼eYÜÊñ+¦Íëùõ™`À%´ð‡ãHØVÑz(‰]íæ6 ¨Ä¤1¨„±g‡Ç•àËŒvùÈEI˜»14/V„u­2,²¨Ñö= VÁŸ"Cíx@…íSV+&-ÁŠv1ñ¢]Lš¢]¸œó«„¹pâZøÍ}úÎ÷ì Št¡Á8.&õ(o¶†x8¶FybÜs§+B¶x™›…H] Ê¥!"ÚNk .bRl6 ºEýeWt ƒm{d š%Ô¯úä³9‚W†œ?ƒñ]Ör$RÆ‹dƒ˜\n¨¹*¶á=ê7ÃÝ=o¸{Þ q1ý§vqkå>³™í9µ&Tóæ.­x•9\_þàç7÷ükËozî1'‹D,Èñ“Â$“ìñÆ<)•,Tb©À.Ð%쀳¤šð®l*S*(¥‚RöB)”RA™)(3eÖ e¦ Ì”µ‚²VPÖ½PÖ ÊZA9aºÎè‹A.º¯„ç2Š:ôKQœ~•ôµvýTü[êÕõó'9˜ì[ÉúÂ${U™ì•ÊÎw¸[×¹÷‰Á "c4™d/ó4QOû€¨›Û“…I14ý,áģA•YnRŠô'I¬S=7ÂÇé[Ê+\ÿìT'·bÙ3ªd•µŒUBßN¯_Áˆï™¾° <`ìÌíÕ}þQªE´ˆ»‰y(™1޳âd̘q³"æoÌ-êjКë^sÝk®{-Ü›Îk‹éTä\Øþ‘šM2®é¢FÁþˆE%—Ký´è ÍY8ܸ©<1¹‹¦Ê„«TIÅrŽêå3¹þc rõ0^ÇKê&³”†šú™*h…¸6L™¦™öêgYou 4Ñ@cEC©ö5OGô.˜}MŸKR%zº ¢ ¨¬ŠD:ô<»"þµ—>‰n“ᆒ((Ðÿ½½¯dÊL—QäP¬ ¤aÜ}zBËÏDIOÅ6üÍÏ×}Ëe’ƒ¿A™¡¤â¥u76‚kJX¨‰:|ÀP»È£D¯b65°JÍE–( YjT ¸bÀ®Ê>Ì÷ÑåüDÕ²ä/Ñ/QÿÚ– í¯G÷år0žÉÎôŽrã‘g´ŠÈµÿZ¿m9¥Ý¥ËȸÙr²Z}ÜbvôãŪb†{¼¯\ž÷»¯ŠÝõ¢Ð·!Cèè; ÙeâŽuECëý/VÔï®èa-Vü¾Èa=áÖ’?­…„[‹tMÉ&5=¹Âxkº¤¹õ>_‡Çhõ›‡Ñý²Œåša­@»©R§W÷Ònvø…\/„ùºã|©ëSçG‘”‰_ÕK¹1kX,"ÝõËIšùhÛo½ú%I[Ð$·j&y-ξG—ßÀ£IÓÎ*ìS±»pÃuØA…[‚ÞFÊðª#ŸÄ‹~^¾olïÑ’!D}ð~rã|MÝÁp»¯Û2hn¾Ýá¿¿g{Ç'ëG½ÝÜš±3Y½žK]ãwšô\Ø"Øs®ÎÈøbÇY븄H0‘C ¸4¸úŽ;ÛLõMq”¢z‘-Ž¡¡Õ­÷@?|$½L9²Öuk˜…>ÌÀê75v¸š!_»Em¦Db´™ò-ØY.tªÕ··ÞÔ0¯žð™Ò¥SÿI!n¢'mf¿ÅPvÈŒfZsÁõÕH¿µ“-“Êtx¥:¼LE0¬È’nî¹À·Bº÷Vp#Àì˪ÞÛb¶ø0•Ý®p z…]®WRüÑlÜïf¥Ýâ†MW&MÍ®K¶ê3M2ã+„;gš:¾—+ð©ÉºþI§ŽIÏe¡Ý“ÆÂa³dÍ“È:áïX_¼Ìú¿I^½þ¢j¹èvõx#Êýrz0Ú5Œš2\½ÛjØå“­ç}j¼ô·éºæº¯Ë “À 6øšß¡«çðK8[N©Ævëm4íë%©-’ÚÆØà­‰Æ>Øw]sÓüxÞµKA§ÉNàí(ºL;è¡eON\ªg½iº ®1˜æ€™ßeù¹8q|›f1„´Ž×æa«“.=ÔÃÉü}Ü,ôOîàô±^ó£ÙÄ,È­*Uu_沟Ûòü¢ù²Ÿ\æóz±L³U^®/ÞÚ~W~D»SÞI§†´Iš§y×¢s+S,“Ã]èG” Wp4&"/u†¦€©ï>P9C*u+ºy(·n ê€ãT㹬‚AZó’?п÷hN%Ö¥›ÍÙ&‘-Ùšc ŠN$*Á¯8O‡B$—Ÿª46@ ^ž,ùøR÷ ÑûÇ>îDûÉå8µf̰íšs}‹»0×5o]o5ä—ÚkÈÃâFtÜ´Bˆ5_µÒpß”£€õÎýÌÍ· …›w7½»åã88þîZé׬»ÈÔ²éGû²qqô€ƒq¤~ÿåeuR‰B >[`ŠU¾­’ž0ø¯¢ °àë!ʰé¶ß@^éÊ/qÎÀ™}_¦÷ä?›}Сû²ëwa9|SºšÝ»0>áÝn‚–¯x<Ïò¸ÐIC"s'û;üê³ÈN \ðOgIlaÑçƒ;ç\!øt«TiáÓらxÆWN—¥Ð¨ì*Ý…úŠ‚Ð©Ó”îòÆíB.“oy)0TŸt‘¡`'Tà á0Siöbï‘\[VXÄs]Õ“u/"k…³b ñV¹¦+—õ~D×tãä©ýl;ýÌã»D1ƒp&ÖÏ$0þW!T)èM÷çCõ³‹µ°.‘؈¨_‰ùÕF¢«½ëüzÈ¿z ä±…ø]ç×Ã<ˆk]&Î…¡¬0T FÂÁ…N%:®Ýü–RÌ®òÊÔßµ ÷ß5{¨Áœ£$é€$r%k’ê‚PíÓ¢…R]­“8%†TJ®¿õÛ²RRX"h’«ÞŽs› ò¾nvö‡¹á:ë±ýÌóÐêyÓñ¦ß)ÕÓïºÛ5­u§kšõ 8íú¯<Эã8B“¨Šöï×.ú°W ¤x2î y G2ôXÐ#Aƒž;æúÀó«JÄsÅ‹ó¶E¹2Q½¢ä¬õPä=ÂZs¾f|Í÷=ÌjK49](ü|™ôó‡æ*ÍTK‰ùpG2ï›Ìs›<Îc‹.Š,=\fW³ŽêkÕ]}‡T´Qb_=³ŽÓñŠÓ¸ ЇB·PRˆŠVô‹¾²Ê™B½”GVÜ´ë4é6ZNì÷/g]³ÖV˜×6>&¡T¤žCM'!»Ù=°@'â¿][w‹8éï[¿½“÷?©ÿk€éëØØÓüCøÁ6]/›³À—Ó.ù’6ùLtø˜’ñ·ãšYb÷V·¢Á,Ýk]OÚ­ëþôI‡uýrN«mãÇw§D\yT’¡äê¼*zw*ó¹Fµè*"õUCÝ6B™e”6Cæ¦+ 8˜w´lƒÑÛG4k\…sª°—ò`…O]¸ñˆäÚZ,ÙÁ.5Ðó×ð|›‰<4#þ®=ÀÿpoÕXxî­ÊU¹¹²?˜*×=þëj/Mþð! õ!õW]¡Úùð6µÉä*'@ïΣ¬è2iíù+G€YûñìÁ~ ×í;AŠd^éø¶}X©§-­|‘H >*/â[6I¢î]ZtÇ›mJÝ84st²f/âøïŠ´ûŽ'q¦ž?¾5Ì›øY¶ŒÓM%ª‚C]t¹?B%~tΕ'±b{} ׉œ«ÞØq¢º9¹¹Ï®ºB3&É‚w¿«EűŒÔ“D¶[ôݾ—ÊÐUßÞO„ó‹÷Áªž} ò0©xAmcñsu]qßöFOxBÓWÆVH?ø•X¿˜|„{÷S,*­‚Æ©·]@ÖH{ÕGÿ.ÌÌ3k£b ö`ã4aæÿ =ìÀµV*àc™©ø^JýîÂÑ%µm5ƒp|}dYë–¬©zÕE¬S½Pâ…nÿ‚")®"•½¸@–Dgv ¨Š>½Ý¤ëM¸^J%:¥ÁwBQõqtû4йn0i]¹ê²¼Û‡E˜X?²&¤‚¡¡ÔcqR*Šé3 ­UN] d•ºnÚˆŒUʸ°º]þêòYª(ŸnMLxÔ××Î0ÖãªJu°PíiÐϯ)F¨5´ }ëqodUÕ6·!j„¨˜uÝ!ø*»° p§;Ðé¾Þ1Ź•œ±ÃVVÒh:ö(CwËyM”å~ŸX¶é[,Õ™VÜjF¿O–^¼q¾v†¨w\eTˆBäõ©$®?íä—& Ÿ S5‚±0ìóhq2Úh-ëõÀª¼²­¨™Þï€fµUœbf‰ì.ÌI†24wyP[b·)‰]̦ybÏ-éôNŸØ+¡Ð†>a>Iç•FA¥Eü6¤Â…æ—­ …ÉJ'/»†ÅNÇA;\[¹ã!Œ0(¯F¡T·_õáRu‹:vo…u1t  ˜Û tWõª˜éÜêVóVy'«°É³~)fÜ#ì(§íÆìŽÔçÿ”¨˜º\ v挠S9,ïŒ@güŽ>¹ttƞسA¢ÞÒ-ð­1«ºÛ4­—M…ÃÚî¬!ö\Ûê-Õ˼Ê4Uƒ˜ÉN(F[KçÎfÞ7sH¯`¹TT]cÁú§u»JW¯æã˜ÿ…â–þÓ…-}Fl`§£Ë©èõ"Y®dËÁJ‘diÀõ€!M»¹%pPØ›9 €ÕmÃÝÙÖ2.>ëîÓ2ðéÞÕáv¬Æ\Ž?yVñVd:‡ž•"Ùê:•¹0äªß>—{ Â+EBHÓa^fíH»G£cu~_Úϯì6Ï+ì×M©šF_AO». x«bO~“*˜¢©Ø¢)uE4O>,Í“Ksm'ß$e9ÁǪ' ˜çÕLm¡Ì²å4Ia¯¤æ¸p~*»}Ývð~"x|Íá²K5¹;¹{,Ã'\œsî~‘ŸàX-Íkw˜¨¡ÁžFh¤ÞT[Ã(ÒeØ-Ò/È\è”Ó¯3úOu ÂzFM‡!nA]}MU†Ò¼CN¯ˆ iBoTîŠéŒsƒH0³H0S$ ôF-ów¥Ø®¢P¶ŒÌZ'â]ÛGº°GH…/T{±N J+µÍteÝ?Øý°Äqƒ•w!îqéÔ°™-/g.ª0êÓ 8°¨[‘±6"5Cïu‘Ëޮ»2iXvv|ÜÙi¯?ÌfHHþžz¤ƒbD³G¢Ù#̉Ëɦì‘öHT/%WÙAÉoÔAToSénµ±Û˜b<ÅŽ[†¾æR¯­ÕwÕZ­îQ4°:ï ´Ñê¼¥/ÂýXk£ â¨u°ÚÅbÊ#ÕRbˆÁÃ4˜SÞ¢ØÕŽ„$Å’ð,–Œ0G˃í^•…˜9Q¥üa »W²4+ºxE>Ú-îÃ㮃ý¯/å“ÛvŠ+ÛxýÒ±lõ¸¤m»×Ýc ]4lŠÕ±‹§7ë€kÙsÒ{KÁ œ]¸–}‹``jãÅÝݰ7‚A5ïKÐöÃeS]>WnJS!Î>ëqn“¡4m”^ÊÂI:oÌaMµ¯·4yS2+gÊÁQ̓ˆÑ,…)D±ˆÑ0Þ2q‚˜µ¸¥‰¥©úèo:¶ˆ”Íån³»ù•­~V…åe®|Æ{Ì©XˆùJäÇb ðщȫtÞëËì:,w•ot˜Žç¯1¼XScÁÆ*rq¼o¥a54•k¬&Ö°½"`á2»`½"Í.ê•줮Bo.ÙXÉ\äpkp’¥ƒ;¶¥è®ïÄöö˜ýPžïTs¨ã{û ¡/ɺ8MŠJŒ +­“ÁöO„dàh(ê(êP´l^o)¿»†IÎwUòUq:;•_òöQPbù€>`á«ÞÕï ï·8éŒùSƒ—dnù¥‹b†¿àwvê⇿ÃË×hwÇ%¬7È èA?„z?mÞMB¸ÝéÄT+}á–΄נ¿ ëÌoAX>óÊ+×/Í‘ÃÚ¶ôøË¡ô²Ó{Gó˜ßá}ý3  ç;Œo0 äc‡íõOŸ£‚ùÉgj—§Íï0þ²¹Ñ êüÊA̧…É-¹?E_Ù\h1†Û•A¼ÁïŒ|p±.ÌÛ´0».}a—V?ƒ¹Àü6ßÛL?„ãmv·yÝàC„·Ãâ3ôPš¹œ $Rã[¦/BEƒÌ«LgVZqx8±!j4Vz78œ”ÙÍ ž¡.,š\hšPZµùB·9 ž¢É…¦ ×Ãí¼Ðmƒçð¸ÃÿÁ|pahRÜ3|ÜðLlÉ=Kè x¶¸³gU[): 8“¼0É@É…L$4?%&ya’¡’‰ÿ7ÌJ‡jÄᦜ²b!ûZ#:ØÒð¹0,j’a»PtPLö7,°mšŠ«M:pʺ0t¸PtPPsy ¸³¤€%,¹§‡x,%,`ɼ!"Ïý¶À"ïÄÒúðCCLÓs1d¶q Ô– ‡™døG³O æ‹y4ïghH:('$ ™WÄòB§Óˆeè¼!Ë,U§d:¥Fœ/$Ó»º‡$Ló4˜Wd's‹ÒôB§TÛÒ4´m² ·H–Ñ)Õ6 ;°miªÛ–¦ßfæé€qÀROY%.T"P®)ÿ¦€èT¨D‡>QeT ée¢’˜E5](ªąJ¶È÷7D§BÛý Ê…T¢íñ:Ã,ÈŒž§´³@Q®x5Óã‘ +­5T ³L0˜Ð™qÌy¬?ðœI_,CÅŸê%ø '…çéqÉÄÐ']ëj²²¹týÚH[çƒ3eÅ)Ú¡꘲•úªó.îl+ꨱ*Í…‘CºŒÔ©÷ þ©:ì¬Âv0-º™rò`¡¢¶+kˆ:Y,J~qÑײ’­­ôm8‰‘d{Qâ‘Å1>Œ"Qm•Ý\ÅeØ7Ln%±˜mDÊT¥Â¥ê&î· TWª9ÏÙgP1Xþ#V«U™Ôæû¡qÏ@†sn\ÊXTie½FÂÍÛ‚vn)Ô“˜8—bX‚Ê 0ܤ 1`|b|}˜¯Gw±º‰¥zÖlã¦Î§¦hÑд­Ã"ù›T¼øvF™ÔE÷hT8z;ø½˜mŽ Nò¶å?íÁ«ˆ'?;ÍÄ›™s1Wnqö{ÿˆ >XßBÏøôÚËäÕŽºKíݺݰ™îÕÝxj€6Èý§6 l(‡µ ðá]uö1š]äÉb‘ÌŽÊìCLQ{®5Âïv´¶¬0 ¼!¡Î‚ófÛË*gÕt!ÚyZC« ˆNäkç(¢,p‡ÿŽx7Ñk¦N\‡ñ²ê»Z³ccÞ烠¨‰¾cÞÚÝKä­ÉvÇ.8WäD߯Gß) §êZ%›¤!„ùÖ€cB0nl[öAGqš×ÕÝ«ÞÒ­ na½u¶ØØ$hlƒGGcÿjaCzöHØVb49Ð\о楷“ûvýÝÁPpþæaâî¢-W»÷\½=ÕPX[®Þ–j/¬ž]¼ðýÒ+Û.íÞ-õvCƒiÕ°Yên††BjØ+uw]z¹[m›A=—Ù5£Zvà^(mfbÇ ÆEWµãzÅ®¶þ ììW¸MvÅ»dW¸Ifí©ô€fŸ¤kË$€[¯È~…g³GÒ½Ælv\b äŠv@®häŠö?®hËâŠv,B6Ì&B¡½=†®=„pʎ׻…@®LhßA0;AäWPœ Þ è>Nv5 Aø\z1ŸMO [ºƒ†ði´Wt®áÓ¼@€ÏÕ_ò«®‡7¿c ^FÖáÖ‰Ò¸„r½øÛEÁ (ÎïØUø~ÊGaTƒ¿qb1(üEò7*D°è$eÊ7–Ñl8ÁG/ÿ£¢Dz_lÿåí¥`hRVô Î+𹑟 Åâ0\têƒÅ|<{f¯ð€O»Oÿ ¸Z¦%H€¹¥U¬èsµv¥²Æ=Ús%²æ£ð=´eK÷žm´¼U;¯Ý}OŽn‹÷¯ÐÜÂÔØ3Í–jVõ;(Ÿ<ìBûÀ¦“{>R ÛA¤jØèpŠo˜õó‡o:\Ödp€| ƒ7ïÖ °O‡Ð¹—Ö!ô4ôë(÷ÝjÜ•„‘³c×ÄDÊÜ»s°M§¶ìÚ¶°t¢ã,ü=äú¾Xÿ†ò3³qÌ…£g?! ŸíA·Ùiç"ª ÙáÓ ûð 9*«=IeÉ‹ˆâ|J0IaŽâãRäøà8É‹’:[ZPF/„ú¤cŠ´|%e4ËÒ2N$viF¶¶%ÆÝ5¦Yz Ëq¶XdçpÜQ—Ž .Œ»g“Ýl ©€š$*’éN Ø&œ&à M6ºxmÓßq 8?ÍŠ¦î‡ŽŸK½:_&éÆ>ÞmÓîíÎûÝ6;æÖÕEI.ŠmÍ:7ùXG`ñƒ}q*)s*RDKQžfs U’®³3ÃÐÀÈê-S·¨¦x®ÜGéé;÷ ùûqô¤$ê—|rWŸ'¹D}qñ'Ó öÀË'èQ?Øó¬½:¡¯ƒ’« û¸¢ 3óZBd|¢þ^XWܳLÚöå+¹û[Íሜ¬×ß›UüqÜis]ášÛ‚5«0Q-N3‡qn ÐŒ¤71[ô‡>ÜòIfù¤/xÞ‡ê\“Nœzé@´.Ö!„±Ÿ¸Ü»-59Óá÷y 7ðF-Ü0Rg6âmÉD;^œi)^‚* ª²¹B¿±­³³uí3tÝE§Ž~!û¶X$Rǥ𽿒TŠ}]/V§ñT”–ð¿ß$í¢”ð ˜W‹DNC,÷0zfVdx8”6"­U}(Ç)˜í²HIJRœõ²¼Å´ˆ÷Ÿ«›“›yŽ`&˜{Z£_ÙS|0úE QvR£_9ÀÜó:ý"Ì-ž‡k`ô‹07'-`nM†€™ÜÆÜoOëè˜7A€îPö:ý"Ì ‘Œ~æ.}ëpÌ› @4qœÕáèA`¾a3r Œ~ææ^ÖÁèA`²»F¿óˆµÈý"Ìc̽ªƒÑ/BÀì‘„Êk`ô‹ 04|Š:ý" I¨²F¿³¯‚^ù`ô‹ 0$¡Žë`ô‹ 0$¡ÎêÂâÀ¼ D²¥ü2[¯‚@‘x™5àdÞ"S42o‚ÝåìM8Ùï‚€‘˜ê Ì› @ߨežG¿C‚¦ý"ÌC­EûpÌ› @ŒÎîC²^zlÖ>(ëÕåq5ß%Uýi<;3J·RÕT~8‹!òÅÅ•(ð¾Ò¾[WÈS JƲhYÈ*’Œ|HGF®t Ðkœú/¼P¶C;ΫؼzÜ?¬PÎéwXgÖ›[ý `BÐùo;ì:îœXØ8Í3o¤šÐ:è ·mN€±L˜Ü·0oMRï…cúF®Öl8SóbÒfmrï9`NÌ‹ý~0s“ûÀã6·Ì[“ûŽÛYæÅý`&÷7˜¥yñ Ljr»¬“™úÁ¬LîǘÜpÔÍ4¹'˜Ò¼Øëc ÷}̱yqÐf42Ùï;p.Ì‹oà˜Ü6žn)úëóUœ&Å©ŠþzôœÑ÷…år!á_Ùfê£g^øRÎçM:Ĩ{#]KØOÜü…ê›j–Ðá4½¸£¸æ°¦îÕI}uN꜄Թ‡WºÔêl¯¶?⬩vRmoÜ?K€%R® D`}­r¥Èôó›c#8On77ŠðŸ©¾›Y­ìÁF¶Y7žË”{¥`pY÷µIRC·Ÿ H¢Sb­pYg=P$þÝÔŸiÄ^ª UÃÈæoK0ƒ_ˆ<#_ˆtÎÁGbÅq¢i§=çœ\B¨’ü“¾úB©\ã¸(Oý1™#‹(…û3±~vU”€ðÊM1øY=JÒ$Î…‡F¢~ñ‹Nœ"‰•È•­z* ‘ôŠ%T~“ =ûÑ€|†ð«F‡¾ÎçlV™»‰C–> lI€ÄhE$•SMd•RÃ!ÑÃòõ‚*”8ªž‹uÆ]ÇÕ'©•îå€ÜˆMa•A¥…U:±J'ªtå5©4E¹í¥–¡w¹ éT“UBuK¬z%¦N飿#ÅsÍ™±î‘€ÑCSÊrIÏþZ'è_î`šXcÄꢺkuŠÅçŠïúè251-ÔÀèjLð·5è« 67≹†§EÁ³ã¼‡TIìÇûÜ&$…›ÐÁJw¢¯Â™wökAjN[lc ïTX¡Ñ*çCNo›hÕI*\!yNO\jÝSFâû†nÊucYƳSIÇ•~rá§³´ÞZv³Ýs?üj¡!¡2j§"LQ°ßKyݵëøÐÀÂò›ÏÕ¸»;ä^[ÌÃ<äîú¦Ëóìm+.;=ÔÝNŸ’A *?ŽvX船O~Ö]Qˆ·Š]!ÿÉZž–¿ÏV5Â;4»…ML‚®]ÛXdÀhIb¾®6hà:|6!çázV­¨ |¢©Zm¡I`«ï‚¿&±g©Ê?ýlÍwN£çîýºQNwöOá“«˜Â2ÞmKÆÀ-ömœÚƒÃ›¹w»/dX[MíÛ-¼ê=Ä{¾‚ùi/„-º2iôöZÐkD®ñr¦¸ƒ0,HJî9b‘-ÉŒäý|À0ܘ"5<®†×ü¨hnæf©ËçÙvî$˜eæm6ÔS:åhÄ…–j.Fù2vc8KÞù øØ Á»¼íÃûtÕªd* cØZ.éÊfFÞñÙå6—¹n„«pt…æZ1„k*Å gl¹`/Â8±Ñ¼:²n…Œ§®Lško…m[€L|Ð]R¸ŽüAèˆ:Q°EèàNàïKDö´Ê鈽š ¦Éžéºõøx}¥àm'EòÄ*Úqk^æ¬cþVSüú#iÚ²P…©)ÃÕGŽºÌªa/>Œ~ù4ÒVç©8IÒAF#Û.ºIØ`ß+§iݰÝ`ûk]û!aúá t5q‡‡®Ñwz}gð1p¦r åºBd]"¾°œòhÊ-´2¡$l]õp® ºkÎj뤵;›ñšôL–ëaÀM ¢{Àç·Òðif»ýÃè™(’y/¸çûûíF€âÚ Í\¿î`lãx{‡,d?“9býNÚ³˜í¶†ž¿’`ñ^÷µɵL§CL¡-öNçñã}ÞT ñø¾:m&õe½ßýö\Ì÷Û™ìˆ$׿ôB» .•ì‚ËÅ6qÁ=€ÝÉaô”Áã&å2¾Ð)¢xµ2óTÈÑçpè žR#Ï[SÃëðùýMüoøÁ†¸Áþþ¼`‰Il¾ÿP^°Tó¦^°r4È·ä¼­]_™Ï0Ût6?>9}}¶X¦Ù*/×¶ƒ¬çnºu A|8N&¤ÓB'só°ÓÙ@hV*b ‰• Êt òuûÙBŽ’!)Lb…F3ÃDU–…Zç Õ>\du…Ê©Š*<Øh¡IÕMQ*S(chö®²&ö Ä•5·Âq)ÞóúxN5ŸÐWJÌ Ê }ÑWYnu9¤W‹e\÷qÃ${Ò³„N%'—t4aùý‡£I‡£I‰å=rTXSn˜/HÏ­H=ÛFA›>žm­hÑÆŒ.÷ÊF@M«›kp©pWï×RóÜZ6Y§÷÷ÆÞ%{£—\djY‡\nÙ1Œ‰xÙÑ ‰.Ýu‹X] ÕÒN¹uäè\®Ñ·¹.KS–¢í¸Jg¨Þ¬òlȹMÊQÈJ¡;z§K«ÀJ÷1ö ž=¢t€‚†h{ÉñÅ \ÄéIŸš­4-!œY.ó•õcš¬e%R7~(ä‚Ààò¯+–œœ–¤mSDÉ0—R\Cøîé‹ÿͲ|UADªÅB Jh%‚÷Ðâ$eYUb}lœ3i¹]Š7ê,<°_®2Y]N{€Çy¶ŒÒEy6æ:ùu5?Í)?Ö«ÛtËùýiçÇÍûPvŽmXë’ap1³$ÞBàNæ¡á&ÕK0tãó£$]U¥dßx`v34^ En]f+ú¬Ó2ÚëîŠÍÄ\Ò¤¿Øç[ã×Y’n«vó2p¬ª.vúaU´MM‰7I9r"H ¬ÅÎadïýÈQnsó¦%OÒ¡hp ÍÑIÖ+Æ+ûÁkó¶.à®ÀÕ(º‡|7Få@æ}ˆyäWqvã©KédkçåáþÍ›¯:qªÊÙ ”0Fõè;1‡xºR^ÀêL̓H¥Jaö˜2=¯²e2-¹BD-.†æ=zœÇé,N†PëDäR`@‹ ¡õÌ{ôPTe1;€Õi•žÄy21S&·UÙ¾O.âüÆ/‚[RÆ‹a˜©Ax=¡ÌGü  Zšåçâdn¦Lvß©ìЂy®:©Dáorwáe BìÿH'àˆs†QN Âïç>ÂDL¨í¡F!Y†`Æ%£ÌÁìχ­ £J¡Ã§AtÛ½† Ä%¢ÌGÏ×BÎŽq;bþ’Žô,°q§ßg«b× ›Íè‘®&Ÿ©6Û$­_•ùEŸÄI:þÙïbˆXž¤•0š„RPPëG/•…Í'h8A<”í€ ñj§J.ÆE5Ýη¶ÇïvïÜ|¿ó3¬ŒòÑÏ“Ÿáègü¬E£q.µ·dµmàX$ÖØð³¦L=•qF¿2 ÈW¨­g££èÓèÎÍÚóïŸ=ùË“ïî?g¨ämß¹¹ãeâjªiªH.Y¿ýöÑÃèÙ£ç?>}ñ¼.Ó.Ul–ü$÷ 4í£#GGKÉGG< hmwíæüO}ö'“›7oß>¸é&ü5Ù¿½G¹µÇ¯÷îܾ9þ¼\®>‡ÅÌDjèrl‰¢íùsà£T”ãÕÅ»}Ë•§\1=UcöE–-Î’ò0ú7™í;Qê¡]Žã™¸výÚõèA¶ºÈam?؉dí“{7Z¶ýgp™‹Ìv¿*O³Ü”…ŸÑ7‰dá/‹é׳¢ã*M–b1‹y5Ž«¯dvõy4?Ÿæ§DÎúRÌøú$çþ`®y<)èÎ =‡¤ÙJ’€aWijӨÌΚhc GæKg"3üµ¢Z­ä”Ïb©ÚºÂí2RTCÌNe¼-VBÌNÇl .ãâl÷š¶ñžIe?…ÿþŽ+ÁB%WÃ…2*&æÓwÔ[OÞ”ÇØšJãU¢dæ§ô¤*“…÷¨?åräÂ,~©d[’ØÏ?…P¤Þ³2-½'§U*çóÐA×®Iæa½ÄAy¹ÎRžàñˆ[7Ú½ÆÙô1®¾ŽÝÂ#Yבü-%ãhƒÍM?0{²øiV-Ðä-Y£€ÎÜ7õ/”aô\7ú›xv–ö\”ˆ7¥z 12dšRÆ‹=kFÅq\-JèÇ4‘Cbi|ãý~‘;Ìwnžû°ßb~>'âÍÊÇíì)B'ºq´JîÉÑ ]P“G˜ÇÂÅü|A¹áéã¸(›Þ¨Š~”\%G¥($;‡Þ%r °›¡Ô©"z³w—a@/§tµÝâ‚A,Äq †Þ¹ˆ ê)ÙG1:ÔÂ5‡©†²=c i{GVöe.Ä4NÏìqt$ÓG/îÿå/žÉ9b.þ|¿‘u¤J¨%ß}.Ò“ERœŽWÉìl!F¸ǯ·QLž*s7ÌRé–]“K¸‹ ©Î2©¤sˆ¬àGźe†¥å/”z' Ì u÷ V4vêár÷ˆæqY<ß¶šHº•rÀüc ïkØG”l+qe­R-B(n«t]»4ëðe30 áÔó+´žë6´® ÝéÕOöîܚܺƒ:ÐR™nß¹3éÖOä\¦šDO.³Âþ]ë(ºåmj‰\ÄzjIä¨%×BÕK‰h^gR¼ç(v-e•XŠ2Of:¿ƸÞW?ê: iµ@àé¿mYÏQÔ‰CY69Ii+‡XV£Œ5²’g¥X%%íŠoÍ;\z®É&,©#ÙR@Å’iÊ  ¦QÀ»h 7ÂS¨Bð.ûsÇ’VÇ_ªD"B®É,·©Bíƒ'¹ì¬2&´©"䌩lØ<¡i‚ÕꬨVÓ†D’´âHD6ob©ŠÝèÉ»ccP*I¥BNªŸæJKw<½d"„§ïëB ö®nÑê˜×zÞ+‚(ÐE´¬¤ˆ#…W-ÝnD"Á#OA-ÞÞy4}úÎH.ùd[>šfåéŽÃ- ÍI´IÅ™œìlðy¨îmD.3‰E¼‚.ϰëC‘n¬†¡š’4j£Ê´k;éÅà«w3fäªå8lîB€ž¾Ïoï‘™ÍI’Ü n½pQ$sŸÌ¼óÞ¾@»ÞAEï¡&/s# ¤dÆ7ÒÌÌ;Æ#7ÏöKjÀ«—{ O€ÆødÉ2JÌQmQöo3 ›Îó§aùûRÞ]H®@”˜)”T ÑAz/ ýd,Ƈ¾úJ5聾hŸRä°Å`^5¢3 ‚nµXÇ‹Jò7ú$[Ì[Úû|–åÄ«ZƲ³ +¸ËRëÀ‰Tä+c|›ªLAjm-;ºÁ˜†!€Ur°aY]á.9IýgU•f k3(º˜B5“öò+û²RQp>u‡’!*t~¡iÄ4QÖ]wK=©t‡„i‚=°…rµ'®†¾ü¬-(øyDƒ€TËmx²½´ò”¢(ÝÖ’LH·?šç{¥ ‹¸!+M*:¹u°wóV÷J ˜}kM²{ _g5NóN.€„û6^d²‡¾œ½^b*`½¹ñB5tù*×¥H¬QáÉ I×¼ZˆÓ¤[haÉ«½©d‡Y©'óÝ4A)[ˆ8‡åÂižU'§´†õHvLë)•g2Dš[º@ŠÓêøxsÊ¿=ÿÑq²0Ù/âåÂë:j±\ïx3‰^ŽÉ¹Í)ÚÏy¯Üíïå*Ö¦ï9™za®¸Ö¶´­Ûå¯]¿’ƒj˸Wš'ˬfÜ]¤çø¯÷¿}ú}ÓÄ2€3"Ç^]àê5¦EÕ¿¿ƒ9lìôR®òÔ:W‚cÇÈþ~:•ŒàsÏeµ Q¯Þ v럻|Œ«‡˜¡%v¬VRé¦V &«ºYiThæÓ54·Ít/p÷Á›^ PlVÿö&ÒåY¥pZ[=†Æšl|&s†^悳]ÒV 9ôê€ÃIX6ÅÆVþ9šV%)*Ùq S„S/ÁÈMÅû…èØQë£î‘?È.­#Xd[B}³—úÍ;žZ€/A5[öŸp°Ú[ÍêÇ®"ΑZÉ æmó;ÍÊnZ‹¸4ö&N¿€ÙÅóAc퇠yÜØÌ>0‘;Ó¼ÙEAÅÍãSIÙþ¹?®ÜÒ0jœèS„6·}’þî/7¡~LÛDo£yáïo«¼¥ªµy®GORÉbrå:ÏfZD‘xÈ—Üf©aJ=«Ò½lW^™[ܽ[; 5.0ÈbFLk|ž φf3€ÊŸÂœ†)6®p¯æ*+ÈB-ѵf-I9˃&Òd×ÕÁ¯2•´ƒã,ÛNý¥Ÿ_ÑKùàÕ8žÏ·‡d­Á†BOîês(r>ø>Å‹7/°ù˜Ådi0M±Û çnŠFŒ>ìäÐږ㎟ÃGn¤”ð )SË9¹¿ÊBeñrJÙ´õ©0>%òøX¡üá.émº@Õˆ&ôãiVXˆŒ³S©Á’k+@G¬ÜþÙ5Eýº~\ÍÙ²éò'ûJƒ¨¹]ä’'/;ä$ 5£Y?Xc—‘ ^ò2[ àŸ°Òžc‰Û—MÜgï)2]¥:Fä3PWÕÖÄðv·úU[×£0qæÙ§)í“ÿ¨99¦ ].9x)§ó©=ßËáù¥“;Ë#èB©<¬° ±ÿ§^nr•Èe±Ùõø÷wö}¯”BÖP_¸Yg`G‡šß+3;öy¡vL€W©V$-¦@ðcÚüˆÓ Þu´ë˜T*änä¡Ê52Ø<8¸;—ਧJá‰J®Üæ@xÔÑ´á3hŸw.r®\*¸çBŒýÃL«H,êÊ:ê IJÜCmÖ)9„£ª œqžÃš•èžË*¿à€ÒVþ1®_`âE¨s†’~‘Íh†ßä üAÚH60NmZk™r  º2Gž!ÐwI`9Oå§1œ¼‚h8úÇË›¯v%gõŬðkŽÌKDáI=Ù`ñ¦ÏôGþ¡?tÏqEÌÈÖ`^§%Za¯¤Â‹Šhþ=O¬+‹Ë~“e §­râ~ ”p†¿œ kÃ1ÒCO¼ÁE=ìçÕŒ ˜¯c(ñœ-ÛåÉØVÈõBÇ›Œ®“bÛ§bh 5Lb|žåg–Î!~©ïÿô¡çTi+0©¸B`ÐŒÀ©ö³EŠõöòìM"ŲÀY @ ^kuݽA!"¹U!‰[£z"3`6RaŽ€C–†ÑM8{3%$ûè ÎB™=µdp£{ëާG\(ß ¨k¥Z«j5x3wÁÖêRÈ•-©øß¾o°ÁGÊÅJûñ‹ÂVÿµ–òWÐ-npÆ;oD+ µv¡=¿™P®Tz3yKóÈ\2¹€q 䨉ò¢IdÈC6Ðtn£­_=FI~¦¼”'VtGppÚþVû8/eÃ%5Ò9~#˜÷)@‰ ;ÙAÉœ&ŒQôI Lí³Í8íîX¡&yÅak+‹,;‹Òd†+ °"rY¤¥51ãÎJvÆÆíx“Ì«å öÇÀeË i6‡e½…5òh)”^Pv¬6ævqÏcÛiŒ‰öžìÛmïCâ{0ô¶ñÏrºÃtxàÁê8¯¼êþ{˜Ïawijƒ²úœRsƒƷ“$žE@p5B!‹€’P’~ôr°°—Ït¹O·ißÍ& æ7ô½RËæ¯ Ø1›y óU6ZúµÌÏÏÂó£—s€ÅKG׫«p÷…x#Õ…|!–ʵÞ5ÕŽ3¾ÚéùÖÖØµ=j<²q£Éè‰òRrBra­#cÄÑ$Í °±TÁÆ©'Ä^°¢ñ¾Å²Ä†¥S‘zí¾²-2Šªµ5ª”Õ! AÕgµa­¿w¶†.ÕXv Ðòºn¸=ÌȤ¯lô8ññVŸ±ÚmÙzêÖ¿ØF#g=ÿrò 柚!¹Ñ³½åŠâëÑÔú$Ç,Aª•>æ€VU³ Ý£pM(¾‚°c¶!Ûfk/r[Î{Ù¢*åLŽpp®xlMXîÞL\ËøÍöMîÒ½˜A'I·)ƒõÙDJ(Ûlfçöðz s´êSÉ`Ÿrß %l‹¬hR=ФDÆ:8ðÖÊèÓ,lÀδþ¡’–Í¡F)2¡ÓæÆàµ×¥^®Îo0ösû?+Æ8Æ× 8lc±Ë#-pÑûÓ•d„"3º<è6z-gà3\¼¯Ét²KÖ©º›¾¨´³å5”·é †t*ѤÚ|É fsr=­8±^®aÇæsžv”®íº(þ‹Ž2§ÊÛŠ‚í#oÌH­„EwYÈqïå°‹7Ž¤Ô€+Äe§çh!â¥ï6¬µÓ—äíãœÇÖ™uÙÃé°ôk:ìÅ3ùäX‡¯mÂÝsDh"vÀ%‹>}w=Æ«ø.;ÑNH3,B6”± 9ÚHˆÇÞé´œr^·`ÈÙ|0ÏäŠùl(sÞ€“¶3԰»°èìg)1BFq$³Ñ'¸•ûÉ|<2—~´½Þ•|ð-ÎÞ2ÄïÏ»ÄÂLõ£/?‘+ÙOŠ_}‚„OНzÛ-½»[êÝÌÓÈn‘3$)nìq(è¦pDf{OöÅ'¶G6 ?°]-q¢/-ÔI:[›ÙÅ&æš±ú|å1”¬côY4Úu8Ë«¼d`]5|?8pج‡·¸{,*|æ-™MÇÔP"Q«w®`àâñÜH/1´Ä…ΞŠøÔ³¯ÃÎ 2¿¼o¶d4Øul«@mâ.Ÿ ø”@ŸäM‹Øâ‡gßÿðèÙ‹¿}wÿÛGJ~¢N¤)¤kßQÕÛSŽ›—ŽJš‘Ó9ÞJ Ÿ>Ö6&&ht¨½Ê!Êk}$¨ a(¿áʨ«ðI[iËë¡Øõ 8ÇÙâÿgïÝ÷Û8®áý7|Š94$°EҖЂbŠ–cn$Ycѱ8Ø&Ð$[ÐX4 Šf´µ/ð½ØWçRU§. €dfÝ3±ˆîªS·S§N먺Z$à‹ÇMâ8•ÛDï·±ðZŸ,ø€úð|âaà?y×m) «Î T§Ê.÷ “ ·~WíÓ¥;ú`Ù2þk­šéÛ,ž™©_sõ ™CãòAÔÆ;²I5–®z*kW |]V^Fd¡nöl±ÆÌìü3÷LÛ„T¢M÷Lw>ˆCpÖÑÅf:q‘Ưó¡åuÅ–¹Æ6¸µºýòu$fv|iÊúÖÑBˆí:Ú_䨱öŒrÂ;aÙÇBW#näÍæY^Ïßlº19Àáj¥Ê¨»æúÖx„°rEæÒ¾‹ nÔVÏâÖë¢Á„ïNP)j™¦k§.¬ðÐu g˜¼Z*wügã<›v чoî¿XUÁñ'[ŠÃF Û㮸ò¥4)pérGp ©NóY Æ`ðUŒt$9ÏÖ = Ì)æ·ÓPLb¶Mzñò k ƒÓ ò}‰¨aÕÂ6º§„öbòˆ]΋_,‰ÿ:É}ÛÁ5l„Œ•ǤEÃb:qHÞÆ±½€m‚D}huV._t‡æ…´†}c'êS*ª® @s C˜DÆw€Oì„6Z&…fI²’5KŠ%µ?t´’so÷­‘rùPV Šþ&Xfj\¡¤uÿ6NkXÄ#ºŽ ¬Û&-/û>ö]í;•[¨+&Ê”Òòs”ž‹‰ D&ëñ<厺 Zð¬Í¯Ã9bxvÏE+¦ïveá¨T©ç¸,sOmI¡w3¤Q¤Ñèª#&S[ÖPÐ WÔè±abÜŽÐÛ3ciw´têM’”C¹óî)è…BÅŽÐMU)'GÁ C$ë5‹²1`Û50GÎγŽSžÅÇÜíõ:OO:Iï°óÍÉ û4‘€T’Ë-K¦ Æ!Ô8P5Ô?Oé×S¨/ÇÞagåß ±1öè:úv³žºcMm…)¶µ5Æ3ƶ/C[9ãeëÙ­°Ê€g}ãÕ¦64 Ýàl]fwßò5x¿©Î¤Q­Yh¸hωµî¶Q£²2nTfn/rWǯ²vƒÀ}V^gñµ]8ç†ó5OŸ#nmËZkhI €ÔÀh]}|ÜU¯TùF«²è¡åY„øh•nCÅe†a)i©ÚK­ÃÖ³ç’ó×—A–Ñ¢àURüå÷Õ×åxÁ×Ô«j‰Uf-ƒëIB<˜U5]´{¥#±âptä#ñÍæ‘ÜÒàvsxsÀa €ûé”U\>½©f¬ÁN>x«–ìÕd©Õ{M£®QZ}xó”ÚA( ÿ²5p‹„?'>î)è%ÑñÃÙ–&?)–ßÁG²PÐn3!hC›ÏÎ1*Om˜@âlpåÉ•›n‘pm R¿AèÂJk ü*Ÿ˜K †=jÎ`;däEeÃ),ïºiXºFMŠsô„Ãúúü¹£€=Æ 5F¡¡6®·½R5Å!²ð¼ÊÕ•>ÝÞëlïš$ßLyõì¬ÒQsÑptúÖÓý™ªC怜߯ng¯i7c±u¶©e£ W¨«ïѵðd–Ò'òs×f+ ôfÓìš7›  à·fo½ÙüJnAp­K´(#¬Ò¡Ýà.Ϭ ºK@$ \K±ÕÊê_V\¢þ&6ùMlb¦¹‚é|¾›.XæmÂWqD3aS¬ªb:Ûu»Iˆr6½·nÓº™¶Î4ôgCµŒR].pýÕšd'>iVyRg_ £WÄjdÜ“s›ô5ŽEú—ò ;nZ¹l÷*ÆIÃk×á6€Ú¿&g®‹NvÍí´ä¶aëíÉz{k×Ó¬æ.DÈ­&éd¯ý+ØEvŠƒUþzAY¹¦|):ý—  cy­:¾ü¨˜æÀÅÀñƒ¢»1’u©—D&bBì”G§ÎÀÂp¦UU[–¶!Þ¥ þ‡ì ý†³\jþ .õ¤¤Ó¦å®Œ·&Dzê|–O/˜Õ¨ÇAš¢åœ‘›ö ]¾º“ž³r¦>‘‰SÁYtœ3Ömaß´q:Èîø è@a5*2–Ÿ½,®)T¬Ö'ÒŽ2$Aë€AÛÁ 9`ðY8s„5aNf‹‰ÖMÀpÆê ¢9&ïS„•3°Nl´÷tVØDïñ^w1ãAò½qoYÔ²…]‹±~lªK÷¾ùýr~óôð>ÒŽÙLèšý‹[]¦vй#ÔŸ}woGÝüÇðP±»×°9±wtGpTí•ÓOq[Ø ©ð-ö)®q¸  ÔœRéÞ˜.|ð#v\¿xA¤¾¨@ÊèÉb| ]8K `†¼0xx fž#š¡~hä‹ÅL5e#yÞ`àT=¶}±ùHDzD¤a’^¶ãý½TH³ýâìƒFÍ+t d5g:ðgãÝJ¾)kÅä\3'V-æÓÅ<ÞÉÛ ŠÜ¸¸¢{¥“\¶Å†ãCmÞ¦#Ps Š‘¥SjrÀ]Û†9{í_ŠëÓ*Ÿ 1KÏlá»QÞÚ3Ñ’E1<`ªï uÊBhO„ FÙe~>¦Ò&GYÓ’cÿæ(sbĈ«N!pwüâ·é}% ¾eÜóHà¾s0úKíœQªA©6Ô"v¢oÓÜ¡¯m”a[©Œ ®Ì°ZëÊýÈÊɸù$…–î6¡óÀù$ÁI¿==¥Kü–옶Ù6‘áQe0£¼Ìþµ¥©éüuën~è©«±…6¿×1´Cà7½óTl(zT×F¬Çæãä&«#1‰ÆZUØNHŒ·Ç=:éq`˧äøâB‹ô&…³ ²žnÈb-ŒÑ ÿzŒí¼C>ùH‹V¥HÎZǹ·Y –|"*CØ¢‚v%ÝÕ«3±Zo‡| Ñ h!\ÆCÌW¸ù(ù´% ÄbÎYJ»¨k›ס?™‚ãßš «~÷ÑIÔ5qÇ!š¢ÜKÚ›+u7^l’ ÅÑsÜA‘EÀcz‚ ì¼sbGê¶§§ÎYv®dÉ‘ZN«|0‡L=Ô_­xÜ5µ¿ŠÚvÂ< j×#uÄNMÄÁˆ¢¢«B¸}°” @y@\¹?×ÔÁL‚˜K __ÜÙÍP M ît†b’Ðõkƒ€îljåð‡é~;2±kKÑû‡<¡~°þm«ÏÂ[5äYÅa½.Ó*¥[†c¹·’–ãðƶçæû6V›¡&Û; ?hŽõðfæ8«ÁƒIo…ŽŸMtÝuXä®x뼜íNü[ wX.£µ1?è@¸ÑFeÆÜ&på’•=8*s¦¿9+»cHSçÎcÖ^Ÿ×·¸…’RÁè÷\ƒêÎþ÷ËÙ;C[›S_ym3Âò]"¯¯wÅ6Øì8ˆ-žD¶l {w*°ûpÃè¹ì ÉKm —UIÁ±ë–5ó™Ä'7XO’öM‚{@?Ÿ[DdeÙ1ïMr' IÚõ˜øwôö“X 4g·ï^Û9º\†j6§pË1î?Nh©Nêu3ƒ0žuÚîD‚·ásY\wGùøt¨… Ô©ýd›þÀj1ú “è‘hw*ËF ø¨ÿ¥!±ÂqÖRò”RšêÌNZëØvuÕÆM©ÀÝÖ:» ÒÞ`(îÛl°ý±mÊÀ wqwŸÓ;ßWÃx"$aÙeYšA[3Ö˜±ï•MóÈÅ"–'vyÊ•­K×§„ßbFVjºVñ‘×q§ZàÁ+]e‹C‚Ũ›Ž¾ì)ýów3ö×êßÂðÕÿ¾×à=³bßB޳d;y:Ó~õ¿Rý¯Rÿ›c°•ìÂáÍN dåí'2,xŸ€¼T¬X:­Š7K­þ.ÎÎÔöwRyÆTÂW€Ÿ»¶WTW `‰ù¸àbÄÚKý7§ݼ%Û»¨›PÓ²«ÀpN«/ô5mæ@Õbî ŒÍXõ”{Q[Œ@šÃÁ݇¹ÁgC4¤sÔÆ !",Ý¿R‹Óˆt‘Žî\’ªðÔW œEFg)E»§'gù&my—ÏÊ|âjó±¥§×ÈNöêÚ¼zßÎȬú¥84×é"ÄõÞìzQ7ÍIŸ‡ºÆ–`ë3ØåLãÀªùȬÓc¡ƒ\o<’¦ç »ƒ³SÃøº0itìíȰѹ¿˜ é]›žK Û1›ª„” ±ÕÈy û-ó,ꜙ<ŠÆ` Ú¾X€t@L rèä uáæo5†ñž\«¥º¼˜Úÿïi»•<Ík°—ù~Dí+ÏYBÉÉ¿› PŠ] œ#œCrÂn²Ÿ6_%~³Íü—µÍeN©ü…Âæ312[Œž `öÆ–Õã|(§‹£¯¹³FsK|eE©´·ß¬c±µ#ü«óá¶¼Ýâºck ܦ¶†Ò]r,FE¸|Mi‰Å:˜ß¿…ãPÛivsð>‰ˆ/Î'½Ÿ$¢ES: ãjâ |%`µ‹«%“´Êè–‚LË« ¿Öûºs«æ…p.EÏ~(¦hbî1X2¦nSk Æjqÿù_cÛÆx#¢o‘#§±#˜0°Ìy)»³ú•T[b!Ùf¼qõÉV­Í¶´¨t¦T5cÒÏWÚàqTœÍoeáîì¨5«fÂb‚B¡–b¾‘hÜ´ÝÔ Î9o½~m±¡pæøn«ƒ§ãõØ@N×ÒæThÅšEµ¶ù‰^»÷爹ET£½­çex8±é‘1,bpìéʆ‚ ·5RÕDÌŠ©<>n§P§y.³€ÉÙu·5e' ÝŠ.¼ž—$¾3–ôÉ3þW´ þ¦¬ª%$†C3«šÿ÷¦“гc@.ðf÷†¸Ù1JãÜ£ïc¬¡C„DfÆÌ-v„`8L9ò%r â;Ä„˜8®êÌ  m-…‡_+8‰ÐÞ®5ã6ËÇdu‡ö«Ää2² ü§±}_ц{ÅmÔ~¯¸õÖ¾ ÝÒ¬Zœ_co#ØÕ¹1ì˜#¹g®»Í˜Y¯;1“Há/´–h¯e&!Ÿõ££[‚T ÆÌ1 Ñà™bXZ:’ö‡ZN-+¦N?MšÝ¸µGŒ €š¿_*RNz“-CêØ-4©ŒÏ¦8vúÚø‘U_ž¼ðcôÆ¡5%(BîiM×ǯ)E_Sb»ŽLo¥Œë6Ò ;‰›·‚ 6Ÿ7ÉÐÞX26ö˜¼‚šÞ0pkmÁ~%&Ãá F8±—ÍatxÃ}j0œ¸•á‚Dæf£…•s¢Ÿë² YUh¦8†ØŽf×Gï5é]étÞÅ%˜gæ¶v–³ßTfS@£€ÐÙÈ5²@<_Dß0ÙÓŠ SÜÒ.žÁcÌeµ˜[›É#ž¢þ±$ê¬9Ÿ8ÒÒ­$EáÊï/ÆÖZ^¨{Ͷ59š³’?s.ùÐYƒ Œy,<®<yâ`„þ?·¤ÇÚú;’ ³‘0/OO½‘²5G@p=úô¶eêR«&ë*WÌí¬š`ö1†ÀŸ·¾Yƒü®"Ï Òªfß7<„X´š(t˜:ÚL§­mÞê‘.¥§Bˆ=ÓÓ“lH×ïò­ÕÐç`ªr6©æk‘´;¤b!3£€9g~}bö«cËöZز¢gÿ|¬YÒÁ``vA‹Ñm¿t¯+²S½+š{óý”s©>cµ}G²X}Jª¿étRý+æR^ŽãóZšØ*šYuJÜvµ´q‰[Q±¨ÐFÁ©àôäòZ|;Ï18’ ¼‡ã3q./Êó ã¨Âi Vc—A€‘f¿°.±Vø˜R”Ú§Ø\oRtË)0¨ÐÏkÏ=¸ .Æ Ñ*¾€.ýrvÍÚ§Û¦¾Ó}ݼÉ”s%TFèlL\ÃíArðòQî‚PÁÊ€*òŽ/¢-àÁ6³7c¡ÛDQWe¼Ðk±UÇ+º²Fë_ºÉ͇hà~‡«©¹&<ô7Kz$x·•žt›"OÔúâ’(èžå…‹6ÃZbÂå6“ýx ÖààO†Ö,»­B“w·Ø}=”‡7tuž»±›Ä ãkjºáQC™¨ÎºyZÖê4ËèŸ>ÈðSÓ›4µ†ˆ×»La+A¾i@Å|®°Þ¹¦zd'£Œ;lß•3â?ú‚ßTÈÙ`î‰ûØî„å΀˰au­üç#Fâ?<)v—§sÖú"”»Ó!;T§/}že!†/™‘nwÕ”¬7¾jÿÁng'>ZÚÜŸõ³ºz=7ždOÑ5Ýuåc9¦Î¾ŸtË©fùõÊ;é¿=ªK‰£Tž[RnœŽˆÖyqM%3m8­‹úB1t2µìˆP»IÎ=*kðìŽo"cxp®kéu½fS"fì/Ò €yÀ¸(æñ#‘‰†KÌõùÓ¤\sXqºÔÞ”aiŽ.ãݼC¦1P˜~æ*L)„`‚ß÷vìÆi§Ñóó^óAúlï|׬°aÇóc[R-©H+ñ ¯®¢ÒbÒrìñ„«Þ]OøøÛÈùL2:CäoÕ☳%¯—ävå$wÀÔ&·–m%ß ûO&°£!X;%çúÁø`H`å‚V:0˜v@À‚r=øÄØÑ¤Ö΂Àic$Xo`¾ Ýå'i<Š¡Vl'Îã‰Ê\aC“rº!ì:ÚªM¬®¥´ŠëC€]-¨¦p  phsN^ °uM¤<¬Fˆè®Ã±g¾?V· ”Κû0'IèèhÑ4“w;ì¾½ÓŸtö Ã1/cCXzdÚe¯gi`Ù¶¬ïä$JG GXF â­€õ«ÓºÂ-ŒNa}Àuúk¡6:í¬œSÓ×÷ ¾¥„ØÛ¬- &˜?¡6œèˆÂð߃æ qç°²ý ¶ŠC~0 i,©´êуÀäW}]‡dP_퉊iüÙ= o{L¼õ{O—/-«®*råZ´Z§ ˆ ;FÑ¥–¡~4-h\øÐÁõiùõQš¥-™£8U7Ú|l¾cëº%?bö£ºÉÈ% Âcè+Ää!eß±ÞHkX˰u¦•Ñ Öܽ°½ s.3#l†ñG‹xÓ Ý7ôª8ƒ û"x˜úRJn2 ÊuØryä ·¢+ëƒGn›ŽË€6H˜8hB”àAý'‘»Â?scÜžðêçWÆaxâx,K,»è¹U\úæTž#=}±óï0Nèo~‹( okGkÊò±%rÀ7þnµ¥÷ŠŒÂ/¥íˆÚMߘVè8 ‡{דMA“u§Èã è (³*†äãvÕÆt»»'n­S Ë«mûõ*F‡åÕvD­u•ÃÍ”(ÕÁ êü]„u0³Ò±#í˜Î·ï $ÃJr|Çà·@n7°é,ÒãÚ!\…š5ë¦Ù–GÔ¨Dôe'ihù&žžŠ•¹7´·ö æZ´ô/"¼X}JܾóªŸtr7Ñ–b¿©pÚJc/¯ÃÞ‚„!C=.ÝÑöí!‘ëWª¾MÄQ{IZô46 ñ&-ëy—A9ŽÇ§éAÜØbá{-Ä=6› `<øãbúŒ À N†%›½‘fù°\ÔÆ{TŸÅ?-],Æùdìì0«5WSp;Ê/ˆÚl™]!±l+??ÇH_gBõϤ%ùPÈb?V—j®ÈÁ"¯èFl}lƒ3­„0E¹ i÷“ïLø 1X™¤ ò£´sh°6ºÞ– ^ îYmÃJv4Õ~&ÐzCmèÕÖG‚Q½4kÈN@Ž˜n(1LÀ9¿ªD7"ÝÏÄÜè«ýãàÌÅN®4mÝÛ{T+¶äýçͤ•ÓgޱℨrçC’T3”}›TÅøøs +rÙÃütβÁg~‚‚Z XÏm«0WV˜'‹e‘”N”#Kõ#Àmj¬¹HE ;f^5¦G•s)?‚¼\¬2b¯¸ -WØ®\Ó}É1Òzs¦‹x&œBF¿w`·.¤‹…#Ä=ï,^еE¸S3[#Ô>ÄÓlj†€Kl3ç\îVfÖÄ)ð§UÞ:ÂCRØë$j—æŠ&G1J¹eð$6»Ý|4jûü ,ÔÙ¨ÊÝ¥Â]BMŽŠw aU›¹ÌÏ'ùš“<†óò°èÓ¡¥;ÛŸ{m \d0!NPñ8Í/3A9&ŸîYQœæ“K·ùÊÖ ¶6j½é#eŽÆï“I0´Ð@ÏûCq^¼Ÿ²§i/µþsûO½í?žûTh?AD®»›8 îfÎí݃ æîö á·wÛñ¹ûˆN5À;±Ä‰AhÝêìÆ0Â*íò-XТ©2Ö$‰ÊgáÌ¥G–ŸvÂ䬢4éæ›  IlÖˆ¶c¤ÆÂ&aøã®Lkh€ÿ@,þ¦keüd#$”mGdÍVìÍ­â[cD\t*8d{´¨A¸veÃÅxšÊ¦:@Û‡˜ FU]hÍwd;=>-:üHe)!Eé#ƒÏ=”ÑE[0»š•jŽ[Ïè¢åÜúîÍÞL@$tO^ÚF@SØ´ èbÖªã BLpÐtñ ¥ú¥'ÄP)ò _iK/ˆJÆ‹ Šåéõ½ @—»RrR`uKˆd<ÉÇE¿ ¯V¿þªý~kŸEùêºÎ;Ë»ºïîà£oÒ»™s‘ǯí/ñÿôóÙÞ®úðÅŸïøCÛÝýlïËÏí>úò øùù#zýå»ö²‡óñô!0øÛxÕ…¬õøó¡Âˆ‡ƒÙY6½þ[ÉË|¾€0²ÏóÉù"?/’cuº,᧯Õ×è6ö-8®× ó8¬¦×$KJÛÉžêËöžêVòòùñ_EÔ n´*v°˜_T³ýäÙð ‚è<¯ æãb8‚?¾>‰£šžlP‚9_1™dÅpñDÕûñ‡çûÉã‹ù|ºÿðáÕÕU†7•jvþ¾‚¼tT5¬2‰¥;ègöüèðÙË×ϲ㟱§ÿv4ÜO.Æc5ÞäÑÿø9töÛ;_lïì%»{û;_ªÿÿ8²½+&§¥êä¿mllÀ%ë2‡Í‹Ù…3?Þ¼€Yœ›~ñg6P}çu }øÒ7x#Hõ‰þŠº°Ÿi.2œ© U¤N ¤(|™¼¿¡¯œ¬$Ô¿g…ùRŽÍßꪯ.zƒ¢®Í›kóç/%‚Ö?§åàô{â aÌ˳ëLmª‚ÒS¢Ep¹È@ýêÓ+Q¸€Ÿ.òŒ–çX½â6óii›àê4#jEÓoÍ|m< "¨R°¨ã ‘Bä_k‚ÇêÐĬ<%Ó¾1yjBDáQ  ƒ×%IˆÝ ,H„Ö„É" ÊÛ’FÛß·j4.Ô×…RR*tÈl¾ & 6U;¾(À(J´˜S0c‹j˜jF·ÿ×z Üw¬ÀÜ$2Ü¡üM”F–p]]fþ÷Bc¬,+¥ž ò‹äðÆ,Ö‡„‚ÜcV6u!"s²<ùùFÑÈd\y< Þöœ¥Ù7¯1J&w¯rÀS|­SŒéÆ5fð jóÐѱLfIhÈ ÓChvdêë‡6 * ª—ä§µÁÍg‹Î‹·Ôø4H¨‘½>´mt~>´‚ðüÐM¯¿Þ]ðj²™—ñWå ˤd$Ó ®NÿÒ½³‹&z°,®íZÍ)ÐYØë/7ЄÚì}ë÷©)ëÍ,Šk?jlV‚lµX‘”¥e0>–€ñÄ’3<‰L$f¿ÈòÓþM—‘ш¾YCÓ2†f¦ò¤ÃÝ춦å´hJßp-%ßmý9ßÕÉ¢ÎDm˜­øÿºà² 0%èX4]VW- Ç¢Ø {ý~1öû>ëçŽ4¹’ú/‹K ü±Ó]î©¿êÞîþö.)rá'Èe©€kžfS4ud†>º™õGÓ8oú«ñ)¤dB› ‘sˆ,í_o÷®‹Õ|.ôÀ’, @Û†`èð ï§]‘·!5ßv'ÂÊX +#oª½ÀQÊ,I¸k"|génnO…ÉÞ# ÃÇ?5Rbì/Òb3}_£$NÝÇ/ª¡!ФŒêPœÑ1á¹4A‡OK®À{Þ?ŸU‹iM%’ó|Q×e>éS~¦AÑÝíè˜Ù}\ànëû–IõeRx,÷ÁÀ§ÛúëÑñ³žµH ˆ7ÙÍÕÝG;Ä·»Ç³0er¿ð;2ÓÛmÕàÔ±¤fDÂê«‘•bž“½˜äÂî qRØë‘ׯÀfyb(ÚdLŠÖŽ€U3P„ˆ­#Ó®Vó‹$—Œ$‹ß©‚ÉF¥aj,­Úr‹ºc.§k``G9úí8-7Ý™ŠP–™@Œâ‰ë lÚTãÄÂÑhƒ™¯z/ ÀMØõIt fÀ¡?ÎZ‰Vö–ù3 fÈ«ÿÓ9B´J0Â-'ŠgÙÖì=ϘӹSŠë@”ŒÆmq5¯Aç¶¼8G 7Âô€v³ ˆ* VXWŠ1EŠÂòë׉~mòšÍÊÊÒÆÄEfíAÈìõÒ¡òMX邾mâß|Ã¥UìÜHóø²4ÝÆ6Õ•¤¯„ŒŠ´½FÅ!©˜28U3w?A!u}·f·@³™Ú›'›BP2Ã`¨ÁyÕ4ÜHAtà tN˜Ï4Ó§×Þb ÁVøt˜CÄÆ„ÅŽþ£ÿíÓg/ìϧøÓaKRQ5ÏNå¿<Æç¹08cg+ç—‚‘…få¶„HžÒdV=Î R?}Î[u ƒžå  :1@P;d¢唨5¹]Å@”X½ƒ|ÛP5Ö2×–––Y†›`°· ÈZŽhí¼ó•á½—‚ Gä9‹`jÎELVΛ½J,›5QêösV`(\Æðjgæ Ùz3L;ô§~é\y Ìì„@f*ÜÝDbcH¯?* 5«Ft¾+f§àÍzÍ{z oÈ&ƒ; `z’|ug¼yj›¾OjÅÕcQ3q5Õ|ÌÏÊq‘ÁÒ6ê­ à¡ ¶ _ˆx*(ήRÈGRä‚ (â€:±!²‡bü½Ž<÷CÊXŒ|‘ß(þä7ØVË%ÚRDÓ]®>j.ž²Ób ËV¦.œ-ÙST}pÁ B9.GÃ{ÐM¨@("2vuO»:xØË¼gX¦ËPæúHÝ'÷.ÓVpiãeµ²–îF5wâ¬Ö…2Ðt%ëŸ.ÊѰ¯¿¥N»ú*p޽ο¨¹‡Ppv\ö«ãÓh·ºCŠ–iâ£Ê ž•°efÁužE×21@®åÁ]&¤¬ìé¢Ë¹T!ÜÁ½Ö5¦†T˜`Ré*€;Ve5¯Ô‹´-¬¹ª´ŒÏŒV9Ø"%]6ïQ©®ÁñfÉîÇN94‚hÍ2»3IXŒŽ–ÍÒbøÜrÔðZZ¬uYb¬wBL\Ü,&^BFv¨þ¤<x±7Ì7lU9:ìí\M{b/áPT×¹J˜¼DpLµ±+Fxl¦6æÃ‹ûƒ}ÂoÕ—ÐÇszN=™¹ªfÄÕÖB.{uôêÙÑ4P /ë½>þæûW×<U00#ÿÏÐqW,ƒšE -±|êØŠÃ•k:‘R_é.ä@ŽŸ Ÿ5!µ Î~ä¹5¨à¢5/<”ŒTQ3ƒüC­(¶5žtKhÀpîò3ÛóÕíWó\¤Î„šûj£.ÀÖû0œë(hE†`HïÐ=RY Â’¶Â!Ub!ùG¯wÜ6÷ÀÑ5E…‹8tƒ^`×Ãe¶G9JÈ»å$A20€W̶Zijò|As`­Î8™¹#«E™ä2ºÜ‘à6äÀ„9À5£Ø"˜Ù|j/R¡ñR­UÃè„âÔoP÷¸µé>eë"8¿ÛpRµƒƒWgš±ÇŸ,)NŒ,öôvXñŽ"®:(´@”Ùѹ$`“¶ ‡<½A`¡$åÆ9IeXÃvNF_g1âí‰u!íuobUøM/_©8h„¶½ à:‰]AÙ¢UÊ)«Æ$“µ£vùÑóbìKìqv~ü"æQhV¢9qWñöõlÐE—ä•gdð ëyw¦®Wõ Ÿé(’äÃyPgUXnqÔWÎÍŒnØ{”.5Ù9Ë™/cg¨m°T#ßãyzužá­c|@˜swZü| ÑÚæOa?PËqªû²é¬š¤}ZòOò ¢{SÂE$µ,¨8¦Pæh<Î"³…b¶y›`´ëL°2ô0™ iÈ–ÀH/ìl`'>àbWî!ÐX+ä¤÷NC0MŽƒ†B3Z?ͩݽª-á´n¹%žÒîkÈ׊ñå?¢—NÅHí©"Ì}_Ï1åîlÐqFÙÐ:êô cëyÓÂÙÆ‚ý¾÷×Ýé@mÎ3´Ñ¤¾«ÞxJ|wÄ™C)ÈM´1=/Ǧ|:„)ÀÿDå‘ïäÖMÙ×Kr1SÇ=H=P1¶­iºn!$ÏG3sÉË­dåŠøfk?9 HyFSD7a'Äaùé6«ešu2\ÌŒYžaÑé{z­'­Ö‚Ä(Zêˆ?Ѥ5ì§d©©ûS˜3€\‡@­!u¸kpfh“N æJ¹ɰ¬§£üšM8ð>ë¸L¥‰b‚”¼º|Ðp ùÕîeØŽdÄNGsRñêj‡7)8SìCl´Íc¤ Bóƒds7±K`º}fÁ „']Ø#7G8¸o P™SH†>×`FE§Ô„ã!ÒØåÛ£çÇÏ~ ‰ÃÑË?÷ÕúÕÇ"Z„‡Qo}óìéÞWìƒ+З—FODè]Ç Á-€ XÒTê ò£]æïæ" >²=JïéÒñ.áçóbŽ\À›ôM;y“ŽªóQyYŒJHÝ Þ¸UN–^ WI šš]–Ê=Ñv6]i•heBÛÉöŒlŒÁ”ƒÐˆuOVÇŠû…U5´w‚Èã´¿X}§áäl· ë\µæó›¼iÈúËŽw.ý‚´'ÓlZFêjçm± ØüD®€ö]€äGèVïÒÆ±Š¾îSް @àƒŠ…0èBd¶p" º²†©hŒ@>—} çcÁç.ï`O—Ä §à±ç.iÝåú‰ˆRîÕÇŽ`PS&`r3OïÎD]ÏèlEcÒßCf¯ˆgóÍùkô…\ >ŸXÿ«Yíß÷7#`ZÍOQ÷­?X±óß¿f¶¤Tã1ÌîY®h†°D ïÊ¢ðPÁWwŠñš\=ä]yWÊ—b“9¶?©õ+ò±ú :‚>ýB[¹²C¢}-7™ÇÄûôIFƒ:2mqõÿ2h€Ý”É7<¼i2llJ]z,wÉ Äd“}ÜÞè—&@‡‡°öÍæ/GMÏb‚<"ÇÑ¢7¯.ãèªËb“ù ¤¼2 .1š3–ÞÒz0D|”¼úÚó!NäΘu|—£É¯ô¯oLÃÌgs@„ÑŽ"ä›20I š¡¥êêížxJ±Æ]rgôׯÚR=€î–v>ƒç£Ømûº¬¯¦“¿§?†;ÜϾƯ¶—UCÿ·rÃnÔ]»ý>Ô5ÿ‹ìÀÿBÛÍl­¨Ï ù.5lžWP#¸èú<··Líæ]AÚ íÔ†!RÑ9 ±ÝtK”|Ñð†­7õýÇUŸ­ähå¡ëB@×ëg¯^=ëÿð Ò«¨ÁP¤ÕûÏ|û—ƒí¿AlQFô†«%ÉÜûõâ4‡­{©F?!g3§ÞriÖÅØµ’ýÒLQϱ¿ÊtiÚ 7½¡e`D«Ö(åK¤§%¬ŸÖ1²\³ÅxÃ’»Ç-ì¦.+æÉ»2˜Fõ%¦ /8X†/4¼øuŒ‡KP‘Z³ým£Ì4<ë0Ôº\¯%:à1Þ÷åúüf’èx Â0ÒrZxëqIð„|Œž“8/‹j½ÐxÚòËFiXŸÓðÅš¶‚²‰¹];­¨¤Œ#ºq|DM¹TŠÃ&öC0¥ á Šé*è¤ ðôÚÍH¾£í"´ •©-¥}Ž6Ö3x|€;Ù·CÞ*»× ¤¦pnÍ01£i™ªl”Ÿ×"—„ùBGÆÚÄ #3{m€GÞÎØˆ1겂 C´ÀZÈ,ZÒŒFK@ z\€uµ˜ ìø¡ŸŽÝ•’ô ŠÜòò }ÂŒSQž¤ë]Áð@ç¯1V#HeåÔEÏ Õ4GÎxyœñ@ˆáqä·Ô1«„‘‹#qyîœr ¥,ØÅ¹:Q—õLꦃNM™Óyb#šÎÕ?jz½a8Hü«! .E’ütð8bìÛ`€!…ÄÎËE‚Énj@°+Äþý{?‹š€fŽ>ÁLé E·ëùõ¨`¿_º/Ȳ¯.òÙà"m½©[´ÍÖÊXøL£½ì–œÌúiqðà›æE=ÍEÚxоmÞM×t³oZ›oZ~‹«¯õèç „ Âwáí}½ßkÊß É­)ÔWPûhcÙ(J}¸ù)¦Z‹­`ä¶*¸÷ƒ3¾ú©þYÉÑmHC­úø«d}kßãr]wÉInâc®’ôˆšjæ>®âpÍ‹dPQOw˳G5?¶’Þû÷'™ú“zœ ck2‹ÑžÖ~à Š‘À9µøÀc'O75ò†ÙCgì8à˜¿Çmͨï—fz#.1Ÿˆõ åÁÆŒYyÐzó·Ž?âuÖìï —âhˆAùètôÔïªL¯7œõ‹L>ØÑ©7ÁL½G"pê‡9¨­y›©GækåÔË^E¦^}橇ŽÝfê%`ŽT !6L½7=õû„*7O½Ô4SlbÉœ9Mþ0š¶À"pëô]ÛZ~SŒ1æVÃânM+0`V] óU—¿8—3{˜Ÿv €áè_TêHi=\Ô³‡£j¾^îf²‡2Ä ;C…éýöNö¹fãE>Qòà4 §³êŠä~ÖS³„0Egà#“sñ›ƒì?XõÅÃÀÆâÝXŒvÏÌ|浇3*…¢]ø"&‹³³ò=þèmïíŸt >L#ǨWúj»•ZžÚœR}/Mmþ]9Ts¨0|vY½£ˆ“ÔüxV¼S«{X]L’Çó| þýz nÈÙbRŽ‹Ñ)$ÏrÌg®ŸW娜&OG‹I­Î‘ÇÓÁéhUã2?¯ ±º KÕüþzο³é¬ÊNgO’Tv0޳_cNóä)$5\ŸFÛ‰TûŸU]L/’ogù/E‘<~{†|­Vz”-Æç˜é\Öü•²µÿáó/ÿ˜Àl«…Øý,Ùy´ÿùÎþgŸÿGÛ…tí :ˆ¬i¤ß½xQ·“Q>;/F×FÕ'й–8ç’þ›,6M÷ĦG 5¤¨ $gÈówšŸªÅ¤XèZH`ÏRu64¼Œ³`©[ ^….s­”+æcŽ¿­²ÖÙrL‚ í …§Åüª(&N 4òªdJZ_O!|{1V‡!j¦0¿ãdƒ2´ J­ÔTaŒž &ŸÔ(}£x@<©˜"ÈMð‚ò3œÃz aFM(”²Ö¹s„˜mž%G4õÐ ÊŽOÙ· 4ËõFnç`8Rh£ÆË 6h±8—¸ÓÅ–Rkw9u¤­âxéÞÊ ²vˆ5…à Cñõ)rL1Åë:Ì&‰Í¸0IÀLêK ½Ä†wØôˆ³‹\¨ùrjÂ@üȉ+A1e0“)DqUkEÆàC.û½(¸çêºàpxrx$ZQŠá9÷M*Ž«»`±˜ Yúv‰çÊÉ%¬-PaÌ¿G4C$rÐô¡…7!tã©ÚVÀ°ncRM¶C<2ÊÕ ) ‹&ó–¸T³È¹Ùưi¸¢s`;žËAajMJîq‘O Ú$—é®Ä*§j]c™»ÆeƒÐÍI¶îZ%Ñ‘ÖÒ­VÂ9gè§"ˆÎ”%ucã[ š#‘˜c9 O-èÞ–¹¹òÑôBѧy–ÈhÓ RLÒ苉Ÿ5XÞ‹×cUÅ6›ª¶&GpçÃÉižåR;‹'IJá ß—uütŒF[]ˆLzµ’6ä6‡|›•ïôáa$j‰!l{òˆ¬ðÞ¸‡9” £º:×Jd9°•4ƒ"õÏb+¦Œ–ï=ì뢸:Gºìóü—ëù´ƒ¨!hmf¯þVNy’óii§XÝDû/^~kÌ­[‰¦Õ9ËU5iÍ‚\ÂUà'Eú««zÃ/¾[|¶³sýãg?C¨Ð¨K¡’› ë÷µ|ðgUh7qKåç²L5G ë.Ýzð²KiSúçÈ 9à^Ž)ÅÂtdNXílüŠ’)”œÅì_¸ÑCâ\f‡$ÓÆà?`L^f™ÙLðPU+g: |Ê/aœªy¤‡âH‰½¬‡h¶ã‚Ä!b°ô^”&™× 4Ý$íÍñƽGâÔHàPwš¼iN_M߼ߪÁÂʬ²€@ÈÐ|ÈNP^<ü….¤Î¹È§Ñåä»E>9'9ãÁ ˜U8Ôï`̯§PÒ Æ^Qv¤Úèó`´ž,@› Ù”¿êL¯®°RЍ"çÇ+ÉUGñ–žcZî•B§<ë máÝÛÙÙÃH¿¦ÕÉaë0™ŒO[ÈÓFç ÉMst‡×Ò°hÕÙ° ’ÈTlÃWÁ²Pg¼x;Bø£C²5Cž»Ìñ,àÞ®µ“ôb1.quÀÚ 6òƒ@ü33êÌ\ªþ½ÿòJ±„M1º(ðE‘;¤ѬÅJÈiþÌ £›îhð^T?»'s ²In_²]žL*ÕÜ™U²RbBSW­ÒÉu{¾äEÜ8GÆÁ˜ðø-põæÄbì7ÎËWjï¨+k‰·À·mæ2Ùþ y ƒM †¼£kÇ€¡Ùßzqºyx9¥‚ÔŽŒZûÑEÅ!é‹m©‡ÐGýÑ"N7Ýb/…¸Ç®BéuÜtbÍNží/¹ëÅX§Ïæ)' ýÓíÞÄúty%°Xü}zMi– Ï(#³¸¹lÅP›P/µ¾'– íÜÑgø‰auåðF—þ$ùð¦]8ÖU>DI ›‡,‹©Òoöûò ðžïêÍí}gt¡ ýå}§åGZüÃûªíÍœP"±žÂ`U9¢¤d±dÞ·:É϶™éÀ18±@;ıgßòă³i,rŠÓçùøt˜ÛƼ2Gè^Ö¥ºèUpä[¦åbhÁõ5²¾Û9iˆp–Ù(ºÄôió þÙ…¢óæw¿Óeì]Y,väÝñ*Þfý–­\Óš½1?½Õ üÒüŽþË÷Tô3—sbÞ·`] Ã jñK8+¨g¿KÖxüëÚìd»Šç6‰>ùË‹ˆ3;™˜¿¼eȨ‡(³ËrŒ…_Û™<ÔÇ£- _®(\´Å¯ü¼O¡iMPðÎà7<¿Õu V2D ú”r i_ŒÇD„¡\Æ:x#£Hýi0¤kþò¡©]‚B¶oØUúÅ<+þpW|£¹×ȸñ»fjÍ¢wí2s"÷¬CEÜýásê”éèD]¹%øÚ”Ù²í4"·¥ ‚˜í~T-²¬aó‡7æÀá?þºHÜÔÙmo¹£²=¯úŠÆ"ÔÀkq|Ä¡:Eö7Öƒ†NãæúçÝJØšçfnY.ѯ%÷“°óæ$jè<„CQÐN̶5­92ª~ú:Ùg@š¸kç¦H9ÐëÆ ¢,ë>7Ì ¥Cçùxœƒ¨7ÛõÆ.Ætxƒ½ç‹Ù’y`*»6ê`‹p6BŽBûyÃ-›šV"ç;zÂcrˆÍ´d¬ m#äõÕ´{x-Gzæž4ûÖDIª¤ÃUíƒK®áÑFˆ£:ãËÆŠƒÂ¿bÐ;aÍ)!ä[´Ð¢:͈.‹ùU¾Ûュ#Ö&ìèÌQ¨ZF¤fñ;hþ0ïLÀ ÙçÆ¢¦ã95Úæ¾d¹RœFd–L-É(¥~J$̵0œÆÞ÷¡:Ñ\‹9G¨«†é¬-Ô‚>Cù|í³g²²¨­½‹/‰SXê´c¾ҠÑ|Y¡éAr ‚ó( 0†îÝ¿OóQuÞ÷ñHÜÖím'5¸Õ(«~+4T¶o‹ŠÒ¬…Šñ–¼[ÇÓßíõ(6´[¢dÃÔü·DKqã^CÑh.)¡‹_ä¥ùb…„îmê8y’ìÑ ×†ÌÒ éôrL¨óÐBʺ“eÂzµ˜°–©…ÑN±‹!eìÙÒ'‚Û°Z¥HþÙ¹Ms³ÛIŽ#fÁ0æ±aÐP8‹B÷çxךÇ\Ó¸æñq…+xÅ3q› ٖ³8¼‘=æ£^Ð>P;È~¡ôŽ·Õä)ìü>Øëc~Îô1<ß(å1…Ö)Ú&$fæÓ®cÎÇP¯S`@Ðì†î·ò,5¦2ð=F˜rChcÄ$ IÀøû$)v[ð‰“½±h2åQ•§19aá¥t&²<þùɕՊ6RçõÖWDwˬÑ:ù¥œF…2P_œÕ¼ñúvÙpþZF×€†þ±ÔÌÖ±í1ÅåÃDÚ²ÒãnÞøá[–Sƒ1©ÖIíâ»Ï;û L¡Q7cÓÿ&é«Nò}'ù¹“¼fòý 1†íä5Ýê4†ÂeM]&΋xûéH¬ä+!y”§ªë2˜.Ò°JýÓWöMØÜ†zVY¾Ã| X:àõ¾FÇÛ}§ÇA­Þë^}‚ùhgßëÀºÇ±aß÷ÊÎ%ø«*iò ’%EÖ»ªÙÞåÉßÕ†À_°z½ò¤4¸àCó?Ëæ›ÔÛû’Æ{ùYuæ­ìŒlü-wf{wÍî¼’ÝÁ£}ýž¼RÀÝ~ìÄ[õ‡FF¨Ýó,ÎÞÈ/ܾ,{¦ŠüR̪:}Ù¡=øÙž[âgSмl7ûÞ)ö¢©fã³,ÊËRÚ08(5GA!žÑ(£U—ñœ|o—·‹ƒU(“¼=IblO]JŽEtïm„]ö.m{/Úƒý’\Úö y Ûâ}{é5ö: u²FÛj÷{P¡—*£õÖ’$ 2»˜]2ku>¶Õ-H‚²lA0ÅÉAéÌÉÆuZMmŒ?Oú³1îq@0U•[HêĆ&o[˜Áš¥ÁbÁ<[·ˆhCN'HÁ:q qCˆŒf›°CÒÙïôY( n%C´ =%ÃJ¢(ÅȘ¡£Ê'×r±\‰ë ¢ÏjÂÏÄ žÿPžÏê‹|ZôvC:`QkPã‰A­ˆ— á;;D AYu­U*Õhèâ[PhÞ…ßà2í)täOÿ!H`XAáY‹R÷ÑÐÆÈt· 4×´¦²K‚nüñææŸNºd·É’÷ oãÕ½^¿Çë<œï5ˆžåÐoy§“Ò/Ïõ€Yiíã’¢S» `îEë*áHšÐÖóG}¿ ¼ü°©" b\'ÃëI>.mÄÕØ¹º£Ñ.4Îÿ»}Xx9˜ˆ¹âŸËßò+š=o…(Z޵¬Ë)ëÒS>:Çõ²––€¦Ô_-ÓwÜÀ>{¹IaK¨‹R;¹Ÿlïz K ~‹ÉEYöU»½°{ªˆ—óv¹´k ¦ïØHü® ÏQÿ ¿ iì)/Égçãü}ú.rR)(Ì;¾«{PüÇò¶a,s93[Æ©V¿›2Úa×tâ¯(†’R(æ=.Þ8_(ÂÚ騅ð“ç8RìJD¶bRüæ~x,ÛUøæÃÔžx-3ð*òï`xÖïŸsÈšÝï C„ ÷)`Ñ[ºÌN(ÏË9:S‡½ìY¤G2z´ÍFMªIu^`u]̦UݬøÔ{¤^£Öj¡ßÇ`ã§ÿRüÅç.)–á.·¬ì×›»">Í9ŠPf«ÖŒK‘i‡è”Zµ~Ý“Å}“Üa¸Hêœh¾ß¯Ð58 iß©e¤NͧÐIõm+6IŠæ–ãŸaŽfzk 7.p…sg>–ZË&Ö#Äkœoõ[ÿþÐ|¶E¯™w}Oy—Û·„r… åmÜ0•ï¡ØsÕCõ þP¸×|eáq¦ïrµÜ!Ñ_Ó’ ¸ô6ŽHðà¹Y[(êÒC¦˜3dxû,¶6+Ö„r€wxËÌÛhÙÄÙ õ¸»ºm9,Nº@R›Su!ÞžVh^S+ª#•L çŠeôœé/ÿ¢,Í-Ø3XsD¨Þ«qŸâ­ññ0›œc¨ÃóùE—‚µÀÁž¢½iÿJ5½ÚáS_pb>!XO)HP´Eˆ³]û³ vö’î¥N‰\=Xñ¼SÆ&<œð$xn,äpÐ"x-VâL¨±Fåy ‚œó|všŸ+’®ÚB§}¶LMêJDŸÎÇŠ[Õ§£{ÑôìêÜߨ­‡²³®åV챎·ù¦£(©Ò+Õj6瓜ÀšÂF¼þñéñ÷yöò5ŧ@m˜ºãƒ?7‚a› 8žØ¸Ã¶b Ðb`(öR!Òè:SHŒ ~bÚ^…ÒT€æVáñ$ ®”ø=Sœ1#/,’Q{+KÛÌï¶ýV Ù÷ŠébLù½×¦) ×Rg× â&ö›ëÕ-а²¸tv‡¡ò @ rfŒ/˜RÄE¨;$_Ëî IÈôh]òë{›¬è*M׉™mÚÑÒø4ÉL3*iqïW»:›hÇ ~ÅT¯;µ›±öl4MHD²ý+­ÇÚkr›uáó‰Š ±U¬ÓDÿTuôá!Vp°÷ÁRfÇYv^Q\ÔSw@êé)zL쌸+,¸‡P Uiøýz@ "i øìEPÞAŽ)E½µÕ¡<Ï0¥)¤÷iM×SÍ9Û¶—„ûo™mõ/ûŠ}“¹©Õy4ƒyŸ ¨`d[h‚aÔž¥c”ˆãŸ¾Ÿ^;:üïÒï!xØ6ôºÿœ•_'OÔ;¸ò˜_¶E×Ãýºs !Üäµ5yt9>EÖ^g¼ùÅÍß’®•7ΨiÛpGµôaý†ŒÔƒÆ†!vgÉAMq«ä„*VBpß “¶9+ º@¸óÕ8Y=3†äaò·Äá6Ñ@½lžfÅŶ׫Ñ3uú;p%†ïsú=W%^S„ºÛåRäGûžÍÛb$‚cÓý!Ù×Ûe¢Hjl¾Q@ +ެt6*!´R'ÉÙfÅ@bÙ—”yÁæiÌÇK–%¯) Æa´ôô ë¹v» 'à~‚?ÜK<Åz€hœó¼ ûCµ¬¶\‡z‡l û;i­&êñŸ°ä÷õÊÔ;‘éçÉíÔeG]Ñ Ì,°¤¿ÕÔ?Ø…Ÿo¹…yãØyÜ.0À%ý˜ÛNÌmKso,„p7rùuýE‚I´³Ä‰VPœÛŸ;á [@èYæ£póv€i­9pÙW‘cˆ m˜ÏðÌ‘åOðÊ)m¥@öÏuâÂ;ÝÙKÊJÓÔeÀqšù #÷Úµ'¹³¬uÒÃFSWSCÊ®[×a™>¹“ÌÎàÖBAàÀwzµÃÒ#;ÿØv»ãå*ç>oûp©\ÑeJ9Lù4­û;ª÷¾u!n:lêÚ å *v„¤BõÖRÌwÁùvG j×y³ʵçÊTSKWïÞªZ8Õs˜ÞQÝŽÕíªh)aò¸ß][w%=‹ Cwµ¸êÛRˆ%s†í®ÀýDñeÙÈ£P$öÜ"ú¨Ñ2&¥s\\µIpµÚ>RÑ¢—jÐÃ)Råç­7<ÞêË6_½Ñ'¯Ár7:oQÑ·Yì JdšÍÜê{3«|;à~ ·:8AúwÀœ# + ¾*Ùtú¤ÖäWµF^žÚLesXCyŽin£/E9ÙßN¹;:å °wÒ•[ˆ”kǽôÁ–x)±ÆV­—ëX°^ Z³/k¹/ë`_nܶ 3ZµÙ`ïÐí‚Ɇþ»a…œ"ï/òE bÃÛì§;TãÞ%æŠëu7é1 £)ÕäUÖlVPDŽ}©XЭ8ÜÆC   „ |Zœˆv6ÎØF­JD7ó á3ÄÞ ^ƒþY¸¾††\‹äºGNýpøsÉážq¦žmõ0¥Y{χ·áu#ãêçû¼Šz²ÌÜNסT¦šT½M-åiTüo9ËÐŽ—ä;d—£c{åËeWP/î!iµƒ1bm j2Û' w µö˜Ù¨êXÀ\¸£XÒè1ôî Oˆy%´ØõŽ‘Ь˜YØ÷×zÛ¿ïÈï‚å¡Á­ÁrHByköã7rù¹ü”\~$½t%P+)§S\M'À‡0Úñ»-e]ET>n½(6w·‘‡÷5w”sNåx"'³¡Œ8 ­â—?nâTZ¦Õ{½ÿ÷ÿˆ ¡#~–Ê$JÚñÍÁîóe’ÏfùµL˜î†À Ǥ96¾ˆ¨HõãC ðõ¥êׯÀaœHže þš.Êíæ -äÜ+d«¬Ö±&nI~a½mÚ„Bò]?œG²˜&óÊ^ MHo¬FjþÇØ“:°zÅ|ŸgZ5Q‡í pñ¢hðé¶úâÜ@¬ìš,µÉdÚüiRX)}ýXóLìê:Þܲ§Ÿb4é\޾¼•lLÊÄü£@BûÀ··–»zÐ5í¯;º ˆ+Þž¬·FçÜb¾=‘^³áÉàÈ—zzI|x˜´"éÜ|<]¨òþß&¬OÀ’´,KŽ›LüÍÜ}‰û§™ú:=eQâ*rº¥#¹—°gì]>ZP”Á±‘tÄ{é®3 ƒ PaÛ NM×=°nX¸.%€vï EÀÝS@¸&€öíº¶R8%« ËWibà í{оۣ1²yH8ụ›÷–ȤývEÎ ñJtgIYF“)ŧ†2…ç6ÑL±ü¿lLI9›æXŒ…šbðl/ú9çe7a¿Sn;’–Ý Éõ–Ծ׃èLb#š7Jˆ<ì’÷àB-À< ÝDDê˜W87>´†à*åY²,t³o$jíõYP~ž£¨NI˜“}«®(°A Lñ`’¼ <[¨cãì9¥:o¯J©J‹M!†\o˜¤Æ‰ÂJá—v¨¡aÈZŠòÕÄ ‘¢·z"½ˆ)›P ´c¿ÕiàãM.ùžM&¯xÿÅü Õ9‚o7èÆÝS-‹Ñf""1°{Mþq‚Á¦‡ëtÜöönûðÝ‹Ûë÷âט>ynÛÿg$nî<îx-§îéÆÁG©í¸›6?wÛáíVr?ùb§bÆÒn˜_/5òó%ÄÊvmM@‘=ŽÇ^îàu4ÑJM—;^Ë‚\ ªŠÀëÅ8íŸ ^õ2„3šGŠSfˆ$&ܲø~r/Û;CdÄF:Ø¿ûÉîÎŽ§+ taœÓ… BÂÇUÚzÜÀs@ƒÚþA]©îùÉàŸ„¦ÕªGG߉Ds½Ñ˜ŠóTg‰—Šð@;i“_§I›ÉYätzMºiÉäoœ\Yó î$±$œé‹çÏÚ8øx’ÎÔªn¯ÎÓ†ÓÁIx"ðתLl”Ygô@3KuQ)ô'HX\ÖŽã—›DÝæï>ƒFsçp””Ô›ÍéL§6¥"f:™k¤ÎLÝ9â(û#Ó)a)Ò^؇H^8μ&óÂÑ­™^ŸÉ5ÅWyÅGóÍIc?’¦›i®¼4.¦§’»& òd…±R½òBEµ<¤Vs­Æ6LÞ‰¸ï`uBî@;¤=¼êÖ"û†}V]‘ ­Ú¸SRN Ü·à€fP°½ £Å£Sv7‰]´M3fÆ„, ôÒG¥pN«BüFÒÔ1Y¹ ºÖNúÈkw½@¯ÜÁ(XÖÅu{m1Ö5Â…}pÿáŒ~O3œRLÜPÙµ‚}ª£ø£Ê³ydj(sÐ-Kô07ŠçÛ]žÅ&Ù™]7P<…’S[fÍÄfÌiG•Á¦öyÎz-üÙÙŒ‹½ ½Z‘;(Ú1á»ôÀ…×$4^‡4ºááW,6 ~`! à£‚@¸˜6p¹²ù´u¹g/,òˆ´Û·ÅÙ!h ™`ç x”At¾IQWz+66X§ýntX@ q«æÓÉÎÛ:HŽsÕ¶Âõ“èŽÆy˜_PeèÙk5ÉÀŠ&Óée16Cõïõ´P4/ù¡TçJjb ðóüj†GÖjª‹ÑIŽž={ÖIvÿø‡?þª²‘ýSi°$Ò‘ÅFíµŒå~l³o«Ž6±ÒZøf|¤N‹s/`0êÌF¶µ?fK˶oµ¹e³·Ûæ4q&î–ö&D9þbLv'õ‚eۼ؀5<>à!+cßÌ.Ù‚ V-&Ã?%ßýüâY´2DW”mü›Cö½ŸïçN3Ö˜e͆ޗF‰L~ï}<2áš‘áY;ÚáúÕ=X#I…Ó“uâ;ú³LÆ‹¥öšö1d'e}…´ë\¼vµËöñF=>S%lzïVù³Ë‡ìy°5ϲ’Aè¹–Nþ·ÕÚä¥Ý܇PïáÓ_½‡UŠ ¨õ>Ö ÷ûòŽÜ¢”R‚tHH)Y|>RGßÈž¥ž~Ö¤"·Øêþ>5³ð‘[O?1*° BuÖÌhX˜K§ÇÎ‡í± {…ßcwý¸dâûáiíiÍ·ebCPmö ˜Æl‹*C̃%ËFÞUº¥5ÑuíÓ¨ŽÓà1¼®ÅK@Ë[£Š\üÎÎT]Úᘼ!ËÆ#-ãy,ÕeGìDاKz¶¥ùá¤Î¯EpÖšøôŸBba (§oXêWï Øð8cÉÃ4ß„éˆÝýáQÜ‹½(ël©ùiZé®´§ÃWõpN¥,áxæ_÷Ø0Å´ÖêØ–;IË^ïù/·›¦ê–ãÆÏñäºöZ§@ß´T»W7½Ž(m¨ÊXàT‹¼®bŽ}N磔‰Ší¨‰PóÝhU‹`ŽM¸‘UŽ|ÃÛ 0–j¾6ù”ZA_¡•>¬0‘¬àެ ôû°ëW5àyã`¬è´ûR+öl6ƒ•Ù<ÐGÀÆ»…œ››hÌKà×7Í>Ç3eýùò§ë–“ôM‰jTŸ¨ƒãô:áPÇùì´T]Ÿa×ÙöL Oб6&:Ɇ½ #‘ ­4Rž{ãÅáaÆà³™ÓÛœbö$ªx£?Ç=¹<(8!ôߦŒ¼ ð‰KlA¦[7G¼©æW°M0*©•›+rõGN¦æùít[±zœÀœ{'"S<½fEÀd’®¥äÐ2+x^ 0šÕF!¾Üzš½ÜØ@44z2ø37š¼ÞçFÓÜ,ÌÉofÚ|ÔhD0? µ´Á罺9£®ÆÂ.«‰8«ƒ8TXFâM›j(ì •ÞNÞ&”ï“äŠlêP—GÑ…i®†Å¸Jy‚¶ða†+º?ˆƒUK¯L¤Aë¡C7ùÍ%5ô¦¨Æ-ï9DìšìOóó"ùì»ä»Eéµ R5¦ã­Å´Õ¨ûW 6h)4E+³a‹˜=kÈÞ:]ŒFPä´Ègð/|*-a{>¦Â4 ºé¶Íz¡¬b¬½ôšë#Œ”ÅD‘PÎ_ k.b¡H0PýCO| ‘=ÔœÅú]­EEÝá`Ðeó ú™¢æQÄ8= `š°í´×Ûɾè$;Ùþ礓¨àïÏÄ‹ÏáoHò”=:9ñB ,˜œƒŽ•e>î©ÓÔ—CÆ¿±ŸÑ‹ÏLÛŸM=妞v<± ·…ü"7Æ£ØÓ @XPš–¶³øa¥á;ùXn\€¸;+XBÁ“v´ùWÝ}ª-ÀºÈçÙ­Œ® Šª #ÄdÚa¨§· ú/ÌŸ¿!ámØ©8Iî'°’Ý·ð B§aÕ†_&ƒàÆë01~o"“Ûrhr%h-òüókHÂÕÕãΖ›ŽpUíQÜ…w¦ªz›ú.áÖÛøéùÝHÉ‘rc$·ëÆwÔzLU]°©D$ÔUMµx<×&)F2Ä>‹N{Ñ —µÏºÑö×Dç™ÚkCU¾N'¯ø&lx´LFóËlPͦ‹8¡ °ÎBúhÙ%œø:£¥ 8)¸‚ µÚt­IqU·Ú½}Ó c ªÐŸ¦ÎŠ ì˜ÊQ‘ÎZ½û'ßÞþ{ï?Üß>yÀ")( òAP8h©Šð¦ÂwørQ¸º{쟥ý?µÆÙwN¡Ò¾£ä¤êÒ|bÛ§2¾nñ¹‹ÿd£êJ¦6Ù¡ôöW®cîmx¹EùAЩ_½s*l%‡¢…<5?÷œó±o4É™º.Ðc:;ŸU‹iÚöËÂübãPÈ홞(ƒ+`TÿJ­!Oû6ôÑ€DJL¸,ŠAëé_Œ|6e;W€úØæŸæHÙøzXLg`Ü0Ýü±.4I¡¨ ˆQDà}­È‡Ùf{C‡Ám€õ:‰¨3,ëé(¿îÊ` R#4Âñº\±mGl!Î<¾úþµ ´@b1oÐf6RÖ²ŽšÎk‘;ä_Î2ò"Žélxª;bZ±§íNµ_lìfbU¼óʬã¢ÕÛÝÙoPˆÙO×È µúy9TÈ9)„šÙøÞŽáfVTim#½ÙßÝ9±+ ª:^æþÂ+XtàÕ¶žnC‹L-“ªLJi“ÝJƒ™VI<5p\TìŸ^5`c£¯—jA`Z>Šš;(j­ƒšIj±¡mVDl²¥è¹»?Öœöè„Æ¢I¥–k‰àr›=âPµµvŠé>lµ/…=[¢T:áÙÚ"ëS…Dà\ƒkÒ68BTÊ}”ìîèûP°-üÓS£ÙWKyÒ<q÷I戺ªFdz[ï>j»[Û¶Ö¸£q·ø;wGB8~j2°"Ù†DKnŠ5Ôת~‚äFk·})±ìø;k¬ðI±E,øMdð›Èà7‘Á¿¶È`ËflFjÁÙ]ÿwC ÃÀ0*MÇ;~¦ÙœùÔœr2£ìŽÔïÀ¦Ò~n¦m̱ýÈÑ‚SÍ”½‡ïÀ6 V!2Ñå˜øÐdάˆ^q=PÌnóB#õ3m/?7£x²ò ÐƒÒç±ü+V—ï8±X%¨s&ù¸ }Y«ß‡Ð•ý~kŸ© Èó7]œŸxÈll™WøSMëïõÿ;_âÿéç³½ÏTã_|ñùŽÿìò?»Ÿ}±óhoo÷3,òH~´÷Ù£ìá|<}—ûmÔ[)æv†?ªøáÅb¢z“M¯ÿÇV²}<ã!Ñc²˜ŸmÿÞ¨>¾ÌAÊGÉs…^ 8LŽ«jtYÎ÷“#Hj–+æJçõ^L^UxÁ¡_³-à°š^ÏÊó‹y’¶uÔïnï©®'/Ÿÿ%Q$ ©b …F³ýäXá$Ïß.&ç£ÿïÿž%§ð"á‹êìë‹Yum<Ëê≪ùãÏ÷“Çóùtÿáë«« eÕìü!|ýôeåÞ) †ƒ™­èê¢Hž>{ùúYvüó1öõߎ†û‰™•äß66@›qè³´xÔ~óÈ+µu¶ëj1Ssbg!C ¼©«zƒÃïœNg•¢FÞò ªw’WG¯žéÂ0*ž+ZÇ/ïolô©ýÅ ¨\‹Ç ²óª:¡ˆåá”øaËè©ñ7Aú'Ð<“ÊǪʚ«&Ž•ÆIaÊ ˜àpŒà©Z°ÙeÚ>ú¼çÙª È„´ÓåÒjJ‡Ûèºm i„¢îÃ4‡ªÃÆZŠš"îj•”ÓV’²ÁÔ~rðúðèˆ)Ø3"XBžúäÉ“ääNÎ<µ …ye}‘ùŽåT¥™­ŸXŸŒú°rVOÁ8ƒ!ë;«Â+ráÿS+«§£R]S,¨A–ê§W­v'I[%8~¶þúôoôS†ßßÓOݼ{ùR½³T-mUgðúè%ÈŠÜSO½âŽQsôêOð#kµO܈h²a¹úóªÏÒýS-G&Ò+Ã?5Ûí´d„&<N3ƒ<\Ô³‡ –ùÍÊÍ…òM#ao=”‘:tõÿcÿ>Ÿ4ulœOS êgV¼Ÿª3”ó§˜ CÚoþ¨+HŠ(ü­3ðìv‡n÷-»ZÎäF<Ú‹É»þ»\qië»_*òC£¿úß}ÿ♃#¦{ùlpp»rèŠÒtÕ‰”Ð+ËÿÊdwHO[YŸð×Áœv$¼â&íB&$gÚsÜö“{õ&ÄÁÃ}øØŽOºfæ¼¢†zt º ëg¼ª¢Õ†l0q8™k5T8( Æ~ç8užÜˆ–ÐôÃ혇ê;iÏǯNdðÍÞ%õE1ÑV®MµÔ…ÓÿV\­ýQÌføC,*·@§@7Ù|3y3Ù¤0ž›æ/#£jƸV¨<^÷úä5%fX£HuŶß?ΡO£0Õá€/&°é‹TB\¿?&–†ó;^,mu&ž¶”E¹%`¢\à`À7MÛ|€Ð¤ûáÏue}¾^Ÿ¡¢fÂ0ß\b4'º„Ú ,zvh³íªEüA“EG| ï&¿ÂFȾ’!ÞûòóG»¾üù`.øèó½/—3Ät1ƒ8÷Ä73¿¯MÉäi>¸¬ÎÎbêazŸ Ñ…ÿy>Þ‹áþøú|–KÕH6(ëLÝj&“¬.žÐÅŸ×óâã<-UåÇõé×Ei3…Õãbt e³|ñDÑÈr¡ ‡Z8!—ùy•ÏŠº ËäñœÿúNµ:«Ï²I1ÑÿJÅB|¸³³ò=­Ñ¬}wÌ7²È‡l+¨Ñ…lf³Øµ]ƒíçÚ¿ã$gz½Ø0“Ù‹ÙØç7v¥x¡t¨w´¯µaÔÙ°¬¯jE+Ú`Q)úXYóMä*"²¿+‡ 5=ì{C•c! ™â˜Âz>¿\TêxÄ“ ½ñ¸¡w™-õäƒöíÇ6<½ÞÐFndNß×óë ÈO¯Øk4À'eX°^'‡Í“¾¸0ÿù† ”V@@‰%`Ü«ÕLb×ùx´±aUÚÒv‹i{ýN̈_TÅÙ-Ï®³I^¾+NóëÂ\Õ^›§ðæ •ê²¹¨á›Ä“±w·­;y6¶ncdûðwÏ]Á§­Ð0ñMwGaµ,ö 9Ff5J‚àѸUÜ]M*Jg7o´$²ÑHþÛlf™, b;%v;á†A0ÑM“¬»iH¶¥7Ž‡á ¢Ù5ì‘õu©Ø÷¤Ï3¥îª¨$ÂK¤GŒpí(ÔŒJFú†Š­ÌSªßcø£ýøÝ©WÇUæ ~(åÿŸ½wïkãH†ß_}ŠYûø•„…Ð|QŒ³HBŽ-óbï.我4ÀÄ’F™X¯ÏgëÖ·™ÑL’}~¿Ub[šé®®î®®ª®®® Ï5(èRþƈNˆ@ˆ“ÀèÜhóBáf+àEunF¿³2)]iO^¸¸›kª³Þ ¯ÕQú†sç†ðɸ.s¥“&c­z»þœ@|éMÅn¿æ £Áv¹l‚vK¶œ {#i""(neÛ^²éxˆ- @Ö¦*š³,H› ôËŒ»‹ÀÎ(XÕhiÁÔ¿èÁ Iñ&Ê¡ ì9Õ²2H!Ónõ;GðìJEØH‡èdr3Ç1~Ϭ=à5Sœ‚8¤+=‹¤—}gTi™Â‡X:Z“±p­»ÊúBA¹p½;C‚uÍ–i[©`8uŒ„£Ï%Š‘Ü4ò Mq fÔIô9çÌZ¬Y×3ÐiÜx8ÉÖ—lïÃ\šb_£íÀÙýqä‹óÎ(O÷l‹2Å™$8nǹy]ÒðBáƒFKã®öøí6=”¿Ö )‰¦œÅo䔹‹¥¸qv…°³•«©Uæ²S7$’@z@c1L`*qà*عLÁjéNKgΤq´Ô‡Õ#àw¿à¸|õÖ××Ê #õU-39¾JÛÔÑYú›þa^‡˜Ž–PqV\z‘’ùŸeú§-Ó¢ûe•òœ©¢Ó–JÓ[6×ô@ßOƒëTtØ9@V×iîšU§xJy½M’ÇBô2}GŠo® † ’ãÁ€Ñ|ô¼žŸ˜–»”¡X$䘵Xo¨Êz@ÅÝß1vºþUPKÔ4¹˜Y×SøÊk²ø²œ(à¤{EŸÈ‘‘ÝRQÉ·ULÈÈH±ïÀx~™W ƒH©rF›Q@í±?™áNà;ÊU–ª"þ,Ðr0ÀwÆ®¤GÒ7Ç>ìÉ\³Bð½þÒãL U&ßs˜P Ya® .]Ds1ô¡cyc ýÊ ðÓQ*LËôgæ\}ºà`› º>i¯S¤fö–80:j!†ó¬Å{Ç´šéUÁÀ->#΃¥{ˆ\ý/_-? ~yAêDÍ8…CSëÃòÙv°œ#‰ö• Ï‹Š¸åFQ›€†/aUŽÔf¸`ö”«\úÉ%.Ãê-ØîNž–mߪ[ë'jôä¼CMÊ2=ÊÙŸ¹ÓLA.ä‘…Zþ3Èn çW‡‰ç+0^L8‘D¾Es+ãëÚöcÎ0å-Ùü%錴ÜßÔ›T·?V–d˜*¿xÚvtûÑð%çPs.wו׊јTíìT3b†WŒt,¸Ucþ ®Ý©IûÏ€©0+Êh˜Íôô+¯†áHŽŸI1Ípò_”UqMGúÌ‘×î—ƒ/ƒ¯_õ4ºÁh’À(•»Y¬N°æéWå‘0Ž0FGîI­M§Ÿfô~Š®‚Ïx8\« 37U(­Ïö?¼qIiH†­`Žn”ÙeÚ´Ù8F†h44ŠÛ{²ÔôÕ«„”0©JÆä0øñÐ $ʪ=û(r)~}£´Îä_V¬Tú®ÖhÝ#±ÄS©¼„bëðŽB†z~ÂFEQSJÁd2Þ‚+–e¸``_)ot"Ûr5 a*Ë.]œöz‘Ë ¯vÅ¥'ÁU¡(4Ó†L«§ÒÑ]‚øSßv\ŽÝ1 À¦,†‘-[9ŸÊe¬ÌÛC! } Êâ0(2Ôž¦:ÛA?Ì]B÷2ºB5ì†GŽÃéàhð€ë;ŠÊÍžn¸bTs´é™»Ã¶¬H KkùCTäD6‘¢nYªæcª<¤t-1ǧÆÓ´´ž+cOm.¦šE9Kävæ¤C? ÂÝH[RM0ðlásr)ÆÂ'Í^ãâ®ÉRRÄqQúœ«8BŸHâÌV“L*ùÄ*ââàz½9Ù-‘Zþ²íÙUò&Ä‚™y0CÁtÃðÜ×W³p4´u2¥øñ:^_7‘ õæ<¼˜Åvp) € IÖı‘ÂS•ÚÕ{Q»Ø^ 89§­öÍF r”гިnØËÜ©ñ÷aÿÖ€eé¬)Í*Š+Ümf. ª»’:;·ç]Oû«ßyÖ9l2¯áŒ†BFYçê&ÿf5ç\ùF3nV”nt‘ª¢qX¦ª¼Z@¢ß¨ª¼ZJÿ8mÝ MÇQ¥W‘EXóµÞ¥”uŇ'¿yÉð¬@_xSùDæŒÿ¿/lß3©íീß}×å£)¯yÆ„ÿH‘M2ë IŠ…-ð<¯r@NvÂÆìã ñM"Šζ>qÓÐ`˜Ð? ðŽ®‡ €’<€!‹§—Ñ(º@RáAR%'(.™ãÛáú(˜\`¼=ÏùäÌÔçlî–’óŽu3ƒÐg Z¦;.7fÃ3‡¸¿Có%xýEŒª\†þ…¢¦—_—­‹0n½xh{ÀlÍ“@1qé#"X‰&kpQÁ) î„“Ï(çkDÐh± ]cf™Ê1S¸ä"¶¨0ÛV8ê÷·uÏ3\åÖÖ;ÖDzEN7é¤O”êZŽ=¤cNÔ™—ùdÜä­GĬõ²äˆ—™§Žl*sº(ý¨j_b}ñoß —±—¼J z¦ˆ¤|ØË›Øæ53Ö‡º9d8º?P«àj*ÇÆZX_Zs”ç‚ò¤‡Pâ÷%ñ\IÓê:ËÌéçÒ²£ƒ2FçiSÞ¶:ØZ–#¯@ù¼ç÷´…ú¿á‘~²hÞö|${Ê…Ÿ[!‘'è‰NÍÁž»2\Òq¥C½pÂéX ݉×[1Œ–éÊo¼°_Ì0 îàÕjW<ÈÌu"×k3{¤Š)!©[ôTWí?o$m¶_àan]yVŸÔ¹ ÿ«ˆ¬ç³ÎÏM4ó®Ä¢®[ä¶¢¼ZÝò„2Çè „0ÁP¯jT]RHw¥9ê_˜½\æ@[|%ÛW'»Æ¯cFçtÚÝ^+xR ϯWÄ^ùdµ>eÑÌe4Å£@ŠãkfvàúÜ0\¨ m‘ÍZ[Ï»Þܰ÷ŠY† ­6Ð6WAË™y‹¿¬EÄÞ£ã²\3x3ƒõÆspã!ŀȞÏ{å»v>ee»5íÖËîÖ;ËÞ;™;ò\ùí§ÖC[éÊ«Nµ¼Øòm1â‚R®‹DÍoMB-†ªðzùeSØ.=vè×в¾>ŸÂí>ä)-Ç&k Šòµé;‹ºaŒÛYýUÏðO5Ú¢;FaƵî8t¯nÉ ­BKÍ­÷TÉÚÍâ<Õ0‡6Ókù”}GqW%˜üx3iFEÞ4–ç«cCÔ`9J±€\ùIޝ·Ò6xUN2²þC@ñâRyBði·wûúô;“L·fo?Ò<ÕƬp¯þ¡ÂXâÝŒWÞB5Å|›:”›[î-†¶ßð\^!0rÿö^nç;˜Wj vÍÅè[…3ûéEׄ,…âÍ»mg7µ¥×4€ Yy _«®€Ô?,ÜDwâcÌó3–€øzãçùp[‚&¿Šnq÷A–¢‹~(l¾íV÷#VËtCU–s<*‹ÅñœÏ¢@ øÉM™ŠKâ9*6»L²áÈËCðª®]º´êôë|¬<êl IU™‹r8gSÎ]jÁXÝ–Uh–êxbËeÖ—pn†ïp1ö Q õ“7z4Ùµ,å§@vñºB•ÚZ[^±V…`Bíu€f´ªR Û`u;ü$:fJãW±¬â×<‹5É¹ÂØ(¦·X­ Èn‹†ÞÖ„FE‘Šø˜3#Ú”О[\¢ò´Õ탌ÏN_=?¤¹1Ze`mÊ.4Ó¾=R^ E ¥8¶mPÀ³}J_? ¾§-mÛÞ]—“$›{<Çœ|{;{’†þ2R¿Z?s)åñèR2͇¡#<ÝXsnc6¢ëÍS•ø,W|5„”[¥W¦Û<×þ*­ùUnµ[´uêˆ}"R>i¬??}\ùX§/kÕïÿÅ_äÁãÿ*³ƒx6òë¥?Å+Qeö(»³iÀü°Â9è\';ëÿ8=ñ×ÿ¹•Ùtëf>¤•€`bš…`>^-…1F7œ,Ü2Uš6®‰!' 0¢„S¢,]î(ê¨åß»eŠËÑu‘1æß09Ê{E›™rìÒÎ:YogS«IÉV®dkNÉf®d3WÒ^NVG ú“)5§ŒÐc)U~”<~”Pt5v:Ù°yˆ•Áàû90ØÖ¾QT¤ÝwF®‰,SÆÜ!þxky>f«õ¤Ýnqyý, Œ9Ð1âé’àǘâ ¦wÄå%rþ]¯Àø£ ßãåÇW”õžàA'¤sŸ.'iý2Lë³°_ŸD0¶ã`ƒ õÉå^Ý8èîíÿ­þÓñ›R‰ô$PSs¤ò¯H£;{?c€)~?|÷¿ñû›·{øý7ü¾·,‰ÊCýø ~ÿð~Ô{ü(Ua†¿~¡BHÚåý¿á׿vyCiÂðû{UþR~ISôí¿ýð¿úVû)~?~«jø{·Ký$ö¡nŽ‹N&ôßMbó•¡{Hßò¸Ä«ÏÖÃÏ}åç>c¼Ç?.èÇüƒÚ{ß-{®Äñfx%õ8ýR}4Uw5Å‚ž4NQévç5·Ïs_Ÿp=VÁ,X€Ñ F¨ 4s3wŸ-¥­'¤[!³ ÉlG“Ür¡XÈ´Œþ˜Ÿ$ßt«…“sO£s#Ñ;Ë$³[¼ŸFÜñrÊô è[w/·Ÿ~ç§O·²Ûé9é„6¥à&|i.ÞO§°—[²•>ž‹ãñ¶ÏþØûi†I½a÷üëåóFã¯c?av«;ä Š†zzœ,A÷¸Ç̘vë¨ZÓË8 ëÞ ÏºWw(–ö#¶´«[ýïúŽê&ìÁý †•´üÁ¨>Lª¤Fÿ¬7íf}:<'–¥×”볓ãLž•Ö±e\SFV’Þ«#µƒ7‡oßït½îÛãý#9tZóöÞîá#oç—ã·ovŽvw^¿þ»··¿óÚûppü“÷ ŒÔ~×ûðöÝÞ‘,V¾»êfŠ»°Ï!…öÀ3aqt2(˜T ƒ8ä¬')_Lº°Â³£¾ô¡™Bõè§·¿¼Þó^í{¿íï1vGûÝãýîîþúÞþëƒ7Çðü {øË±4·îíÍt6X>ocO }öS£øsQü)¡¸aè˪o_èÛÜxMFŸ•aC;ùžf@+IK\áÁÀVe8ÃDQQ4„¥Áâ囤ìU[äEFŠ_À.¸ü F—rôžKåÂ)ÂÅq™1©ï¤Vÿþ/§ s¦Nˆ¡³Áõ4 ‹–ì9k\HMV3Ad®3¯ªaùñŠ iAÓèɤÎ<©6ž¹Œ¼jÍ©B]v«ÍEó-çº öU~â '\éŠ^~P„:‡B©”âÖÄH¦Ipë_û8¯{‡$BæáA[8 Ç‚Þà2šrjdYur" 0Ö_Õñ8]´Ê7)BÐTO•}t› QR@ñG¢û±ú\Ø…^{'Èm+‡•´þ þ¬7køw«zX¹â'Õꩇ¯7½yð¯×ìsdhŽgcDçW¾a cÐ7§PYzéF¿Õ«]ß.'uaz¿yUØ—l€Ü’VÀR,1Ô“h½át×Ï™€^ú¸(1P—\Bª´2¾}y/[…ßÚ˜¾\%A7ði4Òœ¾Mûr5]ÉwàŸA­ÓTÖØ—K~ð¼M†Ùùæ Ljºx†<ž"çÃ6×èMÕ{ìZkv)x䘯rŸQ{­ª ¦ßü±dN•°™êÙÏ|0FÿY‚’^Ý]Dq˜^ŽëzL‡ÁEèU…Ä¿D?utÞU·]BU™F“u=t pµ$Ù™ Öš–’h4ã=3ù~ÃÜør÷íê2dZ»PbJÅ’Gçåš(_©¡xRúË HÖ”"AYêÔ5ç&±0Œöºe†-‚Ö¥b¼@Gb#$ùƒs/}ÝÓš¾\°á'—V$ £Ê|Ú¥ìî5o7w‹ÅÎãdßbAV”Xâ_9ݳºàѹ(‰¦AÈ• Nó¡ï2Ðm9mº ø¥ÆíaÄëÎHy ;† ˜¦Snp­ÄdÁË0`&÷¡¨}ÆÓR'pY+Ùãde[Ñ€ú ÖÖ•qi¨Ûñ^Y‹F¿‚ÌÞ¯fv; Î»oWÑ£{>‚ÑËÖÞ5ˆèW&"9©E‰ £Ä„®”qØ7ËCnXèú–˳Ú]sÄíĬBv…ŽbÅð˜-‡ýpnŸÁlß";]€Ê»dª¥¼®Ö•HßÂqoµØó ˜)M S{öÀÿ¸0LwYKÁ•â’SO\{©9ôá¶œáqyÏć83v L9Tá£Í¹ƒÙNØ“`|æÇÇÃÍMß'(Ûqˆ.t7ºøÚ1ÐèAŠò<ÓOµÂÐîÎ óX !ÛYµ²9Ò奨¦æ oJôu7C¤†øÑï~âlë,¦mÜíÍjJn>1±p%¥‚—Bø¢ã½VwààßdѾÄSlXê}¡—_9G­“<ÊYa½ýI‚ûE@£çc—Ù;]›WÜ{¨„è, ,¾´í‘èÎN¼öùǹÿ {‘J©¨f[*Ýñ€:|q¶Bó–…­<ºQ1¡¾: ,'îò«·xbCÿ¸ÎatÆP%DwAä”Ô‡#ÞeqHbfç}°ºa³ú°Î„½b}© R!OMÀ.)qAz=¾»ÔКÙÂD m ¬·sµr Ã:¹’HöyÿmÍ}©@%­íä,W÷DùØ5OO——.{â„t¬:•rµ2®äs`«RSؽ6  ð9¢˜ó:5G§öÇP{Ñ÷>‘œå} 8µyæfÑMØ¡ùãþÐWº‹»}¤Œ¢…zQ†Ÿ °«ØÁø+à?3÷Ã1Ô‡휡$²/š–-5ëGËþÑ.YJ­Œ»´Ís•èŽûùΦQöÂIFã˜D0G¬¶×ÓQ81lnÐþÔõÝ,×Å Ð<¾¸ C‡†ãn²v:lÎߦP¡@Z%‡Ûè\©D®«¤¸9Űm3 ÃI“®•Oœ¡Ê £K®{ç@ýõ¦·¡~÷S"ÿZÞuá¹¥·Á𢑶Ó5œ.47ñƲը¥Ÿý²„Ôò.Ъ«ã}ÒR„ÛL OT³éGÀÓœ‰4»®¯bµ ˆ¶ª=X $ ÐŒ‰Õ4,žKc53 K:jåž´å‰ÛL!¢B<ÿCE9s¬ä†I@‡NbWFT..Éâ‘ “â´›o«ûTsðàò&s:!G舨[Û„B«™Zjs«#8ÇPÒ†Øa h³]IY0í.+.~Zå• ìx±@$w2^ÑMw²`59²Ú6+´²!8÷Ǭɧܽ²žªð:«ç¥–¥X„tÚVà&¯>:‰K‘ðσÞ0ü¬Œ¦¤Â3 §_œFÈ1ïy’8‚TιV¡Ø»‡Á$‡_®Æ4rõm}3QáP©ä‡Ÿú¶Þ¬Ö¼‚—õn~ü´æGÝʃåÇó6çT`CCUAšC51Lòì±8û ~ #V%%G~µæ¤«Aöðx»€’ùPv6­Blr…ɹ¯¥mtèWs.:­; Ó¾:mA§½ö]Ðá(˜ÐSÍû9û¨_¨Áƒ|V&¬Í•ñ£á¢Õ7o9€iÕó+ÊêÞ*Õç‘å*]m2/Ì|LGÇtâ4œûøpB)N†Q>–vY7ÙÛÆÖxµh¼šµ·SûÝÇkåá.è²¾ñ-2Àê=.F‘Æ•ðAD™Ø.àÅw¡©º(«‡ þA& F¬V0'ø!7 KxÍZGR ÚÃÙþÚKè»Qóñ¨õ˜T CÜÚвMƒ¿‚ÊÁ@ŸvÎÓ¢b­eÅÚT¬+fëÕšñ²-ã3tüskŽf}´@„Y0´ñ&$º2%@c3³r áf£€÷y[7åÄ'ÄRaçÆ–d²üÜD•Q¾·¬;™r„ºÜ¶sŒG$™#ÞIáik¡íed™^tÒðBcËÉ –¬ÖŒ¡A§6ÍÂ)°àuœâ’ÒDWÅ«æ Xë‘ÙÃeÚ]†#É*­à'˜ÃÈDLcËVî€Yonì·«„gF›ofYWçj ¸V.to³%Ô~ çÒ:ÐM›*V¤ˆü)}±ém…yWÓ^ÊÏú‰5lv]Xsh;Â|#°0ó[F'*ºæd¶3æPÝ™I‹`v’$„¶3pÛUŒjsòŒ}5›âZËtbŽ:ÑÝ@A)å˜ÁÊTaˆsã¹e ¾xÅï¤âæj^³Þ¨ZD$³MíˆÐŒG]Ð ¨"œšÛLµ´˜*Ã|L†l<†‡Þl2ÅàR(Œ%a+¤åÈ ˆEæ(öíLNÂÇ­ÓytLM„Ðé´ZÍŽn† ­"Ut3™CåBÅ4Æ‚Ìó Ûvå.ã†rÌ8U†Ú'4ͧYØ.V+¶2¦>Ê å/^(ªí— i.6Ø‹âÛÌûøjYHDZm±PÊô8Ó%ì{å„.(žÖ°iØæXTJœ„". ç„¾´ït@%Eà)–Ïa˜¯ãäØF¦ :šÀC¸ÙÈ·2‘ÓÉí*‡–Ÿ\›¨øN¼ŒFE|“ ¿è(†Pôþ8×Ýòˆ#µ£§Ê¯ÄHŒÜœëṬi\‡Y3JmZ¥XöØÛw˜x쨕K߬ŠZû Læ|¼›ºñå–9­A« a8ôX]7ÀÏ$¸RÄ@$;ZæP5Yt&ðNQ°OžõY¯ciΞ§ä ý˜¬ÐÖï"§¼‚èxÊz¥ìЖk’ãñd•UÆöZr§,$ý«S…¦3ëÀŽrúælRêH€¹;¤ÑÄ\JÉï°`v¾là©%17ÛÔUpNB@éZ ´r ¥~Þ)×è› ÖáÈ:žŽ,¯Œ‡ÐönÝÍ~…wÄ««€¹Br·7yªË'éiuÃ~ÖU ™`it`ï³ÆãñØlvÖ¨¿úAÞÅ po4õÖ¿l‰â8§Z„š5“évÑNÕ¦wqtBž8U–zcÙx9΋yMtIÂ@S"aU¼]5œCc­ìË=Ú\gi{ÀÒÆ¯Éqœ.V¹ ]ɽÓ]îaìCôüâ{ˆŠ1éšEMÔ2e Ä2Öá-Š8xæÂ=$W;Üzö£ ÒA ÷%Éêc*0%G§ŸÜ¸NÕœEÈŠA?b<õÉè S¯ ×!Fœ/<Ì×®™ZüfªêЉêL©üËä“¿Â9 ßÞ.ˆ{‡¾NR¡ê¨Lfq´Ha:q0àŒZë>ßÔI¥‡‡ßÕS¥e[Çî°‰bQÀSE\~ |e¶–9¾u„± Í]30"0mŸµö‡äl€9.)x AÚèßxN<·^o0ž†y‚(,<÷ã0±Í*6¨ 娦Wì; X\ð¡'ÅÿåÄäE›Å9•a››vÒ®£Ó5%Pf[£ž½vaz.Ñ]F£¡uÔ«ÝʴëxfƧP’ ªÞKåÔ™?æ‡Ê$Òµµ·¢Sp'z»°æÔV3ýˆ£eÒå{'¬aäêeó"[›E« ¿’geªT.[¯Jïò7 e:èè]¶XöÌ9ÚÎÓEÖº€»¥JOÖ†»¡]¾C'L+;±µÆ”Ç"«oÄG&äîÃÉ4 ’:—FR,DíÏÓÖ:tqn²˜ìvÚ¶·úM6 ÀŒ@•5võu¾‰`öQ =HWK·Ó²КPƒ}ÿ©Ì™»E¯¨Íq^FC&*n•·Êt ‡Ó5 i5ž¹¯•Z‘8¶U^]¬âÎQ—ó¥XÆ!o-6»[¥²P þIEÞJ.m›R©dü+íÅË[Ü ªœÍ<…Ǹ¾mÒèæ Æ˜&¹n°½W¶×0ZмÎü+1øÑÙ÷}Á Ëð’v \îüÄt†ÛÔwh0&}’(®k|A‰+ë|t!I¹QëVA°ÎíM8?Pw|º†CUŽíóy"ÕûVȸ!0VÚ4ÇMwµ$âÎ~¤Zãx Ö°0<©ÕÿòýiÉà ÷—p+¤–»“KŠ´ÉÚ§ç0'ü:ååŒÿ*HSû&™,$_"Cb÷YLÁŲsŸÀ^”ô™59dj3žVÍ#ZäØSrü«Áuøó=üùKùTôŒ\ÔæXÕ¾5oŸv6µ<¯·hT”æB¡ÐrÅTT)]­àÈ=ß9õ)>›ƒB)_ã\ób÷ÙmöOî|M ÔQ4 ƒq¤ ´û ·krUáÛ¨©,OóRBË{ ª£I/Qª4=®§&FoRDH÷sJé×ôþ¡º4JûªŒã±µ¾ÐÌ ÝÅK²U{Ù)g€yÒj4:xƒàTæ{àÑØrÁee) r´†Èž4Jˇ°²PµüûÕ5¦[pm%)Qð«“_Íì*ã[ròëchs±©>k§gœ´©—××Ë ákÉykMy+7çz¾e£z'bH%f‘ïÕ4«Ç9z(©n«™ÌÞ„*é!Ë Õ…(/é“J³Y]ÃéëM2ïž4kX› 'éú` A*Ô} ØðyÓ T*áchŠR´iKSª, æø6Uö>óήüÉ®<¯ªWT·ðF@š½`褜»sÞ)gËìHÇ;HM0 Ù÷‡A<@[øEÀöU*‡½XPP𔢳\Ñ}»ø,»֨Ьlp#9ø¤`Ê’eS–8S–ä§,Qãžä‡=q¦,ÉN™®êÕ-¾Äq‹)›,ž±dÅKVŸ±äö3–ðŒQ#Uœ-͂ڷ`AÅ\¦f Ÿ¹ìjEN„¥‚ÅÂKCž°PÑ•õ€‹4µ&%F=¬®5êrf8oyÃ<ìÓ„É—i!qb\çuæwêRR’!¢"FçH…ì— .*¤K E¶» ׆،,:ünåR•b ¬bUÌJ: »€=…%mظ¨x Ń«Î)7aqJ‘ô½Hùy`ž/•ع‘ Û.K^.*2ã?‡­n?W磅Ìð[©î¨ËÉ–2X{þy¹=Þ.1z=b¤°€Z¡8Þ6ãMÌsbÈú…º]ò¸™•k5y˜<Ä‚òÕâï U\‰yuð–©É÷+ 5XКô×®Ep:TÕL›Ü‚*sà+Æ,…ã]‚XRŒØbÜ’9¸ÍEIÔáïèc­^O$ôzèÒë=`ÉÄ›#(—‹ØôtµÈRbó‰¼ÞzÒz²8bÓ, GKC6ñþç—T?ß!pÓþð y^Ghr| Gøå¯±? q9Åè¶œî1Z“ò8(•Œ„§˜=JÎѺ„)›‡V©10íp Ëè° ~âõôlÞ•Æ->X¦DÓíòF9“ˆáG¾¯‚V#²ƒ–¾Fî+ ™yj–±žm5èe–L#¶@Ss ’Ý‚&‘Îé I,^ld÷  ÷•I&&Ͻ²…£ÿê0àwâöheûÖn8<…rê,«Ýa©eÕŒêŠ`€ƒD¾¿ŠMü¤0K·¾^«<¯8‰dnÌ´hÌê6¸`Zs˜² ¢g%×çÊhhç97;iºièg3Ÿ"qI=>'x62Õ‘ñÍKLJWæµ’œtàå)PÌ üû˜ý禠[¨8oÕË“®,cÈHÔÐ\•Ñ@Hh-QÑ­I± UúÎ ìP[;ÆœüsMpÝMØÏZۃˉ=ü‰ý«Ý|¡zs¯5Œ”R»Ut|•.éÌù×Y’:c~ìÌ}§Á"-{Œ;ùäUÖ\§ØñÊÃýçQ±>1&&ÂHѲ5€9ŠÔîW.½úÀÞñ08˜J |?Ê)‡pp ¹VŠ…þRÎÂ-?Jð?JT ¼68C»Ð>%òªX{-J VDñz˜Mö1i„ÃÐñKPØÃO…Ôtþ Ì–«ëñá„t@Q0;2!ê+o&Nãë"¬xáË—/¥G'•òÏÑåÝ{»ÝÃ2™–ÿ ¿µÇ¿ßøñ.À[˜]K ±Ì©3‚ýÉ•²Ñ‡ÁJé¦p‚íè_d¬< ï 6oxùžV.ð|üA•fÂìb$¼½Ì²ÌW¿×Xª{F”ßPg 0“¤²Ô³r¿ªsˈ@¡yŠ:TÏ‚(åÛ;)h0•'žÄ­žf‘àÅßÂå>“• àVöÊõ_#Øáj,º² ]} PÒb¥á`³Õn¶[¼1h?µÊ,ØÀÎúÿÉ5Õ\ºåh7Ÿ?‚¨î·¬v ñ g‰©ŽçØQáY‰ž.¢èb ¢a€1j7’Ï“4žM>mÀl $ˆÃ>Ž Æk-½ 0zàçS¹aøÇÒzæcX¿e±h+äàÒ1 {pÐ ÆŽã&;Ä3ž©çuÐ’à5ÿ¨(z½N"¶³îyjÞûCýÎ)´‡N(µÑužÃDòsçé›h#Ï>~„*ó€¿Ç2ï¦[}_¿Ï¾p~ Ý~‰˜-*ã§E…ÔóîÌð%ôÍyˆÙâ _Ù§ÎëÁ%°lÊ}ê ž^†£!lÕòo†ÑìâÒ}ì"{8xM#ï¼I.)Á}á;¾iZü âܧž¬B‰Ï~üñc½ÿlÐß f#ÂòïOÒèÔ&Š\Aô8fÔ~åýáÇï«Pþ¸ði1*}Ìã÷îhFѧ‚ÇO½õ4W‹b¥¦ØäÆ<*ÁðD]ó`šŒ] UPÍó”b¬Ì)àù`Á.¬nTŽ hµ¸,ì“™? dì6x´s¨jfA첸"©ïîþ¸‹_éqe„)ù„A7Gêæ51,à„¹©Z}‚¾…=úQy€KGúË)¯ü(‘XS·Ø9n‹Lp€Êˆ%n\!`U»èÒ†U¬^º¡‡kŽÇÒàc°ð2h¬€ ú²Äëx»oß½Ùy}ðýÞ‡ŸŽ÷wv÷<ÞÏ`«fâyI ~¸P÷þªI0ETár]<õêz¶wpV~Ðçå+S€>RuIM©Ÿ©Î0DiIUú¼(ª/À¥¯­ Oõ?ðTÿéÃóã—_2Dðªk–Iµ ¼…äj£X8ŒÅp¼¸\¥»AHV—¡2!ÑmÀ¬6žqY¯‹?+ EqóGŒÀ·´¼j_WÃG-y ÜfÜWCåën…OáÒ̎Чë}›èŽ„åJo:¥yL!RC´¢÷¨þàíJ«îV•Ä,?`õèÑsŹÍ˺’aô¥¾R+DÄkÖ¯¡M¢dÖï­,w¦/‚ˆD ÉmIÌG@F Ó'¡SQ^ßLƒw~˜ZÈ–>ÚG³~’†)Åó*“È‹(š}ÕÚ8ØâÐîŒ#Éi?+ì– ]ÂÂA¡3§Ý{jÑ[M¸.’*Lô)+Lò ~ÁnM›(…øY.„VBï©m}ð j¹¢ä–¡ò)¿•ÌZΩ‹¤Öm>«µRÜô7 „{—ˆÑûûb³à³3ÿFÜ—~4Evﱋ·”WG׿geñÝ5ÝîóûÌ_NKù=»ëÒ}öˆØÌŠlëß¹#ÝûRÿØ>ܯæyËÏŠŠêœÏ™FU·-¸s ¬uRD”™ÕÕöj^^Ç«yyõmȶî¤AeÕ½š—WˆÛ…µAXì»æ 'ªN‰¥¥áïf-ÃY#°'vàn­{X˘æ<±‡vsÅ"š¡Ì‘9û¯|”i.oûõ\sWÞÚ%ŸUŒžeᜇ‰X  sÎèí‘yô2Ž¢q’7Õz©À“pÊ/ÕP«)h™)Pås›˜‹nM™åóH{ƒC”µ¾ïUÇmµI˜GOCÔ¿TÄÁ ÎK®È9¦õ˜7-+™‡5¤(¾æO±þp¾¹W}(ºÏ1®'ïFpD×ÏñÈ_1š¤^êF©Äý—´$¸¯"Nq%Q›Ð­D'ªa‘I×6ö')§ðÂp™Pa„t+èè…]`Ò-Ú½í[ ˜<,àY·°úðTÓbGºãÅHf³„ðZ2zSžÙ-~£ (ÕÐ-÷ƒˆd>Œ3Ô&Q¶—¦‹`h^`Ì»ó6=Xˆ-X•ôouC}™[Aï7eǹ†.¤¥k±ôÍj¦Ö÷îryµ¦nýY·[+!·P *0ò֯꒱¾õgUÕè^&måÏ}è}}@tÕkË·wýü^“w_Ÿî}·wkrý¦^„÷· {OŸûéõŸºË}þ#“ÿ#“ÿ#“ï}IßjòV?¸‡ÏjèÿGEøÓT„[ÓÚý~þ#,ç~Žr–‡dÖGïY/¸NcÀI”ð6›$Ç8甫@gÁE3m—Ù1~<ºq­Ú,±ºÑ¡õohtЦ1¶`‰ ‚ J¨ˆ=H™Äüs{«ÃŠf }ìSè¼òC8ºŸ¬â"Ø«Oþ Z™³ð£ÉO+"ùéŠáºjO[T–¶RYÚßpf]ô¹ÛŠù½Î]!Vôù½`­Ïïrk}nÉ×~߃Yëómg´÷(~ͤ"ÝCý”¿Ýº§÷ÖAýù}4Õ&òþ{“ûÜó¢-ø‰‡ÔŠÃ ÂØÿ{Z:SAŽüÁ¨>LÔ¤Ÿ7~~ÖÂ?ëm˜ ¼Küò¬GfŸ‚ÑÇá”.`Xž3;V’|iã§ÜÞF4†AR¿LÇ#ˆE³x”PÂ×VäìŒçõìÌGÃÙ(P!.L€í„ÓÆ“ŠbáJPLæ$óÓ½g àO9+e|IÃGpx1Œe‹&Ÿ1N'°æÈCL%¹5HAÝà rŒbJ¡$ÃüÃÛ׈d0J0ÈÑÙyÉ%ÁX™8;«S¬¼²ñHfc•ž«ó¬—8;#J‘Ð?ûáˆpI3D¾Òº7 1á¦ÙàñR„ø:·'Ã^%Ñ\Å÷æ05¨$RCñУ!iÔ§|Ù61‰ú5 ]úW4Ãûåöšõ³O‰ èm‰ÂïÈìÍÎË=Âu D 9šìäìLãqv†‰xá”tÒYñ LjƒPB—h¦0<Æ0iÔ˜¸Ã@ »I°EpÖ‘ÑMM⥌ätÇêB#…:5`Rãt:¥0U=á˜ð>]S¢ŒÂ¾ €£SÇaÞ,Œ†u¼‚Ïʹš¨pü‹¶¢è—)b~Âð'ýEŒ‡Õk`9Šœi ÝÃÒHTŒ|Eå;+VŒ_Ì0XŒ]£TžÆ!oñ>7åï8rþ¬ÏaëMFNŧþª”%jÔÄ»&û®VŒŠ»ˆ> “™M ùÒ4†h¾Å[P5,z`Ú’¦ ãéqî"k¼=¸¦d`µ’F@ 8‹¢h˜fD=¿6`ÝËÒ(Cá› Nó$©]ÄA0jÍö“gÕz¹Tú ØZ –ù}™ÉaÌVT|³1Æ4§¸H¾7Æ}NˆCD]ôL×°Å5 ˜W7Á‚¿á,Höú¯Ù rœ‡ºÉLA.‡ x’r‰E —ÅÙ±CYõ"Ïd`^VèhŘÙ)8¡È\jÃ1Fï¿~}pxtÀg앲p 4cØ ÷¢ó4œyÁ¦±ÆC ÿÖëõ*35‡–„P(9 L ºa^ÂL€hº¤iZ&ØŒ±Ì’E25Ëo’W®©®®ö¡ˆ\`ø€¹ORYÞÐè,©©—”Õ±¤¨q¿ÄhƒTÁ™P¼¯L'JkÓh:ãùñY«’½Î„äCga]¢†‘Ÿ++jõˆN(aŠw&P{°´V ÒdIb‹qìñ“ÊzÆ&™½vJŠW&@~†Óã2ÊŠ-Lö˜Ù¨ØZçÄY„è ך§è¥âúþ$ôeÁáZÃ×\F5Ò¨øç1ŒW £)¾ôÑTòS P4f]“sT•Ó»ˆ^㨡 ˆjUŒtÈüE3@âiJ$ š|]%’4^)#)Ty»Y3¬s¸f夠ԂÒ@gåà&Ä6$bò£¤óqò1}”<ÀPƒº&¥š7¨ëÙ8élJòÄ›Üÿß“r†¦ Aìÿå ¬Ч‡¾œzj¬a ö0=r×SkO„ÝUÐ`.g“ ¼¶MÃy bã%~òaB/Cš)ÿ0&"¡¬W ÒO¸€è˜ð‚¥¢@gT IèS Æ'­ç\zB@¶67ùÑ4P¶í&?#”ú3"ƒf«¹…Í ãð‡œ˜ù£Kݲ7–-@IÄS0ì(Î4À½¨p[jí‚DPÛà“Þ! \fm x+‡C^[c\ ÚHÕ’ ÛAð+¿¶ëj‚¾˜\’™êÚšlÒù'öbĸ(rÈ}­Adî?´tC¾:§„q %¬)†VXùÖŠµ‡rç¤vÓ'öä·d批øí²ÊúAk±UÀõŒ¾Íœ¯es>y™ãw-—ß)ÏkåyÞ, ’íÕTzÄ6@5Ï ,B~Юc­ ’ëOûÎýáÈBÈô®=¯¯Ü^¤uNòFb’5"á¦$ë{2 ª$rˆæ‘„³aÚ&ÚåhCØÙÙgP€‡³ñTëâø`ùCÙãÊþšw ¨çA“ŸUƒ¢¡bÚV&cŸ‚¡P»©6-pÞº`¡Ú?Jdü¸ô?‹nF¹€×uÂV:ŽÒk)º÷ Þ#,òæ-耶‘‹ƒ$–vXÆ:?á;mðYíM²–?×,|h,Òé¸J²g 5&d!Ž—‚2‚·QÎÐs$ÇøÊÆ•¢Ê|ŽÔÄufökSû;…­f_x!!y_‰vb¥ûç()€:öa¨÷ŠzÎJ>íü lE Eò—**¶®Ç>kû“ Є/í :]X"£E²ÏQÈæ]ñ [®¹Ý_Á É.4èi÷ÔÁí3Wãíë•C¯¾<õÖ_zåPOxqɤ±ÙÖÐUŒö¾‰µ€Îô¦¨ÿ'õóÁù*Él„’ÃŽÍôèÿ¼öþÏ,€½ševÃ3Ì»%šØÆãàdßí½’=%ÚŽYþýð0­ô %·‡-PbZ²‘YÜ^ +hhÑÏ~Ã8BGû¯÷w½]h¿æj±îýðîíRVzlùðÓþ»}P«Hë@Çú[öMµ—®ß⬠ƒQ{ ÚÎy–YÕ»àH¬0½ÐcE¤Œ((1Y!% ,˜=ëö:Å(ïar]õ v¥,“~}êé_¾Að%Æá#$¼O¿ŽNŽößlW¾ŸL½Ç2½ï?O«DõÝCz oOñB0}ÿ(3wø‘5à⢴TÉ]ZTPu¤ÌëL |@ûHÙû~óS)QdŸÔâÝ—–!-Ë+¢kÐÏQDZ?:tJ‚šZ*©Thc}-v¦_LN¤8×ÁÄjMX ™F…h í¸â¬,Á@n÷Pù nÜ¢êÜ‚‹a k£eMÑÛT\™ì‰©!_AþôÐBÖ¥c?oæ…bÀ„Òîtê8Tí­K Ûæj'Óú$'0±o¬M/òTŠÚÍųzâo¼ ÏÃg=/CYN]Y;ê[qR‡)?F`³“Ef×lFjx'ïä@çŠûPñSt5ŠÐôÇ|TX§æ–¸Ô|²T*ýBÂIëšùPÐë®R‹²¼ŸØFh>$Õ‰6G¢Ì=;CCGO¿¬Tñ$—Rã©sJwÎÎðxÒ`â.’ôŒDÔL lž€’3 ®¼"ÝØS'bæ³lX¼pt¡y3A„”­¡æi$ªª^ÔXh)Ua¨#¦".,© ž¤˜‰‹rà·š²TM=c 9Å %¶·‘ CŸžÀ>eÜ÷I‹ùÅ‚±?Œý$gÐÈ*4@=…~B{uVTÕsL£¡O£w’$¼˜ 6RRº¯c»Ì<ÆÆ×5„fʘ| ±kÆ©üêñºöV»]ì.ªzŒò^ íU½@Zä"Aé[[Ož´—zÌ&Ÿþ¨ð%`_аý°€ÿVб®Í+PÇŒYËKÅÁH„åÔ«càŠVäI£‚ÆaP2Op³±w l%Ýèv½S//ï_íÁögã ëàJÂ÷cõ^?Füdp\CªTߨ†ˆ¹4z„¬ió+`vÀ©b¡b8 lbÝ9(õs 7à:˜ÇL ¼v(˜Œ´/ƒ“Ü;¸f®JÛ²&¡B8S‰ã 9œ*q!Š7Þ¯ã]r@¢UºìÉ%iÃÙÎéÜÀ¤F6]ê ü`Xw&ÄæQG;Ã{Ç/¿ñóÏ/×^t»õµ—–<¡>Xe­š­8P_+ÖãêœÊ|TVyQÙ;®¾¬~_~þ¾¬Á—n÷ä>~üòñã×/O×àaÙŽçO‰¥‘çÔ@ÓÙæv|ÑçxÙ à=©ÊƒЗÇjÞzæ0’7 ÖL.XFømÕ u] ¥tÿõ±þ’!À Gté'0êê¢Vb#µ@; †– ™ïÎÖ¥àó€  ˜CÛ%ÚAß âq8A>L›Z`®˜]“Ý;(2 } ãòP÷ùv&ìÐÉë/ÀE¸ÿ¿Î ©¾Áöަ’†Q.±˜A?­IqTy¶Ë˜vÇ…c’ –UÁŠ»Ør¦3ëŸàËey¼Z®é˜èŸÌGôOá&ú7qý+uëŽÝº™·ÌoLCšëèGÀ}ªÕÌ8òŒiáJµ Dþ®ä˜MÍ+!«&æx Rjèu£Þ(Â¡Ž©`F£%…Î{ãÀGã§*çF¶ 2±«ŒC4®Wª{ÚJ2M.{¯êÇWÏ=æ¡®.o›L—hF™Ó|©€F qéægöÊpÞ8T‘£ ¤ˆ»-¬šÅN—² óùsVcÑ4/Ü dg~Ñx*¼$•šÛ±ÂžQêL÷þêÖ* µù4W[¸õ¬^ÛúÖ¥ü­ÙÅ„ñÍ‹©fIê?eaÕÐÐ7ÌÏ{è ·­$ D%þ5þÒÇè‡ý¢ Pý¥'à²:Cþò ¿jÈsu¡\#_F¾7òµýÕtŸÕÛûZXHµ÷G±¸jþŪÜîÏæTÍUTŠy…²œi^9Å|èÝÉia‹wS ¶ •#"ž¼Ð‡ŽW¦CùeÍ{¡·øTí `S/e©j=Kˆr6ýý% xˆÄ…{ÑÂ-K>Ó&@­Øèsç^äxŸ÷·yô²ˆz½Z\ݤ$Ír ýÂóŠƪ7—¸ æ¢.µÚ‚·.xôq¿ ²GQCJÿ›ù”¬±9p,¥3ûùQ£óW?áëtO昸·¾ŽÎSât>ÿÊRȧŒì¦ÁG}äM©æÌ†_©Š 8Šc}ðÉ«PÞj¨'†¸Ø¼‘Ÿ¤U5yuü¾ï2}ÝcLGnß‹~I,GdrwD[ÏÕbŽR¶ïødë&GIÔKŒ5ó¢DÆðšG~óãðʈã£×ÒK;3:Ì¡‘¨R>‚)<©”¯ 1½mˆiqå+§¶ É4îN•fÃv?I[. þš6ðûiµ&¯Rô¡8)šåS§r;‡VÄê˪Z£TI›²l­‡Hiãå‹´ Zð§ 6áÏüyžÂŸgðç¹Z9¨­…PçÕj/«UzK÷#ÐŽ(ßk2"xP©®5Jåkí­xrE z ¤ÊJ"[|Ðí† `ÊQÛÎ 9aÉé8æ>­žÉí ‡R³‡ hz¾dë Óš˜èµÇãØKi6e¦¨ ‰Ã#äœ?6Phþs wÄü1Þ•ñè†oÙ@5Û†’híR‡Å¯…¡á¤àßMú»E·éïMú{‹þ~B?¥¿ŸÑßÏ-ýeÔ¯ÑX\)c]$Þ/øåkyåF¹üJòÐ÷`(Âó4>H‘‘EIÏvÎhó™6b–€ ž”SrežMÓ’6~ª¬âž÷êFÅ ƒþì¢7„èe7&WXJH]@rk-˜s< 1&<˜ô3“³ÅÓRsk[;Œ‡”ß›»RÞv/`·ð²³`äÓvù^–ű:öŸPK=¢`Õ†CÑ‹xm $›Ù¤íy¬ÆÕ)z}@ôS²ëû#2†‹ºHÄòïÑË/‹xj¶£Pð.‰ôw^8ÝÕ/øä+>úúoÔk «e××ñücÑÌ’$úã»H+m€‡ÄÁÐ^€ÀÅ"ý\# #Óøó1×çA¨šf³9x;ð©s¤‹³OŠ¿(N¼ÿÏëõpGÐë cµ7 qÓµuÐCù…ÿ¯þËòc‹±Õº½ÝŸºÿÝ;Ü9>Þ×]bé:G_Ù?Ùò ÿ%)¼è¿´¤HÜ*h?¦ÌÏÓpETª÷…KÛ9ò‘¦—UÚtÎzìJáüJ[Pé—IQ[bÛ™Wñ T|ĪÆËuÇø‚j^md+>uN‘2É‚EIÏT}U÷¯§€Íë༰~@o½¼&(Ï5€wxd½içA¨OÒ×AÄÆ Äã6üÙ„?[ðç üy žÁŸçEŽ|X;· V>üÊ÷Ûxrøõt­òñã—ýWµZ^©Içå·×ÍÕ2/Ââ…d_i z$å­³›Í½uçrA=[Ù2¥³9g·Õ3>ÿ#Ÿ•PÝ€FKúŽ¢èS‚ÞÚýQ€ìpÐ)¸ üQzùàì¬áõ¨ ú'ð!^"Ĩy EJ#H!‡†_u£·ê>aˆÞN…®dKçnðLO‚Y¤XtŽ|žgH³újÆç€IÒzà%³)!œ á„w·}ËKÂóÖaŠFçõ¾Ÿ.{Š*çOG AšT‹ âNl•ÒE2%x^*™§óàƒ0 ÒS±4´ã2Ò]W£ƒ¹îá]c¾Ÿï`kMð§?2u†…öÄÉ÷<™Woük¨’yØÅ»s¯ü› ɼø|òíG #}¤ŠåV‡Ša×Kׄ/ƒ1cûúQ ¯ÌÌÐÍx…Cd ïCã‚×Oðü!!¢3 04uú.ñ•ÔàbL¿\+•ûd…Â{æhY!´¯"Y?:oƒ÷7åSXøf‹¯ZXt55‰J:–Ÿ8KÈ ¥DäÒ†ôºQi/*¸ô¢ÙÅ¥w”0 Ø ñ2]» ø`ŒNWp,ª¶[9v/®;¶œ †÷©øÛÍZþ ¶›UêCÆXXêzY©ÆBX•`5V K5V*¥ñZØbc¥Róñ2—Êp:£ Pz3ÁdaÉÆÒ’1*‰å€¤ñF}¢xýöMí¦v½dˆLí&Õ–ÊÛיޓ9­~–¿Åv,Þmñ"¹BTÄ0êD¨úÛrÞðžEÅ€ÐüQüŒôê+h.ËÄqRT5ºA%ué›ff:DŸôÌBs91,2“–Õ7Ï=ü Ý$*¸x…ˆË³iŸÒfcþyòLnB7ê[O7Ÿ ÓhkkS•yòl Ë´›[òäIûɽho ]FF¥¬§U‘ÏAOñ95oPi;0ï½ä½VB >˜ÑFÁók¯ãÝð×mþ§UoÀ3t1[®Yðüª^;àšõ­åàü[€Û\ ÜjmÖ[ËÁõïܪm 8³ˆ•È';çªË¸XO…ì¬ zTà1(нÁ,ÎÏë8…nñmNú&†!ºï4·ß#f·üx*8»—úVíæjßæw¨«k¿xõz§û߯ºû/ïÂQoþÍ8êv¥Ý(=P{“`HÛSk¡€Žg‚HÙ^Z­ôG°rœ|ÇÙÓÏFCÔÏJÚ¡K9+éÛÈžYj µ’Ñ“º]13#cï·¶½/_—¬JMy¾$ªvµYU¾¾óúÇ·ïŽzsÔÉ­ËðÜ€ªc|ò¸R¥»ÉcصËù ¼êo:™F©x}MÎËýñ“­ËaH;Œ)(ÔÑäNfA®,¶QÀšú©b]¹Î0-·+ÄÀ4 e±nÔ<˜£^;Öm´8ä;ÃñöUXUà‚·D8(Z ^y­Ù|\.×)tÁIYТ9y”œz9g®À^3Þ›£€³æUÕÙYq³ü¨r]Å¿oªå5]¯¨Òzy­µš[¦Xu>¥ÚV—_$I¸RÍ bÍf‚=7ÀŠ(„9KNÔÌÀ·ÚƒÕ£2õ¿óæÒ(vÏlÙ)Ü:¾7BlrÛæ’ó÷þ °|ô¬Þ:ôþZ[æŠ9ÅŽVæ#ùÁŸO~ˆf°«&VÐÁ¢§ÅJQvãÔùÙt¶ÜŸíS¯8¹¡Û[ý$XwϘá„kd^¿úáÇ£WôµQo>Á¿ŸmÊÏMú¹¥6éçsùù”Þ¶6>þxpto°îV7 ƒxý †Ç¿/¼¼Ýï þ=Áz³ÿãΛûuQ»oåØÇÈ&D\èøf yÁÊön2ˆ0xqÎ|¶äSÆBX‚²Ö2»Xf‰úRÆøÏMPûòïÿýZhÒáÂ[RxK o}-´ÙpáFý¹çoþ¶~]·PßTmÔÛ‹[y¦zPo­Ô‡ºêò¦Fk«Þ’6t ëU¡MÉ=ÆbÀX|­”Ü"ìú¾¢æ¡ h’'²>Ÿzìù(Ú¨µQ…ºòo’h¢írê_¼Ï”–WQפ½~O@Û…Û?ø£$¨é~l«/Zw+lt£ÑiÁ1ÔÖŠ!kÚTDlm6š[ËŽ¡ð°b Yþ¸£(<‹²Zξ,½ýÄŸÃàªd8‚]žìäv$”èÜÃ>'D³ø†Èב™Æãh‚'Ñz„áÕ3á/Éó-šzɱb 1ÏÔ¦~„=Uªä'ìIÀ&ê–䎣:ƒ:ĤÓW¸§x3K1ìä’%Á-Fç ´ÃXªË•‰Úr8óøŠÊì`Àà7Rκ+šÆ+€8ŽÂ8ñ Ô䦬¹ùÞÔ±;=š wIKtÎrÁ#[çbå€cÈ­_ýzz–«Ùæ8*[%3õé8¬yÍÆª©1.öŒî›àz Éwã"ðl!åÉUP<îÄÿÅŠß‹WÈzg<% ï”’ŽãC¼1+ NøM4 ?·;ðÏ-@›é%ÅÖÂs™˜ ÇÁ˜·ú{dá|"àƒdo"¸Î{4|‚`Hà*—H¢Ö’ð%,jZ# X“Pûf1Qâ' 6ŠÇZ%XÕ1'Û©SH˜ƒñýÇYœÏèÖ tÀž‡£TEœ¡³ÝðbñͬѨÄ3§Ö&­Go­rî¢ô­ƒt¨ ãj¡XfB!j¬‡xõ¸ÅJûw#–×þ¥OÐÞE2Ï‘xõ%^OåG¯`®ñ¦GhòÆG?Á€[3ôÅó=îKݱ?‘øù£HÁëÏb!$¼1€´` úÃ!‡Uß®qyè¿‚¾ó“E'&¸1ËB& þOg•ሳáh¹<Ϫ¼Pðˆprð ¿GW“ºâ’Õ?ðxÙ’Ž6A2öðö ïÛÌìÖê[)ÃÂþLhµw%<ýŒCwõžÕƒr h´÷~–Õ|~b¼¯|ÉLKϹìa]ý²Š)WÇÈJ~ “<Èæó'ç»ûË*øày ,ªrò@ ô¾éº ¬²h" ;Mõ‚l£<4üüÝjevp\*<|{Œè ü®ò¯Žó®j~©µŽ>üÄïßwñW‡œáý¿ñ/ø~½y»Ç?¡ü|ÿêe™‚dêOØý'ós@ß—´LtºÊ9r…誗„ÿÄ­FnFòü´eÊ Wm”Â#µAÏ3˜”ç4¥Tȇ¾wÕñÐpwUõ^xm¼é{¥MÀ œÆW¤$LQ7 /£hØ#÷&«••ƒË08—«AÁ9Ïôål2ŒY/¸ |ŤÏ)ö<) üM1^·d‰‰ŒÎ+ì<ô™)í_ÜLS÷Ñ®?ñý‰ªë+ Òô/Ò&S–öbỼÓŸúƒO˜,T§À³ÞÊh+¹Ý¿Á#ÔíevDjÆ wŠÜO„>¹ž Ã!žHØ­Ù×I$"äÊ•Eù1ÐòÄ ..8ë¥?®yn òÞ9uýäÕ_LøBqZ*Eª™b‚‚=ú¯¸ÑOnÅ ´u8 LÛŠéwG!G“±Eë+w–(»d»üµ&áü`¹…[pÄÎúÁe‡[?†ÿ€a³Êñ¿XìP¨çRU†zð†^t ˜‚­T š-º†o œK]S¤þ‰$#—vádòl _@ô#â¢Ä èÌ‹\“i¿¥ïRŠÞ˜%^Þ_ª5CŸ§uÊÞ K?À”ìanÒŒÌd>5Tt¤Šåh@¦¹ $ý…WU[î*‚@Í´'¼»PÇÄÝQ  Ó„®LI~ʉË;ÀBÑ%»Ãû[.Ù=iÑz RUÌÆ¯`¹ls šÕ w xDVÙù’`„^sd.Pv‰:@µFA®#‚æâ_óZÊ7À¬B{5™ŸšîÂwPBN|ÎÆ§c± †)e|£eeBgÆ< Ùc.²T§Ç¢æ£¼ÂÁ^³Þð6Hnò]úü©y†¥FÞï’Å׎¤[Í‹ʤÊy9"\î7’F6ªtƒÝ=-ƒ‡‡ ¬M>J ³·…9Ž5…¥e-]P®HÐñ’ÏIM%o¥5+²wÜp`¼BqXWø1tFâ³)õ§«†‡ŠWB‚Ü0e¸U´Š2o1eO’lªÅ&‰µ—œrà çÞÞˆ µ“3j©l ܲN"K âȤ¦;cíç/KúȽ_‰Ôr»µ/‘Ã6o£K^!5ÙÛˆt×öŸýû,¦â…stÉf_ 0œ›U¥fB/gœµ«Ëh$k¦æd¦d98œµ’¶ÛÍ!{œbH®”Žzs‡£€“MiŽÜŸ¥%$h7©L$?ÏŒ3açNd˜^ þ´;Ò2Î*=mßÔ>!•˜¯*°!‹™[[EÈ‚Ã4UfÖD Õ®‚Òý’ÙÐû'@б¸>8Ó[«ö1!”ðéE2O^e;vçelSRÈq’V.6ÃZiœµ¡ØÍ"sÀ±¤­„¡ä›Æ€Ãá†ÃäꆪÔÊÆI•ã œ˜„)Š.hä)†Jo\RÆzL V™ô°Fÿ\×¼Iï:¬Ò?×U•ÓW}®È‡ñ¯šæ)™{öÉùUËû—‡0éŸÿÙ&°‹jý¯ªq-øga#øA¨áµúuüöxç5·tMÊI*naa¬}÷qæø¨„ѧ f~h^æš ŒÑ±D ?\ÎÒåD¤ÊDgØô¡ÔÀDTé ó…a‰ŽÊX.Ää <­¬·t~ åÒ繬š0ð^Zy“ÔÜzÖ‚7Ÿ<Ý‚Yjn¶OŸe|ZOõ§­;ƒ¦‘kÀ–õùV½õ¼TZt¦¶Údn®Üëé8\DMÍgõö³[ÃÂÛ€0“Í­'­çy’Ç.¿Ý{Ûñp åù3Ðîã0e/y•ÙÞÕUúœsF°ÄÙB Y2 Ñ äf1`LH‚šg>i°†×—DV"”ÏZ)Ï9öd:ë£%0ÎE”3š¡R}Τ R Ĩ{Ä”ÌWTg“範e8ìe¬ãƒøšÀ$B)*ÅRua³˜HZ ²š’=È÷k$,èï¨Z §LF sÖËYFõ1oAÇTĤ—?ü-¨‘µTçQlÈÂÕ¹‹a“–æfÃZš·k˜$òÈòHälÍÍÖ³§Í§mÃ"Kï|Ž¥E9 ñ&›$²KΙ“Hm”ÌdrÍv!aÅÜ$ïz¨?HT`H¹&|W1qÉ5»h<õµK;B6£và{ tÆ:÷Zƒð)I’äI•]‡Ž£ªz”bþQÚ¨öo¤¶‡·k1è,æw1½7fO!à˜o}Ð)ò±r¬' áœû´Þ±#éIf)I-~i©/mõeS}ÙÊ݉ ðü„ZÌF ó# ,fÚ0{ÕPƒË¤µ¦þ…M¶ô/l·­mÁ¯MÊ!}Î:kV—Õ)C¹íšÚüp–Z£ÝôÑiIÍC…ÉÇ&bY`ì\ïŸVÅaæU»çC ;Λô\Ûʬ~o¶àt}Ó~€½o?à 6œÖÐlYuqtš›ð`é ÑÐU\,kº›Û[·›­¦3[Íül¯°v(Î.bÙ¯1¯ã–ÆÿáõHKñ*Ò+»Ž©m0°²°´Ê ,YgÐUu¾c¸ºº¡ÁpJ!W רgƒ oloT «gõ&/`é¢ÈOòŠëH]óºoÜÈÝe˜iñÎX®ÛhÚï[šåä9MÓpÍ{²}ËÎÞ~–Õh默O~ÔV›†U‘\G,óΈO–9#n¶Úð×V“ÂalqPŒÖ“§››Kcbð¡òè†ÈŽˆtOÿ軹¸&#³uê}¦ÕTÁ'¥"kÛ¹·&wÿc‚¹V’( *µˆyPYXÄS|9¸Â°t©˜?†!M¨äž.IFOvÃÒú€bòž¬ØœÖ¥üešN;4Êõ‹(º4£!…îÜH>O6Òx6ùDo)IèZ–®ë×ãQ©´ï“¥ÕV ±Iyþ“»ÒÍ€’ xªÄRðx*8Ї?äèìqV<¢°ÿ‰(ä j6%S¥ßv±Qm$Ã^á°–” -9¨f<—fë¥#™¼3‡¼·”æ39ëy•…oT’AöÓÔªb!ÏůTŒŸçÍAp—)ËB°ä¾±ôòš¨)Á8 FŸ±ýÃd0So`“yå}Ž ˜hjPé¡<}LΚСHþ­´hw)mã¬Wª<È>„Žè<«qÑõƒ\祫㱦±5âæB)!Q ¯ÓñÖÖŽi9¹íÊÓ5T€;Ã`š^v<4ttðÖç(>Ú•o”òá(ÉÔWH3sʧgÃzˆ@“Íx ìrM76[)UñŽ…Ö™³ÐÎ,’²×7Ðz¡()è×<&ôÀÓ*ó-éYØÅ!uäµ~õØ×+ô!Óžê]Îˌ޼ØåðCÈmŽÉëŒÇõZd^ÄÛ†ÊxL˰58ß4té&H«/3¾‘Þy(R “T 12¯8Ž8ŬZˆ‡ªÆ}ŒT´§.n ~ ØÝfJ(ÈCúâC_ƒ0Xˆ •_ÞþÁÄŸ]Ì0ÆÏÎpˆKl!¡*͈b@Y41/ÄDW*¦D"YÓN^¤œ"ùó2޾ɻLÖô!£ Ʀ*‰}; œ“¼~HçWb“Õ{¾Ü,ÜÖA‡ÅPÒ‹öزŒð]8L¬$Ò9Ëu¬EPDCu fÁµÅòUòku©ú8&^ùÝÊüng~oÊo}Óq†Fóé³çëüó¥Ñ„| ¡µæÓçíâÇO×w†h£ä'„F‘aõ>ÃÞ,Hoøœ‰âžkÉHøLJvB„€y¦räX“U w.`ªQ%Ô÷<¦Š4•¥‚<䵿Ó|ý+cˆúŠrJä`=б„UÞõ¶þEìO/í¡}σ9/sJw- ÕsXT çbÁä•Ðv±¾ *×?fæd‹ &xdSSÐ8q6%$Á`qºÉkVg_uqæ"_-j'K‹¦)´yM- Éù`;˜ fÝ×-D>ÃT<-âBsÂÙºB‹\ñ–à)±;KÑ•¢Ð³·«(ì¤ÇÎzâù§úg­<ž÷$PkO<š·@•‚‡ú(=Š‹–1YN0.¡Üp©ZóY|ï‘\ÁØdèX£`e‹–LÑšXeTȵ˜9ZUˆ:Óè"÷³'* £ý¸©ì6Â\ xH¾Y[ë­ùóüή؞SñV ôÔ‚¨LÉãG º³=¢¸•Q³X¹íÒÜj?{ÜÜ|ŠEšOžm‘}³`|ÑÖ)‰s0º% g”uŒADpJ&h fr7Ò£ì^Ë©»®=ÙÎkï­§mÆÌWŒUëñ¾÷ngïÍ>É]:GRp%~)zžÒ»4,ák+›†SJ©“N»Å‹¼¼»~4 ó4›“r©¤5zâ!g]ü§MÆ1{@‡1Ðø9ìnØ!ŒØz ë µvX@Zè(…fŽjà÷jÌ„ÉÒõÐP€8+žóЇưÙ¯èM8ø„{‘d‘Ôwo H#³òò“™0”Y¹\„¹w6+ÿ é_ ¸ü Ög?ˆ/¤Q-¶Ëþ,7ëÁxì;2䄼Ûá)þÛ§7?ûÌÏv¨ ­‘gÍ'øïéjúŒ;€ß&¥ÜP 7›à ÎÌ$½Û?:ØÛï# ŸvÞ½û;AÄ¿èÒÏñ»_Þìàe’åòbn+³áe¬À¯èí]þx=h|¼>o¼ðßþôáO þøòŸ5ñ}Y¤T žÁŸ§òïð —Æó±» úHÆ™þïFѧFñ?eÌ÷fà¯ø×^4Ñ}7Êàödú”åmpécjBr3:Ç€¦¬Š³?¼Ï­ ’½¯M¢Y<†ÀÎGþŜŕ¥©dEƒ×ÔòדFçÉ)ÑÌ_O苦›¿ž<í4·NU/x:‡Y¯3¶.Ô+Û\OjŠKØvºvY²ØãhɶŠìO`ËD¶ c tÝTm­•é’@ªJÄMAÑI×£óõdÀ¯ð¢ÕÖÌ¡ŠâZRäìLn{­f{ƒ\*…·ä®^ƒ"Õ3’ Ktö+¦öë#ïÄ‚*ð Z±±±È·Ï\Z<æåüÃl”ÙqÎ&|—ïÇXT¤Ÿg1=Hü0ÇÎlÈν6} %-ì³ ³i©{¸~üÎȪîb­v^”V+ï•>œÏbº/į—ue µØåݶ4Ô|i…mW®W*¬áÑ»Wï¸ò\ÜY—ÀÓ ·Ëq€Ö±änêø)^ab9ÐNÉŸ~w}$VbháÄP8É(à»AŒ stc ©ÂÛx:v“Dû ”&Ú”£KVß§ÔôÖç[4è%jc‘È~ü(1¯ÐªA£¥–›`–¾ý; bÚ Ã^”ZÂg%àì¿ÑËý}EÆþððH®ºRÇ_íÿùó³¦ÖîhÅðw¯òå+2þ]UÛ uÅŒµºÀ)cþjOÚÖ£oXÍw–R—¸­qÿùçc]ÛâÒ‘ºfÍŒùv.À _û“ JwøöhÝ%Èö”©7·¶è¼$µÄws+/ôëÈš`†øa¡²Ê4—Y ñAŸðó½ÿÜú~þFÅÖDÔ‹gVeõ4”gCüNuŠHV0vÙõ­¯¬„9#ÁŒŒñ¯Ü²Âý!k‹R‘ºF­r7Ö¼ ~Q÷ˆ¡ð]K¼áQâ+ßâíOí1Ä{y´ÿì¬ëç犈‚ÛÛß 4öö ¤?1âYDƒ–Òr!¾P®h€Z°ääÊÎÞ{ –ÁR¾ü~Oã¡ÿ%ÑG"ÃØ¿ªO£dM@>³Õs&ë{?þr@Jiø1¦ôБ”&é窃r€¼u_¿Ö3Ná5Ä šË„¬Ž‰)È †ˆíI ”Ü”Ó Ë 0ƒÄÌ9ö1½®^$ß‘z‡FT]¥•«‚æ™!Ñ AÕäz+(h0t£¢PÓ_[:ÓE*B I èerÎ×*ŒÌ~<ÅF¶£ÆLò¦ òî%N.hùqn ¸ ¢û·‘Ox<—ÜÓtæ©{¢òKIN:­Ó¢¨²Tñ–½u’™~ÛI‚Ìd•”\?4nB!Øl¼õGfãÝ+ öF˜ü§ ľqüÖCoWøiCô'tŠ‚PاM7~þ™Î-èá9`!ß¡çU·fСʡ…|t^Œ¼ž ,_ÍÂÍSTëî5+ù8ýÙˆ¿UèïW°rÂ}­Òß5MPk½¼ ´þyýv׃nltw=êÅÆû7zX7~˜úvAî”U%ªP"ÍOž·¾ñÃE•Å‚NÇ; ñÖ*0»om&&×UÊv}Ü’%ðé°Ôqű­Ð‰ª¬Dïè„eâ«3’Ô0 Ͻ›h&™¾Ù4L–cÆ'gUÖ¦NÄ¥ÈÚ©n?f]o8·xÆÊÖIŒdy% ²F%Ê8geñWAÈ,ëh•ùþÁþþ;û}b%˜)2c†N.ôªÜl2KÐÂM!À÷û¬­éÚH ãžc8L#‹ƒstJ"ŽÒ¹OW‡3°ÐV·V•e vCùô(;ÍeÜö B¢¦MÉ/\.§–è².ŸPGÐ~€Ð÷¤Ä™òYE“ $ :Y¼€^x¬¹@oÝ9üÐk>þ¬Ñn’¯²þ½ÙÊþ~®u×îßU%ò6Ô¿7íÌï§–"’·ÍˆZãVq‘Èr;,rÒ`(/ì‘ò šªUo´Ÿ7;^ù¿ƒÉ ºn€nÁ×Çükï2ü$¹Œ‰½í&½Ë‚›_óÀÑÛîþ‡#ïèøí»¿Ï+ŒÖS¼œJïíææ6â5ö“N#뎪ê]ÂZ•1SÚ{»ûË›ýî±7·{s­N»¦¹† Øb”;Ço»^wçàÝÛWš›ÖrÍlÕ¿oßý¸Ó=øÙÑ2LýÛSåvwÞítw^ÃÚŠf hg:•løøËö¦1ÆR3,Z™(0{;Çû `™ ýG`–È=e¾‰×´ö}Q T)¤°ƒ¸Ž0HÃ'Ö4þñ꬛UÐRÎSÍvVj6©ëìÂ]—› 0·³3åqvV•+µÀ—}æ yÂïÔ@»/=íYñ^WÓ°ÓÌ lÄñï14ôç¹ldÚ—cH‚²|î9¤¬QlýcRD|Ú!*3º²-cqSnP¯¢+ÜäžlÅTÓÊÆÇ(sʼ†„ÿ‰€Û†â¾³Rt´~|¸»nnø‚~¹~ôêgý›5Nú*çfÑì’IµÊé5º@®U)ÕõÔhê{øäÝ»]ZjêOn™ržÍW{˜®ðÌ£W;ï¼Êz÷mwÝkàýkíxm½i5R©{õjÕ%ìÌzX´3k%3™·JV»®×Á&[7ž5Êd††'[ðëþôáûø· ¶rZq²3ÝÕ“ÖÖéjZgÆÖCº‡Ö3ûym° Óçˆûs>ð"¬‡åÓêiժɵÞC‘v“ 7Tu]o7ò~§ÙT­¨ÊØ4±jÙiô³·¯ªA+Ï}(¾9¡M»0Èþ¼ßÛTË%½·êí‘ÝZT´²Š:¥¶/OuIýDùë ü”Ã#—5•쓱Û'•„âSÀ6ÿùxýÿ³÷îmmcK¾ðû·>…†~{lÛØæ’„n²BH‡= á’ž}–±6²å–dˆûì3ŸýÔmÝ$Ù˜NzÏyÎ;tliÝ/µªjUýªOÍFw™Ÿóï,Ä3’ÂìYQÆÍB!ey\kÄÙoˆŸ%‚Éž}hžKT¨bª,œ¥T ÙÁ€IoðWÌ*ßîv{“Xµ_>Ð}-WÖOòè÷€VLM ¬¨´q˜öƒAb5:º2.‰¼M¥{ÝI›¬ëë4_nm÷®ªùqò>µlˆîƒá·Œ)4dØÜzY‘+¢&’ LèA›w$þk´“« ÄW‡0S`ºþ~}p¨?£¿Û#Ëýíã›(áDC[Ê4ª‘åñ±½½]Ñèv‰˜%ä’|k%iîÖ¬Û_H䨉/@RØÜW+’L.Wx×U,¾"Ý1ë\Æžó`IšÂʯ¢ 4¹ú³ÙÅ-ûœÿ3å,Ù:´Öj•L¤|³–þ…µkœ¶J&“ÚEj[án¢¶ÉŽ’š&…‚¬Ž›­uÁ‹Ï½Ï°DçEi¿ǤÐÞû°:‡µ+y,øñ¢y!» <“ÅMʳiÍ¡Œì[ÕÖgÆDJtʯáoIz×:ORÔ“Ö»0†ãõ.²yü©>Æÿ8Ñ; ±Qþvðñ°ñBòئ­QŽ_ÑЕ0³ûÇøþ«^Äm MµàkŠ¥4=ÖÉL(:)å5 ô¨ÚÑØ=wæý$¬Ô§á‹âá7 `‡ïn­¦R;û·#V>ÖNZçï´¬wà?{ó CAkŽë8ÅO¯ýjnŽÙ1•îÃþ¹• ¾µLü< Òþq"Läþ›¿úɳAâw i ï›÷ÄZ€‹=þø¡õµÕó×ïÓuK}Ú1°Õvö?ÞP—Æ ª¾Í&·á³Á<„·³qã)Õí¿ùL-ÿìŸ=< b3 ƒ§ÿã„ïܼÄL­óœñ?ÎTiq¥ÊV¨¥øÂõ¨]Úš´æHSwæ¥hâÁ¹a’<Ӣ㥃ª—Ί‘ÞÞ'ÉÝlÊ1œèÁúwù‘; Ùx ?ûR`ƒk4dCÔû‹1ìQ†M଑ c¢ž­áÀœˆBÿgÛ=0IoÅÿo:šnYżR“wF×`D*L¨û–žÝïÔ_}Z|tjaW­÷áר_ŽQÐg”•D\-„Á´Š¥D*¥]–åf-c;Om„JæK ;kYpÉ-œ÷íi,X¯34‘@|HE‚5‘utÙ"tÑ:³i´Üd»+á—µŠBÔÂÍNZ7Aõ-d­y©±=KúÄmò;–?QÄHöß&ò5ÜéúüÄC¥o0’¡†Œ^I­2€²³ñ£³¼?â^1Æ4q³õq~¢ É @ƒ4¢/®xZÄ­Æ¡žÝÎ`ê—ÈÀH‰%j°ÀK403ú·Òƒ0ˆóÅi†cà?•™¸þ|YÈ&Öï—Ob0ŽønoúaÞwñ”9B àØµfž±½åîÝ+Ýóh{³Ü”0vŸÊlÈÓ)…ù6սѕTS©³âU %#Xq)ûùgä€ka]˜Ä¶U¿yŒ–žºRF¸¸%2jkúcè`x%V«M`×g–HçfÓ7»¦U¯‹ƒÁ=ôyÆf¥7a ÷*ó\Ô_!Ì”ÈlÒ÷±$´…ë-Ið¬Ë˜C#õ Ò1ŠS:á{ )TÅxžAËÙàXÌ•Ó0Žn¥\e—‹mø›!¤Q?¼R 0ÔÂg¤“.§¦Ifå’á{Ú€•ø ÿñN1sE7A3pÃñuú YYÆAK[ ³ ò'#ü2a›]ùh’ ‡¡¨u'P>¥A«ÜÉótbÊÉÅ&‘?éR¦ÿ™ÑŸt& à!ëDce&†, ˆ«â> t0™²ß„66á=. (Œà .ïµS¥gyJ¢'†ˆ°)'7˜òb¯Ãñ€™^¬aS‘¤ðÊÌ’µ_J+žö˜i°'¼”úBâûÔú7ðçRKör/”Q±hj*ˆùÆË;Ûºr³x‚0FDÀ6~ÀuùòB/ ¥B.ë^\­PÏFpÞ~µëT¾J·‰O’?’'6ÞyæÉ*Oº{UØ÷E%Rí[ÊTNRw{§û‚:*ß¶^ÚßžïXßž÷ì”/žwõú}Ô¹ŠæÙ¯j|¦ì¥\PBÃàMl(㣵KžPÅs¤pÔX»€ÅôEä%î±…§Ž²ÜRX ¦X:(‚„ÕÀ."›Ýd!í=­s½™ËN3º€ÞS¾ …Ä LþÜIdj-VŽE¶wíœ?êidûY…:›SŸ—«tÀ…‚04¾a׋ŒÁvˆX§ÉìvÄâéLäí$Ôl/¥_8†‚"µvIMÞaç OÐDŒVh2L’ß/ˆG ž‰\oW–îƒ|œdSåãÁ† e‘OƒMÏÓÙímÌw‹@GUAbý+uFr LàtçØ\dyÇ )r‚\€nµ õyÁ«£‰2á ™UHA~DV&)®%Ü Ïû±^hà‰áHÐÖ)2†²êwQB[~ÆP‰} ÀÄÚ)û$„C_ÇŸ…\]µ«yúnèœÁE­Èµ&ÒIÏs#gˆBAéÁ¢JÅØ+ ×O-Ü’Í<ô¨4²?õ-É4 ‚±¥I®8ÆPÀ¾3ñDÒ&xŒØºèPï\/IÄS Ox©S·MMƒÆ²v5àÿžD$§ÖßÔœµ~‹˜¬°§g”[8Ã+0¾ªaFh»¹2èc{ú}[?, xNŽÕÈÄÏjŽŽ$ã®oÛA5É^Ìï°ÎÒñJd­Õ²u%[·óÊÁ’Ñs 'ǶHÕúX8´HÛ‹î³x™À£ØyY†<²Ä帻nú×Xùd6ÆX?~¤Œû¼BHÃnç$ºžrš£ÉPẉ"ý‚ž&Т0·ôE(凜â3.Ôzp˜õÞÐëë§Ò:µ,pc\dó)ôÜ¿Ökéš•kf˜´Ú:%¼-0©Z†8ô¸ª¬%sÑílšc©ŽÏ…·iRò¶Œ«|“¡•Û§¢9Eï4œêøüdBgN<¨¹¯Í{…5¦žïú÷ÓV+xeåÆh[‘]½nã$6ùcŽ7òò9@ʟ˾#_/v«µ{:ÄðWKÿ j…~ɬsÇàü Û6i¥$z®é±ê^:<~˜Ö»;¶ëæ27VÚÍõzUÁ†U®íŸþÒ5Þo•MØd_Lú¡õæèì‘äÏMò­Vž,O˜á*õf‹Nò†˜Ø)¬,uÁÍXØjSÆè¾®¨P›¡;F©:åÁˆ¾Ü“äÌ<«dg¨÷ª*U! êuËš±6Ù¿–ø¯²;Ÿ»NÔöjŸsvÊÛÝñ;"Æj*”és‘CêEùL‹±Ͳù„T·YôÊ‹Ä@5ö 2X ä NNe9ÌgßÅ0À©X,Ý^zPêm‚Iõ‘í~üÙÔ>¯•¯)Rvó¸QŸI<÷žÑ ©ØÄø! ³iÂðaÔIíu‡¡k震Ψ Ãp<…Ã8dãšLÇ´Îñ2XƒáÐ ÈÅ"ERÓhòÔ#fí ej]/Nb%´†gl£Ë‘°©õBºðó°rFøKyŽBÕ¶šo\ØdüÒ[@¢Ú¦®Žå)–L_¿!õ6Ú¥(¡\|Ôa{Â6nâ6…Š(ò«CÁª,=j?¶ºL#QN* rÚОRúúv§Ó¸ØÝîHÝHÐ|þam óÇvÁó(Œ¶a°²éP%ó}:cL‡ådU6½Bç˜Ä@ÆS¿Ž®w‰ä8xã¿hw¶¸æ­\Lï¬\X<1ùž·_v¬|†½¦É•-b…(9‡äw”`Ô’¤¤šyhzÆ •\‘@ Ai$$±ýé¶›¶îK˜&¨1(k;äZ²fµ¨Lf¹¸Öâð®I.äóõ­&P“5¬¨ÉX ƒmdM£Ž1 ‘Ša·€ü—ˆú?óël„!2ûø>Ã+,&¼·i2›âQáúœ­âkyv(ÅÀ–˜×&Ô¹¥¦h‹‚£ár°"³§-²‡»,c°qaØŒ–±JqkƒäÒB„‡vB™Ò‘8•˜•ok;5GÛ$fŒ¼Ò‹&¡OW¤1T xLX`z#19¬™)âC±ÄÊPÌÊb¡l éJQZøY³<"zØ‘ªëÖ6§õzAz|禖3Nò,Túÿ|Èd¯ø¨¬š„ öÐ+‡…—Ä8rym w#X_£\ø{­QÅFá›vçitsQ› ÚÓyBã(W0*]dð®Ÿ3ƌܳéÙØ\÷6õ¹[Åš†lâËœ©t÷iap¬7íôçNÙAþ¼agú–?Ù[묽Âü?oÀwý. ãW\àÏø¹§»öJêq³áëáÞÚûk˜l¼öÊj†•ôç éé+[S]egNéLbÂÆs³³ˆâ)1¶µ@ב(‹3È  Ýø†=CÖþ¦Àƒ&¼ÜÜÞ|ä„$½Â9É¢“¤ê¾|I:¡*q·dë½ÂE4ºÜivhô9ås¤ÜÊa±c*œõÐb»¸{«EŸâ òùµOèšG‡có±áèu:K‡ƒž'“âNZ©¡+š9ÁÿèógŒŠÇÏþ‰^)z‘dÇíyï’4‘i*Ä}€ˆ>>Å0…ÒRáhż,ÝúÍÜ›Îr 8å0˜ÃD€X)˜Œ\¯‹™H|46]W+sq”ƒ¬¨+FT'×5m ƒk3Ötu#œiæ ëF#­®Hõ™ë]룳W GÅ$Êbž øÚ >Š»Û Ï Ùñì&ÓÉtG]i®äñÙzÄ:Ù’§~φª°F ª´}Äng*—ÀV »ÀðÎ6°¥jÉL*.yj,Ü:Ó,$VÎNašõÖðýj’š®VǃÊ(9²óÄ-’ʈ¹Ñ·šÃRjœŸ,‹ Ów1Ô¢ ÌŠ £·TU¤–èE-óäžoËK=!*¡zP² )G¸*º¯Df6ÄÍß*A}ÎÂôž¿\– 5·?¦¼F%S¾ŠU§Z7ê‡p@÷¨äV`ù€Q€4X7{½Žy(c´wQ¯]_cÃá7ÝxóÍìÉé‰ÜusSÉŒ¯ˆ ?ëòs0±U xÖ…"6 EQ°ÊPjÑLáõ”½NæÿãGTVþÃÿ±ÕÝÎüÖ+ ê#Òûqj]Ó·‹Ò?ƒ°Ó%'´ò¾¯æ>§#ËDÙk½Òo¯v¤´PŠÁÕVUÈMØÐ]¥º) / Ó¼/Ç %…Ÿ-·:Hàü©*dÛ)„B;Næ,ÀýC§ÊT!¡¿B!üƒNQ³¬Ð’b ÍGºƒ2(VZÜ«ŽI-#†È-„b§¨²-²e‚+®MÑ 6ya`³Åcâ¶Dbdžd…ààó΂1Ùö¼)ðZ9çq‚™€Jû'Núû”$[›éè+oVoêäšr®Àä‚¥eâážìrbеÜ$0˜Àɉ©üZÝÉ­bd¡z§ªmÓ,Ë̱¶ªµ•Ýž:Þ¯ìÕÐl¹Á½’ø‚Þ-ݽÚMBOÅÂÁáÚ«‘ݦííÕX'ïÓ¾½Úg}ÓZQoW×e¦VÔx²SÕŠÉþJ*>nn*5È©…ø+,=2Ž !ÀX`kår“-jè#5×ÜyN­Ç=ei@2¨j8ýš€;¨+Þ¤ëf±(ö¹†¬ôV‡g-sŽiv –A¬^ti<…á[Å'GÊWÉø«rÂ"ûªÞFÜ’pã&mMº=ŒÛ‹n™)ja.ñžmôüÚ8šÌr1Gh0¦x5XI'£œ=Õ¨jy3´ª&çéÆt#s§’|H Chc! GÞ¾ óþ(ŠÀY×+V*{¹ÑM±@ÑØèTð=+d ÃÔÏüÚ^ ~ë,Є:¼(„N¥ ê ý} mœ`<«É›€øýñ`o€zÒ8üšK¹ÛÙÜítvw‘ËÜCu‰*5T§>>L09•š†­Hê@R«¦tNM…~~-uõv7_P]T‰O<ººR¾aïîQ’‡±”ÌŸ«Š=>6]Ø1ÅÒMˆ[ìÁ½AGÕà!Ý–9¥n»ÕÛíô¤Ô³ÙxŒ÷“ßwƒ~š€„_h®–.ºÄ‡ÁæÔ·ÇÈÂ(¸ )†ohzd9Û¼«m%ˆXû¦„8RâÁöïÞ³ò_;+kTá44êHiiOŸªhw œöQÅ­•}5ßà ÕUŸˆ’`à<ÆêË(Ça®¾ýýª„YÊw^ F ‰“U;Q(S\ÉÜs¬ u?y¿ÿ·²Ê]‡hÚUŒ<–G°3Á-œ›+üAÑ­¿?Üõ÷ý0"´Þýcd÷ß{ƒm4%ÓBкãB>3Œ!7y€¸ EzF$ ª5t÷äðôìãñþá†|¨•.¾t]KæçüÝáÙá§³¦ÿfvG—¦û¨GËÔüðz3¤XȾCKý „Øþƒÿ6Bó¥É@!Áá­) QçÂ+VµGg$Ïc{$Ø“°Öd”8wHtvëÌRõà¹<5ã)giH×7𿳓Ãý;<­•ÔÁ졪ÉÚƒh8 IET§ž4VÇ%}On_ÿzH‘žÞ~89;<|C_àœG‡]‚þœ¾~ÿñììã]ÀIšÄÉ-;¸ŸÌÓ`<#­¯Ø‡ûW tqéyy’Ä7ÉWÏ&uçü¬t÷PåIÄok—T¢¨W8g™ŽéŸ&yrK&¯ScpëõGGAѪ·çSˆi™®’‘,EËœúG-[¶ £[q÷•ý·{ë2¬ŠÙÀ÷w¡öœˆ{ú 6]2ò€N›¤ ZÞ7QŒ–ç’ámþö5¡*µ<·Ž„Tú¿§ÁôV£ ±Eq©sWpf^ ¡dȧ*± Ñðñ kM÷²»hÊ>û¹Ž·Ì9éIô]rÐÏÉ4É*S“™bj"\òøb^¤ÒÄ‹înAu‹Æ ZÆiå‚1þ ¼,ŵÓFCHÕ†S¹?ª§µz{½ñ¥ûÿ¦à2*ØÍò´AÌ®;UM3©ÊµŽSúõŠ"ª˜[¹ð!?’r– |Áò³¿ÅœÒ±ße-Â_þòKlBÕ’×F2®âöÊ> ô.ÀFÛ¾m§zÁÒØ —‚qíÇè2ÃZcÑdQbMù*7 “GGÚ˜ðHa²ò$DCŠUHéx ýWþN§œÐŒcíÇÖb:æ9/òº˜‹Ýíç5!þ„q®Röâ‚" ñωFXçi˹¬ëëÏQ0 f~‚꧸BvQ0‘.ò ?JŒó×{.Å_ZüiMyÚâ2U9·üôøðþ@j¼‡`¾ÁØ9£ˆ-©pªΩurú‘S¶z›í7ïU9ÓB9¸* ?ÿênQî³Ooß¶?·7õ')g¨Æç8ñÑ5|x…H׈Á$Ó¹C_§Ðw?òoPu”Ü5k5ÿ8Ðå„RÆ´ËUóðVèM0 ëbÌ0É`å8HÆQÛž·ž=[3½#RäÚýÓð>Ê#5ü³{š«ûÀ™›äñ\jhÜé)Ì l?š ŠëJ“AŸô`åL7²1š»¦ÇÌÖ>9æ/ôI—ó¹}®ÛãLOan΃,‰ý£8€vÀ`ÞGh.H³0 ÒhŒsÿ;ÓðVÌ€­ç@eî0ð’ì{è7í¡?ºÃ}¤…µ-?ir—à¸W¼*l”è.˜•ÆZ‹å¨Ñ—wªÐÂd¤³ÉÆh–¦s{¬ÏŽ>ðÛÛ8҉ÞüÖé‰[ŽL†ìòVØòÄÍ~·¥>·‹“1›†±Ú8þÁ¾@;ØL ¨ïÌÙ”¦$÷˽(Skà#ô×5ÜàRœð!P5ÛoúüèÃѹ·’wóÙ:A4„"?0™gÌ<,ˆžp“¢çGÃGffÙoö…å’ØZ¦ixŽÅÁ=ð€š6`Pl†Ë# ?ìê@ÐtC…Í*ì “J%ö`ø¬=i?7½§ã¤ÝÖð~<êldA—…jó Wxu·vv”[¬ý¼×y¹SLûb³øèk·[•ûk¯SJÙ+?¡\€0âvö*Æáb»„&Nihf²:æ[Y¼ý€M!¤2†4cà R~öcíµ5`7å¯Z&âp¼œ‚rÓÅðŒ€"ú=¡ßóB>†Òe 2òvæ"ddZ@Û 댤…ÖVwµÁÔsézâRÓÅÁVjøÀ&Ô®dF-Æ[GV°LVÙ2 ÀîU '¡!ÒvvAJÑš¦ŸŒo”ª\-Óž.yZ’ôç‘]£T‘}ˆ ƒY?4v"®sNá~ZÁƒZÓOHã2‹PAYÿ¯€âG?ȇ: ™çÇ~uú0ÿþTÛêÔÍȾê'4¢VdJý™y…È_ý‰y@öMs;7]€Ç~8¢ ‹ÀÏõœ 4ÂFL|Xþ ¡›*†Âd^‡Ýæ³Qìø"•‘k,iSã‘ ´ßj=²,ÖT®äCæ (”[µ©\íPw: v_vÈ·~iú›[›ü×:~iú[›½‘ô‚^¹jµÖ¹¶/:[V FüÚô_>ßÜZVŽŽ]Ÿ*ú€ÖYQÒtG¶U£éÙjs¯7{Ýí­† ÁËs6c¢Bim£ž‘ííàx`3xöísÎ#ÛWÆÝ…Na@¤ßþÊ›“fý&ô1´VÄèR œÕ•:ÌY8ÆE™mä$zÏ¢Ö;Ã;½K£5´šø/œ '9.‹K†>ÔË­xœq_°h°Wûüá]~Ý«½­x1Hé¶´ô–À^íüô¸âˆ¥g¯†pº/v*ÒÜDi>’TÝ΋ªVò½Úö—Zg{­ª`Éöj¿¾;¯xf{µ×go€„Ó½+¼><;ÇÐ>þáñ/ï÷ßøû}çì£ÿvÿ´fY±þ@ ­¼OæÊèé2”/d]í–¸uºóî=6›E8Î?À軑æt?C[æôüЯŸ†ýäx8”ÏAôÆð[‡¨ŸŒqXj_= Î/DFw ;éAÝ&Ó£­6‰Ý¥˜æõAxÏ‚hBMåRd<}=eÚøïÞ·ô…"÷|ÍkµÑ|ŠàCn×/¸ŒB¡vþ¢v]³¤å?ÃØŸ@¢] Rƒ½^ëÅ«¦_|ö²âYw[ƒLV B XÞ‘8w8"6`CPd_64'†£‹ü!Ä¥¦  –x¿A6ÆxãP i=Á’–ROéO °D›VÕX<"& Ä ‰2á-‚˜úuŽÍ åwüú[ ïaÃZjœÃ\ìkoóUUÚr°¬…´³ö5;Èg9ìT4&§ÅìQÇG³ s>7nÉs#x5„º `„Aôzêß:›#`/:Å‘0,Z²-z( íZe`L¹Ò.uìâ9¬µ / :½ézš,Ø.Y¬œ¯¯Q§i°±DˆFÖ¹{µ·*dÏmܬÅE¯vR£”²\ßNš5ÓR--\TJ9Ï/I‹Šs ,Òª¦Ik€Òø¯gÙ¨–¡ tš ™x Î,cÏú4³!<ˆþsÝŽšÈQÀ›ù˜ ‘_> õG³ ÍÚk:7Nk<‡ã4¡¥Û)yð·;?¶×<M{@êvn– il£1umD–Úì`–‘“àŸ>Šü7@’¥DsYŠsöµ•A`íõa;c`B(ƒÖEI*·J„¢mľL_V¥Žo€vˆóµ?‘ûºLÅp—2xº]¼zhD »e j0È0¢­n§Ý¥Â¼Z0GiØÚì¶xÕŒ-–è”!§ ³œi8MCb³ÐŠÆ×ð¦ú̦Bšb™¬‘&³1{h^lJ—{2ut9ƒ !Ž®h±eMñG÷Òñ…ïÕÕ`Ň çV0\P‹E¥“ïmnFêìö„$^ÐdÄaª\)ÔÔà§8ˆÆò‘-Lå Ð\ëPSÅ3öÕ€¢¼Ú¥sÖÖv·ýœò›ù*|muÕƒ¼Å†Á ™æ­—›51¿6uhz3æš±ŸIèo5fÅ{ÁSL;í1Ü¿yb k©?jP´; mÿ°õœúÓÀgq~[½—Ø îHrƒwº­îf{ºz©\\£1B]óýªö¨“ ĆŸ´>JW|ˆʆ/°Xì«Z lRœlR»GR¡ž%Æ”Ýtüëû %«ôÏQC£Ò8Ûq eŒH>¼ß?;3v$<¯¥JyJyà3,­×ÜM£iÕ¾¬•ßÒ¼³WÑg.TãËI0_‰K£ÞŠ3rA°ô´`iT9¬ä… €éCÛ#_ÃÞéul¸b( í³ë®3½ªÔk{<&W›Ý«œµ¥‘±7£{óˆõ(WZ)ÀÜ>ž¿;üpúñýáÙ†úT«Ö6I!Ú‰ÁjÒ´}a ¤-§SÙœIWwvøþD±Ó³ õ©X©²vñc|ñÙu¬®‘]âµ¢ªAš²È¢V£Äh=ü:E-ph©ñ,˜À¾ÍÅ æD£Péƒxbâb$#ĬÌΜ>Què¤5œMšl%QTL.œiÝà&×øžŒÞçŸiä}:–ÝŸE$xæ¼?d7Á:»ô—F2I- IQÛ~.‹á!$ƒ¦Öös‘’Ïf7BNwýú$™è˜AŸžÞ ×ÒGgàd¢ß±q·pV…áï K`‘pjþ(yˆáH¼åä„1VFø3æ|Ó0Î1{ŠãÖ¦Zûh ‹ÊGœ6˜œ,Ñœxõf~%¦¨ »t‡D>Ç~öe¢)Eâ  ÎÛl2Onž~³»Ld—u^Ï`Àû0‰¡ÿ †¼/ëÁ÷ß’ï½I4±­ë°”â>Âø!¡¶]ZΈõÚõßþûE”_Òß‹g7áå¥ÿùðôµRqg»ÖCl›LS}0#¿ðC«åGW¸šþ_¨õ¦V„d(5ªÉ}ô?>µ‰PÜe½ü.-´õdI[[''‹Ûi·ðäôð䙪Ë?§±Þdš]õ‘ÁÐv®|ßwræàÖVÆð§!Çê¶;ŽÚC‚rPLŽi’É&¿ [‚™Ý÷°tX–ÉØå’ 2?€­8ãµ[Ó?ùxÖb„]ÆŽ2^kQØszÞ.l«ŽÈ•ŠÆNøòdl_šà€ò CÍÞ2ˆ,Þ¤—ÌRWäÀkųTxúšÝ ¹bñ…¬ë –VµZ{YU4À+cÝ?µ1u=.GÚÙ£®ÇHÅ –±Rn“2fä6‰ÂõÚî ~ÿô޾³BFtë–Ç6šíii¬ë–³œB¨À£.káHeñh ]3[­¯áüIñK.–Šr:=8µ3ÜÕõõTŸíTHíòúÚãbÌ,_8ðKŒFÇq¤è\kšÐ3¦pLWZ±pFVNmA1f š!-qz¯Èë5ý%¯ÜFtïÑVè ”P›ù•†Œm¶Ha„bͶêîž‹*¦Â'ã§_àÈ¥G‡ãDÅ_˜Oþ¿”GßX?ô02OüÚ¾ÿ^ÏCÎ;‚$-v¡ŽÓp"-¦r÷'p´ÒêÄ|<ÀPCZçäŒ_+ ¡2 çÒðÆ|³$ò/°-°OÈ4§VQ' Ò¯(Tvb†5¶¹ArŽQ‹wÜG·È¸©ÍcøqvyŠçÍj¸h2l aÕrÍž¢(ne¹ÅD_äzšÉÝæ¯m¬ÁoóœœäyöY¤:”è|úzCèPš ¦9@b6¹9:ÞP„æÍù†¢/ý놖ãã 8 ¶†åŠ!3Où†ì€»i)Jg-ó×kµu-]3>ž¨h­à&Šg%ÕþÍb(âkBt9r[,\رš$chñ &£’<ñðÒ1ùÖNF•Å-,LÇŸcÛ†Ÿ8‰ƒˆÜ¯¸×Üéks–b©™v!a~ÜÖ JþlbüÙØzžE>¥å¹~ý»0' ªª¼Çªb›l+˜À4À¸¬¤ o Bt~¦Ú-<Dè˜å ±®„PxÀs¾;à `aüÀEQÑYÎ'Ôk‹æ¬ž f1éçYõ„üpàÓ†»ƒâYÓnæÙ¸÷WÉ@ÎRb$RpVÛ²¦ÿ(#MŠX;i”»ÁV½ÕÛ=qºê’p¡õäí·D`…K:'"d¯Üè58N6TðJ*­öª²&ÿ²¸²EëêÙ˜eéF6‚4¥Ð–W8`ºftú{4Õ_6Ä„e°± M슲¸Aèà‰·¡KSÑ­‡ ¡O‹*¿ '¸–T_¹Ï¯_ ¦ÈçEmZ?»¥H_¤öÔë’j¯ÄØ\«‰œ™)]>;ÐäUTÒ"¿i?ƒA!T¦b+P,¼Ž®¢¸ã`î© 8T(wmB°w ‡{4C“¡Ò¼£îÑõ5ÞÄÂÃø8|ø&b.†í;»•P-Ud¨šì8}ô µQ!¥M¨²%d…ðâРñ?›´LB&Q\‘pr*äeèëõgÀQAÞC2‹« €¢@@E3U¯åŸk²¨QfçÒ(Ö°2RY,k8‹QÉ4ûv4òM¦3rYaŒ£fi½x!/jïL¢Ïôa•'$¸z- ŒŒä|7¡21Mqëá hÕ)Å ›Ì~3jÄ[á´ÉÔ'ºÄòŠ [¬&˜µÈ'Ò=Ø›ÂVß*¹Ç³÷ÎåëJðê ¶!¡Š Ð\u:ƒÖÍwnÈÝìÊukЦ+õ4I€ÇNVI"¡21´6Þ) y˜ö)n n²$ÙÎGL0ÅM+¯ÿ¹Å}Ø´-¨–Ÿ$.RƆ]óX*‚ äƒ¢ž†;ù>DÒ¤ZÁá?a„oïS;Ú¯4f²m“m$=øIDKéRŒO,B^ÈÒxh¥‰Ÿè˜¡„¯%êuO¤½r.CE$Ô±cm7Xçø¥jµ¡˜^¡Õ¢„8º#ÎqU-°¯†,3®[Ò‚+È¡$OÂtc‡©ŠS¬Ö`Pžš>Íl÷2f}$9ÜùŠÖ›ÊÇó+6Ô“&œÒž«ÇõÚÅ´ÿå/—Ϫ꾕¶¨º¹²«Rƒí Ø•'jÝQã[còO{ý‹8W¹mß+õÆnì§lÕ–²&´WbO©°Ì­»¿ßsp`tÐ]Q”?šX&Ë•H¸(Í'd-ØÑVªQ„B’¨pOò2„ƒg„à‘œ"Ð0_±úÀ™ª¡ÕÑ| ·Žê¢ß,W›™Â#Q¬.jM¸ ÙaŽ*;óußµ6Œ¥ ù“§~q{ô:¨2â~Kd¯¨cTêë+zgâ3}éѤXñ¸<öOŽ<ç*úd³%mGøC^ˆ =Ç 2žËÛH!^!ŠylUÇ IÎATŽìP¬$㾊ÝC{að®†ñÁ-ƒ< ï°îo9nYk:KÛ×ѯà±ÓÀÖµŒB!ÚDÇÁ]ˆç«šw0Q ¤U*]u1nó:0\¬€²gë܉êX­¶õ®¼Ø6H›‹&Ó:_Dú&€u1‰a(éȆMFøGÓ>=*D¹Q}c•E7Äôì#D;šùš75—)â­ƒýœðŸyS„\Ó¾¿ÎqC¸ýëÆI¼l²lÎ;ò6ˆr '{ì:M<À)ì‹¶©a.ÒÕUA†‡óCå0 Pù›Ûj Ý2…·Ñ‡cNâÛ’+g÷E½ÇVžN ¶“m«®bé)ݪ¯‹MÄzsYÇqèôõ|¥U¢Œh“‡ã¬ÔißF¡7R‚²¤ OvQ»2eª¹z\-tíU&J›@M“dÒRMµ¨¹AËÂá6 Æã õÓsë¥=FÔ7K˯&ä&ž¥Â_àn•A·¢gÀáH#W'EÀ+d¯‚±÷Ô ‘ÁÐ×í EvðšÝ0ÄkZ±ÙO²Ü 0‰m××(<’b™Çö-A’„ÔÛM¾~²Œåª‚¡+Ò-•R)ï!?*Œív÷Š›Øc_ ì€Ã‰6:É¿[?–ï,ä­WÔ¡ ‚6<?ÞzÑÛ©‘æ½÷ÌŽË×Hø8’g¼.ÞNƒ¥u©%PQÓê¡ï3o_¹ Hð»ZåmmMy¼Ž´R2ΰš¬FÔ,;/ª¾ že¥“=O‡ð«\Üiݽ-¸SN£s·³G¬p/ÚˆhV¨ÀØ]”蟌M°CëÊÅRMâf"îåJO#jNº£2‘Êw©æQL¼*È“@a×-a7Ü’d®¡‡¾†~Ûa±KKžùR¦³íŒ$Šé[A,N¦,AŽæ °¨Âñ4?Z|z¼¿`h Ê‹X–p}ØN΢Ì`Ï“ZÝlÑËHÉÿáÚ®ŠÁîV˜28Mzòþ=úÈ-)޾s¤-F—Tr¹°Ëv«¥–´ÒAnz‚ .+ ƒ>šm"¦ú†‚Ãŧ’P¯S ¨(Ǿ­ëç è‹5=*«g¯ˆ^än0âÇ‘Ã)¢Äyá&åðÜÄ啟,òÿl:º·Æ [“°½xñéšžÛÙÞÚæËv×Äí¥|ê›|ç›i3E¸ kžfógŠ…üJ­Œ Ë©'ò=Ìé‰w–cxÿ5š›kÏ‚Ï È,»ôö'2„ŒÒ0 ”ﻸ“è{¤ï&såíîY>ÅëJÖv²5éš”…²UÖ^óñž^Ôøh],åâKF\`„ˆß´Ük¯ ººƒgÝBƒïSIȶ¶táf¤fe‚>ÿI/M¶šÕ)8FI`T÷b@Zh»$>¹ç½ž3#…@ü O]ï`'o6 )?›Ýdáo3´Û¸nò%`”{ʦÀ²Yïë’ô0ÐeUGbƒ´¨W*Â/™ÆH{¦=>¶ñö´E[•3í(ï¶¶-Îpû’Ä  †%Ó”€ b««\ZÙ–RÆBêü5ª–RFl*ð/(2q˜–ù)[¥däY…&þˆ›™dâ‰Ý\ÙXU&[˜ZÚÅâ¸Õ^)sƸ0|¯M’†Ô€h|A*×¼¤•Ì$ƽ÷Öðú˜¬¡Ð"ù8œ˜û$¾g%b_¹‚Têc‰CFËíùB a8KÑö Ó³Q†’%`‘ë#šLrV€3Ÿív=F­? ¡UbHéÿ: øÚã_s|m ÔM³2f‹ „•ïBnÆMèe³Ô FC¥ìZ3fw·ZΜ üž‰C3K×n–9\„®CöŸöÐwÁE£®@$*i®Ëih<‚ò󦸼•où RFM$­³í„†=ÊP¥ë„QtáF`;x:¡Åƒ%´$3™U©Lž¨kEeŠ iQ ,!¤'n±Ü3èØßô3†/´Â,n›1ƺØï’4PJÛŽË¿<ÚzBi ÂàŽ§Mcµz;%\DF ð&D•¸Š(ÆÖl3†«—MåŠæ6 ÆO1¶Óž,#öZ‘r˜-®|”[qÁ@DÇÏýx9N&Zòt†–ýc‚Ü«˜L[äR+ࢢ±_ÙQZBI¶bsÍmÅÑ;ÉJVmx£)›g™ÕZMäÙÇ‚hc`ƒdc¢[Â=ù‡XfÈ·ñ%Ÿa¾PˆhMét©YV5èñ" t6U¦˜„bäX D9=tzrðx14‘WÑdå -Ö¬µÓ¥J Í <Š\!SrlTM% áENƒE¥œ©@¿ÚY¦Í¤¨Pòò ÅÊ9ñî Eš .á‡Õ³ÅN?t”^_GƒkíaMji”Ù„áxEŠÑJäî·RÉ6êûD€k÷¡¯Œ0)gÕF 6Ì}¯0ûìõ L†¬Á] -E86=åDát?e`þ„u)Ú|æ‰Ðœ¶ÇƉVná©|›§¢µ¡ÔóÅ¢Œßû­x UJVœ°·Š€ê;L\òUT¿Y¶ÎŒl2ñ´º_pÖÝA¶KYb2àY<àÀPsï?ÍPÊ´Ð[m։͙û f€É%r Èz(×>äýÓh æÃvÜ;W«MuP8¾w–Ÿ iKÐ`¸M¸n€úîmâpA;‹Ç1YÏ ¶÷/–œ9!ݖݨH)LÕ¶…ðÒ¿ºBG…««:šî6}Çî®®ØêÝ…L³ëm/.¢ó=¶­21>Þ×Õíz¼Óª¥f=¿îô‹`ͯç¸ñ…òßgdÇ;4*:¶5«oåÿ³üS’ÍlCD—Â-µ»”¢±ä¼¨± Š÷Z$òõq™2­EHÆ4²Œt” üª—ÚÞ ‰'\j/(æi—ÚžÈèæR›U8ŠµÌ¬mn6µ ÆJ)ñL«Éì‹O—™ö9²í«|W¨`6М=lLš×m;ݤâyа7ÿ6Šúo£(¼pNAZ•Š*˜‹ÌªËî¦&× )Q‚jÜ™ÅÔœ d:lÁÈ·X6³¦~A„Ù¨WÄ|¦É›Œlè‚íê&Núwå}Ön`ÎÍ‘È!¿ž@iOã²áÚñ©É–õöŽdé·¶c«C†ó²¯=×ê‘=‰ðkÜGYFváÊq É¤`É@—únÝE+?X†¦µ§Zÿ tB>Û†ƒ*“ॎ‰ºmï-‚@!wÑQr£®9ŒlÃé5ùò®]'jð,â¶L+´UA„¶…Çh ‚¤x²²/‘‚ƒ _ ¾Iaöè&´4­ÜÒ€MœÔâÆ—ÔiËbÏÀd¥ìxL žƒég%Ûãbð"½RÕµr]% ’ï¼Y.xQ4›dNd]7h ê å8ñMÌAx¸üû¥.žºÚð‘ŒôŠÒ‰ö²£a¥ÃÔv‹k3s¥ƒ5—b£8UŒ Ù]„̃à¼è2>à-:Ñ ŒÎ'³ÂáÀqn*H5ù†NUÃòý™ŠEЫÉ)Æ‘ðàÐoŽQÕ!;è(wœÊ1X»ª— Â5FÊ£§5Þ‹–“W¦F†h²¬oŒP³¼¨Oµœõr5±be;7.þé<‘× öF­{4 ЄÂß'™Ý£t·£Ak”‚UÃusU?ø±ë),½ä¢vœìÓkìIe`|A(U Š@'~¼ÆÇo&6OýÃŒã æN†×¨’é` pÉë 2°Éª/y`>®-÷¶SðÚ^Ä+H¢¶r·èÖvõЦaã|d‰lk¬&müŽÄi"¦jiS)wQ ˜!/ ÙU[<ލQ]ÔFäZ0Ô6Û s:à ÂÙ= úaK+½lv#K"¸÷P%ÒÂɺ™Eq®–B”yNHb×77U÷b(Çãr›².[æ“mB¬ÛW¤Úê¸%­>%="ÉÓnгÏÊ%¯$è•–’)ªÑ½qj…‡ýN ƒJÍ,eµW¬ „ؽŠé1fçºÑ©0‘;QQ¦ £¥¨Âsš®iHc/¼ü:ìïž'áv2ªªDy®Â𘠷U™cÈ‚ü¤pE•E·Æ  ã‚ù¹64¯c€ÛI’óâUN]^‡$ëþÂ:¢\žŽNé¨R-\v -Oþ€!¼€ b "Z$´$i7ˆñY£"žMc‘#D@^›—–KUfKâ¿De­Gë|÷b³†bSé\æj˜\´ù£nž¥ZC‚Ø ñ‡ú&³ñ6Zé뮈E›Õ$1ý8ÉøX„u@Nä™°%¸ÉC†£4ÜœrÒvØ; oS¶lñÏCÄ×(£ÓHëØDâ!fÂYØhÀê>_æÐ ÄZòP룮È Ä DÔ,¤õP*À®“/ÝfÂÒê+Å$½c„l»øŠõ±ZAœë›ä(þS‡9ÃÒ)<o±:¤Ù«Õšþúºø <B*ìéâÛã»~®»z#H6µŽ‚”Ù& ¯zEà‘>Ÿíù KÛy 5“ë ?Ƚå_++…†mÚu5m [Шºvþ¨ÊB§F¡g£†=ްìaÄ÷…î¹Â5ô;X Uiñ'A5 †$ZÒ[·m˜c¬Kô<­Er-yòpP§~šáê‹8ÆÎb7h"žíbcJ@’£Ü5Ð@Jr ó1òo ¢;¾æD ¾˜hMFÌàXŒÞÎ3Ê”M„ ×nibËapÙ½øFÝÔX/ôí]Oyeç^Ȩ¹—ýP¹Ø\Ú™¢`ommí‹óHéRrMðÎ Í&É… ,Nö/ u`˜iHŠýÀ¦&Ôo[åYX©œz**ñ°êàéÏR$üñÜ]ÑA”ÆJ"DºÃ4Jh€à­ôÔ¸€¡pGçG§¢iDià•¥¶0N©ÍŠBX©Je¢¯$+~§—’1å ¿‚èÙHI˜ðm.õë¢'ôøS] ]_S”sñ£ÊÊ”7yÂf¬U½‘½¶WÝZÙóµ@àßÐ+˜ª ‚¶ó“þb†§UhàŸ\ï" V GMÔg£ÌäiÝzÓð÷öèÓ5Z¿é,ÔAD\äÑè‰ "Ñä`ÇQø·xц$—:ÙN„ˆ¿pgÝT›ÑáðbwK€oj«îðšç°±ëÅÚøY›…ŠÇ¿¾Žåt„y£€”™ ÜH ­®ÇpzºÝ‡#ó¢ôކö"sǤs·ê[;s«ÓôwÊ QÜ=kù|Ï–âo/ž)WKÙ ŠÃ)»lŸµ¶v¥¿K;€n–Ö‰žÞa M½QzÑA1)1Wé¨ÍÊÜì©Pâr»‡´u¥d$Œ+D× núM…4 2«Â;4_†1#®fV „7—d6¥&k¢M&`ÕÔÔtŠZt5áÔPœOLKË/j°ýšþuq__ìît.M¯×E·sY‘å»÷Ã(h?üU€ý´²S+Eb3òP-‚NiN>gœ³!­Î»Iò‡ƒ[ZÉ\F}Mo¢þšS²s–BÿŒXÔ¬"‚0ùÊ#˜Võhˆ.ºÙHEÜOSÆGþ¦®Ù t[ºÏ_¼lýd#hAg¸´¸ûüåfõãç­ýA0ÎÚ¹)ãmˆÂ&¾o‘#w”o4ÉÌDI'ÎÈ­ ©sÉ7õÇÚj4]Öwgʺ/·¶[pÀŽÓ—[;åGÏ‹¬ÕxzxvôæðøœùþééߨCš¶ŸŸ~ú° ŸÖjkêÅþ›7뛺(”‚º'Ÿ®­‡ÉWÕ``w#½¯P1ƒw.õ`QÐ ‹‹§2’ä.¢YùL‚[µ‹¿Öð×rÆænqða5ÙÙ7u iõ ?8Ú¿¹¿·æiwú ¿ÓxòóOço_Ð×þ(Á [ïÑy¡k÷éË×~çË×áæ—¯!þ áß üëÁ¿@>ã³.¾Ç²àï ø÷üËW8•ø"õq“xB7µòð@&mìg™bß4Öªhê”p’5=º”‘ëÛ„t¦ Ä ‚™Óz£GÏ¡îÁ«Ãx+2¾b˜+ÑÞºÑ3P–dxÆ@ÿÊØÃðZU…o¢°¢ÄDá6Úèý¦](k0% Ô¤ÀÑ0¡“@W;7èq–mPŒo(œ¾?³ þ®ç`c’Ì&~{c:ª¬Ê@òèB” Jù}ÿñ"Ž“,}+;Îlµ\/·…íÆeº1Q_”) Rt#v7vQ™'UÙ«EßgÈj(.¢…m\Mb–$¬]Ù¸º,3–ÓˆÀˆ[˜þ…Ò¹Lb¤³ÜbÅW”ù ÅþY"œÀÛ,1*|¤6,E©CÉî¨ñÛªŠgYźæB¦Ð¦_L´¸B7cI½uÝá7‡‚=Â_1*-—1÷Kj[ax­N:Z×ClU»´òU†¸T»;¸K_¯ªX…úÖþ Á}óäÜ칂»U’ÌN¡|~ºJù¨ØnúÝêÒeø ¥óÓ•Z{né¥ÃŸ¨pøeáÎôׄOx"2=%ÕfG¿‚POæ¿:• †º5 Ô@0PÁ@ 5 Ô@ ŸƒêVg@cÿ€yy-  ß,q/6é*óè›éÒlN£>¹s4:?EG{2ß`Ä©¡¶9]†Ýs3œãCžµ¡”1 ôvs§×­L†³çtëåöfeRöŸ¶ö^zÞA\Ñ¥¼¼&ÿ¬Úû¡Ãb2Ë+Ií.• ¥öîÕPþ¬­ûo,ZÝn›)°ò5~[½—m&ÏìÐêí´wÔ“,omoá&vȾS–1fyÞ¢,ˆç·ºö¶EÁJ]Ðc¹‡ôsðcowóÅ®†Š-‘Mæ­ínûù²’ø Ú«‘­ø}ØzþÁMAúñ²Bä”2GiØÚìB—qh ò¬>Jt‚V¯¥sÀÎ (<8ãK“+ˆ/ ^·(4Òl„+¾Z“€ôcÁ'œ$ÕFU¾]3÷Тä¤G¤Z¹€bÎ¥ªüÕ·®Ñ wPÉÎ.€fé Öƒª¥¯¡×ö—§„¦<Ï*»“–4t7x‰Øên¶·áÁ墋)©¦v Œaí©Ðø}þŒ.y(`{&ª!çÑ!Æ]ð<\wÑפZvô­0ÊûÈr ¯{)«f¹3Œ 3”çjy>ÕN§¦G’VÔ±¼QÿÕS³°'ÿÕ ³½ü’Ûb¤<¶ùd3 dÙ4æ˰2„x[Â媓Ê'€H%;™uw:„¢ Çè˜ï.‰ó©¯Y6N÷ß|8tâmî;à=V5Ï_vËÕôVª¦ê®Æ­—_˜JM›½»rM›«Õô²ó²u óBBz Œ‹a· 톛—Îí¿ãe‡â`Ö׸ )MŸ€Z!gEÞÀ¶Nx‘®oFæÅ<Ÿë†Š`m'wªKïá« ƒJöWd~µWCÇÁ ´Âdh÷ Û,ë>÷‚öŠ­6ˆ­£w>y4j†*ŸN<ÏS†O®ß“ZýÜ .n§ÖŠ}Z­>Ÿ¶2cª» h8W°nGÁ 2­¹Žã69Œð?g ‘éå.¡eå0~÷qtê|l(„¯apØ\s]wƒXÉØÌ}P’ž nE³KÝ%è.?ÊÞ[’½÷xöÍ%Ù7ÏÞíaýî³~ïYßÈ/b÷o… :hMöHß[£Œ7àÙÀ5VŒŸ>)M€¡]e[;ìVî,JÔÓ‰z‹mêD›‹q·â÷,Þ´ÚÛ»ÅKFõU•¨®†£qÑít.UJúb%?C[½É­ZCž¸’ >ûP ¤]eɈ¾Hø§Ãð±gV{Jïó&dѵÉlƒð{­Ê•æ‹ iY»ü´ ,ÃcÿçbÛŽŽ¯Î@,µ†ò`”$飬ìÈF—BÀ+É€œàˆ°5Õ½gp¡Ð3¨”@'tã›…6ý“âðDlítš~kõzø«‹¿ðk·ÒBÞ@Š.üÁD˜³a~´×a£©ŒWO¶²é¼E+ý)Og"g „IM+s‡ ß‘~÷"Û /eIÓ窆a;Èå{åf”*±êXPt¹ªŽÅ)Ôaõca7°ŽªÎè2¤üÓWk©“©—€0–·êo…A«Ø©f—Vµé´†o#Í+:k nòÓí­ÍŽ» Ö½Z¸ ×{ÿEÛ°Ûëtp×íðhv—ÿð·ÞKÚ¥ô®×yl{R N½Í¿©.‹æz¨ÖÿS¶í¦µm7ÿ¬m»i¶í柵m7/¬~üÁm»énÛͶý€à¶x.å|´ìjÅp’c•0R 9趸,*NŠ£i1ÒMF|ô´¬ÒÐÝ !òÙ¡¾qo›¶ÁVèo¶ù{x…g|}—”¦ý¢Óìn7aom_^:Sáë4BUïnWø©*hUuå)v©?¶ºÛYÍÿQ'mO€NÖÄôäÃÜw~÷«?6ºþFØ?TÆìÍC4ª´‹ú³¬*£/Æ»òðàwM"œjû‡ãûºeìOæh¸µ°Þa»­÷ŒPé–ñ0Jœ< 9û&z=oû3øæ–Yî :®Ú?G…v° ØÂ¾QŒa~&h7cý†N­8ºåõnÊ($*æ‰ÜfÛEe$Af—QèÇx_Þ—#·Œ…âüä…ï…:eLï‚Q¬ã€üïèSk„ðc0hSéÙõ×Gê~?_a^㚣2Æ…vL“¶ TeeU.]•5a*OAW«H’'7ÃÀ‰¤!81KÓqA·[Nìžxu ÓÒáœ0? Ú•Z£%šË <=Ýò„"™ÊŒ ïwôVc.;°†šH,v–tâÀ:[nf‘„lcq\~Œ@–—†W±°ÎžˆÃ°³¨qR†ãõ‚˜’Z` eBz6 f«¢¨{ A^lT¸Ý退×xö¼ kú, hQK¯cYKƒLæô€—«;fʲû}—„ÅQpFvè)zÞÖˆàNns/ µ` 2X>J‚™‘«ÙPçlL—Pør³ìº­/ÿ]šZeÖOý<‡ýQ¸Þ)_‘"™–Œ¢" ЬË?"ÉqHaÐU|N-b"د¥0¥ÚĚͣp’Z'Ëe0]¢àzÄZéRÐV`õÄôs$ÃáBå¤AМ™ˆÐvKSlx‡Ûìs IÌÍ;Ee”à={rÇ®¯Ûÿ×1=—! [+3A]½C{’å ¯øò»÷H¹¦ûÕ4d$ ›xhS"¨½úp©®J±«Öú¬P_1äëÇó9³´ ÇŠ uý_7ÿÜÁUÉëã«akëÑZõ}‘„õ­ëcåÊ ˜…4bàOA5Œ6 Ñ <NNõ¡ yØ1t§¸sûÆ*åVNÇÔ*„r-ú)¢àq_ÈË­.ªŠ+Åâ)>©½n§Ó)\‰å™™Žó—‹›[-0U†kiÆ1°}YY‡””jK·iÂÕHñ¦õ—\ºpÿ5'<‹a£±õšÛ¨F1aa“IgŠøOØÕÝýÁ‹K y,Ãõ«æ‚àÝ*^aºÀ5ÀEÂA„÷¶÷åSJCŶƒéxñºË¾Yà¡~9ˆ?”ñ¢Õ½¼è’ÕV­¶ËÀ‹:h«¬‘&‘zb—Jýµ_ÊÒ—~ƒüG©óe`JHÑmÒ[ªˆ’öšþÕ’TÒ-Lèÿ¼G%W'²Ú}©/â ]y^’)N m¹v”D} ·y–¦`—?øl'‰è¹j]ü¥j:°Ì=„)Ù…<8וm_¾~ðO˜ð ÔkzYEU„©*+†þJ–ŸÓÏ‹©L­=±—Õ‘_lr,å-o3fXÖæ”ÚŒ©;4–ÿÔÑ ®½¹»Ò>Ÿ0ÎB•š8äÕkéí2Ôú’Œa\õÅ’eÊEZ™àÔ¬wPüÌëL7v*ðRMg–À¹Ÿ).hµ^.kªa¢;Xñ’†­ZËËúA…ÄÅ“à³st%èc‰@¹ 8Ï"p³‘Í¢&‰ù¥ŸT’ÁŠÔ…¹l“@›¡à\¯Øêgâ·öüîÂ×Ðä nƒÎâ–â:VYˆACÆÍ3I÷¤Ì©)Tík›Wh\+èx Ò‹&Ó™îq*Zj1·Fꇾ`t{ƒÎÝ‚q„ O@ È<„£æ¡¶fNñžÂ…>W^Ô.NZUå”òd~Z£GK»SQ‰Žbñ¤xB2îhÛÿòõÅæ*-j×;kwõœëDÎ,ÉÇÂíBíaè™uЬû Êþ›¶|ð‡%Ì‘¡«b=ø?›ðóe¶?ðe~ž^¯Dc š ¡åjê^,±fä_¬NM¤¬{Þ³šGÀèÜ“\˜ïi˜GãG†Ù”a£°£ Âö6·¶Kóò.yÀõ/¾ÿ>Äê±JVæ@ß?sd¶õ^çÒµ%ŒšZÿ÷ij1]JItÁ"¸˜¥1³ëÿ8Øõ$ó´:”rÍY.v·¶6ؽgmP+³bZ7™›é…6põšï·d•@Þ[ÝNæ_ü˜]B½‹¨SÝH6½)Ê$ 9Ϥ;ÝG.Aí«&Âi"~zÁTñ²œˆ÷î#‰ˆ~=VÓ¶G1…[!ÑóǪ#ÂX‘HƯ«Ço%²õè.‘Öþ4ªÀîÍÖªWeñþ2ù2ÙŸü‰cû´œö€?-§= OËùO›šÍÅã±#ì¿'èŸ1A[î­päÿ÷¼üióâý6 ²¯g·ouÏËî'þvïùÌÎWŒÅ@F¤ürŒ…-l8;Ò!k@` ŽWYøuš^It*Ãà<Õ¾m±ã4¹Xµ˜Ì¶öX,͔ԡ~ß°??0÷ÉfB¶rÌ„tvüáŒGÓ GU‡¹€³›€Î¸œÅÖ=x“"¯îìÕ~ÐøÒëÆðÛ‹´‹Ò’Ìàóâ –æGÝû?Ð%ÛÙF¼’̾ÙôbˆQ§ùN¦(Žª( èä‹V-*ðþù ’Ǩ"Þý!È:æ|Mf\?7–Qð ´”Ú1ðª*·œ»¹2poä}\Ðd¹„…º[-¾gÇêy$P’ç±yé=¾j E†.º¯f‘0¼(ÂóÄg•üO6» g¹h§ë°NBGSÕÊŠé¶«UO/wÈôCl{s„]Ž8ȼš„ïÝ|¨ŠÝ‚ç“ u¬Š+:ðÒ%3e8]» {ÇÛé[›ïÀ¡^ÔôþÄfñ~Úémùÿ mÙÙ.n+ã0o{9i›ZÍβΆeüÚb¢kDŽ\)í¨wxãÌaünÂy‚×F:„EÁöÆ 1Ú²/Ø«£.ÐiDO/v:Ê‹û¢v0JbŒ{—&qÍÀ ç£yšDƒ {z~Úç½¥1ÃßÃÁº t[Uôf°Æ•ãѲÄñºàªÐð’“ Å-Æ€4AG™YÜaΜ²Ž:|W¸”¸ÀÑ}£»ÙyÑÑ„ù“†¶4ÈëgG‡ÇçGgçôèlŸ€ÙÏO?žì¿/.ªf»ó :½;<9Ù?}ƒž}ützpH…ŸÄßO9Ä¿¿Ó¸8ðÏ{½Ò ì·¾ z©]œH6ë0›hªDþøh¥*†mŽæo}ä·~[²Ó•¼P¤^;þôaw1Z(©X‚”CîrÉoBDdÂVìÃ\Nü¿0œßû»ýˆý~e\Ä÷Ü€ëÊ }£F@'&Q€½þ‹Fô¼ÎsúOýln>ïv:;;[âOWþt77»;Ï;½®üζ$ìa¶öF>ž˜|‹Bc¤UúºÝ WÁ£ûà@9H¦sFí«4Ðg·ÛêA5>Ø>I4.ÇèÇoÉÆ¹OnvÑ„`å8L(†ñ{tpx|vØ>ÿ÷so)‘Ë÷ùï6í: ³d–¢ò[eÖõ} N‚þ]p>¡È*âÄFÞG˜8 >ªƒ:“ †N¡½›ÄÑ#³²Wµ ÎCã9y*Íç½C‚þê—Ö~>ƒT¡Ì¡ßª/Ӭ¸©«¡Þ¿¥ˆõÿYúñÌp±Š2¨hl"©õí®Õײ­<´hbì‚ÃCâ®­SÿÓé{¡Ts>² ßÿõ¸R‹‚Ï\_c§ò¤ŸÄÔ(…1¥Mõh°h2p‡¥ÑÍ ›iYèÐlÔµ½FU˜Øm}~—oH`‘6âPÐ(epgjï‰iûý3Kƒ„&ó/šÀqR¢hnbB[ÑÉaó²vиÙ牉úË®TVÌdà„ˆ± 9Fè©c°úÚn­¡"ÌÁO>wڬ׌‚€ì.6ë_”qQiR¬Hš*J^ö$N }DYóRœ‹G§aIœ*KЦìíü­âüi*qmA¦èä+ÏÈÊÖ–lý¦ lŸŠêMëMïCYr××£<Ÿî¢%êõõ?‰Ÿ .LeØ”aû¶žYÃÛŠuGellˆ«œ*㠌̸‘'ðÿ¼ ™jèš`q’¸‰1"2ozÙ˜hâ{“%1®>EvÔfi Õ»Ë õ"ʘ"µáJ× p [8ö\Ç ¦läÑ Wa| Â%³ý‹Œ hÝ-A5cšZgP Iƒ vC4 ¾æ·^ùÇ'þçýèäŸÀSýÈhkà5¾­%b£ežDÊpJ¢ Ú~!¹Cý…4ÔÓqa/H>öo2ÀX‘cRQxu±N¯Y†ÔHŸuhLïFyUŠ­³kÃ) ¸¢Êu™«(OdTF®Æ›óäô‚ãèyµ0duÎ=«ËoŒöüìºJ&½¤RH€¥Êꥌ*¦kõ=ѭ発!rŠ\·#þìóp~E x YŸMV-P©a¬i€Lõ¯‹¡°ïñÅýƒ·¿,È6´ó%±Ô«¼ž$&sŠV\qrõÑiXà³R‰Y8V¥&1p¨¼ÿõ+* ýŸ`ª”kår¿G­”¶ŽÁ)OÃßfQJSmJpE‰S×™Z1Õc†nµ"%d9Ô¼xä¦ÖÈ1õÇÜh†Ü.šLçÀ O„–7•“áV$¯^.8âϹs¬[Ž: ÇŠ=°.ï5o0pC³Ñ_AØÔ…Ã¥Ó¨^̃q¬ÊY¹”IõÁ« +ìyDåc0@Y½u dב.œpÞ*Ç𣤠ÁwÂ*ý4ÁðrÕÎ|> Ó(D22¶Ìòd ÎVI(L‡–¢ Έœ­é½ç )dæZfÅLå… BÝÓÑîFUÍP·Æ/sø¢®fèö?¼Z@ùÈåFµ»‚îÙy—¿6Q?8‚„Z%‹NX‘7Hù™"Yðlh =‡‰˜ë)jÏè#?šìS]mJ|Hù±Ú-ðø^Â?¶Õʇgø‘¼ï*–2Îè$ÆK‚<byCD´QÛ%™åÓ4—±¬HJOªˆøLDtåhbØ}QL`2ÓĨøTÞ=ýº=¾àgKqšÄƒ+N¤‚ă`ëµ¶% ÀËþ#ÃK¡ü†½*À‹¦Qxþ$0÷S^ú&F½P„ê{rʈZ/2œÅM!¤lÇ.Ó‘Œ1 iDńִ«ÖnÙªrÐ)gÀy^eée%…ÖÕ‘Ç’è΢ñ /‹¨ýjOJQff3… ¨ËÑiëÍ·O(‚Ô‰=Q³¼”ÒÀvø ßµÒM²ä›ö“7­#y“Üd¡u@«4‡ü¯À÷g¼-7Çj1—Dò<)˜³åÏñÞTÊxo¾ŸŒ·`»8ê€òÇ$Ó¹ãsìûE^ÇRúÕLP ¯ìŒõ‡Ö2E)ú^ Û”dÓ"Q†-Ú±Q¬Î ¾÷¤>L¦ÄÊ<¥Ù*‹ÓlõpI£Ýª—ðH_kœ&à Ö1ÖOcCOšĸSñŽ2 ¼É0;,r›¨öâ,½`YÙ gñøì¬Ä¬ì‘ð=ëp¦¬úío½Î¬c´ÜÙï5?ºœÅ»ú™+Ô]ûÜJÕUbž~ÆÆ!Þ¼OÅû޵vùÀ“²­’ @kŸŠ.÷ž õÇf¥®ÿïI4Q‰álk8l:vX}˜pí’æ^u´ÞÎuFÃQѸ׬ tTaöGBÄ´n$]ª…—<>™Ó` ö‡t‚ŒUa¢áW3¢Þ!{F¨Åƹ üëÊ(fׂ8æ_㋚³r?$‚õgŽòÓ&¬x®aŒÝëÿM±t»Ü¦ÍÞïñD ¤ŒeeâOÑ(e4¹H,@$1Òíj”y(·f 'ŒXk:yÊçÖÜöN\Œ39cY á]é WãÔÉÅá"0Ø» ðn–?Ŭ6šbG­:f¼}Ù&¶(-gî,³%;â“S;ùàsy_GÇ~íÿúÚ×ã¿Lp9ž¤ ù‡h‚./)wÀá»V ‡ìh>ªXµ‡Vhï.œã³9½Ÿ _T‹’·÷X¶¸ØÝQ‘8¿µ¿êþÍ?úèTÚÐß ÎXÑñwi8´”°¸¢Æ< ‰>²Û€ºÀ ¤Âí#¼>}ÝvÄütÌW †3r/Â4VCÙð1†å[¸0ô}À,Ž‚úPºhE´‘’€d® ³h“s<„*û |{À66HÁ{4 ÒQ§‚íÔGÒM’ܙВ+†À.’WìÏ?«‹ÒGó¿zõ§µáÉÓö`Â®Ò Ofñ!ÊFúòÊšMRq¡º¿7aÜÑŽR <žÒ;\_Óƒ½·Aœ•°¯JÛÐÚ¼EB›8…s{Ì.™ã“d@s¨xØoéKØ#'ÉÁò}–‚ÕÑæ·® :^yç/&Ž>ë¼gqÇB÷‡aJñèÅ\PFÃâV<Ôu¢!DŸ°‚¯ÓÙäV “ž*ÞP¡ý(?/Ò{QÎø!¹ãà¯gƒr#ø#Æ1¥Dè0Gú7 ‘Ø%¼×ä “³ù‰ÆÕV÷êFRîUi2%ƒšßÃ4i{…°K8 0Cd Á‹ÜšÄAóD~iuX°ŠŸï»ùþÆj3k<±x¦ ÙäI²ve¼¢·°¡*Ï%' Z¢`àÌž1­_ÎÚL,*!²Õ‡ϱ[‚JEreô&)S+"ÿ!ö‰Èçm×o’¯a:Hnë_(ló§r y= Ó,™R´ê’ÿ_«ßס±0:Lò3""ؼÏçõ¯Íy¡Â ½[ÔÈU7Ù0I]×iêŸ+߯Vý3¢OMPó! ÊßFƒD=…ú‰WŠ¿Vš·[!ˆ$oGŠjÁ&Ê$÷‘‰Lhã¿@=Èò†-ÏjŽMÎÄ1F b¢­FÃ3öÅc Z ·Ir‡èt@FÙýd#Og“;6F3iZaQ"ßfQQYÄ<®9)g|4O!@“†Éñ©Ä1*H›2;¤j’²õã€Åæýh ‡oëV¾ið jÐVÇh‹ì:þI©ðnuá+NO±´^uiO/h“ òŠzܾ®bæ¿ZÓ¥ •Ùî.qfu(Õˆ;xw‡†dìaÌFpÂñ"VÁ›?°l(˜è$làMÝ•EH­`›ì!8tãWeÚÆ7d1õpIdòOÆ-)2LÄ:eƒ84t ~Ÿ“âDrnßñ ±›Œk tQg4×B´sÐy,üžÅ;Œ¥'Vb}6¼A[ÄO¨‡ÁܲD'žë(râªaÝqœ@êAq®–¹&Êömž&÷vû‰âój3zª8vŽp ˆ•.8ì[ ¶:"§ïO¶°é0=Æ{Ç­}u•†Óôêª/3تý\Õ²‚%^é~„¸òé+êOlÿ2¹y¦i)‰¯Ÿ^r‘ðji¿l!ùÛ:®.uØäèm†¿¸\³»IZ‡-µúê¢n´"Œ Ù(6²ææÛÖ‚)…‘?„ 8û§@Ñn}«³ªÝ:†|êìôä5±ù˜ÝzˆÈS ÜÌÿéÖëÞ]·ºìªŒ·[å„ D®RV“„ÌLï 9~ÿž$‹2Z6Ò0!7Á„Ï^X=kkk'Q4Þ÷Oèlé Ûrüáã}èSKáNT"ñÐôÏ>½ÖiTÐõWt¢Sì°?«ð†ÿnWU5Šr||F/w$ѾÎNzø×¿ºu9%=Þ ’Æáçƒ4¨#)N?~<׉ðhÅÏŸ_ûNß?è$xÔãÏ›s~ÕíV4ç& ÒuŒ_¾”r>¾þ«N£‚êÒ|¶ Ra“tmÛµM’Iø5ìÏP§bTebuEå«a”ê»2d'vâãä¾Í‰N·±'OÕT‹s°[­â‘ÕßÊ’ÐÃÑRàõ³ÙR°£¦£º³ª‹×σÛ6¾¯7ÚSz t®Nfï\_]–·l…¦¼™_畸ƒàtMu÷\§ÕP繄ioøu˜³ºÀÀxÈY§áê½løm VûI+â Mj‰y!†ÝCþH4'áäCéšâ˜ž¯àfŠ¿RÃc¨Bá•ß31A‡Cr¶»èýåòvž\õ“I×7þj†¸Š¼Ø+†aUM±ß¼ 7MßÚ´ô)‡¦ôD‘ Î-¤‚ÞàÜiº° <µññGo~z£v;µPíxzCŽEzƒÓ#µ¥¹²ˆY¡Y›˜ŸøöŽ¥GÅ=Š?fŸ2míL³+é•Ú‡öøqU¸ùÌÆãä›möÂóätÃz­ó MŠ#ÿvA¬½(Ôñ´'ŽÚÖ’’WŽ{r ÃÃÕÈê–S?¬ùÿðkYžôï ‚¦zCÆÃº^†Ö[|„¯Š¹ø;½r2TxÂKËè©Õõ[›}ܶ¹GÙø¥Ö—Ÿ[­ò—ôįì‡_Ý CøÎÑL›i:Û×¥®4I†¨ƒ)oìúÛTMkR;š¾zÊÕ7¥G—•¦©ŒSWÇ~Ša…ïi!ÑX”¦= á…©êP” iæq2i}Âý¶E› ä2ñq0§U;&ÖêÃoåµ'©ñùm)ß ¹¥¿yb¥£‡˜ =Ò¿ué>¯Zº¥>T½Ð}ò«zä—úS~Œ)?U}õ«zê.o˜èéWømõ sdsé_“klr HEFÛÛØ”îT¯w6ƒ¾øŸµ@¢ïb|Ú,ü¸‰¦§a ŸjçO¸Â[ëÞIÃWnü_¬ôí’º¦¤‹K“œúP•¾g¥ïÚ°·U6­ ù¹S MU¶­íÂ1¬J¿m¥ß²3àhWeØYÔñ¤2ùs+9Ô¹_عh«ò½XP Í÷ÿº,  ÝÐ.9NïÈã­îöæÎch”õ“,Ѷüé½Q53Æ/RË•„ÏeBgŽÕkã@@ºrOº¢ Ä0m_qÈ=`­¼£I® “çÖ}®-t[ÅmLžÆQîxkTFNˆ—Èi8˜M~…„,ÍCS^Ö4Þ6IŽ[¥’Ô,ê ãÖÍ,ŠÚE„/ì`°ùJƒ#°[¬Îs‚‰Àà{V4=ñŒÖ^X™x¥’·tàiw)‹ 3^(èXð¿Ù{ÛÆ¶mdQøùZþ ®{»’Z±d;Ùz£ä:i’æ4u¼q6=ç(Z‹’(Y½•”lËMÎo¿ó‚W¤hÇiï³wÝÔ–ÈÁ`0 À`Ƨ£1ÿ2œ|$ÍEqcÅÃþy˜ëgê|âH-&~öÏ1¸Y97Óra§¡W6ƒå÷a.#Þ‰•Y–]Èå}&Ê-8ó@î1&0ÏW‰Ì< "ÊóEB9 ü|?m8¤âSèç7 ótõÍ¥ûããÈØ”ž*a’Bo²·® ‹•_²{è[ìå§C?—Çžw”§ Œ1EAŠ¥èàHÞjh€GÂCX÷øÝ¤ûÁ?LV=¼Q¹XvÇ I¿ÈöÒ¶žtn7Z+'ôeN§Ž‘I¤Ú3œŠ»]ûWsÏ®»ÝÚ¡Åiììé}'2§ó·o•¯öÛÕD¤1:mŸ>ÿä÷Q¸XTŸ@;ÿ<¹XÔwø¾Eûøï?·žÌjÑÛŽÿÞ~x±à5¦ ËèÑ2x2›Okýޏ¦a•…÷\M­§P@¡|ýæYëÉ$°^/h×ÅqÎ{i@§ƒH IèNQIÄ/æùz˜¾Ç7_ˆK’Ý.‚ã1H·«G=;c„c ÷¿Åˆ«ã1q8P¡ŒßSNg4 Ol2’‹{méûãt{.¢¬QxøÉQëô€¢Q®3Ia\p¡èH¨NIEEøê1éQé8$R+%FΑºÿ’ÝDz“Hgš±÷kêqãPO¦úÍW[%à‡fárAÁd× Æ ˜Æ³—£ðì;Öi<ù0 ÏÇb%ça> ­lÄ Aœ*äÚAø/i¦•QËJ8ºZ€â96;jËàŽ#\ZZÔ„K½.«ã\ÿšYÙ8mao§ðkSYžäÆãA 6ÿhnÀ/®CY‚ÄÛFZÏÝ2ÃÙ^¸}¦÷ÏRhz 9mï7b}v"ïÓò}ñ¹t Påñu½äpãaдv5 Ó-D-ýœ±|Nú–·¶è<ˆWlâîÍ;—Dêù@D¥Ö«2ö ~½T”Óâ'†ÀýóŽ=‹¡kŠ ©}Þ*Q‡3Ôñ\´@·2U%3ÿÕ¬ï¶ew‹4BQ+˜È²BugìZ­š àì5Cîhüæ%2}Z•~­)­ …_8S²ã¶SÉFÞy`±NDâ:¼OÎ‘ÇÆ¦`äí^x±ÔY7FËMGÏÄ΂}P©l©ôVæTî(¶Ì&âCÒº …^6“‡:7#þ ˆÝÕ_ÈŸ^ró÷üe¿]]@<È/!G“."ŸÜpYïP…¿´Öïj‹ñ•ôZ¹/Ý›Œ¸ÍÈ©(Ã$YMÅæò%¯w“¥Øó÷í(~â µÛ¸}äÙƒ¬¾íoçŽ/xù‹sfôp½G‹9LÉû‚Ö–'ßRR~8"Nž4‰ó K¿I=g‘XØ-ÍŽ2ˆ±ÛÓžŒpîgàa‰Œ»ÀÒvªlОXª[­àÕ÷…X‘Ò`® v+tÖ7 …Ÿö-Ìú²k%[dè3s‘aŒ’ ’{¼â‹`=Ž¥r\5þ­º¾Šêê7ÿuÕÚgáÝÔÀƒŸ£IÿS[ÁÿIÆ*JS"U#ù¨ …•Ú0šÌçýp™9Ï]r¼h"Ó<Ù¼æa Œ-#`50H=2E,ˆÁ\CüŽl[^¸‰º­ýµ*³^ÞÖÌåzt$±Ë¢*ÑonkìÊÅäîmB¶Çäbx e‚®(s µ9%ÎÅ‘.­ÀsNõï~/?ìm8ÃÍvêæ»Îy]ÀʶŽ&ã)žÜGâÓ>ïæuN“™ù^0,…5ÆÇ°Èz@ºîlgœPH¾è¹Â©Þ4$‹°(ôûjod½5Þü;@K‰-¸ãáîS±§áóžF‰¨˜[°c=sAª¼AÄÞ…F2Ýé†c „—ÙZñ0Ò²å ’³ým”/;nËEá^œ9«×Ú…ü ,6õæ+ž’U|1æHW‘Y‹ïx®Ü@„þäTÚ_¥â/='8™¹v,߆û¥òÍÛxÀ×f-¿°ÏÓ®á@€_ö×f±B®Ç‚ôhÈ+ï—ò`0å#²qØÅ‹Jî(F—–Ot3ä“|,›jéQLÈÚòO™'ÍÔ“F¦T#Sª‘)ÕÌ”jv£ñI.Å_c(i€æÿcoïmï­Î*ôÌ›YÒ:–=ôÖ½X¡W« zŸY8¸ÚQÞ{è [á^[Ý]Cr;¤Yj¡a7%wµù»ôInSöJ-ƒnØ”¯Ù1·‰æþÆÜ8ål°R ºB^NÐ~ñÌ1%–õÃXr¨ØÒÅǵ‰øZÖ|8¹ëEH]È´ÛµWŠ9ᯡþK¬7Aò ËY‘òRï<ïhˆçÇØFqiÀ™d\téŸ`¥§ëá%Ib8 ’§c¬+¿;ŠùN7xÌ%ba´Uúž«ºÿY×Ç BžTŽ>Ulº¦µ¼NÞÇIYÒLI]Ä"Sœæ‰Ke"[È9÷d$fu¶n®âÑ]š§ál†eç(LQOÆt¿ô_.?BV€SqrÓaUÕe^”ÎÂ,кï .0gÅcޤ8@Õ ^ï(L¡V/µ°e½­Z,iW+ÿ¨²ݧx5Kjÿ‹¶TŸf"\Èo£«3ºêã„!ÿ<†CÇÉHPäŸzs{|ì„ù1â×'oeÎ!|mlèÆ}Ü<Ž—/Mâºâ³9hÑA´à,‘­ŸÃÉRÄp`–´øÑÝ9›Ð||”j_C ™²«ÃØ~U:Ðð56˜«íNЮVÛWð‡7¢:¼ãÔ¾nÀ# ïׂ0¾_שÕ:Ö^47%ps½»ðÁ¤i ¹íž5Wròö T›¨6'“Pm§¬3ùÐ8tt_ÆñýWÇï^¿úáÐ?š…‹óybZƒ Å÷|.Ö0††ëbŽO¼y€~‡’?Jñçò¡ H ¢¢cN±ÀßÔÞjM ÷@õ}¹þl4ÁH0 ŽbÅôÃ9á Õz´v€b&Ò@ `‰<ШHÔÚ2ñäNø€I¦, , ­ÝÀÊ÷ÚRø«ÆØÅða‰œùâú¬£— T+G³Ñ*A3‡.†©'‹Éxi…I{·SŸÁ’ª]9}þs¥S§YãáZ†lk#C -I7²žÁ'¨„þ¹š íÑÜÚãáÃ݃‡à`O”kìîo íLû½ƒJÚ³—±ì}¡ƒçîd½Ãpy®ù¯DŒ‡y$ßáM‰p­¼@Çï7ðûb]zºž#ÿ‡·§ÒÜ‘±%Tž­nÞ’mcÕ¨³á5ðÒƒ4Œ’Cĸô&ò,ùÚ¼¼Èˆ;‘”Â8Á„S›G’{B‘‰ Á×_8;»BçV}ãð€P‹tÙœbþvbT˜h‘XDuD 5§PWp&õ;‘‰ù‚Ɉ‘q"C½dl")×&T–-4WæñE 0¥A7Äw>­ ¡¶ÑºîÅ›×2…•E˦)®d I„Ö©*©:Px'ê 2uº¥†1 kJW¦NHgâÄSWemÀ²mE©ÄAáA»å’x‡—é¡Í©±l¡ß«3¤u*xúÓ+Žƒëy¤Ù8§>OõpEYf™dNºt‡ëC\‰$1‘¹äU}|7AdšI½e(i:7UWçÙ)Pn„àé)VÈ¡+ÛÐ70©p iýÌ¢h€7Œ|L„$؆ó:¦:êšö^~¸]öa §›/´³”Qøâ@Gd*U^ç{}vJ35ÓŠ …;_ðÚ˜ÜÍÖQŒ%gÆvñe-ÉðtMŽB©xÙºÄ1†#Ç$Ýáäî:kÈôÉîJÓWbʶ³þøÐr’RÐßå:œ/ãá]½>þr#[«2ºc,|›B,h-k„„ Ûóþ>/EJÓºfœjišîÀ NÈÍCMÇŽ7·Qö9¶EjœAøu»e>@!Òk.–mïòwI½+žßˆ?U4(|SÏK¡ø“ÇŽF²™UšÕâ )‰+û^ |[—¥zcÐKÁ[Ä*y7ÃDÀ†ƒªã­Ÿw(b(K‰&S™r´€·ê+Z\±W–ü®¦ .‡˜`ìÁ[Xü$Õk‰üR"¿Dä&!_ŠªˆÐTÇQq‰zR>QÛNš²[”½)º07k@ÿ, cü°Þc±PÙ•H3ÑÁ°­]¹ H¯)%è)«YwRDæhØ,ØÝÓ9댼æ¹Òn§W§©§1ÜAéÒ^·«gn°WÓÖú-&2©¥&‚Ó{Y‚¾”™Ü®¥Õ+l=‘qytç\;«4J±h"|@à…•:ô¾{žHWiÏ/Ž"ŧÜ|‘·¾× [AíÃIJp\:¾@'(¼Ü@á&mªôÿ&íì”Ç»ðƒB„ÙÌ$cÏ©Â:µ{én³jô[-ÿƒ:úJÙJtd$:’ÑðÌ ÚVѵ8FXo:D£E“ ­½‘ë@W•®Œxên‰5Ìq÷·º@ãTîð©^ý[­jÍ6“(¼ˆÔtw#vKü¾–DJ‡q­“ò›9ÂlÍH6çÖAd0—™¡ÆZõþY$nNŸŒN¤"Æéœ] ãÖ† ÚmÞ ¿¨öݦÝmò1¦œaÄæ›Wnfq.Ý{k°¬Q Èv]ÑZÎ@þáÆh ¶F«í·YBÙôÉ÷›×O¥§EŸQ²ÁKÐ/_6™õ¨^”e€ßꞈ(ôÅ6µY¡ªÌími§820´Ã„þ"«ºXêwþ˜]êRíñ?•¾ŸÊí¶¸mÂþÈ">ÿ ÂÞóf!B'–˜jѦàud>¹ˆÎBáá°Ð³ú4÷,²ÈÅ"ÕŠLÅiõ\wôÖn?\ÒŒ!,93P[-‹°°Lõ³7£êÏ„ÅôäÎgŠïkAÓZÊevÅWT´ºÒ̵_IÊäL¦-RϤHr-øìÛ2^–ëË¢••Éæ¢)Q—ÉBàG²2¶xÞ‡ñOäýÊU…ÂØ’  O=ÂÙšÂõÕKó¦Â^;ƒH9A=bóP eO4Ôê6Wa(i¸ò8ŠãˆZ.΢_W!_?;ÌŒ%=Bó„wã>0]Át•Ð!5eæ :¬k¢zíü ÷ÞÉúd°˜„)M}.í¡éI`¿ºÌóˆ-‚µýjm¼º¶_]cv`OPé÷ÐÉÔË¡2Gã]À*i Ö•²ÅWõ !‡þ `WdnkXåî¨íÐUm‡²X(ëPÍ\Í ý‚¦®Ë55ü¢¦®ó›š­#ÝT>Žs¶ÚÉ€[Qu%›ž³˜«JÂ+›+lÚùÕó%ØÛÖîfZްܽä\çKÎõIÎu®äu|ùH9QtÜã¹ùo.®ólj&4×S6òãVDÜ)EÃéÎrhÒ4š‰×wǦ|M˪öDéYƒ†PÑðÇÖÿƒ²–ÕêèQ‘„˜ág<ˆ6çÅܨïsÚs™ßžMÚŸÌkÒÀ'¨~×Á•ÝLzvYÓç`Å3­,šnG߆.AŸoÐ'_Þ)—_Ò)׎F_Ýq§\~A§”¢ÏÝ)œ^n£tÃn¸úºcƒ~Ùà(KáíG!ÒÇ0ŠãyœxnßsZ,57ŽÄâ—ä¯wŸãXëh‹Õ’/e×á¡òe›0µB§/Ïü#—êW¦„¯„P»1ŸÉ:`¡ÿ1šú•Z¥ˆÚF*Úwƃ0¿Ò© Úâ„©ˆ„àÆ4T‚ŠL 0™D£pâKi§[u¾ÜŽùJ’'®JªlZ’?í yW7§ÏÉ#µu“ê$¨@PQLGçëtU[uUÇ+CÅW—˜N»;n!7Ž ¬¦$9_©jcTmØŽ> øôµ]›&Ð7HÍ\ÝÚ+™•y—³2‹·›»7\ÝB'ctñýƒîoy䡼Š#•Y¹íÎЬfK¨z<ëOVƒèð:ê>üêïSŒyqÁ)N–ž'P&Ú;&o|}¨‹Qóœ\öŸ‡Êo^Tfž¿ä:ÏsÕ¯© 8ÅFÀbRÑáÖ^BŠÛ‚«Ÿ8¡ÒªùI ­I¸ãŒH¼78SàÄHé€ùzU<ìAtMæ L΃÷h8æõÏmåõÆ2d<€` @ŠI""ôŘÏY\ÁÁh—”h Þ¬¥òˆcPâ·äL"x±2Ž.Æ Pê¾3j7ÅòHà×€nõìB¿Ÿñµª ¯×íVè%߬)¦hI2 (Û¤‚o·‹„¯*ÎãuÝÿÅN;%¬œ ’g²îCÿ|¹\Þ¿ÏÇ«³:gª…NEõY´¼Ïâù|)&‡x5ûȱÚûêˆ&×P…î–£„çÝ"1ZËΰýmöG¼Q3%µ"=¿±Ì©OIWh"^¤ÊZº¼ÜQøø¤ ´QѨ/Ž—œi»@§õîhL&ÜbbÔ‚_…ï‹(„ö¼Ã)¸õdÉ£JÔöê}öyawŽ@ÙF濎®(‰vù“ŒHF©ÉŽ®ùT,ÁcnƒÑ®@ä+z¡kùâ ”î¨ÁO ê?æQ×±E Þ+Â÷4ãà‡~óßóñd`–´A‰DId™DŠ£Y…;Sõ Žï[“p±ˆdIÎ%Ì[ v)³zÝødü1à®J­²UGn²T¡jHW&Ë^Ì8(0Gƒ4nž„— çƒ#¡V¥(û·T©|1RÅJ÷Á)̰¾ˆg†´¥ìž™AžîØ¢æìLkš)WdõÒ¼JþZöjž6j)£åÕº†ž”Ú‰ÂéSOúJp²6gL~K_Û$㔉ÀÂNÆbè/r­…Ou$§ŽXꈡΠ™OmÜ.ì9D¿>ùíÝÃF‡‡–(Ith8i6;–˜2 @ÍýŽAnq}zh“Yç«Ñ½]\N¨[U(Ç,þ8[ü´ÝIÍ)ÛÆ¬âÿöWЀès1+`Thc—ÇGŸh3JÒ¢‘<¶¨z%Š'K©Ì³Ï·‘fƒÀÀ²„OTéçâ.9Öf1Q§ÕÚö†‚'vI‘»ècY´ŸQ†Ï;’‚¢yRÄç‚^ fIY°W§’… ,>ïO2²TÒî>U{U c5-¡ å*MC5¿jß¡ZÐL´šµz¬jö—ìôZMFuë•Ëyü1ÑÁ5ðð"ÔQº>Ã0]( 6XÜkSc”ÍwRæÞ Ì%c4Én×~]áC¥-91øts†àÆU@8˜ŽŽü&ïØ[Ië9E•`g!Ó½bvZya9Á£,|8‰†K˜f´(¨„Ñ 3«6®W°ÄiŸÃÔMÃeÿ<ZqLx!&µÇÔ €™Í&×ç”"4BCš‰Ä…˜ Ã$R˜;o¹^`i`6¬t–˜ow¸ÂošÙ¢ýÉ_ý¨>ª3J¿ÑfaêÎVÓî!BÂ,ÐMFðå"œvüOqÇŸ|1‰Tw¿QëÔý·VH›%¬EPà/L3â~b´hSúìÙ“ÌPÍ»â䱸÷<ãÿìÅËWiœ«˜ºú(b^õEAn-í$b{ ;-ôfúb¹¿ýt[††ä€¾â7CtOòá¤ì@@é‡1He_å6[. OŠŒ?2÷U5‚¯’"é<öTNoÃÝ›Â#ÑîŽQ•“, ävP2ʸžEˆ¤6BÀß.ð¡#|Lˆà± „þ¾ÅðI7Ó0£Dáä'ëå9¦Ó` ãuà¡@…ý~”`þÐD6¸‡1w ºeêV”©(¢Ëp½±É'Ïß¶öÌÍ4Þ ‹¦®f·+ðVLá¢ÚE!­8Ñj¶a‰bEK(ÖW‚tLÄJÁÃV­DÇOÕ47]4¿9mUŽVܾ²²”6EOðO…ýÍò;ªŸ 8:ÏS7¶Èló·²,pÞîÑ·ôÛô| "ÆãŠã­Eg»DTd1‹LCÆq½IR.Òvàî=ç€2Ñ© oÐf8/^‚‰E¦ÁWw ŸÁp+”‡^lF’8âÖgH©²h¼z©ñ:˜“…a‚Dp³hÖŸÌ…Vüu…ùâùK‹…K’·Ð*|JœIF-csÙ%C[tžðæùh3 š¦Ãê°ZUDÛ¢Þà*hÈý!Pql"ôÅî)üê>öø S‚^*ô WÇ®BèæßXe¥g ˆµÒ¯¤Ä»MÕTü\ Щ ÐY/ãpqîŠYezñ8d•øŒ² žSÄ@ €7f˜€ô­ÆÉtû¼‡G~`І8”ñÄn¼¿XÅ”ïÙ@I‰¬e F”`Ç›O åĆ&µŸ PÒæWҊ¥ë2Bz3ÜnŽDýü¼õ:Šp}óúøM«p͇þ_øûMÿû•àèåóÖžC%V¥©žÓD³·—£p°"š‰^ãÆ˜€êG€>'ŠùT#”yd`CçˆJ†¯°8œ-ãpÖÇ™ÍÖx|µ“,דHɲÚÜ2zЬ[ÞÕ¢ ÙóÅ’Öf¦M‰g%ˆâP1”dŸÍá¾Xõ$縷ç˜K=Ð9@Çp|%ƒ`‘Of€Ø:ìhš%xÝC,& ÑªîÚÑ4;[{ɪ—D¿®"º”#£mR‚NPmº²Æ~âÕ·ÚëvwK\‰Þ‹ i8µ“›Kf¶L©9úᇷÏOO[€Lh·ÖÃýÀ?}÷öùów­J FÓI˜ÀC1«ÃÀâŸÓ“7+‡kõ”ª…h¥²•‘Æa"å!nLÑc¨Ã‡û¦ÌåÌ’mA8Š¡AúÆY5ûc‹½Sðð7ÅϼòIIʶãM7h¼ß)ÄoÑF€6¼tÂXÅ…ŠyËp”Ô’òÁ¹xâ°«,3iÏ=eào˜¾¡)Ì`ŸOWK 2x¸I¾ÜSZÍE‘‹8ü½—%ÞfIÞ«åÑÜ‹–—¸œì¾8íÂLtëÕCTÜýx¼Xvwù`Ûx홯]^‡ikB伜»V14ÙPèJ˜N>-ÆŸd¶j¹|…‘‹q~) †ò·к™õ—šÄìës#<®žB=²°ñ7¸]Â;bÇHdÕtì°µ “Õñ|ÙI8’]Gµ¬K‘î1@½âá²kÈ ÖìyÇóË™©Lº0eZ¤â܇` òl®ºe{÷Å<ºK‹Kè>f #Úˆ¤bœÉ64О{KN§jŽ6'è²eÕÏÍèrJ÷õœ~cÍɱ,v3¦E¾Yaÿ¸&Ý<cSɲ?75<î²NýSŽ[§cbÉ5Çœu–¢µ”)”WrÓfÕV[¯J\i–¦™¶³a=l)žËÛMJÚŸs,œœ¾ÿ•Tüo;Þ~wC•_¼ŠóÒñòôÎè¿Wrw¶’+Ò9MKçØ?#éßôÕ:)ofõ5Z›†¨µUëýÙ“+50›ðŵÚNH¤îÉUZšæÓRëPÝm„ç¥'Û–ì2?]4—‘C8¢]žÏñ´Óö'áxÊqøU»nvJ»…\#×G®”ÇÚe}P_×ûõ%ü6Ö:~]ÿ§üÕ ‘ÊÍ‚Qîj `(W5u9â¿Q0±w¸ÎlÈ> FAÌþáAǼåñIÓI@rpøÀQ‘‚ˆ‡;¦æ§oÍæŠï²GÓ}rk©PÞWߦ@ÂGÎjšù 01c¯TOö‹A0ñæ`(“l`}ˆütæ@ÝûåGq›dJƒ?vŸ¶w^¿—Žc­­ãÿö¹L©ûO®Úé¢ü¬\y£è©l ,™3 Þ·ïýý?¥¯³áògÕ$¡OÛ÷d=² p<|Ÿ®+¿´j¥…ÝÚ<ïOÒåËÔ®ÑgЦkuª qG XSÄŒ µ‡c¡ —Ú,¥ku ÂŽU²Á%3]릻¨t†¿…ô;4êûö”ƒáïOŸÁ v•DÒ1•´­=´%9˜€%OÃØAŠ‘Ñž Ø\­ËÌè€Ô–w`VHk)AƒtÂCQp[õo¶"GIE}º¸›ØÔ¬T$ïF¥¼„¼ç—*'ï¥juÈ»Õ@9¥¦{ÏaMÿlG{©nU>GÎ$éí¤ÂÂ’+d6ª<~Ø­2'¶<|zöst){W 2Ù9ŸÖÒ®Âmåš½O…YeY!‚ÊÔ@Ö¾Ý{›¢ÆŸY¥M_ñæMZÓ6<.ØÑü¡ÖošC¿e´å³Âzj+íHNX<6ñ«Ò>ÛDy›Ôzr1P•4›iï«r”Ã7žª©GT>§H/×LP£WŒÇðBˆj¿šýÆìëM‘iR³JyÇá\#,²Æ?˜És0r¦Méïžœ“G ú°Ó‘Å¡¢wÜ!§U‹(>⾩ÆAÉþ¢ùÅ‹¤Í[YŒ´U|¯8µÖjÒb«ùïÕ–C‰š«-ËÜÖ«­ö«ãWï:bý@7E¤bÎ)`Œ}—Å\ºTfz+SòhpѾwüü¥¶ò6–¾§KgtÝ¦ÒÆšb›W7*ÐgKèq™8Œ£ÉúÆÅfxÁcc)ÃÚ¶ÐJôÃeR¦È=]„VšzÑ÷‡¯L‹ ¬tɆkM[°ÊË)/U>;é–Á“/¥ XÒ˜S¢@@J•°$¿Hž€ä5äDsΞÇ397Þ…iLÞKÊŽ» "î¤÷eª/XTnã}åu ™(D!V¦7Db,9¶ÅUì/ÂÀË–¢Ð¢  ¯)ôµ3‘ÜÝÖÓ×ß<²š,÷Ao6”nˆ"g(ÝKþPÚˆÈ9”Ü¥ ”i‰)ešSâ&£õ6(2£µ’ÂÑz éÑZ ŦÑz+$éÑêDR8Ç•(‘žãòŠäÏq7Ü•½Ë}JÏž¸xn[&;-w€/½Ýq+”z”—+_8Ô7È ug‰› õ›£p õH6 õcÈõ¯¶õý¥;Ö%Ô¥6’·Óûxepè]ïm¹ïý5öÉo¼Û-qÐ9ã V+ÎòÔãÆYõè.R¤ ^~Ù&þM¶ã‰Dyû…jñ¦øJ¨ÅÍ(‹Õb¶üµ¸¡€C-:JÜT-Þ…S-nD²Q-ÞƒK-*¾Ësš/>aÙà:Gú¿cþÈó”¼bæÞí©uú’Ìãe4¨Ò9ݨ14­Pð0÷ôýÐÅÓp&CÝXûÿ7Úþ{ÿ7Hl0ŽüŸÂå5æÇˆÎ—þ šù?®fƒÒáÄ6;[Ý*¨‘4š(phk/Ïa\ˆÙ|jÆ/Âp€7*êÌ#¬¥;Ì.+¬R6B¯ú”£*9AÿÍÓÿ`bÂ~ˆ¡>°OÔ4=Ó0é4‡0˜g6/Ê”ÆÞ·ÎÞŽ7#è¸ð˜ŒÉI÷žŽ9Ë›E‡KÌrP˜Vb‡Eu{Pj¾ ±ížé{xA+‰¦a¥¯ž’«!Ýô„ÓÞ ôú᤿𬠹öÏýˆï^Q€“cÄD IˆŒšïs¦Ð`¸·û¯|/pá­žhÛ\¶MbÞ oƒÁM0èªÑ¦»Š»j ¤x¸tXˆ—ǦáG>kÌHa||ŠäðÕ}ÞÁŒ±ƒ/ƱHÓ¢ÂôP¼M9žFêοÏÑpèæuäõñl¸þŽœAñòÝ"Ç ˆ!Çš '˜Â‚Q »tšØÅ왓 †i|¥ix ä׿õ7• ”U ¸ HËpÜ.åÕgBÎ\,¾Á*tFíl¹èIûƒÞ5 ¥j¡Æ ­ÊÁqá÷ósƒÆÔ£õ´}úüçVûé›ÖoOz{OzÍÏ0¶ß¼}ÞzôäbQ}‚ $k;r†“À+À¢C3¦~Ù”//†B3@•UŸÌuU úU]t:6ªž9Ò®èIB¸^½—ßÀq–’{¿õæÕ'ƒhY}2«ÿûªöYÑö¿¯U¨dA!$ÌE×Ì Ê,ýÛ}…ýÃßü“ºN±û7·‹iq%3*aESîB1˜$(†—- Ü—Õe€)eµõÞÙ˜«z/Œ?ñ²~Mn^, —·(eµÅŸw9Åü5(0ª0íŽ,ŠnY4U«)\ SNê'”¸š¥*#TL9~ûÒá$ºÂ·>%ÓEK‰Ìú1Þû&Û C]º4"ÌiRNæ£q_зúY´9F*;ã»Ì-¿±»kfrZpÔŸ;]Z}ÒÃÇwÔe‰ë•#—®š#vÓXK°'‡!Tnù\¼nì64ñ›ÕVtÖÇ6Ö;S–ÛêÒG܇~Ù„áÐô ¦é‹ñ`NdŠ#[µêso6x­HªiT¦R/@U¦FÇš‰WLM‹õEýIØèQ¶J B™æÜÐWîæ„ šl.06µD¯Êt ƒðY+Ö1…º-º¬ÁL•ր굯ëðÞÔ8Àëòÿ£í©|Þ r_bšü°xbœJÞ¹‰»käZ¼`'w ÚõyÃØ+cöºX¢Ý¯÷]ƒSS zPä÷¬ÑºçàOù>+"?‡êÇ’ê?`àodKYM°—Öö.ì¾=loÔ'ÔH£çÙ¡]¨X¾XÜÛ…½²_l¯`À¤UJ¤cA«­7kî’ÕÛÞ]Ù@wÀŒRæ’ƒ!¹æÒrÇÚÁ-%Ž4ÇÙ¸_$žéýÜ›k¬0µ¡›¯¿àˆØ¿Šgðí’ã_Dqo&âºà,"¢7ÜLÛ½ƒÍ4ïm„›Œêr…Œ–?^šxy9¾©5K&œmÌÞÁÐØWCÃxx ûü¼ìHØ·Äe¿ÄHpWo þÁ×|wÝøMŽÂ·©³h½Q°ö¸mwË¥TÛÃl‡>àç…Z\±É¨VÏ|±:ج n§RæHÖÃ?B¥>´8øðR©™®ûS¸>xÐ8xxÀe4ÅÓÆÁ~³D WÅ÷%quds5¢SÿÙ7"E–Bâ§·;u å®§ŽóâÞ­ÓF¨„~¯r»gmÝÀ |ÍNïfó%&–EÆÌ0&¢Q» 2<ŸàéŸrUê˜ šÎ« æÕ~Žñ“W‘²gˆë˜ob19k±/§3L;A–ïq¦<ûáJ^Ï{sôÅ8ºôþGüxGŽÌtÒ9  J‘ü‘0cÌ\Šé`bÌx¦RâÈ„œ="'¥oV‹‡UÊ“`§yH@yÎ8Mf6‚g@êœ1±Â›b”#€‡âm‘ƒñjÆ`Fp@îÖ^~2¿·åëb*ò&.cöÓOÇr ÷"ыĦMâr½ˆZ•ã -ÅÅY€c*9ILÇóÉ¡¶lØ †R„gœ(h½ ^æÝÁ£ä~(±XC†eòOc$ öVœw;¦xVJãè×Õ0{*c„&{í ð ³(0×*¬˜«ù$Ú¨Ã|ãÙ´U—»fùLÀf£;±³(9T­<™U`ŽÉ·a±®ÍòC=gEäÉlsŸË×X»³t^à*Jv»Š¦þð)Áã ü"4_«̼(rMp&.T:Œ2}C)žr$©rœ€‰0ëÓ sÌüê6àØ6áÆËsJÀª†ŽîøÀD„nFuÊ×ÐJoJsÂÚ°æÈ6 „‰[(õHA…Ñ|½m#î¶!Ij¬«0`{µ?rØgE¡¨ÒMØ6+‘_(ÇÙ園tÍ0£ùŽî!3£“G©hDI™€Æ¤o‚†ua¯áA˜ j B9!§Œ„R$‘NæÏq&w22êsÇ'±ß “Ìúf9÷DeêøÍ‹.í Ë®½Â¬ë…d·&ìÏK–t¡¿un5J?S3òê™£Ž”Ê0pÓT…Päô£˵÷1ZëN˜JTåêb÷ÕÆ¼íÅaÿc´Dò»íN·[S¶>y‚bºktqÅ=Z‚ùí3ÀCÀæJ{‰[­-̪·ÅËž¶Pq[(‡[z•ÖéˆmŠ/ŒP`JNÔRÿÊLͧŒCå³N Q«iÝI¹SÉ’˜¡æ–™`=#ñcàïì(ÕŽKK»/ãˆãä4Ì2PÜz¤Ôn{BiA¿´å/,š‘‡Ë½Í#P*JF™¼ãÄ£òŒE9k6æcÝuvZ€¹›@b f¿J»sƒ O1à$\¦Ó™Éwz#Be´åy]Y˜­ Ü^-&–åY;”BŠÐ¬9£Èò<æä¦çQþj ±¬Édv´š~Õî¯[4a´¯Q,:vêŽve-Ór?1T^ (|íxø•kc0;kXÆ[™7,òЧ4ûda™ll™Òš´ ÜVoí´‚ÛÖ¸ÍÚc<7‹äK”Q³ŽCoºš,Ç £ ui³¯Z• G:!sТ鳷a—FEj¾Â%ÏE™`Ëòmv3!•Íþµ„„^ Z Çáâ=ï(a+X’`ÌzÉWR€0,&ášMçiSÎa‡™ë‰´Ç`Vüº2ÍæÀÊ©3­éÔnäðOY~=LÊGN¤Ñà¯2‡ḭ̂ ¥?®¢XNØúñT f¼Æ €É£|Á “OY®AfþK´Ò:Ê“™ÛPqKëÀë“rrÈŸ™rдGrô"ó\1;ðû­ö@„éWʺF' æXé¯ûi2µÑTÁzBÆlÉ,îwÁ€¶~±%6Á£°ÈM¥l®&Ú‰/ÄF¶J"ˆ'"] n›',g=0d„»3• &B2·îÌí=Ìz$w ½Ljèé| ž$¼c”£lTŠè‹1grgiD$@!æâÍW{ÂÊNÉD»×j4;ØÑ{{j¢Ò¬FuÑhŠ×Å{–ÿÏq…3^® Œq/gK+ds3ãâµ’\>ñ¬Cž²ei%7m†ð6‘bªÆ–B[Qz«¡uV‘ÆÝ\ =,°G4è¸ ãÝJÁûËzCÉ: |N‘¦Q¤©‹¼'In™=£ )…BæŸC¿ù¹¦£_3\¢¥£%¼ÐÞԈߘÊÏ9P¯ïº¿a#>wè’i8Œ¥EFWLÃÖÜÃlÁ Á‹²´ÂÃéG€'¥Þ†é¢T?ÈŸ‰¯ƒ;²xú8FÁ²\ÐÑɳÉV6*÷*¼ÃK Ožj‰î? Deˆ;óÞ“k°®[O®ó„)çØÛ³p_·ª0©ìþAM™Rx`¥.^!2 ‡«÷U¸XÏóµMï̲Y'°8^8OÁNÃÄä~Z¥RS¦rÑ®§-:d¶¼;’H+LßÖ–D _@M5W«E¢ ßIvÈÛ·@‘«¼!0å9: "pz¦½ÃAQ— 8§œ¹µhši©5µQ¤•š-€)Îâ&çŒòYâÙI#Îö|≡Èbf£/$ø~艕/mVâ™Ä,¹~cy“ä«o‰<†Rkž ]:E}²QˆµLm2ÕP-†÷žôp³õ$D ìIËàvœI-rL¤ˆ·ûQlÃ¥OõÂÉlŠåùÔ¿$ÇÚðGû4îÁĈÇÀÿþù‚Ñ`¢µß¶PXÄìUé¼#ØÇS[ÔD0‹¢ÄËÚ¿Ì&úºÚ8¢ßާïÊ£¡«×D¬AìÓ²'“ ÍŠH†§Qƒ”˜„°ø°1˜ó°H¡‹i½‚`¤œæ‘ëDÒç´‰Éã`GºÙ˜;'–ÂL–š§O×êåN^£»ØÎeD^cYO®M÷³X‰‘¨®fâ ;>ïËFró‡eÊS' ÀQágTh~òF;ž1€¥1\M&;ÃI4EãÜS[HÄÇ#)ÜHNOT×íb…IW”ìRmÝ®¥ª@ÇlM –m¹Ž!\·w³îl$ ’e ¨ŠDú\é¢a‹–úâqrýŠyä·Êºr¿{ð~}üÊU…f|(rˆ¿>{ž2ÜPÈ„;ÏÇ(ZàÚc:æ­C×ôaÀ†´á¼ãì?[‹séÀŠí<êÓVÕÎÕZGæŒVûˆx\ƒC[«%‘gXEȇâÆZ5mËÎ`ì ò£WÌp…ŒK`Þ"¤ž˜õ° Ö±Mf}¢AÏùœë:B!YAW|¬ DÒ! °î<ÄcHâ<]7nX(†LýbÏd"¯Ô¡ ¼Á¥[Ø8ú«û$ß\dÎUáŽöÐšéø šCÜ.D•U£ú¨ÎÆždàþ’e(£Z¦®E<'•J‡44øü˜—Ž0’“UÿÇTïä⪢±‡»iã><WäyWxŽ'´îÀ Õ [‰dŒØQ4‹b‹ñÿkaÒÔrcÕ˜!g蹬33|6§H°>"!Qx±Ïaö_Ãê󹋵̕GGïe%<”~ò¼g«““[+ÒMLþRCsÄxu-^= šM—Ç«ca,±=€%‹*ÃÇpÕÀпçj…çqòåš< ¯±üC(HÕI^IR¾ü†"Åé,è,ŒenTW_¸³nµ¯1?­Þàæ?´›_ÔúœÆÿ…Ûî½Fòüÿ]„óùÿ¤~<ï=ñ™¬Tñ~,¬é Ù¨VzC´@‚ÂÈò KÁòôŠÏn×-°fkNædµ õŽ&Ÿ1!+Ò£«þd5³iu_údê×qÎÀé Üc:6š*±3ŒíˆÞ¢\¨ .ý|ë3w4,ryöŸB(·Õ‹Q~ëK8FÙ¶xÙ!Ã߉/Jn&ø[_À1ö*Qœªµù`¯äÕoæÕ—æoæ#½=âÍ|â*î€O›„GÕÆÔE.Ÿ0¾˜°tÖoÅç H¥{¤B\¥Ó­€S¬aY‡EÎÙG¼øVh³ÃxËà–/7 Å†tþ­)Á‡Q´DÆkŠ]}›ª#܈¹¸Žw¸oß ûý*¹(‚aþèCæƒBYŠ)Ûç(ºôù§hý<Žçñ¡ R# ȉeþ¶¶¾…;¿õñQAKškj®àýy†x ¹~\¦ªðO¢æâÓFF~èÙ×°‡{æµýùĤtï0¸Ü;ÏL3EÇUþÞ¨ã¨÷·±Àb W“e`®Ùú®Øˆ˜hA8`.ù»ÜŒ3A E˜¡ߊIÎi£‹•Ù²«ÎÖ7›©ï|’>–Kc퇎¥gg‰R]Û´lW0mË?Ȳ˜¤Üß®¨P£¹¡Tà£YÖIëá/ÓòÍòMCªšÙRf¡¡ƒ¢I”åt¨‹¢ŸÛ%*è¤ íÉ–Ô…¼.t`¨Û>@¨ï*=Àöý÷Üßö:ûÃBpe øþû| Ȫï¿GÒøRlš¬f]<ÿ ÷0+GØÊ|xŠÛ›ýVå™­ç%¯°×0…õ¤+U½éuóE¶f|ˆš˜ÆlE¥kÈE{§ÓŠÀ ,Υ¦_@¹×E…gÅE<Îv­Ãç1S[Š÷8ì„}ÔÍbR•c4kb^ß‹éG^Áh³àB]duaüjüL–Yž1¦èrÞà¶«ëþF•îážµÜ/iW叉Р—Ž su©ò nâù.Hé^Ú%"QtUÓ±Be‡þ0¤.®+ˆKSÜVäh¿KÝ ª*³›,Ù@}ȇ\µö;ן–Õ? z—W·ñwP? ?Š{føÃÈSÉu«Ùü]i(gÍ]øÏ®uøk,OøÜÆò}¥Ðïè&¡ÜÊŠ\”©D ®«í}ss/ &d„{aÅAŸ†&ü¨u³ÏyK®7j–lW…¶l»ö ÕŽØ2¶Ú) ôeŠ×™€ gÉšô­ü†]Εü •e›˜ü~ÿÄûƒÝ}Ü|»Ì? "n¶+#0ÇI¤çñŒ)Œ8þŠn¯1µ‘Pµ¦ú+>]ÜM5WÍÉŠeLçú£9æå¢k6c–¥T]Â''{¾_!Û„wÒ+Ó<0ÍÕ !í£®w!‹B…°Ë?Yí¾ð-€õø »”ÐT-‚_}åÚ¯D±±–¾¤lZðäSíìõ‹þUÔØ¨Í-yûÀ¢XÑŸKsy#Ð7Â?$Æe¬¿Zw#¹_C¯Çg»¶Ù–‹¶[tªFé?¿ZD}ó2¬’°kA/½©k4ëVÍx‚L\’œøú$Ø]Ù©»D0œ$Úÿ€—vnºÀoµÛÖ¥ãT¯koíYa#N–4˜RˆÍÀu%»ÅĘŒÍ%.)¶’Š¥>³mÅTÉ LAe|hؼ#ãÞ˜´È±J’,7¹b-'dï¹u’ÇÞ}Lu¹}8#}±Yø¶ˆ Ò½;÷òRƒ–þ;{{8åí7–R/œ•Ïåió|L#Px'‚ æ+ô£à«ÃhÀxBÂXÜ"g§DAwE)~€y)UÞÓ¦+ žy…amÜõŠ’~¸@¿Í_Wì<Чþ•U…ÿÆôñޝ"àN¾q…wã ¯­-äÅMý[È­•-”¸•”9øú¡b0¯Â[l+P†¯teËX¶UÅTׇ\Û‡-QÕÕƒá‡k*¿Õhn™Õ|øÀmñ xpª§Ñ,¨bµõaÕhîíS=ñVø¡÷¡o¢]Uø=ã?Àû}7>\wnmm…ð¿BpÕ¢Í"éÖ;^µ+y».ïF]mêš­Œó°dßgigg¡Þs^”O%ÓŠMßíu¼ñ<9Ф{$ޚܺ‡sÀÉþÖ¡V¤[޼œÜ›ÏQ‰ë]yÙéáx»× ¯á÷p>WíɼÀÝì™8‰a×¾Õ’_>š¯ÙűX1ÞãÜ-q¼´½ªþU¿CÕ«—Ày…T(«œJŽOî¿?¨(ß!føsB>¹ºÿD®-1#¥dT|o¢{ÃñŒoòã¼¾ÝO¹ï-&òj‹¿†Ë6Æt‚ò©€He+eaNcIß7ÞNÀ°8ߦ³| $…Õ>¹òÝ!¡Úþbâ3 Ýk+èIÂü²' 0ˬÀ C<$k`Ãx?F±¸ò‹MÓwèþÂ<Šû¸TEØŒ¬ f´BÜ>ì‚i­­ã¥Ó Ï®Ð¶s?ÄùÖ…@’hÚzÔ›÷c'ë’Öã ;e(rQÐÚcYsöEÜåÐ`ÉV&ØÖMúÒ”›<Ù*ƒó q­)ïd®Qñ’‘q#/<ò¯Æm`ðF„á”(.ǃ%zƒ™àÕ =5/†R!k¹ÅxªmµÃíÈ8Öq]ÄnoÔvkçà–™k9ÐMÞË^gÍÃýðp§™ÞêMþ‘ÀÕ&9¹néÈr xçt« íŠFöìlî‹xhQSi¶}ùÒÁ»Ì˜ä$Šb×NønÂüU!5_É\gHIx‡´¶ÃÙ ßᎼH°'‚ƒ<}}tüÓëWÇÏ9ì•ycåâÊ¿Ÿš|ÿÓÿØø&ÿe¦Š Þ*ÆÀ”~b|¾·³óØ|[>»&“^®I_;¶«úOúÿÒ/Ž^½NõÐ šÎ3¡ðâÐh¯ëtÜüŽî…â”d0 X^tãr0ŸU–Þ`vþ˜?rrH¼ƒÑk FPýÙ©;ó¤U™‰X§ÆsÞf1w#Üm„/CåÃF!pôãÀ7àÁ–XsÌà/R×±Ô×áb!/}¦5ö“«*=½…Añ…¦‘À\Ô „…ÔëŤ`:BíL Ĩ˜ßf‘m$¾ªòzîÆ¦ ‰hAB±Ò–Ãx†t†§ÄÅQ8àË€K^ÊñOž}J—K›øßg´ä»ÿÙXŒð½Sù<AU ‡ÓjÍ(/îºá?ù²@Aq¼‹‰Ÿ\Õ:Á“ëjÖ(Ù÷° ?JÆ >H-cÊœšß1c\F¸Ò­p,®ÐŒ=¦k*ðŒ8ƒ’o”Á˜$þÍ=y9ôïxÉ&ùXnˆòÑž¿Ž–…J™Øî(ž~Ù~•u¢‹}X•:z˜¼V$†×n:æ @–•ÄL»ž-Ã+¾4©ýøé0ßî\&‡þÖoõzýüÿy«Îª8N¤ 7L1{F„>_\.5º’wDnï³%jê ÝŽü,Ä­4ñö]M\Ò‰+Å[UÑŒÚ+âríØ4jørá=¾}X³„^½¡–Ô„Ì« €£1z9& ¾k“$!iê&ŒØRá1ºvþj·:_ bæáýQË÷Ì¢]Îø¾®W=‚Ÿè=U¾´›Cr£é7i“loï÷§Û¬Ü$Þ"ï…yätÐyâÁïOº¬Ø&Û¤ætÂ$ENúýí{÷¶_ʱFÏAì|ª‹#!â Óq±r†µõ{±ýÊØ's4‚#Ÿ— }ý¾Î%{Á¿©éeFy±‚h™GóÞ{L•.n+×R¾4_D?ÇÂÊÙèr¬•¤ í4‡²…ód`ò¢—ÄX†F•Ô!³1Óó~Žâö¯ˆÎ˜søšq»:jµŸ¶zésVÔ³V?ã’•^§2:¦{Öƒ:Û¹OB7 ¡½#M…bñYÛîfôó~–ZÜjÆfþóVû…½‰Í…ìB[ÜD$ƒË´Ú?´ðœ¸ÓÙ2‹éPéª\™nHàsQ«r³êt õÒ8¼ŸÙ¾s¤)=T&‚nÞÂ}÷NÐÃ_}ü5€_vÃrB”™1ÿä9¥#^YV6÷?è~€¿Í-•„ èµÈýó¿[è5if`´c,ºFôÍñyÏøhJ»´ôEØÝÞÚ ºSÈÒ²Ï^–Ý?Ó:¢DšU¸aé`˜xÉå*”Î/]1(_¨ “íÊKå†cy©f¡Ú•oZþ… _üKcËïžËûzDlê`7_ß,î½”Ó ùÏϠ椕ñvpõLA¼¸%ᆵi¿”îƒVÏ;³-Jú!˜ÍÿhHtPÆrŘÌLC)‰þ)#Ñû™Íöœ"*{’ }§t¦Œ²j’0Ú£ [þoŸ‹ç3•Ìæ¼DÌ“ ½ãþtœ|Û¶ãÂ9»»éª§ÇD¥ëÉö{G“1…RR—téxS‹)ø”£Ýi*ÖèMñdmæqÖ1Ä\\Ú“-‘DP W ~zð[:ŒžÍÑÏ)Lµµ´kW 7‹568 þeÄqyðä£tˆp[¼åhR–즌G¾ ¦ )²¤ˆ”›‹c…Å1£Ýrî´“8VòÄÑU¢Çè7 ÖÆÞ 8‡íÎçy{²’Mw/óì马¡øfûr0³cµ!Ln/ƒ¥@²aIž5š¶ÔºJ¦Ð÷R7ë0Àª£˜?pkœoÉrYÞ`Ö±¥³)”¹sKæSé=p TÐ x|À˜¼¸¾$áÔ½{†¯¦rÂPÄGëaŸ’-ÌV'#±äU•ŽZF®KXí‹CêNüuý¤z¬kvƒÓ<#2±j„x4¥xPøš” UqK¤(õe¶²ÐµŸÉNâd¸Y®¶Ì5Á¿Ý0ºˆ”R÷dÍqÿ~±¸_SäðÂàc¯ÐÕÕJ¡1·¢L§’6õ·>Çe[à•:°h~KDpÞƒcxò¼­Bt±@CæQºÑ2nÓ£3ý‚gHqQE µAJEñg4¢|5'¦ÿu«©ÂúÓó_[ø@PÆð[å×Ê¡Àæ€ è/¾p¸ÿë ¦ðùì•á£È-›ø—Ñdb0’)TùbJIªµmRÊÌÕFVÙŸÇtûŸùmd&Kð²X”ñW¶ùÑÎaUo3³fçE±¸Õ¶Ùeó+Å0ñÔÙs‰ÅÙ7ûóU †A†i’ÒZ`o—6jwâØ&HÓ¨e|øLÀþT !Üõi bã²ãÔsXû8¢<È—04ôGˆ®“qL‹—–:˜ ÒËd09%Ž?aAbdÎnFÞ€…¨fÎ(`o‹ L•õ‚úäÿ…È@öl>N!YµSî‚Z_!(T”èÓ­dÓŒHÝ ÎUË"¶Æ:7ãpŠf…vŸR[8DÀ\S)«R§–8ÂÔÿnç/0+}ãÍžïb J^©WêÿœgUÄÅIkÚøQÄ`¥\iXïÕûð† –'BÅ‹¨>„Wâ…3ípv†xQv좢“µr}3²3ëDëVBí¾8(c¨¶- ]‘—Ðrã1³¼Ÿ'›œÃ}º±‰[Ï ¼©Y⮦„pçÖºÞâlj\àgŽRe³5;ÓÜÏ9¯S§ùwDôŒóSXÕЖÛÓÕ(ÄS~$˜±úÂ?h«ù€¹±Ó„úüã×ï~òOâ9ÚˆÐJ̆ÅeÏØ§ÔwIù¯_={~|ú¼þî?ßy^ëN<ÿ%'Õ´ß5zl›¸–zxŠä>üöܧ³Š1gAŽ“¥—×.ï5¬PBà :Eøy@Sz£>¡²\TØ#Û9 Ø6ŒœL ÀL‡å›aƒYø$*ª›áDØ„”€ráâãulz;,€©ŽüÐÒD~ÝX¢z΀vAõ´¨<ƒ¾¬ñoYÞ~Z‚âê(M4<)_oU–Ê¡@¿Ë/úà:¿˜ÙõDÀUoÂ÷t©2]faÇí¸™ß€e¼>t—s5ç÷èª-–¦˜¡¿ïI<§—¤Ôñ„N¾Dž…3«UÖF Hëò)r_„"ÒÕ«™iü;Zû²Tko$UµDc6 …,C¦«‰pª>ÊNYþè1ehȯÁ°”ÊWôjº0ê(ÊUe˜¶_X£Â$*Î]”К)w1òÆšý-tö1Ü\²µánÂ?²eÒY¥Ò[¿5ÚJ³¡éUpèë¸TY Q¼õáªN/ªW5D(Ù›ÁK@ôÌ(øsL\‚¨“Å·œW‘¨Zº0¹„›(²6¿KÚDcllšÅGþ`>*âñŽí¤lqøÃ‰ÿ7°4¦‘U¯ž`[ñ¨äoðùT] Õ£˜ÌÈ”M™e{ÒèŠÎw$Ú?3RäC1N…–ã®n„Ǫ%ž $ûá‰ÿü÷–•ðŒÛª>†ïÖ!PºÛ?˜|-±8é\3ª3Ù8F,k¦§Îº¹@šGõ²©iI¬°‡þhOЫ/á cóQesf/ Ð>¾л•rÜGoŒÖ%¸x2)lçñíÅ–jТëB{Á%8„ Ÿo]„’B%J¸…W°8[‘¿®ó;òÙ×"¬t{¦NµÚÅ•tÊ(;”„X)/¶Ðæèf‰ÖIð=«àa¡N ±:Äì*Aw‚†ã˜| 8^®éŸ¡QYŒ–Þx†/*,{6.+¬@¸pÝ;ã5xˆ ­¹æxÅH×V«+’” Q +¶‡gñx ÁÀ”‡a#64‰¢JÝ÷îc–Þ9= 9t)©(£Éx:æ¸j@Åj‚בNçÕàB=<\‹ñz!nnû‹IØÎç“Ad†,»:‰ÇÓ(=†®nƒ‡¡é!Ôb<üæL©þ º•_»Ñ‰B™Ö !TÀyjJ³ÊŸ)a"y±k0…œ9Í!eš,5ýÏ/åíbqÖ›Ç{E!—&á´7wÂ^‡ü0ZQMy,ƒ öôƒG K* 9Ó=vÞ§•­”DkìÐà„PíÝ´¿ç¨¡EZnë×QSkdŠ5µ Rci´'J53¥öÊôü(–ºã¥/3ºc„G½¸Í—n²£…û}Œh÷ ´—“·K >(èPD|ðÇk<¾ !iôå¨äöõþG#-0’žé‘ü‹bÅÔQo’ÞØ#¤{Òý,Ö½ Ú [®ºD{vµMdÙè@×ÚÌTÚ /éë³3¸cP€¹ ·w…9^ç÷{ €Ý1Ÿ&×þ1M‘´—¥”A•ÌqÎÅ‹Õû÷ pd«Š÷'ÿxôîÍÉcjñS¤ìÞŸìà#s¸ÙUýä\†_–(²Úýa¹;¼‡òP<Þ}ÐÜÝ/§Ý)ý&ßýÃîñz>ñú”ˆ –K_TÈ;ÆY"½ºèÂ85…, r1¥ âh‡€ÝA[è\aGt3߯n‡}¼ ±MG;ðr5³_Ç6w»V§ ÚÀT‰5P|@2x·}ÎNfÜ6Ëô¦«ÓšyC¬ébœ¦°xˆ5ó†:Mþˆ93}ƒ,Ä7.YvL÷`PÃUwIú5r ÊÏrÞAq¤ ‹ÏrÊã{*?3÷Φ!•­!;F×È~•R!¶TgG)1J"³5QøÞUx¶©ôÌY\ry“Kóx/oì9Ge®á}FËp\â'«†ÑÍãñ5Y'®a±·qX˜“_fØýàæóy4âÛiú­“E†Õ8±;‡jaÇ«—9]zM©¯ŒÆSê:v¿H—°…?‹Q)%SY‰Ô”Cü ‰2·/ÄÛ¸ˆ1¥:Šiì#cò Lµa~ÙÓ(ð+mîfWµ"L»ä~o>ÿx¦¾1†úV • S{-…´¯fb©‘oúG¸YGï1DR4øSªDz~Ù\¢Ôxt¡I/„v7.„>Ü=x(ˆÇû{»6,„ÔÆôÈÈ{ÍÛ¡þ+Iqìç9}zºOŽ-±œõ‘”€Ì&D^L A¼4v®ÅAœ4îAè[ýL8 Õ9 ˆó®}ཚ-ÕJØ~Cb5K¸öóÕÉ<yÅdâ~‡ú2·f‘;Ȱ)1¬'æW¡“ø‘<4˜Ïj¨ÇRðÓ1^*˜®úç^¸Ä<Ô‰ˆ‹ÀøÚ{Ö û$‹¦Š¾š5w€µ9éñ«MîÀiB¬Äw àÒ™ø¶š[·½3Ô30qAAOåèÁ£,( ŠwMçÞâj#N Jz<&ÄÐésI(2§@™¥)TL‰ "Ö SŸ½ ÊŽ3â%«ÉRªROR òñ Eñ Φ±:ŸÍA§$‹ùÌÜÁdõ¢Fª¦ T žß£25B=q)Ûì"‡¨nÄxQ@Õd¼ÉÑP ¢Ž×œ[IH“ÓÎÞ4¹ŒÁ°% NlÒ 晳’ìÁè*ê¯È3ÁšäÁχïÉŒ†¡Êì§ûÐf÷x¯ät™üÎÃD¤¬Šfü=žÚDB#I’È«Y(Cù«ÌUܹ8 ´þãÒ­Œ&ÌʨíM(dZ´Ò:D|þúõ«“ÓW§b알bÿç‹-X²m>†ÉôMLŸ ÂÙ¬<9Ð…Képb6ãF¦ ¨¸>šÌ{‰…ì‘ É/XI´»ÇªJªïÛkóƒÇ='éŠC4=_Ãpµ üÕ­ÿš¯D®97Xt®˜>E°_Ñ3'Q†CÿfRý*&ÇÄáê¯35ìSÃþÿ…Sƒ÷.;ˆ„f¶5©X»ÀŠ‚eÆCk°ŽÇ&,½ÁP6Žy$Vq#‰m²ùj)t §Lu#("\¤‚p–ÁpÎLÖ ] õz!GçKäÞ%Œ3B=¦ü„;8„¸²KjÊRÅ ü8ƒ*êÞ ÉýžØjÚÀÈÒ¯Âë†)§ ×²ÑÝ®Q€˜*t<–34"Œzœœ`u+>Ê%˜¡×BÄòñÈ ªÂlYø8ðM&Æë‹ðq']+ÓÉb–S[JgE“º`è!^XL Îr%ƒë§_ ¾aèp\YН4óf„½ñD¶ ¥Aa> ^Šè¾’S &ÝH2çÌŠ£'LrØÃÁ5ïA¬1:6>!ÁhLK0.êT’J _u’¢3žˆ9nNµÆüwŽ'4 r~M{°úr°"ãBV­0žL B%¦x‡çZv¸¸ßÎ]ƒü{Býÿù„J¹ƒïnFåéò1]>0§Ë½âé²™3½™:1¥7JŠfŽ~—K"a²ë¬âÿŠ·qÙý?ž÷_*©Ðål2r½Ì;™çËåâðþýËËËz?©¯fÓz4XÝÿŸi¿¿šE÷™°ïï×1=‡™!í2_Å}œs{•„änBz`+´ø×4ê9;i§±X ’XaaHUQöUrS„”¼£É2Š10 Xé^Œã9u(FÃâíí›÷Ïß~ÿ㛟Ÿã„Éó¯ÁroйkHÉ·:Ïá”_ImÆ(j†Ä GØ$¢[Ý+ò¦’¦ó|À›Y[—ª±þ3Ñ*J»ÔJsÎî´ÓŸ^±ãjûm±Š¢‡¾»K˜OrñiÈ1,ò$Ó^m`îòÔ8]‚æÃeS€‘Ù”<©7Ü$–Ïo&RkXŠœž”9f)ù#ê¢írL¥¢WÐÃ1;¡Kf|ú“ïÿ=‰ÒýT#I KD2C¼œXwjofd}å> Âýå\‰{-§ô :T‰SÇ^☠ҘÊrê?*§rß9ëY ÿNF®œPRs1>šð‰%óX‰ŽÌërC¿Û=õ?íø#0jq{.¡L71ŽúS¹'’®}vòÃns"ÀZp µé àì_>Â\ßa<ã¸8?‚¥Û–z‰äëžmrð¶Ã³ûýGVqµE«Î±º§7ø?ì½Ûv9’(zžùhï=Í”‹¤ER’mUÙ^²,Ûª–%–$»º[ÖÈI’Ò&3Y™I]ÜUÏ=gÍêYgÍË<œþ¡ó)û/N\$òJR–]îîR•%23@ л0Ö~„‹CW7®A‰ùÒÞ­Ãd ²N¸Á0‘‘U­/ÙÉ[n8¸"…C]D¶v$ñž!_.YV3Ñg;Òõ*ë;Xf5›ü%ªB9«ÒnBͶXHÄ^Án‘;R³TÏÇèc¤½;bˆ™¦µm§µ ZÑÏžö§^ÅÒ 6Å6kÖx‘÷vzf žƒ„BíöûxñíW’›Eu]iÕêqdŒ–Yã QiX§ ¶£vkM /ɱ€¨ é8SŒ,Hý×ölc 4ïµÆÍS„»Óš§ Ò§tçìÁ™:c®íÝÞëÃÙšŠqý›ØWgÉü$Cïèç{ØDQG‹#¹h‰Ûã@Ø>]õtœÒqd%ýšgóq‚ÍÂr`ÆyørkoëO4ÏøÙÖ•ÇN:1¢ý¿©^ë¶ïãiËé¿:!hI¼‚þL–N[´W×;ÝõÎ °xP+Su˜ÐÓå«@É:EuU6Ⱦš½!)©G o¥c÷çÚ^sÛ7ؽ+Õ" ›ÖGGn»höüIÞ¹TQ³uì.κÄÔîÑôôÔx’Y7ÄÿÒ(ÙüU¸e+ÑÚð]âM¾°Ò3÷|$Z˜ÜNÕî(iÊLËs‹G·s;ìÁ÷©úg7— êX!4= 1ʱ—¢ ’9“õo‘k3K4ßÃxÂÕ{)#+?ثڲpe\U×í\øþß]w¬¯ÿ}í|pæ“aÀq>¸)YOUX_T#ë„sj±¢üYúàò·`šŸÓ¶Èp*“‡Øk¸ K!!º èUÐô»¬Òq‚âóˆ-}4•\õ8hhÍ^‹4¸õ&/Kš{V¤}úÁ• •h4é Ÿ`ŸKUZ1i“ ï"KqèÆ®yô>Jž·VˆbDG­“ E”¯³—xË-X‰f3»8ŠcQ¶ ×ÛVK±åªÍ9õšÃ3Í䬿½É q<å²³Ô4e^SUíœ1¥Sy_d•IÊ@¶n ívM3D•]ŒÎ‡šK·‹,¢½çsE¨ÿMèØÌ^È:”IzgžïŽ”yo"D¸P¶aEÁ)’£–z»#ý30ÈxO‰v7yy!G©w+úÕ+÷ÊOÇ#†çÔ/%žIJ†àå ¿7³mÊ~o ¶x/µðR[3ˆŽk']*7°‹Kuu©ÂÁ^\f­°¦”n¥‚h-©"R¯ÅϪ)EÌÁ´ í%üPØA—`†Ò¿vÖôûv[acT†Ü¶Žî0º\+¢iˆj£êàUab„¨¨‡ ·ñ á š¢ \»€í¶ùÐÔ˜]EЕ´³,þ÷óÖÚ¶ª½Ùðf™®ƒ%±¨äýI².,éEyY¸êž/éÚG}ûƒZ”g­N§þ€cI1— ;˜çDx§®˜øf"µg¤.ú*HKY¤%.´Ó » =TX[Í®.wäİíW×6?ìŒÊù¬‚Ö¬Þ/+e’Dþ^¹^hÆ·2_ô³›ýŠ/ªéd}Q˜0°ÿ™ ‚)ZÝ—êgî“Á,&‘A©Ä‹Ø-¯‘taí ïh/$ðƒ* ×ME˜óºhÔ&9Cj|P! _+Ù¾N8QP.ˆ9<´nð¤ä:Ìœ†aîq–»¹²¡{¼gggÙÊÇw[Gæ ïæî‹ƒIsDs`H‡™#ë¡E¸a>bfº±^Ë#G›ÌQv&Âh úUõäYÓ´yjÓDkIèw²¬ƒÀ·ãôÆÝÛbÂÉÆ©A-.FSœñà<©$Jî‹@ˆ•t[p¯æz¢FÕ%I(ôr [¨Ö“®òa(o°öþ`D^M•bióìS[ƒ «Øã[f°ô0àø MˆCÉ©‘ðz 1Coˆ»ƒª2}¶œÖ 9?ôHºtB{ ÄÔ7!ã-ŒišªHÐdk8¤î×ÀöA3 ‘tÒàÝ»¿¸ 1hÿbbé{¾×ÜT‹:ÀQ@òWDÓÁ@Ê!-·Ýä8‡[c¦Rº(Â*iŽ ‡*mUò&l³t\7µÆÊé8¶eóZnОׇ4H»BrõèÛûˆš»ßµ¨õ'ÔpÏï7Àšé"c‹b·ç(—ŠùN9Š ]âˆ3³xz-Tn¹F«ËË QÞæñš A`Ô>ÈPðÑ œð:Kîß`?Ä}0×I©³Î¼3*w'Í’º…V——Ò}ÛÁνûà8µ‡qhF NsnÆšL¶kùœ' !üPpÊØiNØdmÒFrb0eY©¨T}V†hå µ´ÊšÁÓGDqÎÃ'‡O´å^#e©>ó—Y‘qŒ¬n:ò¦6öÍ^±Ž&§» 9tÎvÒãÄZã@¸†ðá P>g0²½õlRŸÉ¢—“ÐÊP”@+9fƒ™j% 5ë[nÒlͲÅlsü‚Ü†× º|4W“¾=HYe}e“i/zzïØ†LÉ ­Pô’Ò¾¨ÇnðÖ‹6ŽH_õ[ŒGoÝèKʧ†ÿ#ÑV”Ž[;i+䨏-H?t Áù†æõd‚øµ®*ϽÅéSÅP©ŸŸÅ²Æ×oŒC“~– b9yІí‚Úo…™лrøÅx©.».ñ˜*Ñk¿m"3寲Úï×ßÛê;1ÛigÞ·ÍûïÔi”‰ÄæïÞá¾Ñ+3>1ÙQôïÞ½I ЏjЫ¿Ô±¡õuN»XÇvá—>~Á{6×qÎqŽ~âb$ð/fá¡Ï®´`U;ÿÚ&S‡xéTFÒ÷Ö–sn˜æöÛ³S=•ÂkTІí×#:hÑ”‹ å/i•Z"U¼ šÈ ~+’†eókÙH/ƒo²Ù—ˆ¬ÐÚ¦>z` ­zP(µ/aÒnè[?eŽGUܱñ)+à3gtÀ£ŒÐûÞ‘˜9Ë3wN›yëÝ»eDñö·ê»§R$ð%S;OÈQSs}öœ$âúŒ}NsY÷h½×Œõî×SÆ;ÙœfQ¯=¦4Ù£"ýÓ*oG:¦;™.{ÖtøSêÍUåOx¶³!&›?Í5KZYµò¨²à@môà3ÂÏskM™Krêû¼œ:L%«P}¶ý1“€rOÞ¼FT¼ ×¥Ñã‚”•.å‘¥Ì"œúyâj%ÔiçiÃ:Ç•œ"a’´!áù^t‹ ¾UûCa—¾Ä!Ï6 h÷O-k2 ue‚SÁ&‡‚—ÏÓŠëùɹ`e<‰ý›udJšZÕªêÃc„¦$~P³Ðn*ÅÙØ k>ÐëÿGùŇUç‡AaO;Œ»¡-Ù…T€€3?úô&éÁ“ƒÎœú4Zš³Ñ§¨ÉÃ'«éB"”¢À¸ `™3 5“A‡ä-½IPÖ\­’2­nˆÉSEÀ¾Í«°o'&ÏÀBjò5•®;r)´Úsæ^[³Sh-߇"3ShÑQ˜Ñ¯–C8pñÚ÷b•SL_ç@[BÓõ¬Z—Ï .‚´†}záÄlj©uµî‘$O–ቧJ «]1Þ,";Ÿ: oßPžzêÌ O¥X)ßÚê š¾ßWô± J|…£éàœŽÍ\«-¦i_¥= u<1IªA~;å´T5Ú‡„ȲÃ/ÜvøÐnæv£z½~ *á“D¿>¦³*@a<8?¡¯-NXíЉ›‚a;þÀ¤`ñA[g8þ¨åDÖ9 ŽÍm†v! £ªÝ âíd“Œ.åš’T•4(* Q‘wDô!8µœ:?‘Ï‹SD4zÀiûÅôfâ+ŠèÒ î7ò›3ʼ9]ÃË20Ð?Ö¹’p®_@…ÔÏm‡»;C÷\]¥V¿žŽë¿…y‰Z[bé~Pón¬ÕsªX€éFu¥!¨pƒq¨ãf%­ê”·jÑ^£ÚŠ:+׈Î îÜ Áµ§SÊaI†^$ã[Ÿz÷e’7âº>ñYd¿&Q˜ù€<¾Ñ;RuίBqü~²¹,ã˜ò˜‘JBUËZ(S !\ÊÉÕS·R¬(@X—H` øv“ˆåü¨-¬k&ßo©îÚ¡Ù³Ò›¬ŠÓa²3Ê´xÓù |Ó_e*z6àçg°©è‹qw7 ƒš½4aŽåÎE>'·¤¸¡ˆì >–)˜®“Âp+ÚS¿Ìk‹îš¥{˪tefK?ŸF]Y Ý+7ѨÅÊ«SÙäBV¨û?“›kQ†þ8 ôÀà%‰ÙØÊΨb{åãÔ¹Â{âm¶)~;‹€o"xÑ‹gÎSý¼±lÈ à×iÑzÊ‚yšÀ¤V€ÈæhžeФ–ÆuÂùb_Lý>­7©i" ÔX/nÆìFÌlÂì¤Éß3?¨ìÎÈlo u*ÆÇÐ}YAøuàëêÿ¯£EÞ9ÐY(¿öêªò ¬ÜoϺhˆýË¿†c€Ói‹ß‹wܺbÓ ¦£ié½B*§#-ßùt¼Þ%×ùѤF ‹æçèKmîQ”0×å<ßÛYâ|¼5u3¥iɤâ5ù¯¶ép¹â±”æØJ¾F´Vö3fŒ¨MmÅ 4yòuŠ¢ÓR£¥r0ªq?ï\xò2yRà®0’4áÄ Q­À0*L75©8=Nuéâ2Tec6ñT–ÆRùO‚ÉÜY`L|TòûÅPá{å Ô/~O/†^”}ñ3½ ›0)lMµ}–Ü©ß|×L2é‹HOðþé¹)7×—&?|úÇós¯~÷(á‡x-Â#ŠÂÂÃÒÐꇳè-Þ8,Ã(yG'áèË’°Þ²Á—ß*‘g:»åôÏCÔj¯}Ì#;dù¯}cÿ¨“í5s—ša]ËMÎÝ&GÞc%sû⬤|yše:P×øâ…×¹˜"ÙN¸ªL $Û-"øDpÞ¨ÁçÔ?Ö—rø:6¾€ïcš²GD㛥Q¼ýc þÿ£óÇ%§ T[W»šÞ0Wá­&ް:ohUG]âOǘó$£R>Ê0È ~¬¹²0£û)ñ<ÍaÅH2X†¶Á1 2¾+¸À®ó¼‘9’“Dø•`ðWœ/0FYPÀ8sÜEfû‹1äÛâeë¥ó‹,9o§­+ú=M!¸@±.Lé`#Ôm…‚rAb]–7&.:3^´‹Qv%tpÊn)Êâ§Èuª~uŠkëRm(&õ­,XߘÂñ.:õ­`}(iÕ­–V‡]ܨ|w±²T\áj!;i W¡x¿ãÂD¬"HÁ4ØG½ä{|÷uFa':YÁ(•u.úÃA¿!Г-VÒ*lë@¡¢‚ÅÌÔ¡åä ÔR®Î[tqbe)ŠE²WdÞ¿½jq쪄‡=rPø0 †å(R0ÛY` þq¦¹MÂ]ˆÊz•)ätêÉa(ƒ+G÷óœè~žÝã9Ñ5χï»9ñ}W€°Æ·AÔ#Úoa™¢­ =šJ"eìc“µ¬¸éùß݉IÀ˜´'œú'tËte1Ë”B‰dZ¯Ö5Û6Á«A)‘AôX™dïÞõeìnÓ íºµ5•]Q`Y}|Ãú¬Émñtˤ>úò~/žQmþ q7s$·îÍÍFt»Í®O1ëÚŽëŸMau·%,ÿÁU;3{`F=&E30Kl¢«Nº(AcÑpš¯:%ú%KHÁyXÇ꬯yÓ#[Äy{ݲDÌIvAEæô…}ø"uÔbþ2ÙmE#)'°¹‚aB¹È¢ .©ÿm¿…q÷N¿± I±[¢ÃIrýxÞ®ƒòTVkÜE%$èN›Ÿ<'CßHr´–ëõã¢(¼>Óe¦Õ¹é"ÙI—¬ÝŸ,›}âQ ¹ÿÆÂ}éZ• 1OI÷õI:--XK³æ†¬^Hp2Š>éþ¼°êfÿ¬ºƒµÐ7Ëëãb~,ÔîÛ«rfÊ7¢¾U€gàgpÒY€f‚køy XÉžq;2!ÍÉ¿™Mf ú“ý^Ã$Prr@)¨ÙÜ))ø©è“ní•@s1¦°à§¢ï©Ãé?è®ÚçìhŒâÐ dže`GA;xeufµž™§sÏwUUâ&h z2¾©¼4{\ꋹö3kWª¬jC[Hèmgû¸Â½ûv6÷ ;ʲ ±åŶi”T#}{ÝŠ$L„èÜ14 ëaUi!>¥tªnǵZbcÀ7sÓpc,YZ`z5#7iI':›“¦OÖ¡­q›¤} 2,‘¢M¹1F‡õQ£èaÁÆÇÊ"?$À?TZzH}«h´Í¸B•Z¢6¤F\ Ä<ÊòJ'„ÈiFó¦’õ½ÖÛ,‡°­É­Ó‹âç¾~HÍÕo{bᆚâ䃖mô6Êì…ådÁ•)¼TX\%DýÝÏké2º²¸ æÙ¹%¡p޲ÂâÕ”•‹öõu¡TéÎ ñ»Gâ©6‡ Z ^W½{õèšv ®36 X=\ß’z`ŠÃëõ%‚žٸÄpJy5µªØ¢ÏM'5fí”ÔÕíëÞh$ÏÜQr—žœ§+œuÜfêRw9éà4¼F‘&› z"þ}勜”²Gß ÿ§)<á,‡„©€nÅ ¹º!ñWYÊq7·ó éÛIê$øÏjí]ºQRP¥†¾²ZðïéEòhòSi ¼ký4 by28‡wwäÜ©ßiþõö-ü¦ Ó¥ãœ|ÿ¤Ùq'±%ëhô¼­Gº° Pú¼t§p¥°¥Æç<ÔÑÕôUÄ%$)’U¥¢‰;Vc·à0Óe3+04“zR•O?ü ,MáÏsµV;Pþ ãvÖO…ÿ!g?g=f‹¾G²Öÿ4<)› Á”j§ÌoœÝÖ<­KÎç õ6°КǧaåÂT~¾¬Á«b@>¹ÝN!¡ŸŽ·(Ž Üj-¨©*>À*l(íB3j.®ËÜ—rî …m…Ö í”}–ˆ­èŒùŒã9²‹™;f-4njÊ—÷•©:gºçGHëCî;o]°r Eaþ’}m_NF0Õrn㌠îZ]p•~eGù^§_][¯>¦_}¬/UÌÚ'Ó ™>窥˜†pµ%a´|•K ÈG6Z‡ñêbîBî¼9ð/îì›…œì·ÛšÅƒÓ ©ºUÁõlêUA·ri¿X5¹v×Rª¡fWr•jK…f5DØ>¶Wó©¡O£+iü-“U4¥^‹ ÷|¾ØíT‘4J¹I©jÉâi¼¸‚•mH Íö'Î…?Õ€bôYјûly} "¡QT\7®n&¬ PT-©7%¨¨f“séÞ¤È #u.ÔvϦ0Ô©Óqú\}KV t¸ñDåoðex:ýc¾/ê²]/co]vr+ë%+ @';|£Š#Ûj"ƒÔ(ÛV[e§@ûBÕr¹­uDËm³|c ÷ÄsŒ÷Ϙ,íÛƒch‰Œ;GÙXé†Hžg鸢—{kò«E1cÕÄxŸ]&7MU‘o%C­¬ÔL's·kvC@¬M[æÁXÒù«p>Çæ”°SO»²ùD'[*%5 ÉÈ"õôõ"ÅsÝzóÂÎ òa¾¥© ‡÷=RŽêFÝ2‹²O©…µ,Þ…ŸÂÁª~½Qûkï|6ÄÇ•ûödòæž»hv2õzÜš–Û]«`§ñqo8>,>\<çv çŽ ó„Xkáá嬒‰‹–¬ˆpO* ©-x–%š»H3_C\^“4_ÙÊ“ùPXIïdãIöp›]ˆÅ,ÏÚ…é Ò1 ©Z¨¢ªšòNè¹ú Ì¯9w «¸óÑPáeJ‘´ÄêO<‹ש²O²¾§ǧ«{ò[Ÿ”Ch<ÜÍiÞL%\¤œê½Nþªt•,ôóg`«;) œ­aNð2)¬äIÏ»ÉÖA{E187áU/ïkº=6õnI9ë¥GG%dCü…ò×ãú/)meJ¹)þs¸JÑuˆeœuçä–Ûró)fŲuZ)ÍYtd.¼’áxæûBbã<±¼Å=T<‹QÌiÅ+8uþ\ý–Ñ}·JÌË:†.âÝìæ•@Ì[µ_¨ßÞ–$7qgÉs¹<•/FWo6]fžù´9ŠijO/eúkJ]½È`/*;ü–Kz…´©ÖË<_!Ëf(´žÊÕv³÷”v,ánµ\VÈÌ—¦­©y3á@Š„üÐ%ŒjëÝDŒòž°}Õ[ÙÈ,œTM+²q3¢›–ýY–cÈ q‰=’m«*§-çÔÅ´ÉxÂì‘8-ihl?,ëÜìëŒ¤Ë  ÂæÊÇ…%²óŒ[üLwŽ;KeRž5‘úé·™q\h:aV%÷²“'Ù6ZÀ-‹¨Tž³­íÞÁöþ¥ÕjýRŽ/®êÆMÊŸN(mYBê‘ÛèÏMoJBÌS™¶\úhsˆ‘ׯ`KºS ï×"INj‹<¼ ó/ê%vÃHŸU/^´|–¯|>;‘±EäÍÑ*Kj ;{ÜÉùŠ Ý8Üöy™œ+³ÖùZó#"¿¶A4ÓY1îûŸs*B÷ƒF #*sRÜNë]]êXå â\}©”ý)XåmàùcÉhoólFY2é¬rÊ•àŽèrŒïãE`u£ËŒ–µ¢XǦnï>«n!ÖjÁ;POwmìþ©p¡œnnœ)¬K?ÝÃìžú;'[''ò§“§lß°È%™œ[»‡Û‡:9üSo+Q=ðüpÿõáËôcÔtЂôC‰À›&j¾B‚h‚µÅXð: léÁ,°›ÂåæPYÛànö­«ßÚÍ}$ ҆ŔÞ+ÿ\&ÏÓ™mݲ„É-+áTA%dAå®õ"KV×’ábB w$C]mŒé¼iÿ,©µ¥ª.ÉT}æ°y[DNRV–Õ¥¥U¼¸¼…@Ö?ƒ·¸q4þ_`1œ†xµfø[žI4á§¹_ä$Ç KÊXèik—%Ù çßö1.'bEb^z2tÃÁùuâ/21þ˜TלG]\º˜M{Jw\Rê<D}üˆ³ ½SJš“®Tý¦ EÞs^”oxÁA¤¶šH=W‹4V:¡€!ïëfž«ë›ƒõiÌCFwÐkÓ UT4hÀ6e±ó BÔÕ LÖ«ZÛ£njöLs›·ÓÞÞLƒÍº©a>Íj|³¨õñ­OVn­ŠujœKQˆ—_bf5œ^Üœ­V!ü / –Ï ÷1ҹ藳¡Òîç÷iw¼{“ÅHÿ&…Xr3ˉ¯D—”2t?sì´….ñ<¤Ö"nÚ°×Òõ \´¥ôka(í0ó´”N­œ™{,fÎ<(–%t‹®N¢&Ó˜ eb%|”Wx[²>Ê–½¤÷ßK q>3%ý® `ö™¹,¯}]èÛÒ¹[ªWѰ4‹_€¥Š«¨È{*>½S,‰B¦Åª‰©²~fø¹†k©-@ÒÏâKt ®*űrÒTz‘ÏÌ-UË|ÜàÏ+XPAm*ŸŸ+Ú\üh|É¡jæ ‡"·ÃíT";µ€²äâŽät(4 L·u»Äœ´‹Û“¯bVêjÒZk6Y­/DWk^ÂÞ ñÅ;:“[Ú¯Í&õs÷ê[«C+é0™>#%âÚêÃJj>·P½µå©z¢¹‘ÍP È~HùRÊ:˜1ßbý3Ì–´Í2{äv‘I*ÊšRó÷¥ÔtòšoÀ¾‚nm>®œ Sõe5hú{)ÍXØIá“?^ƒ§¬ÖœU=ãSènHÿÂæIA¥yò磬ã_ƒjSmÛg® è/KvªÒÅ™­3~á6•Éô”6¾8k…ôÎEê¯@ëˆeyCjëK·’.)OINBæk†¸©4ßíéZo 3ŒµÿëÐݺùx„eÇ—šlް ó.—t±/,)o‹…d:Åõ§TW9¿gŠ~aA~[&ÃóL|Ÿ`Ú-ºà(ªtAŠ’«ižI4&Âû✪tq›ÈZèü:ZÙT\lûÏM?®n¯3V^¥uO.¹K¤»3/‘î¶×î/·W×èiU´½²²Üžq‰ôXÆ¡7ˆ¾È5ÒµW\Y’=)¹ZÑ‘\ = ƒ o(a*¤PÆtAò]‰‰ øÖ³±t£)ðûníòÜœëKß)Q#ÞæŠK@`—ÞÝ鉨>¨ÝÙtH™¢Àº¦¹à‚¶˜y‡n8Ûûâ`@íEPµ%Q¤ˆH—ˆœ+´Ø%B¬›…Q Øxásm"CÁK”1ºÁ=;“aÔƒó©ÿ>Éx`m2‡R]ÿ&‰ú³­C±»+Þ<øéûïñ ü¿½+øM½MF^l‡-`½ð£ CIU˜KVvƒ)׎!¢Ab¹õ #µ+ôœ{?q›êî¤ kdF$ãh½ ‰'ÅÞá™kó,Ý "!„#åÀ£«Sئƒ¶[ËVµ<Ë[ _ÖF’°Õ²™(2?+50X¹ƒ—€ã##ï<hq‰O†nì6æXñß‘ãÑ5ÐCP4¡×Ÿ¢€Ù,4Ò®¥üy(zF¾ÚÙ‚±ÝÇ/ ·¦Y±^:ºˆsÇ ߟº£èÃé{øÞ±“~aÑÎÌ¢Ñû÷#(ÙEGÁÙIÒTç¨îÖ¢>¬7ÄSÔPèU¬G³Óu¹Ú~påáZ÷áJ§SËÞVLÃIk¡ü°=ˆ‰×rèÅÄBwv$°7:¥ç/Y¼D¨ åÜ ]ÏÇܨѹçK•´[«íái‡™r OÔV’zÔntÝÆŠ•Ž êèwð¼±j½é{¾^'õGÀ”¨“—Þ÷08Aû.·V¦˜‚c7òJKéFbá+/ÈÑÈõe0Ä+ÕÚ2èÚ¾ë€ù& 刕áe~Pñx¹õУÈ1Ôà¸Ü™L°{>ÈëÕC…£Ç–¤£ó`:Šsƒ áe 'ä"Ž,l©Íh"]Ô²'ƒ„ç/uY_‡×ãúzþ¸õõî/xêG=vùqŒ-–üèùÃàÃVPñãÄ<¼}Úƒo0ÒeŒÁ¡}‰ îl¬/ñŒô˜&jÙsäÈÖY‹m ï#C©0DK­ÚA JÐVêfP˜§±)"jB÷G´ƒø©%’Á;)›"ùt'-ŽwÒoÍ_ ¬«ÀT hviøÅâÿºŠ¥0³Ri¯èX?ŸNÉF{åk® ô@á¤zxërøû(yet92»ÝéÂ77peÿ%YŠi~Uô‚÷ñ„0y§bòlL²¢¦܆â™WˆŸÑ®mÑ]q(N@$b(`Ögâ\xÂ0Œ´I|?³.m~SÓhÑÁ˜ÎÔÌ¡D;·&'³ù¤Í™ %éÿXÙJ t(ªÔ(…sˆP)`T½¨xOnUÙ–ÿשIð;G¹”¢©T/Ù2<`Ú44ª'¤Œ%ü–W—‰P¶%‚<£.k’J•ªª«`"˜YªB9•–ªT4%¥f(ÂR©I·°T™²íT*Ûl©Ü¤;G©Ò‰­¢TÅÄVZjÆ$UXjæ$UPªÐ ¨(Õúʕ휪m8õñ6•Gíåj=ZMyµn¬Ö‚Õú®Z³Uë°jmU­—ª5Pµ®©Ö*ópòó÷vCPJßèÄóé’™ø‘•NQIAöGërzÍ“êŠXkâ¾x Še#$ÕRÒ®Ö^¥r¢U‰ AU’ÔV‰ 4¥JV UÂL¨’àP•¸´—gÈË' ÌÊàüA^¯ë>„:וMÆŒ_W,Ìçuup—Ùº.¼äÛ꺒okë Ÿ¾¿®03KÖ…‘÷p]šoíåuqž“åÜÞËʼ{/Ú{Ñ€kð`ÖÞ‹ >ïÆËZ~®ŸÚkß‹©O#ÚbAÆ8åòÃQoN|F­Û§ íËË´/{(å¿§!xo9M¤%»;¹˜V)ãžèÈ‹ÈA8õNâ¹´Xâž‹å&#=úiðB¬v§!Öî7üßyИuu þuáQÔÛÃ6|Á—ËøïX]=-ÝÁ91‰·<âó`(šüT} e< }ö3ùÓq_†HG“#Ok¡üiê…| –Ú”ì kE@}“Ó g(£èÝæ„]Ceçä¸"ôRkØæðÏ“èYLœyÄ ÝjÍcǰÂrVË‘¤nw¬2üzm­!=;!/™OÌLd"1ó>óy‰ ¾OŒ\œ´þ´™?rtu—]ºÆ2<û Éùƒ¤Y²ÚÝÛ$ëÖ5CF7ý„CLþãg0r£²MˆO¨Ð0C)•†_0N¼ÇôÏ÷v§»²Z˜ÉU“FpiH6€É£~<áÏjó›(ž'²îŽïű#6ĶØ{`-nÀ¿mqÿÞÀ÷gb žo©ñ¥x ÿ=…ï½%zð{ ÷Jìâ3‚Ý€²ýgx²ûð­ÿm@m;â<Ûø}‚Ý$øîÏð´oãÀ¼€oßÓ[κ² Oþ$þÿ!DÉ ñ>?ƒ'[€{Jñf¤ ¿=Ê¡.x‡â÷©ö ¨Z®èýàØ¡V!ÞWê/> e{D7'¾yA˜Þ}‡ðï9ñã@÷ žg€aþî*¼XëѱÏàÛ>ÑðŠž½€o¯±¼¢k{P›Ä¬9mþ ‘bh!Á¾‚ï›Äóm¢cƒ>s‰×ômë&Ø=xÂ}{@½Á=¶/¡¾õ9p•`·óKx÷ þ{FÄö¶ù%”}åÞ,ÊÍ&ÉÎ+âØÑü”Ú„}ñŒðl+ö©õ»Dÿ÷šzïpì;ÈA%gØ[ º™WTvƒäh 8¼|­Ö7¤–mP+¡Ôq13õÛ@Ñ÷Š¿HÓIÉ÷ðü9qaŸämJîÒ(ø‘`ÿTlTmï÷fŸú…åî‚}EïŸá©î÷„[€Ró þD|ÜS°ÙñæíöîáÖþæËÝ[<ÀÈ=ÜÚØßÙ:8Ø…¬ï×ÅëÝí7[û;Û¯è¼Ï¶À”Û>Üþ³*»ªð½ÂÔF{»<µ¾ýíÍ—{›:8ÜÞ¤g`oýñpãÕëýî{0„ßl½Ø:ÜÛØùÔRSæpocçÕÖÎÎÆæÖÆVM[Å[‡/·›&¦ -émîï½Øßè½TÔ´¡)o¶¾Ã½ýí5G¬‹½­½Wû/6ö=mhÌûÛÀš­ÞþÞÞs~­éÄ·_=Õ(×ðÙÖÁö³mCxZs° µ¼¾j"¡5[ȉ½ƒÍ=•²© ­ÙèA{v=ÌkhËáëý7[z¶¿Ç]Ô–¾ÜÞ}ñro|šñtû`kã1u  /ö·¶v¡7 4àéþëíg{Ì¿·³·»½ÁÝÐÊ76÷v^¾Ö€ìÞÞ>ô6?x€=¿û êæï¸ÙÚ”jÎ_¯¶€wüµ2±³ýŠÉî‘{‡ûŒ« ö¶ƒº(*¯U“»@ØÞîŸþÈ_€¨?l¾ä/@Ð÷¯w·ò+œÕyW8¼°Q€ÎýάŽøãÀâÀ÷>óJ§$)Ñ®E€Ø—nJ³UÄÉÜÍ¿jA¼ÖmÙͬ*ñ ·Ñ= –¿P¸Ô´©mŽ ‡{Œ]ÏQ4SLcy ŠÊ |X)H?öbŒ§€’´Ä`$e •H&ù;ð­kpGzí,pG- lûÔ.Kx×HÜÙ¸C+–;Oï4ŠÉ$ˆòŽ ÅÇ@Û;SWëSŒÃˆ.¦8‘;=gÏ&õœ§KŒÏõSH(â5Á , 5 ÃïC˾‹7sÝcÒ—<4Á~‡®tGÙlÚ™»½ü „xg´ 7È€1®"HŒ…3ö øðp3Ò¡3Àˆ¨vcÒ9ζèSòk0DIb¹`±`¤Lq%Ƭ '-«w% Ë–¿“jŸÅóŒq¦Àlrò´–v@þŽ˜Oå:!ú §!vÛ5“‡¬ÓìJ?$Öè®U6 õ†À ž/( 3Vì?áh8—˯×IÐé‡ Q·`wLà#¾L–ïæ‘‹b |X3'&xÐ\?J<á…‰µ½è^ãñôDõ¯Åz)qKóž°zã§jõoû”ºãE·^÷«5ºI‚á\=ÚXB ÇÛ ?TW N¡èÌð[[¡ ðìÃà\!í{á-"]iMƒ©“”(¬+Gh«þšMÀ0ðë±!Ãõ¯[tx 8ŒÊÓ-n²[BÕTËTÕ«ôF̽ÈËæ,ºÝ¥Àeb²‘˜Ï´Ø.] ”ŒFäùG}3êÇ6‹ÚÇv^O(2Ðá˜Å[#iˆïùžËÒAh„iˆWÙ%Cvùl·$‡v% Œxè.ƒ5ÛÉijs4Ìg¯ÿš}žjÀ÷æ2KÃ×WÆ€Ï3P=¶Y•HGW|\i9ˆçcoÊåsþðÅ4e?®h|d 3}7o_)£ö†"U©Ä*„‹@/¥Œ«%K#>ÈÌô¡‚×âw#¬)ª6‹ëë—¤J.„ÍO–›sZÈÈV°¡Hœî· ðÇûÖójÖùø@IØÇIq'SèAÁš¨PŠÊìƒWÚÓ|&e£[W.Î1åö„ôâÆÇ#in4=¼f:‰²[±¬¸-åxËjìÞ†ñ°ØÛ7dK%4ߟãá<ý sêß­m¬<ŒÆ<{C ì´±‘V<:®Õ†òÔóÙø‹Ý«ÀÆ×9Ж;™H``^GÉ‘œœ»~ŒÃGý€T»¾7Ö»#K `Óö˜õó Ø†ÈÙ¶D.‚ËÒŸŒëtt }xb£L ˆû…:=m•«£½n_ÝüŽÏ¨Ú1¼0Y57úmæ T©ºÅÿbäŠñÚ¸€Ej²ú’jêèš *BØhÑ ,þQݪ¦(`™mL-šÈwzm8jSŸ$¶x/ó–ˆ8á–÷'‰aìXÍò›,GļO’>Bݵˆ%´ À(ˆEøb´ÂÖ¼CƒÆê³y¡5ñ{¦@Mݼ‚ëÂ)‡yË^ Ré-J&ˆÝÀ— S±5ýðûB¿E¹º/°HÏV‰­WS*в–¯ÅZf ›Ù¦O•°XšÊ†µtK±jIkÚ uC ræ8/a‰¢ ¼ÁªÞ™íUUœ%Ùj—€ä–>'R™Zs*¶èU§Á€~f®ÃG‹Ž–ƒÄì$ýŸíµŠ6"»Œ“¨À/–ýØM¡\Q 6´LyfC¬b¥çãjªá«³†0]çãšî,›Yð4eÿ:–ý«LÝûPéR™-z?EìƒV3 ðÀzýЪö<Ô[KÚ(ߢÖY_öô—güÅŒÍytFK$WŸ’¢Ž³”ÆÒRyÆ'U†u=ûBuQÃöfW–‹([›Q¶ºÒévÛY]UOÛí•ÕeDÙ %«Q"¸T¼X­èÌfóÌ%S’âEèos^¶‹U„cÏwG á'_`ÙmNÜâÎÉàô¬¡‰BšI zŸ’:Y8ú®}'·O™›Rïßà{A+º÷½Ñˆå3øË*H;=(Ôh-·ÌÛƒ†Øí5Äø×ëIvËzê­Eþ.<ƒG ñì0WÆz‡×Ú¶À°Í¦€â¦èÚ?1ì¤áæèÙ!s¼”ÁØGŠEÙW¡yåp5ÙÂȘ*z2î#]„H,âæ™ÍG‰œ8wîpBÌ~x€:t·˜GÀuxÖƒ§æÑ.=ªƒà×Å.(ñ]úÙ<è%oò –øþôø|³`v ÙÀë™GÃà,û(œZÞpAÜBfŸF6:&×ó³Oë pa©ph‡CáìËÁÔÀ…Ïd4Àí`¾\ú¤s%µÚf(ݘ/ §>˜†¡©h¨*¢~ ™©ý >ÇœR×~ì‚@`R:X¯ŒûÞÙ4˜F5D3õÍwA9 08µèÐ áպŴO5[gJ…hðÃ8J¢,ÁÕÕâXªóžtÌýCµé}–O ¨ uÊ ‰€·æQLù”i£i[~4ñ f¨YZ7}?j™ÀƒïÇ!wv±f0+]ç"aÉ~¤Kwîª;•UÓ‘#@?®$C80¢œp&õ 8´”„R|Z³ê¨Õj`PÒÑICnõ}ƒÍSPºÍQ0øÐü ¯ñà-£w8lÆA“ Ôó0¶ËÌÍ¿¬Í* ÜJ–’k.ŠF{¿Î½S uå§ô‚1úÑ߯ð†Y6V{¹SÓ&ªeŪ¤™ßjœðát0ÉàÓ§˜·v–Ú F1Ÿ2#Â)&÷úëSQø¥”mÒu=% <—Ód“ÏÆtØÓÖ^Rø| à#‡Æ‡7‘kÉm™Ay˜ž1¡Fžî…ë(}|3ó)_£[;3¶ªtC̹ //ÃÝ> ìMËÄ –i½bûÓ?¨±¶ÈP£<»Óˆ%~(dz”\6LÈ9+_ áè`”mœžbŠHJ:‹9½q*¢oøZí¹B±K©ó†î€ó9òX=G@Ýd˦5c¥E™-ZD¦&¤Ä”'ÄŒª—“^4³|Ôn¤˜–ÿÁºÕ·Eä^Bý8<xFœêеyÜŠ$î œ¾Õ·1¡*<¬S2Ë[Çô2{„ Ÿ¿rv½.žqŒ›ÓI!äÏ-® €¤Bü ªVŸphÂ'N(ñó‘éÈcÓÀVѧŸÅÑòzûXÝVMô3”l¯w޹±Ù²3°@ÙÎz÷9T^²”쮯[6ìÏ«[˜iëò±²²ï&m^˜[ÊNß®«£H‹Óp ˜†dIpóv$V¿Õ’ØtK˜Àp×б0¦ƒph:¨Ù8Úˆã ûªWŠäiNYÔX‡î›ÅiQ|%l»7n‘…¥¼9…ÈLsx‘‡dÜ- d^¦XX>µ-E’ÖœIÆ2’a$ äèXŒ!Å’6£U¤sÇ3+A«R>•jK¡ù–ÌI…»ˆj7˜úóPò8ƒ¦«Ñ ’»3ÐÒEÚÑ@QÅ”D¯.¬“5ÂQÅ™BÙ¡&­ØM"|7ep‚½q'Y ¶ºéæ´±9ÖèÛ-üj|5S ÔôÂ8 ÔôQñaÝÜÏ1 ╹qÑÏã G,:vC!‡g|F€¬ºuÑíÚk‚m½@6‚nç•€ƒBââg½ Àb©j‡hâ3“šŸÒéb^¸¡Ñ©ì0ŠÉ­%‹¾tš &‘ ‹¾Œ/¥:U%¥¡¶`Ò—þ,“µ=Ûd]^Äd¥;eåøœ%&«·ì,û:Åä@³Æ‰• "."Ü `e“g(!¸ (~Ü>Ü|)÷àÿžx¶÷ãnjÁ«–ƒ­ª?eNµyP”»ÕæAbkĹ¢â•XŒw^Q¼ _Y_Í»õæAeW×׊{s ™Ã­¶–‚uß'`I/ñ?QÖ_¹ˆ(æ<– Ç<äÌö.‚¥Ü[8'–2Ô§`)ðôÝÏÍ‘T¸@ÑUNÐyúkïãBôäüiGÁ,$3¼‡7h’%{iÿtïðpï•xÝ+R_³jH¹Œf*àblÚ!×ÓhHg¹¿¦PNl\³ó㢜Jý¦óà"í¾Zíh\d~Sꌘœ‘‡Blù6Ñ<ÇlVLÎ ù"3¦f³Æuã¹³Ô·¼È$Zå‰gðÑxé–8/ Ý|¥x–ÏÌÑÛ¬Äj fŽEFh¹cø&ã<ï^dLUîÌ£ßôx(÷/‚¥|jžgli ®Ü“¨B¤E‰­ÍØ ¨Â`!ê"¢Ô0Ïyá«ÝÖS1ìi,UK†O«*‰XH´f hõìQnfÝK™uSÙ÷Ê?_§Çø6jŠQlq -®£ÅáŠ-ŽUÌ„*þæW-Çò›_µËo~ÕJ~ó«æ‘üæW-DòYýªŸÅ§rk+è[rå~ª«ò“]q…äÜŽçFþé¯×5XÉ¢›¸ƒ‹Ù´¨÷e>,Ÿì ZÌw+žé[sfZ¶ÀNBeãæq•W!h&x¾¤³r„sø…ª±è™á£ú4Çé×èÏù5üe_·§ëÑ_‘óOìlì¿Ø¢ÔÁò*nžb*uV ôé ßb,ñ·„ñù%âÎáÇ8 ߆ŒÃÀ£w­z â6?oßÏ1@ÝÊm“PÛ .ÍñÌ®”?~ÝÞñƒ$šV!·S‡²C.ÕE ÿzB~¬ìar]=o*µIȱ„™CɹěäðêOˆnuÆüþšrƒ&€ØéÊmG+]{+Šá¡£uð:DÍ¡Ÿ ¶KrŒí—iñÊÃk7oqA;Y˜8FqJá°3Zÿ¥[Ýî®tnÞj}hdήðgýeZÖyÐ]ý„¦Ù^òE‡ªUöiÔ7yñ\Ðü¬Ál3â¶¶…ókŒÛÌXp¼[Eÿ‘†¾Ú0Òm‡xÙfñÆX^˜$Îìx™Èuwt„ ã¬á:þ!ØQ´‡Ø ƒ¾Û÷F”°El>±ðæaQff“i¤wïb°þû®ÿ! èÁP‹ƒ`dÀ¼æ±é÷œåmBé=3è/qpMŸÚÉÇN’« ŹI®¥–Q_¸œH*Lå•ÛÀÛSøï¨Õ=?‹Mø¸ _î§ ò ž®2~0¯7µSç¨ÕÆ×õ>|\n=L ž>n·–“Ç”Ø-éÿs™éþ¶Zã,iC\ÃÑ—„ðe [ï§Ó ™´D˜ë¦5]äpÉLj"ü±Iø–G¦ˆº5ž01£?Ïù~;³@®ìÔ£u5ŽØï¶½‹Nâ}Cì*ßýnpÑ¢Z¾ß^é.gù4,ÊW¼øR÷ÍâzTzÀ+]L5 VÎ6k€XNÕS&m®þœ÷£ð'U^¹PÃòQ«X˜÷‰Ï‰«êDAIºQZ'½7G*ŽüÜÚnE­ï[—-·5L΋–þ¯ êßz_×oAGî[ űùHAô½õ¾0¾¾•¡~ùXmïnsÌÚ|øË/àF ÆåÌ‚'°¤@o 0&£GO®8Nž³´ lz;-ìd šŠ¨œ‡Sü!ð»iþÂk¼ª@Š?-‘.@Qã=C3í?Rª‹0 ;?¼òhTxchz“ëäY`–Ÿ«¹"ÕÐ £bKÑW5?89½ð‚1€gbî­.NBÐR•¹›mjúL->Í(‡É[ÒCÅæ"¥PÖ,ÆèHóY²VT¤JÖfÁgd­°Å¶¾:°\€¼JùÌ(0CùÌ(Q>³ ·Ó ¹9Â(ŸL°¾Å $à<%;»½4‹ŒTdQë‹ð$Â~–Lä‹TËD5|NÿX²“œ(eÒœXmN‚éTÊ H‹¤$Þ½ZŽ* Ì!G¥ ä¨ z{(£aÉ=74ú g7ÁŠ6áü9ð”Íbƒ§÷­.KBó™ ‹•wÓÌ|rµÎ÷žeª$íÀ|›DŠ«ÏYYBïRä ¥ãö“¦¥ƒ÷5p*‡?Ï0Ï™5Ì«à †¹·"þ­Fô2³]Ó‚V1ã34!ÿ<.ÄžaÑQah{ÅÜÛ´°§©™{É<§äÀš»³(I´kz†(…*FfÙþ™¹RÔÒEfÿJmW Õv…ÀÅÚ®pQJÉLh›’RàBJœƒ#½•èPgØÌ+ÁGàuÖËeh tÔ!íã‚§ŽÇÆÕ…Ï YúKçn˜ŸDÂij”rßÃý:•ûŸÂïR”úù]0•Õÿ+{c¾"7ÃײÄ]xÕºàšx¥UÁR©‚ç…ð³x¾ØÚmÁ™|A;¡@Ò?âêWY|~Ûcs³Î²ÎøüÀ?…9–; ñùM°\•ÿÊf×o›`åÐÛ_á:ök²“ÛŽûm;î·í¸T‹ÿ¶ã¾Ç"‚ý¶1X-ŸÏ1¾¨3ú·-Êèß¶(³fêo[”¿mQf¡¿žýÆì­ÕßÜ, ¹YMè3fàÂã:–OQÏŒwž+¨¼ÔQ˜Î'o¡øÉàôL<EiÂú û’,a¼Ü:M'0;L pŽ®`©¶|ŸþÓ?ÝÕûíååµµ•åìO[ýiw»íµûËÝeY[åÇ««Ý•û­{ñxr[Õôü(vG#Ò×{æ}Û9=›ÊHžH¿5 øôÿÂÁä:Ä,`ÂÙ\¨¤ÙúÄîÎáðœõ{9ˆk÷ûËHís"4/ð"’RìlonílµÿxX«ÍÎÛWÛºrÇèk’‚ž! ëș΃¥FID^îýx¸Géí\h¸pÅ…z2¾¦¬aºžPŽ\:‰ªòŒXuŽ\ÿlêžÉVm_@ãb< 0Dо„’.% € Þëzò_!bJDÍy÷î<Ž'ë÷î]^^¶H(ƒðŒ¤åÝ»¥– þaÇ„úˆ7à §¾¯“×õ®ãóÀ¯aíá$”ð»!€t7’ÄaJu&¹ú»/dóÅí.žÏczÁ]LÈç&€A´[µ…ŽÔ ö‰8TÒP؆éÎ[ sÍÐD— ØÌK:oìú×BŽ8LxGâ5¦/˜F®FDmùg#/‚®Wq<ÞÆ”p˜T†¤ËúhDü£ƒ e4 ü¡°Æçš“¦s>”˜nNÉÌ@5€’GL)•]4˜‚´>”¸ûÆÑïî©']Skk•“\Ÿ´íÇ|ð?€§†aÒSdÀR™¥4'Øêvƒ|ºxìz‚¾[þNp‡×O)žc¨MÆ>hÛ…'/…'¥ÖñÄ;pÔYª#lS\ÀÆ” „ †¿ÄŒ—cjÖÅ+9þx‰ÞÿDñt †R< ÿ‰Íi¾9íÔù5*ÒYÏ€M›n4ž†a€ï>´Þw×Å‹ÿÇ—‘%ÏVÀÈ Fç.¢>p§¢çNGXòáã„kµ %tËcöÁKe0Á£‚i,Ü>þ†vDÌ“eE¼Ç»0ÎÝ YXäõ AµÄÖHEÙOâuëTµ‡ä‡ð±°e@ØtÃÁ9v!BÕ²ãd‡HêÐÅþ™à£Oó ý(É—ð*R kle(h·¬òNDjrz:õF$;Ày¥å™MFî5>ﬢ ´»°{À>Þö èø/Ñÿ!n_€#À{¾Œ]x:€V Ï°"³Êß'Þ>Á8…Ñ=p‘‘ Dû2Œ]zÈÇãPÉç -ÑÝ,`ˆC‡NPŸÁ U{Nèhï ž4t'tLj @:*Zâû¿ ÷,àÉýêÿ•$oƒÿ e?Ä.„á;ÎrðÇ#‰aœX‡Ëdúÿ‰cÀ6ÈÄ·TPŸqžÉ’²àc <è hL?LÑyáaRÀ4–S”ÞØ‰3wLÜ –¹¼7­p¡©ÀÉ)rç?Õ©ã9ynãÔ×8CÌ" Ò `@‚q8X‘+N gsì¡hDÍÝRHaˆC5È8—»‰pFŸàLŽmGzÐÛ 5ÝÑÖëR;k5œ°\qæ«Iæh„ \ŸK¡ÚÈ1I¼P»€VŒC¯?E œí–uêÐÅgn¨Ó¹rA¢ÜÁtýëŽøÏ…‡iPD¦`&6màF.Nð ÊO¡iØ,š?¸»ðcø×Ð PÙOÓ‘§Åg(#/”c ò#jr—~{Ä•÷…^‡n3ç\¦Éî–’ fR;öð/Œòq>@=0ÍC΂ xŒÇewä^„.¦½ƒ® QZS@ëã`(rÖðdwDêÂL˜†ÒÞÀ,x œ?ó½Sø‚‰Ê‚ÑÆ[ \ê|ØpNF1¤p ®§×?L=¤æ¡°ï~ 3P?»òBFß’ÐŒQ¸¼Þ$Šþ[×ûxÍÆu†æÍ·4â" ¯^i%{d¹¢°‹ZÆ÷…ü¨?r< /ä•„™2€z´ÜË 0*üs é[zjHÃ:eè9<— å^} ÓGÄ¢÷÷> @ƒs %&Dh7b QQ‡duq#1 Ïåh‚ÊônÍÆÁ!¯À¬#k!#M¶ÎZù®Á¡ýêÜùŽFêcá|׺ûxIe3ºõ[LÔì[KG|kÆ6°ˆ‡Æôad]¸cfœ\ F(±qß œ£_4 ]ÑÎË÷ ¼1UcÚïNQPµžI_búRšÖ `–âaS¥@˜9Eî¶4%‹»£ü÷ÄC6€±<…&@¤0ø àAƒ†0*Ìu±³™ø ²úøÐinü_„ê?@…Àäåÿ5Eu÷RÏ¿@ãÿ‚š„™/’¬ü6úá"®ge 3Òô+,„mŽ ™ß¢ôს ¿G @ƒ¿b+’ø kˆi&ký"¶Pþu±ûŸ’Û¡íà$ `œCo¸H׿PW. Ë*Œœ4\ÒyË)cÁ\D#5Šâ†Ð ÑrED$ÓâìÄ!v$LI3&Âiä cr`¡D³„jйÛ÷t0Ý…4[îɈø°ðy+rÿMú^þ'4ß»#ªk3ÿŠ«{°¦ðñZŠ0~Ä1R²V@kç¬Ï$ƒ6Z_°ì‘Ñ×›á8»¦¶™C–.}=ªSÚj0½CJ_MtÞ^vÁ Å/ Zææ.È” º¼ÎÔ×AßÝ!Ú48±PŽlT>_èÁÛ+ÙæìÙôcüÐÐH¤ú†uÀ ù' `€Ø ʆ!"šæH€0…†\'ðå>üë™DܘNž•!ÛòhKßîšÔwaY²s'C€µm-”¨ƒ°ÈÏãTk<û/‡~š1.¶pÌŠwïïÞ½kÕp)Å#½t &žº¦Î€ø$E±3Þ½kÔ8¿'Rg*tÍ­tÿÓ¢KïT>¼áæö¾¾âK£×EÊwqnÂö½ .Å(@“6âµ¾x’È æÍ`yQy;W^þˆL¤r`¹Á¬×G=}ÍK%ZE2k-lœljd¬‘ª,Ü¿-ÓÉZ Ä•ˆ ¬¬+)  -)é6’¨E–ye)ÓrX"­Ä0+ý–¨qÌêM!QšàÉ{äÝ;âAú– ”üÂò Q¯bKçÓ¬I)5ˆEé3.[C9µãÔÉgÓl×—Ô—ð÷ò¨Ù¶R3òŒê§«tòy:B=÷P¨iÞ-hF¢ÝV[¢6î^fP8眤~ì~@ÍO¼l¢ãs¡A†³6 ž09ð]”XF€‰ÉC †Žç»É8Ƈ0åzaË0@Š…oà©®ÈmÂÀ Ñ€wé9û§"Jn‰“é0f~ì]˜ªí1ô.–$ c?'Hôiˆ‰? ufÌTWbZΞAñ»wö¶Ñ}­^9lš.Ù¯ÙÇ¡!TâÑûË ©"&£Ç6ø‘rB0¯ÛÝûöËÖØ½R–ß´hêUÄÎ0ÌSÚ^^6]Õ ž´è7suÚ´>ä?ü»I¿™ÝßÒï1¿˜Žé¯¬ªûíÕÃûÌl…‹K]U†ÿBë-w  xÈU†üÞåçrjð‚ÉoŸ+ÉRƪh3úû;w™‰ÔPƒ=1xý@¤Ä’fœ)ŽSž7° \(˜rÕ#¦d~¿ñÂ3/#Ï58Ç’!ÉpåŠÑšUŒ`d{ôûýÞ ß›=ÿÃÃ×;ün<õℯl’*ZyOë§§x­ØIÂD2„¯¾Ç†aÒ-Õ.=œ.<ÕO¾«>Ààœª×}ÇXqmjú &P=VÉV"…ëѱB¤UÌŠçÓêV2×ó†ÆIgÄS…î•b¤éyÆÐøgü¨ù­¯aâ÷°,æ·ºezÂΨ²Z¦zr¤d=JT#/i:(Kp¼ž’gU-ýët)Èôæ"žMt»„nî}mc ¶vHQ½riõJ.P*3c?Ì_t‡Ãâp4ÕÈ^ùuDL~Ñj;+PaqÚÑtZô11pê{’ZÍP8·Ñ=xí//·Õ¥@é§Â§]õFù1š£ÿp[;äß |ã©/oއì vMãk#µØ¢R \/ÑÔ0µRP-ÞßÚxöj‹j_GŽL.Ý^´yÆÊšaóík»Û ¢æá0¾‚mˆÉ_á]âºÛJ´òÐÛ ´"pgyÙz%(2n RN‚È#³½ÜX^^¶:8t/O¨ŽGI›ÜK§¥Ð½Ž—W©3–r…Žh›¦šŽžlPâü ìôƒ·þV„®ôš‚þÓ1¹`ò=ƒ?èºö¸‚@4z”²‰¾×Mè\vòÁ"zs Ú¬õÖë“GWäÂH˜ë©ƒyfŒ·>ºÀCôLÁüNGMü‚i1 }Œ$Ü “g¸jWþ¶†è{‘/cÂ÷ ˆ£QÝ`¿ô[ˆŽk¨±^«ÁÒ ü6xµÿƒ‰Û§Ö@Ê–¼t¯Q,Qª´¯—¶ÞÎ~ôÿCÌÙ¤ƒ#6@db€Ð"÷P§jë+L:Ù2AÈä°úÊÏèE*£Æé>ÖõÂDT•´êáÞž¢E˜Ô#Ó^5ZfÙ6©—¸du§Ãòƒ•ÀRJ4zã’—èÕcñlƒAv}ˆB¯¢ÝZ"FÐÅᙌ‰wE*ÌÜÕÀˆÆbK8BNðRͱ–e[yþYj%–ü ùL;Fº"Gí;®.•”Û®Ùæ¦s*ž{”Uñt˜XxE Á:¬%ޞIŒþf6|½öK=fK ýxp®Üqèk ç  ÷g$í<äÞïs¥_¡Ëú·§™7H“z˜Û7S~Öâ—ª²¢—§0¢ _øì ,z…Ög­`J~sÄ9LÊîÙ,Yç–˦æW›ÍW{û½—{0/¸aE“í±wÐŒ¹*_^F¼6á}ò6à®àhD¤?‚ÓÚû`ÒZ óþÓÆ­P7ì¡§0$ÿ³¯÷ÿ©¥@t=ÐRXÇT_(îZÛÇoßmÜ?.˜´¼HO%j 7f¸JÍáîýh\ÚÍ2:`Å38§•bÓæÅ÷ ¥G [œìHÑHòj˜U}3üßø§ÝXɪ­’JÔþ½ø_BÅý¬‹ov÷ö_mìlÿyëäÇ—Û°”ïmln17"läѹ½žâuN]ù˜Ø®UécÍú-ñ ðÀx»¨;êÇ ÅŠ’òYyî™…ž~m]¡Qr›íª…»¦ˆÌïúŸejæå Ã%+e"¹‘ULÿ9{¹óOR]éXLÜEÿ•cñò ?°YJ0¦2#ƒèPR‚È*Ó¸{çSÿC”=«XJÜ‚"p#òÕ¿½ý­|¯ñlàhAÙ|½ÏPJbv_¿RˆÑ1˜¸{ÓÏߨ¸¨¶ýC†2‚•«ŸJ¥á ŽDôv{û{½l…ü=‘G ””'MÞ(QÅN·…U±IÉ,€n<û^‘mäw×¢rVc‹$<¡?öЇYh=¬G ôY xÙüÃAskÇF–,ª‘i1€¡ƒ_~Þ æXbMñzTIÒÝ—hfRœhptñ£¬‚±ï˜°½¾5šdÉ!ÎËæž¥Vv3 Ö4£oZè"­C{EäÚ1 ©9®vÇÔx&‹ë –pcTMwŒïø~kì„ ˜‡È¥Œ3U-18ò0 1–·5‰ïi¤÷´ø3ÌHekáSUw¨d&«×$Á¡¼€•‰vŽÂ×S€Åp’ rß?‘'¯¤dg+ƒ·X2þǻ߀EÌr•©ëå7¾Ñš¼säò†T¤––ó]C· Ð\HE­2?€Cýk4×GÓˆb)µô À†¡F4´h‰× N›`ªIÜUsÏÀZåe2Šn÷Ÿ¦bã@~¡JI6ˆ©Âê‚¡<ÅØ™ÉÈ;½>tNœ±ƒÁ”¾óÍÚþÊ_-J°1áÍQÜ¢x.@—¾i¯§ å(’åe“µKÊ£Ê^I9 µÀ©ÐNÈ´‚7œËF¼DÄS™ã têÏ9#9ì´­—É‚¯Ÿ¦ÄϾççAª, 䘲 µ‰ÖnŽvçÖ/š°fcàL½òª9òú¡ÚU³§I8±´9Pj¯ÜޝyQcƧ‘^øÐêQ¿W‡ßPD±•¢±à«Å\b¦r£ïaq‹C÷|aqæ°E£9qÙ þÏ=j·F`šz7ãd"\|Q«mS?Ö^ª¡Ù±…å}|=‘Ê‘3”œ]æV¾XÈjsNò²"GÛ5øLmá¶Û¬»i˹ôbÖÖåO‡ªðÕNÛ*km?`?Öj;:Àµ³ÌD«”˜Ô EWvÞ½ÊA(]š|Ì~‹íÅ@}%=8°Û’nÔ½X2쎫š‰øí}™ÖyÍ—×Í=®(w>‰®’Y#üÚD¦¢Æý¹¨óN7èæ|ßp9аªˆß„î燓•÷y Ñ;PWƒ€žÀªŒFÔÛ+·Ïû}åD}ߤ7¿X;^¶]l¹6ÏÂ`:aÍ­íd{¾Ó^t94s¢3`n¾3 …ŸRÖbÈj˜aˆt’³’íÒŠ›VI›¡ºø¼Å6 *æsê¢ÇÙ|ßlíìl÷¶ùþ#4?采߸8ÔwEpƒ ©c ‡Ÿ#ÍøÐî’cÓÚÌQ-âR–T.™r wp¸ñÍéW 6EÍÁë§ß—‘·W^¿M…êl,adÀ"Á‚Ô"€ $E@›LOºâ‹óÍpX¢aÆvJqÇ´°Åݦíju>Äå,C´ôj˜»EÞ½âöÊÒ»wb,ãó`˜ñ¤acÏîéá”Q2ÂÔùQu+?&y=Á#9uRŸ‚춺z7œt¬%ÇIÅ¡}Èk‹Z­v€Æ9/ë3S¸Ë\ì2»{æ.%Ãu?àá0^ Ñ6GÞze;¦µntóጡ)ÓPkž¥‹¢%^5é`øˆæûÀÜÜv ºìó^¼6Tu=ê./寱FE›Zr(Uœ÷xìPÒŽç<þLTïø^¹!°HñÞàHž¢j3Ë[,t´Ž‹áù\r¶À£—£¸ˆZ@ÿÛÝHüüß¼ÕÅ¿½-Ù©óÑÜ@ÚŽšêë:Ý)N6˜–£uõýغÑÏfÿ¡Þ)˜»ºwâÉ D4=õméãvœÀóA®¸F±+ð0ßž‹‘šì¼Ðnòí=yº³½¹'.P+2g¦åèº|z®0ç¿]…eCœ‚…2å(ÿ ‚ò¹*<£@XÆî4JˆÀÓmþ“þJV#›ÁX>A‹…‰=!÷èƒW^ûa¡°q*ƒ¤{þ€#ˆÂ¦á±ÄBê¯-ƒ,D¨âë|(CŽAò oÖÂ0¦Dž0Wñ„Æèœ±Äî¸ÿ„vA'îä FÅ¢n ÚZŠ»XKŸàF­ ÃØdDáq‹ÀÌÛž< ôI#ÐY-ÌóŸ–'ЩÏëÁ‹'ÃO¬¹÷£PÜEIÆ ì鈎B%Ÿ ‡=aAÁÏx¦D¼.ÐU„”ª÷‰Fô„õ¹âwâ—±²%ŽîðQK>dfªë%ˆ†2Bzð¤Ò“ 9nüDEèxBäSXøTàp‹á¬ƒ Lþ Bh€gúH°žœyÉ@,œ8Åbˆûr^'MCG=ñ†…ò§©‡6‹K/N=º¸’\ËìZÆ ø²y(¹á¯/½g*€åX4…¸ÁS‹vCYQÍå³H¡×‡BÒ G^*!ýÜ›qÎÎO»'ò€G¶¹ÚŒEŽSs—½K°ÓFÒ#õºÖmË:Š“wRŒ.%w„ôµøàÓÅå0ɾ{çÃ’ÆËíy ÃàRŒÅt"ÜLÃ.è9µ…¥í0b$…S±i€Âîƒ9 ºCês£î€iY;ýžq™:èyNïüâf„;@Å4¸vTÙµ˜—[íûkïw:Ú++<ìvÔj[Þx1ºn(3 ãLtä]éÈqM¹ÙïÀéÆTÆ…þuX²E·KrôÑ‘kT[7ê5éF‚†è»ƒÁéé#ÕˆÙíl§Ûùàþrça´[{¥Ó^Y[Y­Õv)(Mô+©êhªžÎ$ª=›¨N†¨‡Ëí‡íﯭ®¬>XißGËSÑy6Ö'¡Š‡<(ˆ©ÿ!æŽÐ Kš¾ÄxæQ2Ö# ‘ˆƒ`DzÖñ%‰©ŠÕñ0•0ØÊqcÞD÷&X•4¦5ñF² hr¡HËäœß£ 1-ýÉшÓ!czà ¬‰cxõ¸(v,±WoŠáÎwßÝitCÜyüøŽ¡ß}'¶ÆBžj:͟ߤu&ŸÅPPœ§ðè1ŒpgOµºþßpPá9³)íÏžqè(*œ¿¹|2”B´¡¼'Q‰Ã ƒÿ óYC2eý‹QÀ*àl¨Ï­Ò©Ñ¡Û‚ækzÝÑ…ü((gÀÀ S0•þo©&É{þô¿ÉÆ¢°vÀE%a0¡^x†ÝڟѾˆ[qêŽ1À+ô>b”o1xë?%ž°÷Ü&â? é¼ dL¥x:O³Âä3ÅÐù&Š…¥œ‹û¶07b˜-f†ˆ‚8–Šyûûè ãáldaŸ¿ Te!GöáyZ•ë†è]` <Ø0ë¢UÔÂ~0’ÌH4"NVL1s„ªHGîz˜ –ØýVû½³ ©Øœ£üåÍUÒ¨ó#¨õo`mpØpR7ŠR×"T4Åó·ä ¹œS &¼p‘uÈÝÛqÜf¦iIG3F†x6)¤Ðß–ØšFÿÕ÷R2<˜)öõYèÅûGò­Øi½_“Ð…‘ÿ>˜Ò qpJ\aú‰ØÖESÖñ ²ÿøõ;ÅÚ†·r˨Á­Ã=H™J^$C]# x câ|€³™ 脊w8¬oFpÇR’D'1C†û+[8ÑS.'†cÝqXæ9fYAHµ} `¾ÀE§ÊC¯ I`kña;´Ï¢‰ Ф…‡Ö/ŸoÍz6 %}¨ýC”œÓÅìAn8”Cަfbp2­™ð¢+™W‘*eøŒZ{ýk÷C2Ëq¿rõ·¶å¢­5³¾§ —õêÞ²Ú20¼Ý]Û僛*~È>5BW9é|GJ&—ˆŸèÈöbÁiÈU™ùÁâoÅ´y@3±ž»5:6,w6ÏBP.™ÙÈ ˜wcvAO§rÌû;q5|†ª ä-À#:ž°A%FnÄSL½êccÔŸžâ©ƒ†Ñ\8¨`ZÀU1)Àu04ÀH=²’SeŒÈ£1bUÑ"0K›š^è¤AD±lK Õh‚mx´šÄ½`:áÏ0ÀaÓnFˆg¼$kåDè1éñ„‚xÁt= ªÀ;Eü¸:Ç™ý=éý/‚I‡1îþ0D ðlŠ+c ñÜ¥éUŽ7Vg)$ù2xÆU3éD0NŒâ±±î$œ„fq:Ã@o´Bø·W§n„žl)ØŒ‡)o¸­€kmIÀO÷— w5ϼ+—¹6h ¦#o‘ê7˜ Æ äö@†q@UU_O0sU¡¤G©ÌíD}”B ¤îG^®*œèö)9žyäc¢“i dÁ…GV'‡&ž®J tǓǾJ´ò§ ×t0&D©­`âÉðÁ¤8¥ÃÀ£‰}*ÊѦPÑú.Ï«›îèza× {º@š>‡ßï0IzÄFõ-6¦@¹L§±*8ì”ÈeGÂ+:ŸË =°´¥r‹I±1Txšµ­qó®ó “@5†\ŒÒ ,²XJØêÌCBg±lMÁj€ñš£i"\$Çh)­ F&¨#­ŠÄH•»¤ä¹-Ç„ötê«ã!ÀK]w”’ªW.çåBu"ðÜ+|W};!ù73ÿ'еa@"òLÊÎ'7˜/q§B×ñ$Q™nK¹ï€6?P‡‹°÷PSŠCk“Æ3 D‹‘ F-†‰bª+w8@ß+‘#ÊkG¼ˆ\2!ÑR'5ÐPC(}2ŸÑØ`Å•°‡åQ"h JІ)¼{\ò¤(…ŸµebÔŒ@ÖÐ'‡Ù×^l’8ðxC—xDJ† É(ID"t¸2!*µâ‚”2g„sû¦ÓF¯j¿æžPúùÿ³÷nÛm$Ç¢à¼îúŠ2ív€Æ… %ÚloêÒnÙE‹r÷ö¦xˆP$« TaW"¡Vï‡yžµæ'æm^Κµæef§ùïϘ¿˜¸äµ* )vÛšÕT7 TeFFFFFFFFF$iƒ„èxtŸÆ PÛ–9Ï&\;q[Ekm”œ“ +(DD\5n…iŠ7ÿ9È­†4ã ÀC`ÌcÜVFûJ° ÌMœ˜(5f¹  n€ tý&ÞÕéd"NÉ”³r¨´ï)ÐdRŒ¼*µÏ´!4üëËxt)Õw´pàáMŒ!7É(qŽ¡‘(P&–)µ5¿  ©3˜µØ ZhêV·5FxǰæMMK©ëÒ›ã—GV_±R”aô`#Ô…ˆéH‡ …8j¢¾D¿¶°c~^«ûÅ¢T¦¶5Jg1ž‰ÃlüT;á-Upaìu:»¶Ã*ûmuW”¶Ú™ô»4oލçòÊ¥¦ºiÆ‘¥H  +»½²§-]h]õ'ý0ÒŸäØïTˆB|5þT)øÈÂé$8’çd.·ŒÒ(I •ÅVùmr?kby‰f[‰ÝÓkà5…µ¡Ð\!V9zº¼;]~§Òž­ƒŽZéL•.¾Æ ªˆ*'îGÎP9tFy>>Át9£ÝEÃïô¶··Å)Óô¿ûÝÞÎcúøÃÇN›>þ¿P¸ÍR¼çºC„ÛÈãü¸-4òGùáïü^%åÔïwv¤íËßÞŸayŠüÞöc¹Tô:=qLŠ_Úì¤ËÇ3ÝÇìîËÖµîîcíW ¿ÛgˆÈ]oŒwО0Å8Û;íuq¶·¡ngð¦8Û=ù¶ÛÞYg;K‡¼e¾üÙ¢l£ŸƒlÔz^´ šŽ£gžØS׉µˆ‘UåõìWê"MÇø÷<Î/ù»˜ÖÉòú&œ3åó:ÃSÞ˜bS¿ l³C×Û,?‰¯¸yj6+ÿšÍÊ`Dƴ픩KnZäʸŒ‚QY:Ê»_|ÒÍðÑ>4_¨៪>âšJúi!$Ï ¬ßf‹ˆbÉÀS„=…Pãˆek½Ö‡×D5áTƒF!X8™]‚Ú,#@—"Jò••ÎCGG»Æàl0’ »Ÿ,æT„×"ÁRum ®“º]›³1†Ö„Òõ"ˆ¾Á=·œ p¹É2h_üÜ®‰r÷ Z¡À¾ŠF¥ ôV˜HI‰ûæÕ+²þƒM³•WÙé$XÜ%ÇÂx›\üZ¿ÝÖVÁºœO²ô:WLg :&·µª!µÄG@¼ ›²w*(œ5¨X'{ý¶aƒåUï¯0$î>á«0ÑH½d;ó›¨\èøsI$bÏÑi4;/§Ãt’{ʬ~}™Nä ŽÉA稒´=» ÐÎðð{Ÿâ¸P8Œy?I´ R”ïºÐ‘}½h+¥˜ –_=Öw|ÎVs¤%}b{®ˆá±½³m›¦µŽÿM<GÉ«0»Jß¿Â=Í[~/1nHA÷Ç¡Çx´n^N![þãö¶=¿ƒ˜a=»í3ÅI'¶ƒóv:ƾf(3jÇ”oÆþÐî^9M­ÕîC€ÏOâSÇ•BGu9GíÂHm1ù·ûmW9 T2?†öû‰ÞÙ~.ÎùÄLW ÐA’zŸaÑšámª\N§xNÁcÚââùvÙïa)[È6t+ûꀿ·ä‹V÷ü‹/ s 4JÿÃKZl˜ÃÄ8¯Â›xº˜ú/ašÄ—°Â¡M#ž Çö´`‡:^ÒCÜßÇs¶]Ñv?Ê2à¾vêg»ÝöcM‰)Ìr˜@átJôù%ùžÿêåsÔ…„-Îh¢Am9MZÛ½/<ïe8›„£È¯íÂcØÊ$ǺÀÜ8ûûz‘£,QU6ËpwvZím€ûüméÐA'*›k·úÕ >×}}z­v;!Aé4™ÔNÃÐê¶ú@‚õúu‹fë*tZíº=¢ßÅsŒÊò,†~[dè²ôˆN¼3ÿ|+Úæ¾=ʵ:]@àÈCohµ€Eh¥)‰JWšÀvv¿àýVœç‹(÷ÝÝ¡¡üuv´ …‹¹k2aKÜÐ&sêBùõN«A³£úÝsöM4¥#rNmš6Î:¨”\ßD“1¨ºxÚ¥yþmˆÑH‘hòQäÕbŽá ä3Ï;öÏ/ñ æ¢x7Çó^ 9ý~§³Gá}2\ gÇM0éŒÂ ã9|òž[Ëg8ºŒVZ]0„¬ñ^ØL”UʼBÛ‚vtùNU­S…l¯·'/šŽÓE!¸*½#D.h8Í×à l'<~­3 üÌ^ãÛc«nËÐ ×Õ4*Á k–Œ Í’uº]2‚tùq CkŒ Y4šô?"Ó˜÷ÂpÂxÎxàt¯Ê"æ.îÓű0ƒD1%ÄÖ8‹ÈWgŽ7Jö<ï×-ÿ¶ÊcLº‚”7Ñ(½H(>!½|C!ymTðù[Ì_\þ5¹BýÏCèý§õþ5/ž?£4J<àÏ®0bûŒæ®•úÌÖòü=òÍÁÞx¾ç×è:¯±¨Ý•)‡¾¨ P‘G*L¼ˆßcx–äB¤U3o½Œ&ažóHŠX. Îfe@÷¨PDq‰©[Âví=süú°á¿|ýôàí üôúÍ_ü»øöìà-¯­yêaê-þ—Ù¨Qè6Y- Aïqè`yæRÌ=Š¥¤ãeï±ÜAì¸ÏèL[-ªŸà‰I8D/¼ëK â‰q`"‹bèLM€T³Yf€…4‡(|i #Òâe0Î=àƒž/cCù'”!GaŠøÌÛTˆð÷ õÑГŽH$ù‡{{ÖyüøQ»×é·Ú;ÛD‰##a"Âý>“×7ñQ-0[~=ªšXæ¤sÚ­}Aµ§¦­Úº3]{öúé__=?|+•"aB ^̃\|‘YÐL2’F†y*?PHJQÝHÄçó8ËçÍIô>šˆ' 2sŠ×ñ(ã±øø*—ט/AŒéÔœ.h/HÔå˜8QÈHüj&+û¯F.É߼:úf°~Oê×iTMÎ (œø¯€WÒ"2ºÈSXHÏ[ò=гo^â¦8%ãæœj2yÞÒ z€}~ÐðöÂøJ¡ó€ææn£À¢Üzš¾| "¿ë?[ÌG—Täx†‡·—|diÌ1‘k‰äÑ o—ØŽ{æB”ȈŃ8z},.?lp$úd‚øX;h`G‘̇_¶hf)›MlÒ°‚úIw×y" ˜…ƒo8ÆA& qìýoÒó/AN'_bîT<îÍa¾F_Š2˜a˜…W×/K°‚ÓÓCõìC ´ÄGOÓäûbeXaõlšŠÊº 4™D‰ý õ4 hžSNo}X…äezncøž!Yõ† ôG³µå€«ýƒd~e³(Ñô¢G‹d„ÇÁ®µö?Ë?΂U >]‘×fÔ çe:sN°½).¸Öâü˜òŶË`ÍXSøY [éäæ É©¥"Jê“Ìfˆlo™… Ûüµ´áOêÄâ:Ô…ŸÅl¨rU̾ 1yÓVÊùÅ[°9gy4miUn€‡ù Lx”¥hÜÊÙÂŠ× hCA ¯ðÂÔ’3,z9߬5ÒgÐF½©E;¯ÁôꌮÕúN-9ŠÒõWZÖÔ"F47ößMô8"ÿÞ8RrŒÆ> Ɔ^m»˜xˆ;ôél¾¬Ë´v´¯züZÜŠZEe¨î\4s<3f‘¢)e—-‰Rèª×B±}£jK‘WȈ/›˜Ê‹œtíu»N)Ô¶àÁùVÃÿ"¯“!ŠïÏž4û{§õ†NÙSÙX­E‘–C\šµ ?‘¬ o´ØPpÁƒêXձ̙ÍYEÄ2Wg¶UŒ„씃nK6ãÛàè’Å^PM¢gçq÷xT„nE)‡ƒZ‡±ÃqZèXˆŸtùIW?é5<|Ò«cXŒŽÎÄÒˆ Æ’ƒ9RvØ ÈJÍl; ìÐí3Oh‰¸1’øH¹Ó€’}Sí<Æù40œž~—:Á´˜Ž@V~Ǫ°¶zù¶¤øÒím,›8yŸ=#ÿ9Ü€NÌã?Iù{«!!:Û,~Õðß“[”9iŸÊcA›Cñ`ÿ«+Uê’ÀÓß'ËH’³¤Ï[ÀÔƒ÷©KòçõXëB"b Õï·\ã¿8äÄCŸÁàérˆ.úGj§@ß|9Åj0óè ôq3ÞðfÞ™3B¾dGû¯üï°(²޼ ’p £«ç àÓƒ7Ï^¼¤7™è+mEx,¾Ø;Í$g‰X_¶©ÓÔIð©Tüp³!d¼`¼±!S_§š}ßù¹g§V“{‡,5ç&xÿ‚M$®[§ÐBfñŠ:· L°øünÛñ*; d_ƒS÷{쪷©UÙi=À( éCY Òã É·èªJBNÒg%‹ÒOÏ:~!9ì‰4ÂÒy ‡ÈDàP”[t½c%!BB?&´DFJëy‘xáxËôÛ|K–´¦¥NZ'¦ñÞ‡Ö(ß°Lé•sf¢ å›dqßÃdÙùóQ³(¹0ù¯ñ™¯dºÒDý‡†HåÝ6<¤<ŒÕRþ¬ßì%sÄWåÐ?™>Ú‡z(ëZx/žDµ,h=x7Œ“wÃÚ~õnØzȼÖ[äqŠНÛk•q¤·CìNJ»p®âßAÓ(šˆ‰$E°ø‹r¶4Ð5T ¡s:%º´`;ÞlœcÊ>èÚ‹CGÆ|ÑMà¼>Ã{œÙ|1CJD“º_-¢O‰=Ø´\f׿Y4MG0Ìypê i:á¬ç)h‡èx‚½Ñ8þ  ^ÌÃÉRLVóàAÔý3ÿ%^õþb¶ç ^Q®…#4 ½È³0šÄ&Þ@CA œêÃÊnŸ1"æ(ž`cá8!ƒ¾‰Ðæâv·¼¹¸ˆüe„óТ4†ñ{’MìœQE®7ºõ–ÅÜnø¯­Ã6v"a‘Ùd88Ç .îò3P’£«(š‘{#ÞD^ªöq: 1%”»: ·œS»’ñ0:?çÕßÏÐ` z<üa:â Ÿ¤cqœ‰Ž›è½{p¼ÍN4Za2(ÀúsæIºŒücÐÑÒ÷)BÎ/ãÙ2Ì”x8†­JºJLFQíivUà”?ƒúL'çþ³0[Lü7ñøÃ¶¡ÕèÔQ„›óû¤íwßüíoÅF.ãI8†ýÔe±pŪ@¬À‹‚I1^l_ƒZˆyza{бó¬v24DÏ‹\þ$cIšƒt‡ :'¦ÖS­Ž”™âoš(|¨º¾G!P›A Sþ伩£P ñn%…¼OF‹!´‹|Âä—iî$ä?l~=ƒ­ø[Z´x©âwxx +ƒ^òª^ghŠ‘§xi @Î$Ù$>ŽóïÅ‚‹éopˆŒ.¡0cÓ˜ŽF‹ÙRœØ›* ©/SÝ}kKß4¯µÔüòz& ðòùGõ`t ë\í:­ÿa&Æãt:i2óé"Òëã8ûF‘ʪMD˜AÈ,"nEËøáÑ­…ˆˆÕbT¿¹®¿MÂ륉"J}ë;M%¢ÞzP×1ä…YNÈ)ã0)rÌGéx1² ¥çx?Ëx$yÛ,ZîU”—©|a®Tý½ñ.OÏßåÀ3x—?Àõoëߨƒ AÝÿ›g9æÈo^¿|~\P$50GoëÛçož¼>~þO¥AŸ£ÁŠÄ bŸzöI*DµH  ßêúŽ‡×˜P“D뮇¡ˆæþT…vǼjÑt¨u†ö$…eÚ’æW°ˆŒØè2ŽÎ5Ïa†×ø*´$IhÈK²³ .Ý<1ü´O+eÂV4¾¢®@þ5‰Q›`´sðŸã,^FÉ-}ÃhRâr1ëDÞ· z¹üYp|_E°¬Ž¯ãÄ$¡>2·ƒõ/.eiü¯3¼| ¢ök(ËÞ¦Ô~‘ÿ€\mPõ)G=ûu°È'R÷÷ðBëR²§8†SÇx¤`®—â†?sÄ•(Ï>YL1ª²òâþ#g% =‹q%™„tÓU ƒj¬ŒæÂV5±ÇQa-h=ÐË‚–ØÑ—ÇG†&ð]}­;̳ç¶tÁ{f.Ñ‚³ 31JëG`÷/Gîy'¢š´¶#Ïž×mÿGi1à[I.[AQ¾ðxÔ²U[N÷ƒgÏA9õ7¼Fóìy- ´ NÐ]¦ÝGiKd,ÝL°ØÚIÏÆÑ,L|ùi½ÃÛ®½w7Ã^â*:Œp“B5ø6º .D•Ž =Eºä ¿ ǸW1 ,’X5u-2ÐNùFt%k˜(Dß‹§³QÌw,²ô-^F¡ï.`¯ƒ˜¢H¶ŒFñ³§g”‹<à•!JH¾c6:ËÓˆ©ÝNÑûAOÂ3¼ë™„«Ñ«ê¸ä$=ÝFÊF ™*E‹¹éEô>y_TÅÔ§8ÿò[cÑí¯Q6¶¾§Ùxþå·Ž)iÌgy蜮ßÌ €[µ.p‹̲zÝqwÌÏ}@»èÔ¨çáú‰¸õÅÕGø„7'³$šŸƒhò!^à¹F0\À¢“`í<:»ÄàB“@×¾OáÙY–Îq ¡ùs¢m+xOo¢%1.ìgs³&ìÍc`—(IÒ¬‡‘üæ—Óx”C©’UwC/*q•LxQuàÉîZ/*i@øGxQ¡Ò0aÀ Õ lTøu4;ªC2h§ð(jé.é‹fåòÆ¡­’î"-zW†C[g¨`'¶ÎÈj SH{—Ofðõ%;B%8nêýº_ÎXC¾7äÂ_¬71Þ¼±Þ¼1ÞXoŒ7O¬7OŒ77Ö›ãÍÒz³4Þ|°Þ|oÖ%½¢ë2taöLäs=[ÈTói”YW_6/ Wºiø7ŒÑ?nZç áÜ¡ŽlçfÏ?ظÞnë¶õ–ºÞrãzGµ›zÃ?ªÔïÒ*Ôn<áú¥„°ÜóŸ@ïïçIãÆÀäÉ­ ,&7&7·Æä/@dvþhGöfŽÅºË{ÚzÊ‹ÊÉi½Å¥õåç뮃SªÕÄÁRUø±ºòtã/ pÔßÐ; è(Pß$U‚WþKmi€Æo ë!«¬i¢Éõ«`?Ù¬ÿ7+ú/Z1!ÑWA†ÿƒ(,¹ Û›ÆIw[+[#¢¨6©ð›šœªu…B–lÆXF`Ü.º‹ì? JwÐB<àf¼‡'2×ɲïÕ—'AœŸÍ1Æ1,yʰJHÆœùºU†U¬›«v]5Jw¨Ë²¢Ý¯a÷]U™‡ïSšoþEÔÅÑgXâ³Só O‚;uê/ë:µnLóÅ0_LуÿŽãÉìÞRp\o7Þ*(²þªaZË_Ä4;Ø—M@½1ØaC€on‰áêÞÞ¬¥Ö„ÜÕoìꎅâ0¯^µÏÚûÓt:…ý’!œà+4…!%¸ª¯‡Uc`~ó+Ÿ?•à:(q+°Íæ}nJÈ¿õï®ûñ§{Ïäý… &ØßÿTÔEÀ÷JŒRpÓºÏùÛdäøµl}Mê&p}¿iáËe«Öäç÷Ô¤;@"p7l ¼cmxk“/ÒlN˜ÐºRת„¥ÉÕ$ÊÍb#»Cr¬êÛ¨áŸÌ:Y÷´bŒ7îÆu8¹Ê×÷âOè[Ù1˜ kyÏ(·u˜}¢n«6Œ&qÕ£Nã{À±u‘›“ZÔmLÃlY_ƒwÔnqñ6ß{æî4=¯\@ÿ˜l½EkŽƒ°ÁPÖ‰.A”Á'SØ&‘ä<-Ì ']*½ý€ÞТ,|þQéA‹:Ó‹,²-¡”ËÔ:ÕG”ë#('>~­‹qƒD¹(çÊ'#àõE¹ß?yypøç—/ŸUÎ$ñáï$ùu”¹³³x¯“È©@ ‡èWv„ ³9M‰¬£ Óaã*žZe{ÞòYW$ñÝüŠ2$sù¤š‹›Å I„qpxüÝó7nÿDîl7>`0+MY~òcõ÷;ŒUCþÃŽlðÀ˜ºÂª~ ¯[sBW[Õk*íB5 ìvÞíX-×z [V{¤«†Îf $weµÇ›WÛ®˜Ç¯“yLÇùÌ;÷2)§©*œ$åÂ÷4+gÛÕp4ŸÛìžmßÃO’¯þ§¾û¹ïwi­) ûª*·Ÿý>¯|ª™ví*ôĬÒ/HŒN»(2:mFSUÙ¹›Èðwíz+h€2CM~ߘýˆŒ-5$¾…F_Õ»Åô<Ézv©=ƒ¤v{†Ø8étŒög¿ÌþdY±»9¢ý†¿£êõ6o°¯Å¢)ßJ‡Ñ½M£9¤‡x¼Óëµ»k£st)›Ç£üg:‹öŽeƒÞʳåʳâU‚Ë{ýC{EמMRæ@Ýv¥û)×xA…ü* ©])ð6h*P3ƒ¾+eg| t? G˜¾ÉB×P¾Fÿ:Äõ5Eå¤Ãh¿öõë—õÕéqÆÕt¼§(Ú£E–Ó};Aç¦q‚¡úóKr-C°:Æ7R{—°Õ×/[žw ÞZ7dýqJ=ݘ(dɾ9™-c‚òU^.jx|Y3*\@‡ÄÅ0L€™&MéÙ6:oa~º-âÊK8žðÒm»‚uˆ[þtc¾Á° xý1¸ O^4Qx“Á·Q¾õ¨B£(ŠÒ£/‹À Ƽî‰ûŸìä-zƒØHD‘Sçöý! <(B ‡jîoË÷„¬Ð»è\t‹ñ8œ¢ƒ ôˆÂŒ£ù|I1oŃóxLÞMcüNµÉ)¸ˆ³IÐà c•. "(éR½Ò»!¿3ŒÓ U€ÞN`—¯k†DiØ­qøÔމÿEW~€úèè©B¥GÓ–¥Ú{qc×Åz"ä<¹ŽàýS­Rq#§i"VöøŽk""bŠá–\¥4 ‡œ‚sŠóÆ&¼±åZ\M½˜š¸SèÇ” Îô½šøÉÜÂ,Ã`<Ë  =à{Óp6ãø»)ÞÙ h‘8NVÁ¹ä¯ª1bxÅÎQ¹u‘/( /gï:xD¾>…nY4näôYÊ׺û| ‡éŒ¤'KϺ!ŸÐ-%ÌpY‹Z-ª}ÓV{½.|®·|ß ÝÅ“Ci³ÄÝP]š'axÒLf \N®Þºr8…ˆå!äÔÈ,½ ¶\;.zô{;`Å‹^ôÌ1ÕhðÀž Ã V\½°ê&üô¼Ú“ÍÜÐÇNð#±tFAƒ¨!ô”Qè$¯|¼2âî.òˆÓlŠD£IšsNéœð’F·&1áyN|%Öu±‹“E÷ËåéÉ¢óåÍ)ð(çh lD ¡Ï|qP52Ö…ã1®ƒÑü2ï§ÊZJ3¥¶«Im–jA[ÝéÍ&Q¿+†Yk`8 Ù"»ˆju–÷˜UfeÄÁ1ƒÑ$ 3zI·ÕôÝ{£/в€a<âÖ¡Qï¹Æä&²£Üš§3.iÄ é`@Rq¢õoD¼ WtÄH°¸'P¼Åê¯>Ø‚ÆÀC"Õ VNþ³ {öƒAi¼¢íb“±™¶âr÷+Œ<‰óq)¢-cÿ0í¬žÝaC…c?•òbÜT`œÝyeHæ-2˜¢žQ:&€)$&Pt«(Ò<—pm) 0Nѹ÷sò>OEÐ’é&(g–@7Ó¼ lˆ|ZâGó‚$ø ÈÒy:…áÖ£¬s­`æ¡I,ò@âÐ^Æ$,8Ç\+ð<ºE$¹Ç¡V(`ÒõóáÒ3uJIeŽ3š•PŽ+ Ì%Òä2È£þBIÂQ´/›[ün–æÐæ>³¯ÿ @pAݲç=¡áG´Ãeneåa1çÄ’Apl†XõL g­®Ö7~cq&®œ”ºo¡ÂPSZ8gªmLªÊ†$œ/›9ÇcãK#2f]ž‘ìcf]‘©¯ƒW„ŽcŒø” 57ge±'9‹¾3OaŸñeN-ŸéïÑ´Û:_®™3lvð¦VÊîß ' ~£â7…¿u]¸|gK¤7¤ ô²v|rüüÕþïi¨é˜æ+î¼:<:i¾|ý´áþõÕ~£êIeßµŽ e1ÌZ–Î×Wð_ÑÑ‘lãÛ£“bá>6º± ¿øvE¹†ÿöðx?ÀœòðÔ1¬Ì ™=„ŽØèQFèë3Ëà¦$H Aœ X-_Ž‹qTí¤Å¬† ¡'Ê} ¦‘Á›M0ky7zQ,yã–SµÊ§¦P¨Ì˜#˜8™yQÊMµ°3È«h‰»V©GQxLj:yVÄ-7V- ´å=C¶«™—&¾À˜Z°ù9VOŽO íýß P0аˆ(ÀªÀ¨ßaV\ëBÏ`x£éÏ2¨ ½(FápIœë4»Êù˜ˆu‡Î ŽÂ#g¡¢†qûEœ^OŒ¨©Z„Çw.ĸÉV¾Rìce²]çxYþz¦Û>?Ÿ«@À:À™µZ™CÁ–Ίú.c̓M$*±6Óúobe"½îb~iæ ëH-´†{XÜ&‹]©ø&ô÷qW~3ôÒ8Ç[-µÜÌŸ¢w×6¾¹`ª´°$¢”À(«9^>ؤùxMT\ÀÁ Lð>£NQÜë=ÿ8šW#Íê|Ç9Gýå0-ãøœ.[ÌÁô‘GÞSZ»èÀ¾w¿ØkòßåmF¹kbf"»í>ù×}Çëd1™œ cÜ`95Ô|R¿cZ¨d*ºÇEÃ2óï‹Ä;˜TN”A ;ñ4‘&£š ÅdSWÝdñB× ~P¬mb«Êc¹N£×è7¤û ;mÓúAE=³¼»ˆDi5NÞQ…w?íÎT¾oPc1uïÀFMf§0,òª)m—±]F•ï±öF•·¨×H1'ø/²›QTwš dÚ.äüÚ´ü• vrörv"ÐSÄ較 ö„*žúqÞ!bb·¤p8Š`5™Sßœhä iÆOøÒ{tãÊ6*ãò ö7X¡xÏm0dÙ°£¡*ˆ.ô£|ñ!ŽèYW=C3*>ëbóˆÃ§u©$MóŸ%ênnñÉ÷ï¢û£z;`z>„çCã9ô˜žáùØ,fWÎH c Ž;úåèï¶ãÛ½¦¨ƒÃ®ü„HùVìÖuMŒ %Ia-4%€ÒêÓ‘ÕÍ;®<X‰|o™‡K¬fÝÌ>þó‹#a*B {U<.B#“m\ÊBC»‘Ý.¦íß]s-¾°4 a'MCÃ9°-²óï ùˆLüª"¶ª4*ƉœZO*ó‹|£+âd‡ºEœLøèB?¢)Ïõ8«Š4C‹4cu É–O 4B4o©ù®j ')‘E?"YR¦§Ê ›°ÈU莭³‹¡5’&†? ÉUЕŠ/-’ì}¢¦{šG‰ ý’“Þ¦ož|²i|AÆ‹W²àÝMË·lùØE¾’4E€•Ñp- —Ž5Qä£ÏFî•…ík8ÿ6*¸1DšâhaåŸM+(ÃìíêÁžÛ”Å:Å®®k¦\mu37LŒ֌䇖ÏÂÐñ¨g½-Üå|ßþ4¡ú9Á›·m ̶%1Û,2Û%™Ù6…f›¤¦Qó¢]’›â­*‚ &Å=¢ ì Í9žMÛúÄöæj™…}ß“Hd1Ý/ø‚¢‰©_ŒÕ—eG”.ƒ‘_VÁél‚H4gùØÎú~oÐj,qZX(V S{ª’¤æ ÞÛª&4"Ÿ:@U-°,Þ¬‰5c·º‰ ‰j ßBᕾ8°«Åz —L7©bÎ0ù¹H“*ª‹•À§å­þÜÑŸ/ŒÏcùYCs£ø ì‡v°t–汈:¨õ Ê™g=Ÿ—ÕZpäÿÖÿKÐ ²ôf~‰‘šÒÛ¦´âÝ›* MŸ+V¼¬Q£õj”>Vƒ~/+A¿5+;³ò=¾]÷ž>­ #¼­†ý_¯ýûU¯ßÐëJ’©×®¶­ P§h9Ômú¡Èø¢á7JEkoy—ÔÙl›ÄÇhð˜“Û²{%§°É‹Gny4•N¹†kW¾$²h|n”Þ× «Îå‚B9|°îLPºˆ‰~ü¯e,ß{ߎv»ñŸs—­}î$*K‰¥Á“jï§H/ˆ(;°?9ãÁ/‚ñÁ8­®ûE»{†«šPf€ymÝÕ9Éõ8#†`såÎÜlº|‘þž1o‘oa®Ö€i«Ïu{J°¯¯)“ß;†¸çƒ‘j:©¨µŽ€Ù¢Ð¡€f˜ã±c>¸Kü¶²DAÄ;^Þ¸-ü9ñ‹ï¤Hévµ¤‡/Uå,1P,³z‚lZZJ˜µ¥ RWMi×8­¾·®æFSU3f­¬ùÛU ñÇb¢ŠøVôÕz³hùsVQ¬0g•Ь(åÛ;ÆM¢aá&‘²ôIatŽÂGέ½Í‚þßa[·'÷uç S˜ß÷¦îÜ3ßF@Yd•h¨z]kJþ¶¢¤6dÆÊRJxT•²¤HU¡JqRÑ'§V”]3«PZ37o¬BÞTØTîT¡½©Z×üZAT ,ÖPÊ!˜ªYiý¸—™j¨*ã·‰ŠÄÑt~‚D²±!,[¶Ø²Ò>iÝRšù)£tÙ{—¼›K{Ê­\Áï$a¥ø*Ôˆ…׿”옮*R*Yu„=HZ…” Ù=PÊÂËF¢ê¢¡²t3A¹+i¶(u¥ e1Yœ9z_Y±iˆÁJª­¡—!&$$ïJ35AÄtpóÀ "ÄQYL-7‚ŠêˆL›MA”D.IÜÍ 7nîžmÒµµîˆ—¶²d¾ ¼’¤®’«8RôÌ!Ä-X•õ›kypuûšz.RE¶+ç"ådÊ1žLÂÑÕp‘%âIšóN×ÜÊ-Ô8|êðéÓ÷‹ '…’«ïã/èÐ#a\Ÿ-¦³«8¡G½Ò¸L“hy6\$ •Œ·ÅáKŠIuÈÆ5î»êùvÂöуºïã'Û0s«S$±å@*X!åòe2o|ºz.ÃhÞf»ñ2 Ç®Kº˜(ÜÛ°…—ÍE]ÆëÀç° Açª Z£Úé´àc`­ÿ÷{ ›ÄN@‘Mbèèž&³…ƒ‚™ð‚-F¥’hÑ{Ì$·ñªÓ‡¬qX‘〰qŽ¡56¶Pœf‡þûÏT†Vw\ põËÊól•£äÛÐÍl¯2cë,÷Þ% W•T[e³  ¥ó3•RÚî(QÛ"…é5#à±DÁ†”üPƇßb½%šI²a"Š|›D¢!º2ѲU…âÒÂj¿-&ú7×ÿV{wÓÒìEH£ºqXFà/N(Õ¢Ñ8Ô}ÍK•|³ú‘U×ëfõÃtꈫ$Z´Z«ŠÒˆ×ñGp…â¦u£½†« s´’©D;í%ÅkEn*ºT¯HåJ‹šGv=FØ^'¹={0]–åõ5q³D×ÆÍ’ål®$ò:Ư’°v°) ûP+)@DqZ êaœ²õ¤]ô@ŤÐÚ0O*ÆYIÚUI\#Ô/ÜôSpSqvoÀRU£»"À®ˆ:Þ¶Q±£îYWT;#ýÂgÿX>s•¶}AAg4µŠÒS`Ù_â@þ³Å4û§•ÁÿÄš§…á?@ñTíÿ\z§Cºl&_páxš¦³(óçi^ÏéÖD5QݳœëbLÍQn TEqïÉ Å—¢pCRh³3mÿö¡¾®‰¡l0 Ê8å¦1‚'ü¾–ÜŒJQðÔøÔf¡N`7}] A4ÊÏTA@„)ƒ„AsɽÏ%A *´FPˆ^R÷j:][ 1€³Q1\kè0¥‡Ã´S@Hbã°ù£Þø Îñ¸”ƒ÷D1 £é3©¥)ä@ÆdäB”M" tGòÓ Dêµw:-³Óu³VbX­ë€É€nN¡îæ +1ZÙLè¦nªY¼¶„c4AYÃ*:ƒ>ˆwôÆÑ¢ö¡'pûÐ3ò¡ÇÍcÅr¤[Ë6Ãú°­ñJnËŠåô9Û›¥Ïis‘þŽx½½ÝÙ]—>'ÆåùgÊ£‚ë<óå Æ¦zG'¾‰s2ìá91í&£,ο(Áu p†‘Ÿ- âhBOEHÆ"ž:½d^½Æüعæ³pRB‚!бV1'gz%ñ‡bÐ}§è•b÷ÚžáÝݳ9×6‚¹‚èªmý1MÇþtq~޽a<ÓßôZ½K€U£kÿoivŒy4‰0âÞp±ô§Ñ»dËbY ÓqHÚië]ò.y{&WyˈU\F¤–×7óâ; IT žøQÅ¿¿Á_täÙÂ_áov/ôñö@¼'p÷Aw,}>àŸôœ¾@OPî =Uƒâ½BɨÍÇ®pÊEäh€Ž0ŠsâQø ÐðÏϦ€Í"Óã’Eó•WÃgÏßú‡‡þ·O|üô§?áøïÅ¡Ïo‚V>›ÄsÃ]’x ~De¨)*sÍÕ•UÛg‡”—Õ3» £~5×;ÑÉš­¡jqÑN«m4‹ÄX]¾ÝzT=Ûšà’ˆkA~¶ M¿±Ã)í |r¬˜tXŸë’0Uå(.³~Y;zþf¿'´ÙZËÿxøl?8ÇÝ…ŠÝBÅ× pB…ƒ?¾Ù?Ï;Ee*ð̫٠BFŽ@Øþ©¸&p‚Õqû…à[ÀÁ7Þʺ€&¼íÑ·âÛâ| "ÄÃÀñÖÂó$$YÍBS<³“àM”*žPNÀîéy‹¦M#k‚gÓº8˜odZŽgQŽî*ìŸ,Î@wTU…f©âuc¾ã |xä{¤Q(#xªÒ#@è„ÿ0X‡|p¤K~K%¿ÅáÏÿoF™C6 çÂ@=ÿf #¤ü·\•€±~ÊX¤IUèù1¥¶t‘¦&è`ªÔd»Ä.ø€ +c‘¹éB:"R%Dç0´Í·’!Îf>'B®Ã.ö¢z“IÀk‡Ø²¹/„ÿ"u³îv`³y„[ÒÄzÖµùýÓšX׆' ”±þZEltµ~w›óöùi·ßÙ^§ˆ¥‹lE´a©•´¢c.†Â•×’Þäy[²ˆˆÛ½ÅQìE¾:Ò—ðesÑ9É Î ‡é‚¥Ij† ¤8 ¨Ê±ô¯ñS úbLY¹€cɸåp`ÃÑ„3Ð4?ZÎ/´(ÒÂÜ\”‡ :”Nbéaæ™X0@LW°˜„¤é\ìÊIÌ5¹ÈEøžiŠO"ämOIšû©68òe/ ,JÝ‚R«œîÐÂ~GátàŠ2×uÎD:©8Ÿ‹”PÌO‡h `)4 )O R­'gžyaqBØÓÌqIìÀ¼x-%ü®õn´ -ãš,m º­ÊL0mœHnÃÌa,¶4!ÒÝeeKw—„‹÷5/Œ ¶]À£©!ÀÍ~6[0­ðއJ+ªE8¯@8*Õ˜½†:Å»hHPõb„F™Ö›#ÐÏPž¡pW¯[ô¤À¨Â“&"ÜÃh˜"y‰i3²7ÎæI§³]Ü"‘ ‚ÖÆ _¿ßÝëïôNm<噕Ɠžlާ†rÒ}tº©&. ´ýøíÇz§°(}—¦ãËyžßlÛÇÛøU"zö¸ÏŸÊ ï$1°k îÁ¯þ)KèÉ>ytJ¶•D¦¡áWáU·­À â7 –á×Î)™FèÁÎ)°Ï1MŠš-CJ¯“šTaé¸sÖäpaÉ¢šÐÆv¦.ª@+TÂpÌG 6— gb¦Òƒš^μDí£.ÈØýŸª!•ï0D` ºˆ2q2ÊNÙ— o ä™’{"5eÏô:HÕ‰UП}­?óª&7v#Y8¢|·´NÃ+PTgF–Ã-¹®‰8òÂ+œ'V´!¬avæ+¸ò!€©6}“ Ùc‡Ñ$½nPEOF©]¬I bžG#i)YH"wÛ6›0à‘'–6%ÎëEÆóÙC)Ól"º.ÁØÛ bG/,±á,¥0ñ˜úÒ(óGeüÀIEpµå¬é¢Žn]›’1e°É‚¶LʲGˆâÇÜR»Í”yE/Ð’ÞËÉè¥IÄš®hªqpm¬eZnZ h æ,ï½êc+a7ŠíPz÷pZØ0"½UhÂÖb ¹x˜ŠdåD&‰#î£ô^ˆQDNkYž»§¹gLs²ÈL`*S¶èp¢²dò&*ÑŒ ÎÔ°xNîÄÓ Ô× î:Ð+¾8 ðv€ƒ`¦£™£*ö»¢b³#¶»ƒi|µºÌÖí×xiÅDÈc¾°…2m¾lRÉ­:j˜éö|1Á׿þ”{•úñ2ž¡ª}E‰ï\΄èCqžx!²á681ªáfë YµWêS=c‚½°Ï¸ç¥^fõVDçqû´À~9ð¤Âð$W†Š×¶ÏˆùÓqüëZÿÄy|›^ôüm¿ïïø»þ#ÿñÏü]ªü°y—͇TùãñÇËÑGÿãüãâcö1Ïcø6¥g1|ŸLáSÿã»køþ«ï’ŸÞòI%}™ L žãŸo­ Ò:ä¸ç/ Oл9üBßæð/´Övî–ƒƒ9KŽš\¦!æ5Œ¦0}Æ £„œ†þ*Šf”ûJ®õ’ÁIŠ¡g­UJ+k6Žžd)•¿FAuP Kœ{t\¿¤…¤ÍØ–†oÒkl¤ÁÚ¥¥ÑâÔ¬T7õ‡ªœ[8I}TÄQ!Ïs˜½zűŽP(þeøž ¸ÑhRÒP›Yhø¡q ú»4=9Wqi1?o>R» ê–h´6¿\ä^”䋌å:.D¢³d¯3hPWºU$ ŽSa!ÂzÊßa¡ëHžÀ”MfŠP¸@0^ޤ)-l 2¡}ŽìÓˆiPÈç T—ó3ÀëlO£’ÙÖ2 ¿Dé –3X¶ žÎA¦á»›QïÝMÍ1eô8ž7ñ44¨i8¹Œ`Ÿ€Å5”ÿX1¬…³9÷EÝÇñáÑû(‡Fa÷û$Ê&qú!¨·˜Xµ€Æ¡d0.uBÊèÕ¢YüˆVVº³ öµ8RéŽb›~ ï(JéÇ)Ø^ Ÿ}Ìás¿QÌ-@ˆ£ÐË@ 'BЇß-Úíå_ðp ’ÿ¾±Z3‚·ÂÂràø×wüÛ)þ°n·èV<û„õ°byšlÂâ”ЀLhDq¬&´\ãR­Gõ?à{C6Ë8–Íé­ÿ)ë´/9Žöxñ¸ì0ïÂ?õaWh)øÎ7è.ðSlcòÓ¸ó1tŽ><úøžz¶²žgîüceׯºÐŠ›4募ǣd,¼Zð¾F˜N0'³°ö’"j,V¦ ”éIÄÆýpZí4‡Ò@¯*£1Ê]¿áÚ÷P/„í&§¥@YÀzÐôO5°§Ë®ê{qí6z†§F@R…Þeš[«ôê•ÙØâÒaÈ\µF~»z±T3WT^P F+„ÂôuXÐ^xÛ`è0Ú8%ëaçaO–bãa-Ž©/ âÈ®*âÃjKgæÊpSzk *hg^TÜ?AÚw@ ïìÀÿN׌—¬V³«Õëbóìš— ®E“s:ïECsŽüœB{qßÑ(@)o³sRî´ ¡p®Ï'žuò »p´t"È4uþtü+[ÀƒÕÚæõÄí'¦XPvrh­˜€ÁW¶GÚÏ7è›u¶²ÙM:]ÁÌÐgÏÒå…fï( Õº( ºƒ˽’˜/€°ÅÏÞ‰”Vd>ÝFµe_òÙœg"‘ Ìž×#e1¶ÏÐü•Ji²›\¦…Ëxcš¸œf1m€.w˜Â¤1ºX”:R,Ìçžub3 #ÿ÷OÓéŒNâ Gý_ ΈÆd¼Ä›œkbšÐL§%lg~ó…Óä-ÜÏÇ­¨Õð,1MGŸ½Q¦NH<ˆý,žŠ)˳Ė=”ÿVÃ#Ù…BÓ-qÐ&ß ¥:Ž$¶:C›ìéb2uOP^K¬HsÚÔÕ‹~¼×3Ø “U‘;.½,åý¾<1òðÄž4øðƒ>Ï ÷¡A]BD§v<©)@Ê †4žlïu„õ5`÷xwý ‚ÿAf+µûAt²H3¥ûš LÏÞcƒž€a…Ùè’|MôZYºP.0é$IKâ½ÄlŸ/M£x¾ ¿5=(à•ÅP‰ðׄ„ü1 À—ñ4f?²|pÆ&G¼»gœ´£g…x¬öï$sYÑ¥FH !r¤SlJ§í–“ÝS=ò·«k‡ð0œ ÏÂàÕ6€arùÿP³6<¡ÈùÖ+¾‚Ë€jÛ¯ŒŸ¡2œŠÝ´²~—ÌS–ʲyKjc„_°h˜ûdÛ4S´Óðn[BÙû±ë–ü–0 îE;ë6i8âÜW“¤Õj5høIÐ\³V‘YʦscÛŽ—Îè,[¯ÚQš8F:@‹³ÁYŒ{%Û!$ô*xKú‰Ék¡–'ß@·iž.ºù±¥ óa‚ɲD!Ö,K–ºôkµWË*ôÍsNöea­¤äã³ÝömC1õ>ʪ “À+±_)Öè°Mê 9qây9 ÇÕ\¬ìì-–|G?å ­6¼b¿Q‚‹…ð`¯n7‰bܺAO¹)â.ôéÀ]î¹þ꟣ñf˜ÏfX=cšiækðBlÜ‘”ý g‹ÆnF9éí=FG¹7ÊçÁöV§ÉÅŒxÞ8:õPFÔ«DÔY©áN—F eguåí!ø|&ýkdÊëSUâr+¹äëlÛÈTÔ7)é‹é§$ ¦)$)›ÚvQ`"<óãù2Ôœö;\*`ùŒ´¶ Àìi"5%š¤BWs5ôÄÐ6“ì–ƒóH¨«QP1vÒ ‡å”8j™.”_Þ3äù$'ª9åD>Œ3ô¿†‡0ø: JÊ8§;8°Q }€ÊÌè2y®¼ƒ§7ðÑÍ»D…õl»"nçŒöøÅxy1Úu^ ñ,ßtzeôF'jv~¢hRjBÊÊ ©èc±™ê´eÁÌ$i icÐÅ⇽®°¨ÌJRgU5\±zî¯@³–ëWMWï`Åq‡Š~6 ßæ°Ë0ÇØÍ^¡^0½ËPßÝ0xEÖ/–`.K·TíÆ:3L%œ¶ØÀ)H¿¼æ¢’JeSŽheáõk¿By2ýÊÜâßyÑK7©õxZQ¥ À˜Œ:j·1ê%­ß›§4@¦®OZWÅZ pzgœ‚÷¤ÓO¸´9ÃíMÞ£}&7&²“CCÀó”“*C ÌâÈ &x[ç ïýlùäm •Ëùæzšg®°‚ùokd(õÜ6%¾dTÔ×”ž¦¦^È6I¿À ×{.Ñ{÷D%ŽÓÅ1Pú¨ópÔU(\©2UaÜž¡ðºn~_tû™Ù˾@ÛŽÖŸHRËïõÖÙzŸ•v\¾½¿ƒòŶNíW\Kªr›ÃÜ¢F¾®IÙ{À×{…³¤ùLŒÝn{|vÛ¢<bXÔœ’¥I,l“Óˆ×øq|NæÊ?ß4§Á³nÙMå›h2IýïÒl2Fß‘KüZ¸^8¢" ãNí­‡Äu{ÍÎikÎây8Áõ‡[0&1ìÙ¦KËh¯yØúÉ^ÿ´ÒIˆfüo‚Fð4€jØ*z¦P‹®Ý¼µ‘WÒ§â ‚ ¾$¥£¨µ•w<46߯‹âf\ìè‰NÅfÞ ËSx4éÇ?|¼ Tï-pfަ}±™I×sÝT×Ñ oˆ¡´ñLplétƒñ½Ü’Z7;ÐBHRM²+Z ðì핃Ã0S™©QFá£P)ÞÀ‘î§èã/^z¡ð¨×­Ë ¦¸ê_°ý’ï©éÍKám&éµJôä•÷Üø–ÛN¡ò<žO"Ãë¿åʯ"XÌ òF¯qT6  ¯YaEÿEî?Ymq\l(^¯Áå]0mèðHÒÏ1Âb¥“¥„·3ô»V£1sYy4{5}_“Žlê よ(NrQ9ò-ÉôZš! &Í?-òy‰QÜR”m/ ÷{W5fæì½:Ò7¹Y8‘ØIš¥Â1 éU”&‡qŸ!N¢<÷j‹ÿ’ œ/fèïYâhÁ¼çETÝé=Ýʰ3µN¿^mßTÍÍnñ•áNUm^M×2|’×Õ*ôÃrÀ$ÃcÚ 8{ct¤$¥»E‘î”à;Æ»¢pß¹G-ü1:ÃßWÖóÞ üо‚ rn ”³¿d>u=Üä>µÇóªÈƒÒùÕ1J CòH-±¤²4xíÝÙ;­¯sð¬òº?=QzÁéIr{ŸNýº{{ï{ã£æÔÿÙyè7÷¿ÿø|Ì`ÁÔüþ)mK“ CÆ Ù+Ëù1d‰Ÿ "YâWW,)N?S±òìÙ&˜JR’®‰RxФÂÉCÆDK7pÊqï¯8-æ Tx馸ôΦêNµA>S´Ö÷iœ¥Á1BM#²†×ÈøGDÈÐ×G;úÆ::´_ð7¬`!±t]ƒ]cæ8Õ¤_ºÄy«<âì šÒülDèqX¾&gðíßDä>ú˜r²˜£ìûptÕÒ–p{§*+ƒø2Jëê ˆ¤wÞ ìS¶)ôµ®dÜ]ôC$°¸= UDXu*¬náv„°ø}êX-*L æƒ¦“ÃQJKíš-ô󘌒’['pp,¶^Šx.Êö˜²í&^ÙŸAÊû*ëþZî?Ç‹©ñ|©„¼÷ÃÝžy<¦#L¾ÍŸ‹­—Š#E±ð]ËÐáF•GélÙqùF¡åZ}‘çEµÀG T£wLÛȳhÔ”žwV]‹\è¡©°9|a”ýÙ"AÞ&iÒ´‹]ئ¼S®š“„¥ißgKn~Mã ¾ýÖ%²Ôy(Ï¥…J¤0Š/’í½’ˆ±Œû‚—þâÄ—·Ï8´Aiè,µ,⪙ê‚“¶âý¸¼lžKUœ6qoAÞ\Ã)ªãÄyxÛ6³ë9ˆZô÷A_Kaáž·.Z v6¢žsÓ%nšâò!=^1³1=¢=Åä˜_Nsq Ù#·ô$õ¯’ôÖ… >N´áÖ[êò:Î/ÙU6uS m!Ïsè–„ÓnøQʵ…,:îÈz _Ü–4n(ÇtOŸÀÝ^7†É§à%¸t4ëè÷0»} vìÊ„Ok7 ÇâÊ|Ù­ôàí_ß>&hH7ûû˜QSHMøbs©!K‹”‡ffV ::’Uä¸àL¢†N”é¬Òç”7d¨ Ðžºþ/ˆlGz"WÚ…Œ¹éäêÑNV°ÝD¯lŸßÊ($ð>£hOxÁ£«³5:šopETêgaLÚŸ£šKŠ?>±ÂlÈã&•ôJR{&˜ - !†v øÒ·:º™ /ÿÁ#'¨f­{Bqn0¡;R…p#ªµ·ˆ÷SoŒîØfž’5 ç°ik}™دuKHêSY¯ÍÕÚF­ÎúZ¢±>-È 5©QZ˜ì@Ëù”b®‹þ1ž*YÍR°(¹d§æuOŸK_Àzޏ¹:°ÅÐÚµzBÄ{cO 2LëèèSFÇj.†®EN=¯À¤´C—XИ£ooÞâ)‰]­¿÷¸¢Þm˜°o1¡«Îî^©Mù³wFÛ¾%Ÿ474µ>‹G¸¼`bŸ?GK>5>Žæ”κl“Ÿj¿YŽÊŠ›jò š”l9âxExi¥á]ÚI0^ÀÎYãÀ‘tVmê5â®—:ã“jG“Hh¾an k!¹]`nëK¶—^¥Þ„~˜9«¨?‰³XRd è5ÎCÞƒò禮û‡–ÅÊCÆ ~ V s½9OËåuáVÄÕ !a ÁÑÅ3ÝDƒ]Õì1¡ûHɼú’Ä ïåf”×`.ÂCKK,±.V(­É(|ûx#6n Я} Ћ!è&"Ô… Dÿ´ÑdÁ¦?<íÁÖ»Éziü·ð@-ª`Ɇ²)yñá¡é–<{ýÇOÞ UNh?FÀlZ’koc—}„ ûŽ=¾ûÍ6D·€†ðåÑ^§Cñý¹.É3žBƒ«âsËûŸ¾tº{þé©I¸c¼’G¼À17`'û‹HP¡ÆnÁ>¯‹L­:D#¼ë“ŽF N 9êù5M±|bÀV‘J˜€)õ%ÕJÚHm;GsL$ꫥʢKdÔBZPwœ ÇlŽ!°N˜ñ]«CVQG¬¡4Ññ;JÄÛQÍ^ÌéTXGzrY_c›kìn^㑸cÙÙ¼Šº—Ù¿EQçq}ž¯ÉÃ9bu¥3»ØtuÅè¹ætNi—”%Gé,6Ä̯©<ñqóÕt¿9þ ôú'°Aí6|`Î¥\܆-ÌÉyGzºÞ€Áf–H¸“íl)z šˆ)·î ÄiÈÜÆq‹©•N7e³ c.ų…H\„ÊG¯sœí“5Ä›¤ù¼l¾9ÇD@$×(&éŽÐÑĥСï™LÒëæ´«‰¿5\ æµÅ±)AÍŠxïžÏ)¦Ó ͦt¦¨ …Ø/²þTz(´¼ÒíLTýÐ\“p|d™3ÔÐÒ´ùÆÎgq7é¡ÁQ,¬ƒ3ÓgŸ‹'èxäwÎ]0´¦àaÀõéZhÚ%æ!IN£‡èOó0|(ãJÊÆµ/‰Õ~ñ,¼á°³Ú¼€ý&wžÕÖ@ŸŒŠÀ© J ÿBBcqÊÄD“°ÕóÓõ{µô¿ £¤p³,Ü€kï½iߠ•-ÄèÍꈶN±(ž¡›l–¬ÅZ¦Ê+.”Û4ŠšÞŽ;fô²„¯ ,Y­áw¤é­ "Ñãâtþi”#Õ#£t„WÔ1ÚF"ççPÄ-¶t°Œc4G)hŒ‹y ½\0úùI6;rä~%DYõ=p€™ÓJ±òJ(Y”ruYÇ3Z-¿bŒ'9’ÖUäÓîbiGª1xÇb[[ð ðVD©ðþ³úG‚µs1X±ÜXFî¯D#È#Ö²>E©0y–I[!=ÜZUY„åV:t|nƒ þÕ¨yÒÇ‹åÆ3f×{îƒàÈr¯Š¡Ôg¹¹FU¾°Ɉôl–+³¢8bDÚ†¿Ûðúchô·NþÇø=øÛˆó±Ëø{O(@…ˆÆëO1¶²}î5Lv¢aÓ¨Ce2µ]LV k»~§¦^b[/7nì¥líåÝškõtßÞbNÕ!Þã­M9‚Àˆ3üà9r˜ËW¾ßÂ\4²çY–f{"Þ“>€Õ%¢ðZœGâδoõîCä{Ç/8‡BÁ??ÐAƒŸ‡Šmpÿ\NÇ 2me|ÛB˘Kf´°;OÒ[ÎÑ»Íøï– õïÚÐöGŒ,ÛWÉq ×äÒ `„¸¡%KeZq¨˜ [ ~·!jÞ†tÍO¢ÖnvoÛÖð^FÉÅü²Ù)_B¼ã»s/zÂÎæj©^a3LzÖ)tfãÅ~gÝZ_ZÄ”¢,…Í1ÒóÞ¨Ù¿jöo¡t­¢gû®ôìß’ž”Õ€ï¨" Q{êYZÑúSVK܃՚Ff._àpòEw| ˆ|‘Ñ^6–Ý>‰O¥ ¦xÐÜÁrëGª#5©"€þ†¤¥i·`{C»vì=  ·!€GÊÖUÐÝÀcq,Û.èl +ÌŸrÀ¿ý‰£àoˆAå(ø›Ò jüOS>¨CN,Ž‚žº'Æ“îw·^ëI0ˆµÞDF(`ñ6=çé]lÞœíÍÎÏ‚ LSˆA1RÎÐâ_“§Dâž‚äÙÚÚ:"áƒ>®xIÖNŠAFÐô\…¥Á§ÆÌ"Û`C®t™WeeÎ n†H]q UŒ êåV‘iÌ,Ë– „ŸÁÃÀ—vRXv¾¯KsQŸA¾GA,»íÃ7‘ ›E‚8€Î J®‹·•1(¾ØÎ1Úq£T¤ˆS‘á[ÊýCÌ\ƒªã÷òÛe|¦V*Áx{§NH“c}íïWT_ßx\Ùz•ªkU´ª®Ìç{_4»âμ CiÖC`¦z™âø#BdÚÅU¬Ì²-¥4¨¥§Ÿ®1˜v¢òTEr70ŸjÇÒšöWÜ&·]Æ‚¦,zXøß ªq™ã#3^$ÃpÞ¡›fLÅ¥.ýêÒïýn³Î&®ø}Â!,>‰öºðw§O¿wèÉ®~.~ïê·¢d_ö©}?`:f‡ îPù+Éçâ÷n±LgG€Š}ô»»Xï–~?ZùvW‚é!˜GTð.¿ Žå=€Af¸‡N5‰6ŸLâ&ÑæS¼É´ù0Ÿ¶!ƒ)ßï­ Ímø¿G¡Ù%“>Á³mµŸ“JS·z¾šÚÍ®žà4Å}R*"Ò{È ÷4© й½~»ô»cüî¿{æo ¦w/`H6Ü6ȸý_ñ»küMóé`h6bZÛîÆ¿{æo‰M÷~ÀÐHõèñ]~K0Û÷¦ÿ‰`¼êðæ+ËÖŸ•I{à¾Ò7OizuÝ)J*SØ’ÄÊKx{ëŽ $_F«~ÿlwíÓ?Ë [‘Bž÷Zz7=<« AëéëÃã·oþúôíë7gO¿yþôÏÇg¯¿þúøùÛc uÙks:"¯í¼´éªê®¬·hq§%#í}ÚqIZA×»àÄgM?-Z%F˜¦I:O“x„>ÈYæöÖý–ƒx‡1¼ó¢•õ–õïÖÒ.¡ÙÿIFÆEc”VËlŠ~ó¶”jÞT|0sÛÆšÝúÆ'3›³ÇÎé}¬6;·X_>¿“™©y´¼ÍJý™œÊlH?>{!CKטXÿ|g3:a9áÔo}6£O ÀZ›¼°k¤ ºÅÙŒ:Pçq ÀÚs 4¨n»`íɈ€ÝNÀú³™5£°þlfÍ(¬?›Y3 ëÏfÖŒÂú³™5£°þlÆ= ¿œÍür6óËÙŒ Ò/g3øó3ÍÜ^oxl °_Nh”1÷—š_NhþxBs+AÇ4Û«Ä/ç4¿œÓürNósŸÓx¥x%ب"ÊYŠq¾P¾Œ&³(S×VEL¨’—ÂL¹™}žrJ5Yĸ GQwt*8ç´BñÜãÈPþ0âð£“4½¢xàƒµÚR¹NH :<(*dt3—µUªpÌaf±udBi|ÆÈ6ºET¬‚eØNñƒµv”GvüQØîûÅ^"ÖøÜ*'£tFî+@\”Í=ú*¦»¾Ð0k `ß‚—fÎ*ò§f¦3ÒÕ8ÈuJC²ªv™R"äU™(–®‹ùyóQP/ o,?’™~ø‘æÂS¡Ô¸rЋåªøcšâ]°ósŒ,6ÂÍòoz­GÞ%À5‡Ñµÿ·4»‚ÎM"Œ¯;\,ýiô.±÷‡þF§bž¶Þ%ï’·—ar•·¶*ûrˆ6iF„>ëB¥ 1FT³b½˜ƒ:.ÛÕ.’µz\äw»6;ólÂ;ÄñiýwÆ×¦´ýÉ’‚ë¸(ü­*Ñ”Eàƒ•`ääÑ)ôG¦5«ç«C“œ¶T‚С‚âÏnWVÀ0¥»”שp˜x»ZŸ«ñŸÕ¦ÙÝ¡†Â€~ÙÕv·ïÅ|’Cá6ÝŽ?KkªÉ8Á[ÒgK`g7'ÍíÇ{ÍÞ£Óß™ïØˆnð>¦²ÙÞëaéî9’1ªQÑ Jª¬Åæ§»æ%CC`qê¡=Pø£BzFºi-—QLä_Ú<Ÿ [Gƒí>ø'Õ]½T´_Õñòž`¼8ãca œ°A…}Ìl·L`G[±ÑXìh-.7V…ul »ñŽËh+¾Y:e>Z]²=jgAh4ÍgëXiÅtRla²„1u¶iøÎ¶Ä‘_Ü®?åj.ÛØ5±0ó®I-!±`Ò5Ä;Ü À6Àº‰¶"¯µëV†jU1Rƒë4Ë1XêhÓ‹ª¼sŒ€‚Sg¨ë`± íq–êÇ{Û†ç€Þš…óÑåYMÓñBiŸwëÆ-›^$÷Ù¸GÊñ—2-è ô G•ÙNµ3 6wSRŸLL(çÁ»›Ñˆtèyf(ÐK‡Î-¶¨ªâb?XU¼À›ƒd©y]¸¤Ó!e }³]Ú*…~(·öÎcPÆ}>øåH~F=%{‡èû%P0¦´ÚØ »70šôñÕžÆDÌÂ8G÷UØ¢!\à#3œ¯7Ü`Á0 æœ ‘X?}ñBá`^h¼y¸¼[Ù‰‘~F­‰£» ÌGqP·‘ÀHЈèÓ†¤¤4©30Ùý4S k!G„k§xcùðæ3Àò¦uv– =;«-ï|#î礪Æ÷æsÀ÷†¬7Ÿ m ×Ï„®Âñ™PV`û™Ð6û¬h›}V´ý¬HûyQ–TëÏ…²„ìgBÙì³"möYÑ–ÒÛ`Ï…¼áÏ„ÂÙgGâì³£1ÝÄüL¨K¸þƒéÚÞŒ¬âR+yT,uF˜v ›hßüãÑÞØœÅçs£µõçCêì³$uö9’úûÃO¤UÈTt70ñç…ó²áÚŸÎK›ÎŸohœoþééìfõòyµežÜ߯Î×—‹¼›H…krWFœô}õèÀaú_”Qs7J'™» øex#JÓ|¦å^”„b¸,•M矯lÚþ´^Ú¢¿Ù©³Æ‹¢QÕ1þÜ’‰´mÙ¬|\ænŠJ>2ÈîØÀ¦ðï Þ4‰œ8†JÒÊjË´Lœ¬i̦Ö]šËîÜ^iËì¢EöZèì?¯4TŠ L†"àlÃ…/«·öŸˆgyç]£¦Û{m‰…ü¸}ÇÞØò.yþòå‹£ãÇ’éJH'¥'qáI«ÕêÛbMîlWµ:ÑÎOGâ)´´S}åéªFÜí~;ºWÞV®jôbãÝyGOK»Â•Ô-Ú½ut´´§[É;®¡[ÓcG?K[²Ÿ`@‹kGiCu?ãiõóÓE—¦¿B¯Ñ·7©éÖz7]›ÜúçºÚžj’OÂydønQ\¸}ÿdt™ÕâzñöO·¿S?µ Ÿ€®Y @ÅÃ;JA/p½ý•xûQ¿Eÿ¨Ö\"P Dl7ª$3“'½ëùGtêXwKVVm`54è]Sýî^w»÷·v ŸV#‚"Ô x?@gö #éWðáAð£¤'âó ŸM¡ƒR°G1¯ ˜ d¯!.Ñx^{—þÉŸÞÎÌ™ívñ§#þt¶»=øÕïìà×þ¶| u[_Χ³/1"C“HL&QF_¿Äi÷e>¦-1ÿ§V˚ΖY|q9÷kOë~€4»ÐŒøòíŸý£,ý>Í1óô×z< [ìÑ÷*ò_¾xúüðøyëí¿½õŒdÒž ­M1”µ™[úõû({G×*ø=ziq9Ø5MÓ÷h9›]¦“ô"†ý“žŸÇ7Qα&`ùÃ'Qøžü’’É’BšàsŸzç N/tp v^s¾í¥Â£Á«(#€Éª,Öiw+­’ùž÷”²­ú!ynÚíÈŠ'0q¸ˆ I 6St‘¥I^«û5ü›&a2¯‹Œêe8­!^"„–ÇË­Ò;¼‚1Äûºßô;Â0M'5Y”š”=ùÎMu9¸óëp’¯,¸í.hᾌ£Éxîüþޏ[-…Ãh‰‘VV4¦Š¬m¯gµ§†nŠã–,¦CâôÜŸ^G“/Õ8Â4úE”Œ$—³'EÓ<Æ!æž§^Ž18Vꊑ‡¦þå_~í·EIº?eúæaùv¹Ú÷EöêGŒ¹€2§û[äHÆy©ì2Ì\гžAê1ФIÁÈc` –„³0ä"äáˆ7ñ©%ôhüžÉë÷Áä®N;ha!»Á6Ñ1¨1N°~)â1è½$èø¡¿ùìŽÇãÉ*® <+‹IÚuí§djBËŒˆEÅ´WÃë迬¿Mõqˆ°î÷Ù´Ùas”ìÜ: ½2FïGµ$À×mâô;àŽ•;TùXÌ\à…ÕªN2Èû@cÀõÇi”#N««R÷K] 9O ¹&Tôœ_⽋éTÍø)Ü]b %K¯ô”ÛØ"”U`Ür˜¨ Å"Mü÷a§‹ÜŸMY8‰ñú2éS üв £oŒÅßéb"D Œ„;`–S¨‹,âOéuÂ.a1œðÇ<þP aEsešÕÇ8ú >ÆÐ¿ø©£ rP[é}8Á @£q¬¿fÌÀ®©+6Y¨Ù$ÏW#¿ ”@‚Û \И”d­g¢ÚÁDÅV8›g)%‚ ®€‰6ŠÍlx‡‰‡N œ69jrÐŒÇ8SHä×Ñ?Ö Û<¼‘õwÙK?ŸE£ø¯°-†£I˜çeáƘ㹛M«Ú¿Ùªó†Yì *!0×,¶ŽG—Ùÿó¿%W’kAOÂä*ð|ïà3f „îýXŒ;U\lÈÝÈ5üø¨)0û¸*-X$¯¢8‰…!~ ªZ_Q'J0b/m°C›_’jçÉäé2‹'ÔTßàƒn3§¸¬"²·è¢­`Ä+cqæø…ªý¿ÿ÷¿ÿÿ?ÿþ?àïÿø¯ÿþÂçÿú_þþýý¿ÿ×ÿ*`TÙ¬ð}„Ú å*Œÿ[(€šË¦§Ýu¦§ÞNw»×íÑ×>ÿiowvÛý5¦§yxñ³Xž¼·áÅ…atrZœ0Žï`@Bð |¾nˆ—ãèÒ éËQiªF,Wu°ØJ?˜c[cq+öACÈAqw¯îÑ…Umó>œ,"y©áû×ü‡ÒÂ4q1{˜¾K‘iu¾ôÉ0Ëð6Þ|ɲ…`7¼|톹ƒøŸ ÃH›ht ͼ5±$ÙLïàÔ˜/f XxƒA pjp¡ú`ÕôÑMˆaY…û€f¿ÕUEË­b0½c]Ew“t‘ðB~ JBAó ?ã&5‚„Ñù¦$$šÐR3¿Œ&ç\8£-Ïû0žàrÊÁ‘¹P¿¢$ñß‚þ9¡‡åA‘v…°• ¬©gD ìÊu4 =çøuÍz[ÛúSz™ nâ ® ®›4„ÃpÜÚªoz½8êõQPoøµ­ ߯ékíàë?ý‰¿b“’Hô=—kØ·Oþ:áÍ.˜a…goù+"i~||±ôÊ(°"æ pÁ+9)MÁjo‰ÕÖ›QÅ|,3hz>κÎâ9þÕ÷FQöú’Ùû2òô\–S¦É{ Kî/æ1EÒs–:XÞc¬°a4¿Ž"47ŹŒ"eç\U!¨h¢^–£n”.fµÑl΂™»TUjt©.ŽUô%Ž“ø*G›î^Ñ±šž¬¢6b©§,N}a$ð?î8$ÓÒ—L.vº– Ðg†……á`àDJ„“¤|]>xK=™7‡û°iøòÛ'Ï`‡úå‹CŸ{‡[q|×úÒ¸ â Æ5[]Sr/Cn<ÕÞjú!çx"È™”óWÀ¿óþåÅ¡œö`‰© Àœ6ñ$T6YuQ×cEŠ"()d'óD3O(6€’E‡K!uñË` ˜JJ\cŒ<0Ã%ŽlåÑXK‚e(÷ìD# ƒLÒtâ`ïâÀ§‰ÂØ3l~´Yˆ•[‰…5«tñ`‹ÍËp¦` '×FˆZ›/gh‡N Cï`a…•d_r-¯©ÄáNÎÓ9³È܄ţEÒ äÑ«4—¡öÅò”{ŒòØ–mW¯Pì ñfîbž¢šÃø‚í’l©yÈGi6[ä®5z‘ÄY8•ë".wz*ú®xÈ;hv§À2õ9-Âö M’Ô7<(î_‡”b($÷¸qƒ‘~í¿LC’Ã,½N$zŽ-%¿‘Â’JëÃü†‘iE’îó³–•8ny 8'ºHa›ïXØpW4!`¾YmEA­}Fò¯LCfÇZ ­“½~»}Z¬ŒHÖ œôÛUn«eÒ«6·/ÑBÆÑ(csž)‹U›æí˜·— OWíLkädNÏ•ü‚o‹$ÖI—/_nµZ ­Ó¹ÈMÁ;)•¦Bë2äpŒ9®ŸÃøXPè¡Þ-LIt…E•–çÌ‹ÁÛ¡t/x:õ)¥c€~³)h€!:À¾‡Ç3ŠŒ#g†‡Êd3=oje’rd«É‚`Ÿ3>•så›(ãY†Æj*ý–Œ&>åðåvAs4˜›‡”ŠK~²˜vF{§ÅÒÑUxJ.ÔY„‘t‘hFÂ'Èj^MûÕ’’ÄÝ̧dçe§¡‹pÊ4e yŸ‡°'̼f“fu…“&Œér@eº€!ÆÄ" 3 Aƒ^¤OX- nC;S]s<´™g"„OB,’ÚÌQJIRÏY쉪\s0=…H‘ˆYi÷(p¥É¤D—B1 3{æZàkÁ¿ýíßÍôVš¯°jt^‚Ô ¤öP×ÊALfT‚$5kãkh}#ò-ø¨)É=ޤBYÖÆ&¿²ˆ”*‚GçZÿ{ïºØÆ, ~·Ÿ#O†¤MÑ"u³eIYv&Î8¶?ËIÎŒ¬‘šdKêˆìæ°›’Çû,û,ûd[7 ¾ÔÅžœ³ŸKd7P …ªB¡ª`Ø9ˬ„Ö´Æ/Qº†—À„Më†È¢ Ö \j]|#z…æ=ßÄ8²Ÿìþ0ÚOþ6±k?øa2Î SKüÐ)óÝ8ìûßi&-+IÃ39%ÈsC ôÒ^:Ú÷y»¥‹±7¿]´ó¡?vš AÏ/…EÕŸô§Ã@Ö·(vàà¶EÎ#äÙR­¶”ƒª7lـ΄ã1'® S9°ÑoRઅ.âaUAiÅ/h£—ÍW˜Ò"â¥" ¯wñF ˜Lª-pí+xÕ›ŒýÞÔZ¯rž~£¾YkµO¿ù†nDpF€E¶VÔÃ< h?AO«Œ5Ê2Í`¶×ZßÐ3ÉÕUÊ¢X¯äˆØÄcViM©I§ËÚ6HÙ…†cíodR¦¡R‰šÝ RL̻Լeì‘‘‚aMÄÍIsp´Dã[F/»ÿÀ¤öRG¬‘QÅsŒ*ÒÇ4ÆÜ²·îbþïÅXþöpeùéÑ£z‹ÿ6¾Å›1jû´ÿQ°ô{¾Ž7!w–Ä=(uêïÐáß÷~÷ß‹~÷#º\CÔ^Ø=´:å«¶Ò¹MfÚÈý<ÀÜx¸Ì.K+æ ùsFÇ…Êh†"3^{Å{®YàSÆ&“Âööݲšº5ålúSRO°x¿õ·ÒqÀÌM¢~YÍ ÿgk»ZÒ°B™¬©U}×ø+öÛFb]–gÃ@p®·èîŽÂC+·_]xè†o/<îYz#ÄD|ÜR~ŠÅI$'¨Ÿà.] {1ß}9ié˜fš°NQd›Ã@²LÆÜI´¸ô¹ˆdY]mµ¿¡CVÚ]—k¾{‰ÂÌ"šÖ»ñ¥ìÉOœ ¥6è‘n)„ƒð"L=½õÎöìÎæÛh‡Æü&vn<&ÈŽGÐ'¦ -ËðXRLêÄZ“eû_ê@£¢â}/q³Áµ—øò‡×¹UþÃüL¨0[즨^ëfåX‹ý}a± œ9 ^öιÅþîÏùÅnïÂYð³;ü6ÐxÁk¬U.xüí÷÷œÅþâÃ+kÐùŸdÁ›•¯öVébGÄoÀ¬[Yúf]—óO¨Q\³y± c¸Ó²Ïí"ëþÉzk Ö=¬l´ƒ•®îœÅ^m³b|êi“©ŠÑÔ¶¨„),ÍKdˆþ•ÞšÕN;n¥~á­ ‡}ÖÄq§>†¨šòA‡> )ô†Îßbhr|z*¨5†RꬾÊâ Ø õ¤49‰(\$½Þyë]éV39îÌe'Àå¹[;CΠß~Š;‹Mò“ÖÊ7z£ãëNÙv.?ÒI‰/qÍŸh9†M`oy§ÐGÅi9/Zqû肉ÔM;ÐÚ]„f‘ÐEd{Ú1 ø‚@ Þá‡+(IyÅÌ×ÉÂîZçÜ™ÃÎY*>mê»ò§†¦è÷vSë˜í$¡+3N<>4Ðä2ÐlËB+sf·:àçéçQŸûyI§Ì­üŠÎ­Ò$»¼£½ÖÙèÌ, n•²ÙéxÞój‘M;ÁnžÐ>¦ÑÇbù‰ã¼·zñz¥b\ÄxS=D¶Èë{„R¢/ûÀ‡-¯þŠœvÑÞ¼E-qŸ–t§4­„úXû*V£0èÑ~Ó³ä>R#m'ÀΣTZÖ'˜Ðì%9€¢ô{¼yÇB›)‚Ì®“Q±£ªâþs@Ôƒªšcº!ÅCFE0ÌÑ1Ž'gçî‰!m¸Á)Y‰¹kú¤žtÍÉ"x Ø}§“u(çüÔ-SdžÏe]»ŸêÞ\=%Iôal“³ú‹z³ü·J´ÝÚ:d9ãÐþlRf³¦WNÇxù:³<3Ô-WÃöÊa—¯‘h¹Í°Ñ†_¡ˆj²Üc…bêòY°i· ÒHŒçMcŒ,cZ+HŽ|çÑ8ðÈü X9‘6D…“ïlœù–<ãìôWa"ÒßDîxšOÙÝ {Ôé„$ÆI—† â…+Ð/Q¤ŒƒAˆ=ro½³g+è2C9º"˜EØå©Ú…ê²Ø§«w ëà…ï ëçè“WFÅÄ£í·E Íp-˜¬4ô†Këý‘'SŸ˜£+9?'·øæØ£93^:F÷¶Ÿ¥ïˆÒÜÃÚAàãíc™§EîÔ´™PBˆ*.) KO¥xèJZKeî¸Èµ»„ˆÂ=Û¦&žç;3s0‚ ‡½wãø6!iÀ¾9¤F={KìýÏ‘ëíf»ÑhÞN§Ù¹8í{ƒ³z{8¿ šs²Ý Cnî PG7Añ2ÝÛ(ÇÑ:2’uˆŠ ŒXãw~’£`>ñ_Ô…3O*Hüù7=›³Êv̧¦¢ˆ¨Õ€éi0B·ÙçÜ£i}gÜ^×¢¿.£ú×ÇÄ…vÚ+ µ˜ÉåƒjÌOõ>;RkëU£¨%0 *þÞIL÷ž3oEít2¾(e¨;Ú\_]ê–¡ÛÏ_ï½ùûëWo^îz9L<ç?¿Óoýò!¿1ü{«_âs2À4‡×À@—ñÝ{ämþ…ð/Æy¢Zï¹oTz·+¼eêcr4«éó”å]}´¢!køwÿÎ Ø„ûKM–A³`À$Èþ´=Kò^× àLþE À±sjQÝ{4[¬V©Õõ5ú§Ô ~Ö>¼Å½z#º“vèp}ƒÙh™Š!µÛmÕÞ éÍ„ôóóH®ŠO>¼5Vè÷iíæ}zóN ©'«ª½¾©Ô暆ôæBzónùÃëE€å‰YeÄ,&H)#ßÜäV°ß¿+ôv¤áç{Í[ép¹Ë+µ»üüå‹P€Èö@ZŸ3%è^g>êðØßh8ëü/›Ú›MÈ/ ¤Uþ§Vܪ5¸ï6cæaDuÒþúßÙl OWap«ÈO N,¸U^d§·ØØY |Ñ}ž,|ÿãEí—t_|#œšvÊx‚Òïúã‹øÝt¡-VÙîÍÓ{”Ì"¡Y=ô<‹¶Ø„>)°ÇO%¦­F•z>´Ói¼†1e,„6Oq³N*-ÆmIbŒ™JŒ*D?M‡ÝXâN'#Šï¢`DÌ*(lѺLµ®[ëNØK® Š7Edá.ªW;rœ_È\Úy[§ Ú ;œ5ܘLŸ>;eT ­˜E$R¿…£9pÖ!–ÇL8TÖyÏ^¥êEH'þx P÷ö¯ßp;[I¬jÆàÇþ´™½ºÕp €Tpé°Ìk[%bgw?VéÐ÷4õ¦~x¸ÒÚhª•V‡~5™µÚªs´),/£™¶‡`l‡GøTY±OqYËH4°Ø8qŒ1¤ËXjæFn`åè—í1+Çù¢µg|Þ›«¢:Å8óÄ­NU«ºeBͺ*ÞŒò걊bÍ^%ZÕƒ þM|Ù"(ßov›½£†ØñÐÛŸªHßw³÷ô½—}·/¾Æñ`b#} (´Ì¡ÿÎÒc{Bìñɬ¹‚ëêÖ°TB¢$…Õ£CòÛæ•õ¸™4¤Ý"3HwÔóÃäˆÅE VNµv“òh´F sÍàíb§@'ÁneÝ ‹ÝÄä+L³l9§$¶µÂü»í`(#Fu“:7·\º6 µ­ÚÁr{Å©ÍsóhG•W23fµBUÊ‘hZò¾+Sa0ý‡YdDì,ÎVŽ¿wãÆË|…tö¥CZ{Â[%ޤ#n šÒ8MEj²#Ÿø_ST|D=P6ëa+h5U7L\EåœH æ¹Ðjkm¥=¯îqp}îOôZ;^ Ž 9U±•lðéÓÕ§' QU»µ¾¶¾òdã ?|³Ùn?]Áˆ†å<Çüä¦-Ub’ÀËŠ³ôz™ýd“ÉØz£ìœÑ'2³ƒi+W<²N¦,+ù£Ì©™–Rq„[ÏÙPð øÉYÈGF²2 ùæ&QÖ@s0g—Z"ßù¤!Å€d8tÜEM|±*Žº0Ñ4Mõ ²qÀÓŒz9ç=IvÆJk™›ÌåGÜ›¤ˆe>㥸0\?èçŠQSŒIÇqÁuo0雑ãåÚ—” ©;5½Éù^Èv=÷ô,HÅß@@ó›ÏV²‡@ƒÕ îñZkÍ:^"4r²$ˆ‘2€œzÚüšÿD–¨#r˜¡öŠ]r˜IPuBøoW5MÆã[pǪ`?_ j€}701Þ æGÓйã(€Ýx|Ç} h ³u–­rV‰(v*DiÜT|àúÄ]pýNð…OÈž Ñf`\ Çà…Íz®ÓãK¶C×%l2¤² %:J‰Óð²ð¤½*•hK–«cñDâdƒ¾êN0D1‹‹ º`%W˜0 õpå-y-ˆþ×ÏZ"Oü;Œ}q:ÍtPÛ>‰ÅÔœ7;Úѹ µ kë+9õORyPœzÚÊÏi½æw{ ®ÎÎÃ_/Ðàê%‰'Ó×®4U'ûúæª./šjM¾ÁGØ«Êuè‡ðD±¤¹=„°Ó”ŒÅ‡ð¶¹’Tÿ>‚œ~zTL[»¹2?wÈF{cmëlèzëíµyikuúWK]ûAÌå®­c®£ýs„9\W5§»è5sªÏs!øá€¸uvf,‘ š¡…­,;‰iÄ4zwšHÊå‰ÇÕDÞ&™^>ØK3’ÒF¯+c$Ô‡Þ™¨3¾dèq,‘æeétµéÏÉ€Áé2F˜c¡˜3·Œõ¥¿«!ƹDÈû¡ù?¯¶ž<ùòR½ ®Ô?@¬À@Þ T¨»“)èd£%w—´„º#~Øú}Œ>Àï"iYiwÜÌÉ¢îÈ5ìn¤‹ø{‰)¨;‹CöÙ («"uZ'Ûà®óF‚\l‡ôºŒØ};Îþ²M †ÇÎó×#g.&ïs¦ñŠÑÈû{‘G~b2 h,­£T"åJÖé÷0S“0‘p‹Mê ²…üäd8g+ j³õÄŸ6Øç§ß=YÖÉm`a%nŽŽÄ.õQZ Јôñº·úñÚ_Cþƒy)~[Ò¹¾k“ôôIM3ÝI ªÕðžˆP;©„ÇI @T¾!ˆÙ;¯}iPړ椵•ÇXÿJú]e*jtÑdŒQµ½ò§‰ZšL™sJÀäs*Ž«¸‡(TÒ<›k5Åi4Ð{&`wIovQ•Ã@Çâ£%Y_܈8S´©¬l ’(A ÖNšäè wL³l™ «I!!½&Fi†pƒìXCz (u¼e^þrÂ’ÁNÈ wK÷»lç2žÅשµF[ö!Iq5U³Æyð†ˆ‚ 8@U›5ÈQqˆ–T˜3Ä‘=Æ×@sÙ»€€Á®ÉNí"Fµ[¹DæñlVJ½,‡¦%ìsfïp@JÙ¥œaâVj ·!5Žt?L0Ôó1î§3µ§óƒß-%Øšÿ1íªPIª`t‹Žˆ"ý©~P}a¯z7gi\Èn‚`z˜¹R7Øw¡‘Ú$¢zæQ‚p¿Ï9dãEžÏ:2 ’‹ŽÐGó§?6ÓÉw9Í›‡zL+êÁªIp*Ö`ª²¬p“©H]N´‘‚åSÎ>³Z^XvCÖ³’ØÙHpXQ4¥cö|æÃ„D§]B8%ù‰§›HŒ›á§)ø-…ƒÓdEœ¼ .aÃλ'ÿ–Q0Æœ2ï%üþ¥ ¿Ÿ©†8*‰Î  ËŸ(špŠdã•H;°lã% ‡¯ƒþ½,èFÿ|ªcÚš„-«ŸÒˆí+Ô†>å±õE24êÄR³2ê£A˜æöš™ÝzþˆRÞþ¦Ó}kçè1F,¡7ý” K*à¹è‡KÊÎv*ËžœñÖÚáÞò?>^=²ò‡êÙB$·–s{l…Ê^B¨¼l°¸¹ÖÂSNÌÁèÜï)¦ÙÕÎùMoGÁÔNÉÀXÀYáЮ(Ž–¯Œ6—UµSÎBŒÿ÷>üØÿØ:‚O66J±° Ïæ-¹´½‹»û6î$¿0`²zÓx@–á.áXÓ²äÕfû&&Ö cž&w‘¦®)ËèääÌqÆà““…Ÿ<‚~šŠÿQ¬ß]!§ÓwÏÃè4yâd÷œ¥¿Óɤ9¬0UuŠ Z8–õæª$M‰“Óí'Hd}ø¯W“£G5kß‹ûê1_pO° µBÞryÇíè¼0úä!hòËþs­'ßÃðonsU¿EC{ CÄ6Yœä¼šà@¶»Š*©žQSKʘ‘6õ*ß)c€_ÃýO±¼Ý#-ë!™-Ým‡tÿôÙj©+Œö¿­-΋$¤ Oa¸ãÁtžIæÌi® ,ßôãÏ&xÔ•Eº–½–F’$; ÐO¼¸:A…hsÇZ¬«¸lõ“Ô_PÚÞÒG5îvúiVLÓœZ¦]Œ2°ñÍ\QdZ&(xiv9(à3H„”¿ÔÑu£MÐÑ æ;IѶ}kåˆ'Ô¤öذ¥Yšd“ð±.»Sƒ¹ ̆gòlÆX¢q³”ËRréNþÍ%r©`¯äÃg7µwËÓÉü2Ó¤8EaáK¨O3GÂÕ=#hpÊp¡0Žlà5EñÿºE † +Ø™¬òfÀ„Íd|R÷ Åä—–_úTÂ;X¾ÑVÊ;9ÀòÖJÎÂÍñh€3Ø ÒžÄžgûä ¦„sÆë£ê0•ã°üvÌÏA²6Yœ“+C"öê†ëa„Î L:õ"˜–`FGM<-ã”m–µ‰§ï2í¸8qìQu_uU½§ú†‚%¥êgšÞ#Yïp>hâh°̽Ã,à6“c?¤\''?ã¹89ÝKá,äü}êéÙ1k s‡gsN»}Üé;»2Ä8û£cì5`0º:¯vS"‰5˰¥~’Þ {ùu ÓÅâQ^Îõ\ÒæQfò°— 5£2÷|ðņt ¥dÒn‰þ»U£ªÜƒ-ã$*iœK˜-<°ÊžvE˜´/YÎКEÊã`™ÒQ›Ç„r/k%QY#*k…5õ«'…q±CCž7A‡µ’Q^ß|"äb²_Éïx£šzÌ—gä4ô‘‘ªÞ$Iã!®,ÏJOzi}6ƒ¶Þ¹k i“Ûr™FÍB>¯OûF¦:Éסþñý0؈]¤Õ—÷”Tè@d•…ÅŒ¥’´†Zðhz&A ¬I”LFè·€Óí0«vz>TNîô<'ŸðnÝêXÃTõñÎb­ÍJ"Ã7Ÿfb•dó­â]ÉÛ,ùÐ%iã\ZØãÑÀ#ŠÄza™ë;˜<ÎT­“ˆõQòsf#a?ã0qçÖœszûïa’¨¿à‚€YUשּׁl4¶h†ñMS}»8ziª A)üËÂà˜Xtô3ôß×sÄ:!†Öúã©z¤)oÅb“šÊ}ÄOe1Ê(ÏÒŠýº¾%ÙÖQl873eŒüYûEÏ#Ù”;”˜üx¯ZÄUÛ €¾ýpLn(ÞÉåÞÜÅ„ùuh)Ó]åTH΄ôל!ŒF8)ÐîPÛ{L¬Fèé…Ï“tß «¶kRB)ýÑMÚÇ ºsžwÙ;ôèHTz©¾ñÛlyùë´ç²]µýD;äÇcJ–ॎûø.-Sèù4ž€ ¾‚.÷ÿô1ý«Ñf¹Êž&çá¨ÉÀ@BÕ®^ðçALgê(µfnAXiªM ’zÒTOðïÓ¦jsb–|Ú O›MÕ¡B(µºŠŸV×à=[…gkOñÓ:ÀZ_µî¢Þä\5æô¥³Fí¬¤µ<¤r¬EW5(ÓØí‡—?²/}l=R–ìS«y}\“>®¯4œ»Y‚á(•+º­öö‹SeŽéÄ<¥üªƒÄ?æ±¾°Ýc/b³c¼ªÛD®hÿßÖÂ"rEÏXxi&BÉç ¶uxz€ÎÖzÍâK†‰õQúb\ÎÐÉúJî÷ºUÚ ›ý¤¹EÉ„„˜„GyEŠ»GZ¶ˆCfÉM 31+•5?ÅœÊFØ‚QÏù“FŠp]ú´¤ñûCLZ¸EÎæGðlésÎ'™~Èí Ê5%XP³Ð"Ëhei‰%±Žä"–ûIÖûçOŸ?…ÉçO¸ä?²ÖüçO´è?²^вÿü «ÕùŠ÷ò·#×äo[¿X—¿«úEÛ<`<" ·ô¥Ó³ ÖEöòø}Ì/ñ¨šÛg›ƒ¯êøžt¨¹=XDd-Õ74É/û˜ù]²›¨ê£Ïz~ÀÎ4OÐKd_ ¹ªòÏà÷·vC«ÔÐöhworŒ3}²í}»» Mn?îîòêÃáY]B›øB[̬þ5 ®KÔLÐø§íÇ£Ý%;)®…#H`×ãÚaócëã·Z:ÂsZá«lãT´g+ù`þÿÖ:*»i™»A‹½B†3DJWùá+œ%²»Yódô°âì ¾• Ç“QÎ|)±Ž½8:Õ†F¹)¤Ö. b±ýøÛz÷÷Qc·Í@Øk úóXÿ…Çsñ\þHΑ&öPm°XE UôX‚cÜ—öÓ÷…ÛoßmÔÝ/ŒäòvþØØæS™û@7­éúùïãß ¿žü^VȼZD·,ÖsþkNÿYŒsT·r{d3ó€…„îÙL^s 3%š³k£¶åD¨c@¥¨Úhª±ÞÅã©Í88…‰Ì¹ÈÍDD«ñ±]»Å€>(*=)z_ržl±œ•[66᥆¬ ý-Eý†Ž/6FXV§q¼Û€;ð¡ñßq¼…1 )NÆÆ,˜Kíc«þ1yôûŸ±à$eþBc«Ú]×Ã" VÎ3¯ÌYÅ:û>*†·¶ç†·®¶7 Â*‘(וÎêÚúÊÜðÖxЯ¿Vtké^m“²‹LšœœH×NNæÖ/îôþ@`x3’“{¦¥Ö8¤Gc†gÔÊÚÄ\©¥ê ¯x )ÑÁfNÚtmš´@bÌü {ïÓpmwê§ý¦:E©ÚÀþÈëÖð"ÁÏuË3û'­Ó>õ«-]-Ùï[xõyP¯}ü8¸V>‡´ƒÒOßä. yVsªÑi«ÕÒLĘBŒî¹»y;m†Á ŸXWb®qHŸ8yŽ<²;fß]ìŒ} @*éFgÏÑ1﬛ªüŒÝº›¡ýK ªRb!LüOŹ3F“áâä€À*d.ÕÙ·›«=×Fµ­yÏ­q+%@u÷KñÍ'S•…<ûwÚûWÇz:ÜT¦n Í€QæM?*i€g’“G¬ýñücDç{‚¶ô|Ù£¨Pê?ˆÈŽÎ­1‘ú à:$·rYÀ‘sn¡±òOÑ<Š+x¾BIƒ/37G«nü† ¨Ñ@÷Rh¸;IµZsÛª©¸K{ ¶wàáW5Ýríà$WKÀ?Æ«Y¬°Ù“·¢,ô„®èHù\‹2 ¨tBÉ3\Å&Á‰}¤Ç,ýñEÀn4èáFœ°EÎ:òFî›c/E}U.b2¼;NÂ«Ë (t|XÞ_œ¥Å¥«[ˆLÝ4éü‡V¿F5»pƳPo¡Ô4í/)ÚÐ×è4Ýùåû½êàû·?½~¡^}Pï_~øéý›o¿ýö®ø^Ÿ˜'§UŒ|ñ<Šª¸NtGËÅ}6p0õh¦M¯ù®í<í~©ÎÏ çû§f3(òíQìÜC¡ÀÆ—äMW¯é$ §| õý-_Ón[ ý4õ»SyHGŒºm”?7ÂG®!/nkö›\“„3šü«1ˆ&ò†·u¯ÓÏÝO3Nú1¢¨pº”_Vz6…ÜŽß-Д ¤—#¢l™dIåöË¡¸W"OêÑeë_xµü·BWÀ'ã_W½Ù’rü-Š­Þ"ìg!4V’×ó·?Þ?–&µ“Óàôtµ¢Å ¯ÜU÷È—ËF:Œ;ÑÏ¢X_»—†ÅweË _¢!|¼N?^w»ðÏii6r¬¬º‚&| odÍ'…E„Þ/..,üÜ;uøj x¨ÜHÜlQ© >ïU¶Ý¯¬_HÎW‰ùJ!O˜Í827BNX“ˆ„þSÌÏ&'UhE-,±HƒçŸÉííYÛ=qHcö3Šù œ²í‰ÒÙÒ–ïÛ)”ȼ½´` õe®È²÷¼3î‡î@ãðÂáÙ‚ŽW§¹-/]7š(ÌL;Öá§ìMÌJáÀ#|‘)þ*3G‹KogÀ%€äÊy µ¦IÒWVô9:êȱAá~ïLJ^Çð¿㊠†Á`z<ˆ£³c1K,>«ó@! 2>»§íèù(öV: 0PÓâ™Æ‰òÎc=ù¿ÿô×þæ_yXo/ï<:ütôùãïÍíÖîãoŸm-­´ñŒscóÉÓ ºf)Њáchåܦ´ÏxŸ+ààˌߦeüö1uh™ÙPŸ=8='c22”ì”ç­läòÈ&öì±iÍ~h·(¼a?Ž’ 7!¿N+bÆ Ø¬ËžZ$NC2Í&›¶Î²wg]Ž1 Buüñôî\¯Ú©»õÿ¾ðS¿Z÷°nÛêZC²êºQÖ‹iú‹(6^/» E2Ëc]¹Qï¢ÜÕ6€I0éH_r€¦0EÓ~e‡u‘„ã>~<±p¹ÚZQjm~½Óøæ4»Ë˜¼¡, `N‡£ïýä; ©áY˜þ#ðǦUÔ‡ ß·ŒõskÏzp¨3ÿÌ~0j»P! !Yäsd‚7LYz †C„nÛ‘Oiß!ÈÄ:9G\ï`žÔ‹`ºƒZ‹[oÁ 5Aû1ÆùeçÖ,µÒXZ¡Šg†’úcô78\‘|jÛ电nwægwæül?Æ"ÛÕs£ïn?p7ïQÛôˆwý»Ûƒë]˜„íÇðw{”ì”n?†¿ÛgÁ.Ìæöcø»}6Ú5 OFЩ~ótæ÷à¹éž|Ó 3ïùnØKïl‚îPh3¬EÉÿYƒÈ5Øø?ëê¼®æÊ_;RµôÍ$k'§Ãxú”Ü@ÑYÐE¯Ã.zkòtc³3÷ ÄìÔ?O=öÍãxìq¤›»çµlæö0﹉@1pF. Y2ª·UG­*ûÒɬxí4µCþøæ~®½ª†j8Aþ‡Ëþü.«÷3UKü+¬XY¯CÚ;‡Pÿãî¨ú›wêUCÕ~ÿ@›½jÐ#(ˆ!FÞÛ(°ráÀ†q 7tTÝK[æ¾%Šñ¼ŒÃ¾¸œã,zúØÚJùÜCXÉ©—!ߨQð’¶:^{woœÇ\«õa:ÒN³‘åNéX–iÎêÑdÓ¤œ¸³g/®yØ%_g[Ù…½…6£³0šh&]¤™Š‘±Ðg ˜Ã5œùu›nêH8j½!áS‹BÏ&‘¶þܵA0–ú£ŒDú4Ó Ä‰¶øYDïkýø¬f&Ÿatn £ç§.ŒË‘qi¸äÒç~‚whë¨ Î¸,FÂxšì(¿vR­QÔa;ÖÁ?ÔP€L£‘‚5àt¡~©¸­F¡ô¸AÄŒv«¯È`³Æ6§1Þ°e%ØÁ÷'™7ˆ‹ï¾¥‘ȧˑ| Nƒ¹§ŒLùMX1RÂÄr5‘Œ øO‰–%-×­®pì™+}^‚³hœE8Û׋TúÊ‘:»§Òm#”–ÝOt6 ÚÅ•¯k웾–©´oeåé¢ÏÒ*ÔÓb? 8mRï‚ÁÀgå®4­6PÅ2_šNgC/õgziœ s!€;á;¿1h2k¬äè£~f¡Tvûhüð(Tº…YZ@ag9/"ÞXÔîsž(z{tØ=:Ä÷’ÕG'‡e£NU%¥"²TdÔj‰Öê[*\Üɸj¶ôŒò(í\±@½s pÂëã`ôd¥GbÖõ«`,9dÌÔ `3zf²ùj¶o217£{†ÑœÐü·RJœX†{ÕyNm›*+lNÏ>ôã(G ³x.Öiðe¸À̧Nǫ̈óõ9ŸbX~ÃÁ¼Ïô5ÎBq,µÌe Æ Í$¶â^בo¥‹¸Åïê: Å…džQ æüœü‚ àöÏñî¶´Ð>bˆ_™ÐvlG©=F¢0/Ã]BiÎÂ>]”~=‚Îg@ÚÏô §¦›Tï<ËåAg´xR"$ß &’VÓ¿‡“!­Ù4Óp\¡‘üR¹^±Ä€ÚÝ‚rÆêZŒãK}à3kâ5ÒÍéP"I#YAdˆÊƒ†u=?vžE g¾Q›Ò+™3¦SréS¦Ö•šà)æ#ÂÌÓðšÊrL9ÃД‡Ùvê+MþÍQðø7ûØÖMŒ|.b~%û¨Câ­·¹N*&dÍŠ0 ʨƒõáno[Å› &c.-÷‰Nƃ ¤™šñðÀn)ÃÉ|ŒÔ…T´ö Ôõ$FíÌŠÛñ•\dz±;^ÙÛ•\o‹ÃÈp¢më<œç¨ý Áþé<H½^$ezâU!²rbsãól àãf“Y5©yÜTOñM¦zƬ/ŽÇ×'ƒ)¦™e .‚FfqTÐoX…Bóì1Ë}ˆ—Ã𽕠ONù2azÎ}#W×£$,à`¼L‘Ü´'åá¡ð±m¨Pš£ãçCs]íY׈£ áçµWŸ¹ibçÃ÷/YÇyñöo™"´ÿýÞÁËB¡ý½¬½ŠL"R!ac,×£3úIÓܳ s6qP¥”Úx˽GEüíáŠLJž)4è;v¨ùã Õ \Œèó1žÞ_ÿ;¥ý<•ûˆêŽ:l%²ÿçŸÃŒå°ESGüÆrå.±œÆ‡.š« HÁšT›ry›¤wâÖÆ‰stænmþ0‰ðÌÁÓóHá7çc‚¸Ü¶DïË¥X­`ª ¢_ã)©°ðxˆW ÅaSVH™HàÊí=•@<í7¡7'Y2Yko ~b!‘œÓ&Ý<æN"õ–b£QCgìX+…}ë®7ôMèÌŸd$ãä¼°!ò1!PnJÔÎŽêŽaߤÇÜq1kêÚ65 ò“XU¥SU«d3F™ì…Y¬3£N·¡?-TÇm§²Nuç°N6à">æ‚,Aû¼®›*nSÛR=f=_íæ;FD£Þñé´çZQä=¼(×¹ež»i˜ž«¼ÁÐ2‹ÝÓÊ¿ñº ;ie“g˜¾@y6³Rò)_F\!ÄVIÛÌ™6Å1½')ZÁÍîΌͮ˜¯=âù§Û<žZvHàt®¶+›¦!"Ì·i)‡A* ¾5‘æZ¶«Øö!LdÖe*í'š,ËÄ+‡ê§ Ä óÐ óˆ›aÕfè¦vj‡G7çÕ½Û>PÛdWmmd— Py±ÞmïÞ‚{ƒxr¦G¼µáž. g £¡07A"ž¬Ez—$œÞ_Î<¶¬$“f½Ö´x/GœNõ{v22-wôaÅ]û¹½½»ûߤ¯íβŸ3:†okÍ"ò+õЫÿÐ¥ ¦ëD5¼eAÏèå,[¤¥½€'éSM…‘¤÷¥MKê0>{ìÇþ’Z – wÛßí¯¿Á£sx.i|’ÝE0âÞÉêj:…È„¦r›»NO„="=A_!²™î‡Âë\l4ã‰(Ø|ÛÐ&èÝÛSá]¯xjy&™cVÓJ0dÜté.{}ÍŠ—é´¢bJSÚ-Ð!¤NþjHˆ<œŽ%Í$‘އî§Ö#Md!dn‘Ø}îºvŽüžГ™f¬ÃžŸî¼yw¤ÁõôI>õã³£™Ôc“OÖVÓ¦ìq.$A\¤ÌçÖ$ O§õ¬|½vM†;hÏô GµRÿ#&3{$bš®T“‘°Ãœ|f_»ŠõK™ãËà˜XâqŽeG0OòÆB “]f_Dæ¦Y5vãçðÆ9|naCË̉™1+Yé¬:ÂÃ9ë1yÖˆI°Ð>w¥ZžÌ–jÀÁ|bè]—É›‡ž÷ŽR‚êI|圇½ýšFèN¦¢Tnל¤1òf'C¼{ 3Àyœt\bº.‚]–ùey¤'£*&¾wix=‘qÚ¿—¨o;8’çŠñì(ÃŽ¬S÷v#{xÚû¸@¹B/ß?Ÿé™ìS³XðÅ˶'ê5Þ¼ýéãëzTXУœúž9>סIqCPâ©g‘„= JOá^÷eÞ¤D|àRK¦¢XJÉèfÁ\~›¡æ¸ZÎG³LçvÑr^‚)í£ÑeîJœ$9@>q“%âûEöDÉ\½nè´yΑÊmZ6AÒ+ëhá?‹{¼Îb´<Ù>ÄÀ:©39ÖõQý%³x#qì{khØ ¸œè*f>¦+óÀ`]þ^Ó¼Žëâ&¾È’”Òq@M49‚þ‚WÏôR•ç…”Ì-óÚpg]ÇkŸ›iv·Äͱès™R)Ò yFsöÉ}è'gHˆÖIt†#÷ÚÏl„„Brõ4Š!•ØÏ°ÓcZòckù”âh&Þǜso5‰ŽŒÜ5*ƒIhrrãˆF8Ú 2€j9bӞg÷‹ñ¥wÄÒœ'6iÓyYoùh'á5yiL/`£€wùjT|WúOF}â~¸ŸÀÕ–±,ceÖ^ë[Š/úMOšž|:&CÅIwOÓã$ìb~üNV΃8†Š¬Ú#díYt"×UiغïÓ,3­¯ò,¢¦ï#lâvœ•ñš*vŒ=9Á™…ÕlÄÑ ½ÀeH’ë3ÑÇVôÞnᜲ˜%츨=K®³”8Ô‘“(q>ía—ÏGÊ„$oKÞÔ4ÅM“Ð&’ËÚÔv3†mA$2eºv¾ÈÍ`W¾¾#Xýªff{Ù÷ÅëÙ'4œÑÅß$î=gÚa>"j®XKØi›”YÞ.K1“±E\·5­ÊÑL’kÚƒE¦i¶õÅI쩜иCNuà-ÈöënAä oÈenì”Ö?néòVŽ(Ö‰_4îBáz@sé\\¹ ëJíY4š‡\N‘(ë2À““‘…š¡Fô+ö àE‰™á»Õ=·’ÆÎ¼ŠøRúšš³{´«eoûâ­~¶}÷¤ÙI†‰G~iÇC%àUõè““““Æ3Éçëkh¨ráâèž©ciPqt ŽA6îä$õþÛ7?Ì“õïÞ¿ä¨Ò8*lÎòÐ~`¹­Í)êµ4„$!%}5ý>ìä#€L±â2ËÇKä‹Êœ™ÇNÈŒ%æ],¾°Ž λèëHûjT:™Éϰ-4ÍRžTö–îoÔŒµ–d1ŽšÝm}9v÷;yÀ}S~g£«¼þ{ ¦ÑòõÜÞŠ{åûgØW±éjÎv ó¤èln˜o¨Ð“ê>çXæºì0Ò_-t*×TiŸ¼=•'z£âJ­ \R`=MÌúfS£5ðå㉉U2*j¶‡œDtgê~O•Ôd–!ZVü¢š„’F}uUìß\NïG…UoEüí† bæne¥ßhžòn׳}ßJ1¬ÎÞO¹ü‰0 ©´ëxæ^Y<ÒI“/MZ¼qÖAyšÆŒÆiw­Ù(SNmr<,«fëš·¢×|ëY(·Ò\Œê ÛÍrÊ*)¨V*觬l[¼©ƒ9»ý&Ù%2ç0­h°ëX7è{~BêÆ( njð,£@Žj9§]©©"ää…§x/å8 Ð…Îh”d$ÐTª³Ï_avì%ÀÐ’ö„‹‚+Ï.µÔóÓæR~Ðø†2kàs®¯+ÉeVÌž’rUÖ{ÇTø^:P1þ\ú„Ûº>Þá%æÆ–cÔz^A6¦,·ᚉ©:T¡pð¬|†FWKNʉ¹,ƒ±î˜‘e™!Ž/¼ki‘”}fà'=‹væ—Õ)5~刴VI“$¯ËHÞ-æð4*Í¥r£Gda-=Èa%?n¨â˜fJ7§€¨ªfíÚ%… Xs¹€[ø&B˜S ”)âJx…c±ÍßÄ=;ß“÷w€‰^ ½ ²¡ : n¯ôÑ FX¢Ûå\)Ò0šl1/ýqˆÖÚXÇ#F×y/ÈhJaòXµä<¦l`•fÈ0êœ9ǧ°1½j=ÉK±Y-¹¨žFNF™®¶‚ëo#’Û0Êdg©1%ûAcgB¹®¢^POÙìŸkO6ô|Ò“á…®‰4—ÉÓ Å{—Á["=ë3íÞ9#ŸÌN#Ú™%€Å%‘ekãµXò)²œœ—§žÌ–ù‰„ž¿Žlû8å"HšÀíÝ)QOê9à’!榊ÂX‹Ù* öÚ§Dƒ#­®›‘´ eñ¶ýC{|½¡ «esj®]mYÓ?¢ÄØ¥CÒX²Óºx@¾ÚU+å ÚüÄ6m-‹X)md[Õ1o‡]£±Ün,ذ#ñ‹-?ªhY`d Û쩟U­¤ŽÂllÆÇLæT|¤êÅyl6ÊWª;v”R:yåLVV1âq¡ÎŒD˜[õ°ˆª” ’ r/¡aÆ”6ϯ•§ÑÇŸ'°[ m~ñ)©Fç&Vš•Œ±‘‡Ü*lj?ÒH°Ã1ˆ+1ÓVpGPe‘Õëò0O­éüE9ó”urÞʦæ,FXÊJÒ"à1œß[=µ ôø@Á =Z˜êð§šþ‚Íjx%»µ‡`8ðÚ´¸øj¸ÅpÊyy‘‰£¬´Äs±ÙøÇ*qäsÄ·§à¢«•ÃjÅóÞƒ*ùm\-ˆ"‰Q» Ò)ùWƒé²vÊ«ˆ¢{XZb­¾×о™Ð9VÏ.ŽÞ|õ}PËA!©N uÖPçy ºrKÍMÞà½Öì«>Uð¿FIÕek¦ÞkPå:%7l€:î3ø_e“õžÒñA_éªøMÿBSVù”ñ%ªÀ ÐOš]™Ça5ŠôaüëÆÈ4Œ¤ç9‚‘ž¡->¨<ÑÙ0¾Z÷(Mˆò!±âA´ˆDpVbUßÙiÈÅÍ9öÉÕ œËÏÚÚR¼I:îqâÍq À”çojÿ 4LäÓ¶©þÄî´©®;šú˱j 3çÇj"C'K×m9Î4Õ¯å®sèF@þËQ†¹jÇñË5ã¡k•øÚd^é‚7FªÉ.!!‹%1û³0öO–Zî›yÞ˶¦9ìP?~Š¡›™ï{Ö%F©™ Ï;¤;è9ßÌÕ³6FGžç‚¾oè¤ö5<Õ†#†vÏ®j ¶à«6Ô$P@Í <Ö¤Ë%.küæ‹ø¬1è»8­I·ïÑkMÏÝÜÖ†®ßšÀÌÍ„ÅPO‚71R 8ôôÖO„QRH4f„ñÑ,8’¤Á1ªýF¨ …bmi¼›Ìãvyy—åF·ì„Æ\¬W»Øqù>vèö ô%œ­1áÃò“J¶Ç…T]°¦ãÜpêq,ýlϪa—F^‹¾ö™xš] eWqðCjÑaæë;˜^àŽO•º0Ѭc÷àõ§°¯ÛÙRíÏ…²Õ¾#B?ì¥õ:€70BÈfl1FÛB/›Ýò¶ó£,ÝÒµ£ý™LÿŒh-kˆV[¡0:6¡%¯.~’ó$moÏÔ€?;DQðåL,àq­¾ÈUã×̾ƒTí®¡c°šrêþÊ.„™wé|øƒGy@ Uì‚м˜Yr¾§þȤ¬àŘâå„êËi<‰úªÍ 6\lb´Síî´øS2K9~µØ4¹•róôè&óT€ôGŸ(=IÄu*—4»…FÊò£­`V7ÅAÁMÖ€n C¾¶¿WSË]ÿH=*!ºçZíÇQMǃFS5‰ð7ŸÔÎòR€`‰,wqïì §©•7Å[ r”·8 Dð¥AR‹;‹Yrøå.@béñÀXŽCƒ[4–¶©'Õ–;Õ\]àY\áKS5—&È·Z*ÎbîéÎnÃZhWê7÷Þ“ã0*·¶0,”¡µåžµÄpŒyb’´ód½¿©#ñÌèŠÒQ9‹K`uÿDdê䢿Y³î‚ÌÚ}”o×8æVêës=s3½ß…$5K¨ü+9çê–½1žéž[Qá~t«ß³‹îœ†¾´“®î†Þ…ï¨EщwøSŠu³<¥‰‘»ìgX¹ô¸ &ŽzíŸxa®r•Gö8ÿ9c g¼º_ŸdUZÝ™ð„JkQvC´Zy:.½\Ö·¦¢ðëgPlk>’ë SI$M‹^:¡Ðã‡hDxèåÑÙjÌt‹6f¤9S:ëðaXzú€ ¤óæYFÇúÄaxG¹vVŠí”À®r6€Jº[¦ ÈòM`T@ø:~ÒÒÜÍQzm.îí¦§´œ2·æÓË¢îÒrk&¥ßÙeZ“ùLÏêø,gcÂnÓsäÜ}Z¬.ßé›ÎAVn®rÝ•Mc ~+ðú•¨ r¦'õ]y~æKñÝ£ôœ7uåz¸›Cµ#_ ²]ªo$znèU­g]ܪç²üE«3RÂEÁ^—ÀüZÎÕ™\)™›?¤{õ—9–¸q]¬Mâc=c–oæe--ZnÖ Ê¤›xZ[Â(çj½À8ŠÎÖ–X2~ÄÅTº[Ïnën×®˜ë›6X†°œÓõ‚ £ÒÕlû]|ÇëYŒ£ÚõÚ€·|¯gÎÅêò]ÔýÚ‘¡ÿÿëRYZæ„}W1*†GÇ{þva®'¶ ¶J"Þ¥­¼±ï 7Ù%ã9ºÓ€J¦s}øâ-Üûw×ì{šÇœwöÍéñ†Ún³ÆCûË´[êëÁÚ=ôàÿïŽÚy”®Ü–œDJü§œµïgM¹þÚeƒ-±šÞÎmÛ¨³™ßvY{7òÜ–É×íRp‹;o[ÀþÞÛ_΄jÏ´öy¶ Þt_»¨Iµ¬]glÆ{!aTEOóÀ߇ҷ3´Ðã̽ðÈ døü¹«WÄ¢Ý\§³™mdÎbk„È4~Þ /¿Ã|vÿbN®F-פìZ¼Àvr›‹ç“³Ü~e×¹š{þ„¥c†Q˜‚$㑪Ÿ†×|3Vr©ÎÆÓÆïã ¡ò ÌvæÔΡnÿúaÒóÇ}N”~B7žœâÊwss¥Q¸ ÑõH^èBÄ•MúOÿ¬n®‡ßØX[Éÿ´åO{uµ½Úüº±awVZÓáè1Þa¶LîƒA0¦¯ñ0î1ÃÜΚ´úqÿ¯VKíÇ£)Àãý¼ ·—;РzóúÃßñ2¨_ƒ^Š7 aÊbÜeF˜b72—¼6Uzýjÿ囃—­ÿõÁ[À[=s[ÿ hAŽ uŠcœîîE²tAÔàšÉ¡;{0F}¯j?FøÝ-a.ÓÅV¼Ï˜•4EÔG¢íRýÃÛwtK礵}þF}˜Œ#º¢sï>*†U¨÷¾ƒï¯Þf…¨éü:8÷#"møü:ŽÎ†>–x÷ö@Õ’=G¯<…æ/ã8¢ßšª©WˆY oÔt2…üùá ìÐÞOÿÊ­Õìa˜¦êâÞ‹ø‘ ·L½WÒç×lÎ?_ð’gU†"²´¦y œ”Øú¦„º/òÈuß¾sÁ(öüÓ7ùrjÑI)©—MRéK3iÙÐqò²o‹MbVþ6“™ÕæIõ¼AÄa6o\=²ÿ‰¡xiI×ǃ?’ûuLoÆè¶q<×'7ŸŒFtŽvÙ1K2wá£.v<‰üñ´îÔÊS’ór&I=r訂ŠfÑPž‚Êè§H=·£”cÓÍ‹lJ§¹s»yî4Ík@Ýœ=ÁÛ%¬+‘+¨¡cÈáÑ!› Ji€(à‘!€ì¹3ûŹÏÏümæ½rÖKæÜžñ›ÍwÙl?Ê&›çú‘5ÕºͳçíËݱú¾T£öÏãar1] D÷u†¡ [AK>hÛZ€¹ ä g¯bs{öÅ‹™|¹·Ùº¯½Å÷ÕpÇÉ} â·ô=dÐè̸l.yZ>õ{iŒW ['e½è4Ï‚*¸LvÑÀޱ½ºÔn*½Sà —K©™<é÷m$Èåæò›wË?¿ÛµÉÉ~·ëƒyµ›g 9&g=ý}ˆ}i{wŽJR"© ¢“ž–ŠO!Ç’§ôü÷mé›7ùŽÏ«™Õ¦ê8†ª¥UÖ!=Œ*9­ÈV_y™RÖ+¯p9dwúÅré Í/Q‡£Ü†+ÏbË÷KôÔä­¨~1IÌT‹Ô¹ü.¿¾¢¦'D´³[ÅÃËt;ªÆÓ^©©7N L`¹å°G®sÁ’b ›çNWȃÚtî]•Á´Häør8ÄSvýÑ_Ä—*‚@?§Xä˜ì+$[ð6ÈÓ°‚<‰â>H¿ ÐXß…­#†T{†AË-uFYÛ]Ð_aÇ;1·f³'xLq³©K& ©/wX!€¦:Ç¿ñàv@“Aq)ßÚù“Õ*®™m÷]wåükû`÷Æ«'WIVпHŒìÎZHRdÑõô¯íüR5/òRL¿2k­\d«@Üj R¯n´ws˜Ÿ±‹EeÀ»å ’ßÜtQ–´BK³0³ô\–kùLД˔”,äìù"«ÙUeáéK¹È>,7±™ $ÈVÝ$:ž½–ræ¼üŠ¡ð‚Emm®E ‹¬wÖÈ¢¶® ®o´7çXÔ&i8ø²v´ü÷´‰QôÚq?))Sb·šÐ­³íZú={…è2#“¤¨7×—Hÿ‘õâíZŠSÀ˜K!'çñdÐç@É¥¦ê¬ËQGiUzUY=ëÆ88†Wõ¥Cù·£Gw)韶;«K ý|û _KMÇõõb$þÒ˵¦|þ|ØnªU¼·¼C{¢+lÀ‡M(«ž:7ê¼Öìð:spÀÁôÓóc:°à½ofÇR:òHR7Ê(nçë‰Õ¡Ïºõíñãåe ]¹o00h|÷þíw¯^¿T/Þ>ünïýË7ö^«ý·o>¼ûZ½ÿéõËSôÕi]ÐMæ¨ynü@K†¾°#_é8¨*˜õFËC5¶U–ê»}@ÖßÃ~¢ÞFƒ)†-a8æi8 ;M…÷èbHÈî@Q{o^ ¸ Ü? —gêC€¨ÖTÝIJG•ò€N7ã34‘ħy‰ywèO`àG `\WIig6tgždùÑO'ÀŠé~ lý4ßê•ÝúÿõSŒ‰éºLÝíM‹½rú²×@³za÷`Ò©ju€/åœ,Þ‡'>tÚY†Qø½ï¬ìAXÁ#¶}u²lgéÁÃsà(K»áH½£‹dû±Ô=]HY%Y‚WWW¸i.©4L±¡ŸÃ¤ãù’ÚÅ? N«`ý8Ep?,Ú¹ ;„ “úí1Ô}ývïõ>Ï·AvñO üǓۚ`<=Ÿ »ˆVöâhy¸³±ï«Ô ÚY:î¾Xr{8·üô*øÔ? aÜ\F¨$‘„w°±º¹¹²±IGêëRuu£½Ñ™³ À y£ ýÚ;_ Ù70-¯ðböS¿”ít!äú>xrBôA ‰œzÔ‹Ç£I¢PI ÆÌ5ŰÎ[¨4ÉÚÌÙöAÈBâyßi åüê¥$\›|Õ1<õ¡¿88rðŒÄ·€&þž÷:Ž/Ôd[ º™ÏNN’)à5Mê““gÔ÷,Ü™öE*&×Lþ''£89919Ñÿ|rÒòö7‹ÄÎn\x¸¾zu™f-Âàt™0\o[4m-èa+ŠÊfÏF¿ƒ¿ÜãÊJ-JÌ—]J\óÕ0vaŒÒ³ b߇L¢ê ­º~w0¥;Êa}Àh]Ð%ϰd€vA²6ØÛ ɸ zÖ7F=_Â<†@ܰ z¨ä@ùxîÕÉ<ļ‚q¯7sv̾›Ó ŸÔæŒBî*OD~èë¾»þøÓ *¢c€:pàÀ®aè ”×ø9{Û¢eÒTÅÇ<¸ãŠ÷„ºãSBÌ,hã!µ×¢i'A‰_qè³z§…$SoÍi¾ÚsÆ<(”´UE@ÆcÊã—ÆÓÉóNøÉɈÌv¦ lN‡’^!9§kæ5u yKl'o¤ë׫ ZÊ´Îa«8F¸õFžÿÈøòÐ/²‡¶+9ÊÃŒd)÷sÛMF£iúêüÇ Oƒ“yæ_wD¿†æ9h&nx±ŸÇƒ¸lp —ˆ näG¤›r¡aR¤ãJLqŠKSÑ{IgÞÌ•$#HBî0Z02½i_Ôq«=¥p®GA=¼„´Ìèjž1Ïò< |"À3”q0¼v>PàZ `oœK`=´„lY~Ç}—JðIËwÉ„ŸE©É…ý¿Ã}ÚÒv’‚Ê· ºéó£¶1LCmcØè¬íØÂîR{×»5wQo"¦›n_Ž*=ª¹eä’9Âí²f<]ŸGØê¢ÝBäO†ENºŠêËdŒVâÊŒTR€__ùœ2õD·pÒôN@‰Â½0iƒé1 údÎNDfaö& ¬Ð2«ßãYñòæ+ ža-ðEæj >ØžöÏ"ƒ;®ŽÅ6ŠÅZ;?"¨o:[«k[+­­šS†,'‚°µƒ;F©t p꯱HÛյˤ2ëÂ3ô!ï³¾µ²‚ÿØ"Âø\sº†”z¯Z bЮkµR%)4æ¤píµb‚Å7Mþã`Š•# ?~¸:…Öä;Ç=¡Î¡tº¬5îÒ+b[šÝh—¼ìÈËö=vk®É»û*× uãûõò÷ÀúÝ­O mTüAs6Álñvƒô*à¬ÆC‹Ï]ÆèÛo¯&zÂl€>ZdÀ¯f-çC¡ áo#I³eƒÌ–ìI’¶èc±j+Ã阷e0öÃÜÒs<±&`RÁ5Æ´‰¯¦‘Ihj„¯¢Ë·¯nïç`ÜUßA­r¼²Kh<п%¿uŠuŽÃ¾¨€ëxx2wÝ9\ynC%¥D%ek!k¶Eð¬Ö@ꊷ¾&)Ìß÷ú(ªÂËàñXj¸,±,š,¬¢øUtÇbƒÝ`—Au“R`±FíÂ3›Åµ›§qu»ºÄb ;¥çµÜ ÂY#Ö%n9+mµœ#N˜þ íÓŠ-’ç“ù´™ðõ¨Ón¸%_yOšªc#°!ï3Ô9¸Òµ*ás7/Àæwè#ÏfÞA;%Ø(¥S¯ŒS,²?éi(1zj6OÚÒƒANéùqbš«óóNcëäÄ{ϹàA‹î±Ûð7$¡óøJIôEe)ݤG7î0úŠr÷^rŽö°Ó^§ç&Õ_„6_vçŠb( “e_Õõvá1|ˆâé°á¥þuÅÃi‹ŒªÜ¡ÐÔ¢xLµ‚' s1¿‹¼Hð®ðȜn€ ”6J¬êW!(ßÄÏ1¬m<á½/(þÝÍh)Wú!ž©‚f”ô- ’éæ¼5ÁV)~ ûÉiõAÚ«=î¼Û“v¶}±²ý“믤üÇ@lFeÂ4 §Š:Ë ½¶»o-L(FvQ•V'oȵ aЫ¤ƒ×ß‹{Ëûçq?Ãägä¹õu‰¤î‹nÐ¦ß ýëp8BãôÜ‘¦–l6, dÌa:2bý<á4Q°£,â³úèq§ßPL7ôš€8=ÑË 3}§eú“››ŽÍÔt€_Yßl¯u²ß¸èäèä8ìÁòz•0‘·ÐΖ÷:öû”›ÅZòKEëÆúœÁ’ó¢ÖÂG7PÇíËÈŽõ¶ö` Ó[¦—­¾c3H‚!­ªÁo¥Š÷v̧50 ¹…¨bhèu„ºñ”ûîë3°:ÌÈD­Õ‹–Њ‰*NÜoðØg ý,ˆ‚$Lt1ù*Ȱûò¨ÉÑÈM˜ÂG™.QÂ)aôDŸI¢ðâ °Ê:QõWû Mœ6CT ÑË3D‹#âÂDµ#­“ÉXÞc¦_¯dÒxV`¡á –Ž$Qª ï¡ €nñ‹éäY+!-tu§iù!® e®ÐÞÀÉ»sË27W°,›†Ò™”7[OÛíõ•§+«ë›OæÈhFƒè¬¬­tVŸ>EŸÕ'«+ë6¹üÚ‹f‘Ë!h•Ë0Ÿcd@–Jþ‡%š¼,¦^„Ñh’Š…0)U{<#2±&ƒ NµÕcìP=i7Ô#…: µ¬:ê!~ô’F#7§9„–ÌéJkmíéææú:0ÜõöÆæêÓö\ùIEöÉúêÓÕ§+OÖž¬<Ýxº±ê(²áÌY} rõ¿¸ßÉ´f­dZó™Ã!Mƒ‘*F|>ÙXíÀ¯Î“øÕÙÜô*Òx{äH8rvyÔW)ó :u¡;ùÎ1+ßÄØÕl@–>Âæ(ux9™9OjÆáV{å¨ÌÀ`™Ðµ© ô`Êy3:Ÿ&¨iWñ»èû@SNÊœ×$+áE»“{Áç5eAèCçE•$`ÛâÅ༄—Ðбn®4Ÿá±Ý°ì]Fw'ºIý™è:=F¬(‹x  ƒð.Š)J›töôåõonìàñ…\9ÜsÎr— < ˜énñ‹?À;yÆñäì¼@˜t!ÉÓx'dŽ=óN;!yÇ£ü„”²‚°ùei7ÕzéÙ—iÊßÂAõLb×¹nfÓ{åîìu°P5{}XçÎ¥ý8ª^@ó«šsÄA:ŸšsDíÅ9K$¿’œ 7õf3 «<‘;sm—Їô ˆ+ˆÂ ôÏЧ1¯Pæóƒ§%^&IÔà úÎCw–ÝÞTÐ YÖj“‡²‡«e×òþTúź]èCÕƒJ0›4qŒr¾(“<9ºadöõ ‰â®“Õ@ã¥;¹&, 2Õ$ôÈËÅÒ9<îÍ3ýðô4 «˜·aQèÜ>cO`|–Ôô¬Ågµ²2½óɸw˜rü½´¨ïûWñà2+,ONË‹wý^hJ—IRZ® d§~XVúšÇ=©±÷âçRÒÍ`î½ø¡´ »Ñÿ ^ñâQûƒ·5åƒå†#t] @¹²Rö¤°xøRq#9úÚ@¡?ìöAÜR‰å~c—M¤PÌa¨Ä„ê¶"ýB+vÜdÃäN»AI®‡U5×°VÁ@CN§s¡ÄÛ»SÞ‹›‘~WdsÀš+¼ø#[à»r(Žû¬¹U‚8ê㢖ÆÓã2í!…éoåZA9ú7f˜7qÿ²F+J£ð6'…79ý-¯ßãÓíÊŠzaörèËza+ ÙS X º¨¯—÷Á‘Ƕ,ÈR»ÒlAÝ,WTް%/ûUØK̽ÜóžOÎÔ“õ-ÅÌJUØ”‰L»’±žKw)×ÙˆÙG¾U 6¨üÒ'}á³yk¡J!žQúq¯6†/‹ N:„nãcZgޔ叱’µÔ?ã­D)䲇›öËØs±~Ø-Y¸ ½Rö°Ò?ºDÞ¸jÏí„],ÑE{ceK¹æc²MðP(Ê,ð¢þ“?Mk‹¾fÑV€­-‘`,h°O’˜M¡a®,ÆJXi`Ó¼M{¨·ä«íõÍ'O×6W7ž¬uÚ+ê¶aù‘ 7ÉIü©¥4Ú}”Lt…£)î%$ vÚ[jIß< ËhIti¼Ž}ñ, ÎwºkcK¬ºŒ|+4Öw»¶tíá)ÿ’Ó­n®S9ðñ\±›Ôý¼]ª[´kµ7××6V:íöÓöF§ýduU0€( ×:*yµ«ÿ·`W´8ª¤¥£8ô131‹yõ3W¥žäG³ÖÚè¬n¶a0++íÍÍͧ½ `0ûæ6zã1öËuL“ÇìÉ¡ãÑå€ÎÁÑpFJ[6ì‹Üdý ¾ `i²,WTízYîÌxáø* ìIþƒþ:Óò¢úM0®7EÈxút˸ »>Î’È<Ñÿ–-ðÉMwLi4ˆmЬ­Ÿœ®ñêù?9ê°#IËïSB³žcE …Q²ú¤³¥~Ø£^€‚Ab·;%bW:&[Ì(¾r•sx ;ÌŒ¦ÏÁÈ-EdÓyÇ“´W>ö+L€È¯˜v+ï[_•Éd ƒèØ'†•:h§Û ÄD«)ÌrvŸqÇùÛtG-¯‚Ø´ p;WcÌaw,£•l2bª‹\á™tχíqßµî†a á7¶Ô“Û.\ûµ ýÒ§ØÇä'ƒ~<4áY v?›CëŽÃ?Æ@¡‘±.~ã ™ÀÃ|—¦nY®Ÿ—¦TЈ—¢ïéÉÚ–{,ˆÆ×ÌßH-·iL QP‹%w¤S:a@É[­œªÈM•„¤UŽòzj) ù*y'¦YesN5³ŠæœæÕ•µÎ&è+#?J“¥Ìÿc,'Dr¡Ý`tÞ•Å£;Ê“·Ø¬EyësÖʤNqŸ5ŠÞqŒ¡ûźŸhkù‘&`ÔèȼGD5òi˜8a0¯ùáo(#[ý¥là@iüâRbêÍ+å <6Ì/E:^Ö0JÀÃ’`ç¹ÁÎ͵uPJɉnM\éÖ×676f;_§­Ñô=PoÐùƨגÿD}ˆãÁE˜nÁÆôíïÁb!ÐÔÞ$=Ç Ÿƒ4Àƒ¾ç!ì(·“î_{I´&Q8 ÝVП´üÉ.×?/ûá^ã]Äj;èðÃ_Ï0©H…V/L@ ¢«b½ŸÞ¿Þœ/&½Ío<>{ŒoÃ&Ú’ “à >óäã8÷›99Y“ønüû9"½û †Îºødv½ý8ê‡lÿÕ P§Úÿî…g/RPdÓ,ŹI®ã•gJã‹ ºû ü_û¿M¡ e«³UeãÍŸRç xø#>±K“–2§ÇâÌÑ„ÉÄò{°'ìýÈϧÃAÜ“8©Ì5ö³7ßad¨Z%†àÃèë”n2©³5F´=aOuÃ~86ör¾èD«öäÅ£ý>±ªõ`RCqë+"qV=ÈŸ„^föÛâ ©u$nðã %Acä”Ip,ßp}MÑ“®®ñŒ©pÐsæK—н×BPº«ÀÄ€>.Ã>ûÒö&@ECÓOÝÞr0ò×]{ì¸DO u,r,•ëD ÿ„&^@¼J£ËôB¨C©Ë+›ž¡+¦kEØÀ,+cÜ&鈢±t‚¶£j>ì½ÿð°fÊp+yÍÝ9 —Û°yBˆõFØ ˆ¤ÛèÉ›9îÂ~ùæE²~«A?*-Ò±Ž½hJÚ;ó2CÞ1NÓñqý—›J£NPyŒó°ƒr¸‰.ti0–/°;Û³ûõÖµ…gÓÂÍôNï³ç ÞtÜF9QZ­f ³êö{Âs³oÃä.»LÙ}‹ý9ù¡©ÈQ®ëHàÐÝ B®U¯›ׯÍ’~ZÔXÅ×ù!¥47ìX¡? p3M\·tSÐ3^´N·*:ÒTv¯ï§[†¢ø uÀ]æó_ǘÊkKíB³þgr£ÁJV¢]¾2Vg cv”JÖlâ̯üĸµ‘òÓRÏwA‡M•Ùhª3u”ó¾ñúÀW£2þJ–CæÐ¿‘¨¦«šµ/`êf¶ÀaL˜û@{:ŧ)4“Àx;‰§¼ñuö‡©.¨©7±þ{úq@Îk›ÂQK-'ÿC|p$ÓNäȾà;êÓgóŒl쀴 dU,§ÞûBwÙÐÃ+¼^ÔHãºÓË&u2k¢aõIÏ2AÉæWÐÏ ÆšX`ÿ;›™é1Ù þ`ÇÒpFØ«ÕaÑGîð?YíüZ>ìåÊ}}’ňR´äßâoªï½(ÐýÅ=Øb<0‚Þy‚²+ „ýOÐñÏø9”naž»”lL)šŒäÎH¾ß€ ‹@­ ¡¯´NCÞÜÏ~̺ÄÕ|vgÓagÈâ[•¼Œ¹-̲5E¯òŒQ*T³µ£jÁp”¢^sȱLÌ®}E-ŽÞ59•KÛ}¡s¨ šþ¤‡7 á½q)z×ôx·f¸™­Îpû¨·ægÂA2ÏCv1¦ºEã& D ÄÖAÏ$Óö‘Ný·–š³õ€%µÔú5Í-•ÔŒ‡åvæ¾!£féq//sâ.½d"®\3-w8\Joëvà » …›)ÙËÍäŒñÍããWG3*Ãð{Ùðe½?í[û><øEKÛ¬½_d]íª3˜éå<'8ºÂèôÝs²í'……²²}k™Y¹’|£T¹û«‚J…ß^‘û”ZlÒã+j¯(nYƪrž§0üœ¦W§‹iµZ˜42ÕÏUôòàí¨­þQ®f‚ga1·#„ryaí”óØÃ¦Zù!j¶`³n²QgÝ'Ìëy·‰X»K§úvDø,šlò^À͔à •zÙ:k5óÌÃ…0ìþ'k²’­Dï ? Ÿ6€t[.Ra»‹æ2}.XÎ{goç ä‚óYœC»3…é´»0wË …¿ÓsâHßlFô*ÃÊu`¸¹mÌj…—[^%Äá5ìv_·,­°<‘¥û<ÙËÐÂPí‘Ó.”žÒ®g®êÙ EŸŒ«³Õ|^ œ:˜”(òZ/ÔÇ1È6?"ý_Ä!Qž„Þ¹Á –T]øÜΓ•F¾ †“'nn„ÐÃd»z(ÔRJd]߬A9è6ŠÏýÁé1`óDà‡e¥upŠˆÃªN~ €ç¤XøñpRàYãøÚê£XNuéâ4d2€ˆH3—îCÃ1\ÖÊ(ØQCP¤é3“cFˆvaÞɃ4!l¡ù†ºö ÚÔStYßZB‚Ÿ ÈìSJû$ýå®m£e»TÙåk,òOÅÈ]¯©lö­9yT©1CQÞIØëò0\Œo…G%6Ic/¯ü¨½>GÅÚÒKüs¸œur«¨ÖëvèïáVV¶XT§¸Aû:³ªTlœ»¼#WTàOqë"sý&ÖS»¤w pˆ´]5®â¨ˆ‹ŠêyÁÇàŸ„ùí¡Z8Î&èÆ\Ä÷6aÛ€±P`!”@$_O‹OòÏ•¹½0å]‰O0䥨P˜?cVÁ!'Wå ¶t¢\ª›àv0@}–êïªm[ÜÝf.¸»&»ÛI ²hw;õb·öY:T@·IáŸNŽ?€ú+ïØñaÜgž‹ÅP3à>Ó/ÊÍã )p|‡8bb’~¿/¹"dÊtz Â*f7Ö¸’<ã“:IíƒlÕ>Óž™jòÍ`VܘN^Æ>:pÕdÔ¶k®Õ@,š°VÁ3‰§1Äp­€ÑY†2íŠcR<gÂä"1‡øóAžÈÕ9Ξ€SuûÒ<¦–ñ“áÖÀ0Wi2†‡hGÀl“1EòÁb•¼_*2rò¯IZzüKÛ­‡»ô¯;Žuvü9¬áÖ ã”й¬ÖÄœÁÁ`€ó/àÙ35QéUøWŽãêüÝ:¶ÐØú‘ÝÄ0ÆH°g°k_Æ —>”ÆôtÀ4Èÿ…ôîß“ ?£¡Ï2`”PÄÝ÷Gð›‚BàY˜úww‚HÀ”™²°(`瑜gÀFþŸOÆÜ`7À:ÏTÓþ=£„HƒëB½¡Ÿ¤8Œ)^‚ź4–ã¯ÎW@O-t¥ç€¬O«ÍϪ0ïŸß·b=c QÛ9>á}Á3½; TïÑÙ³ì£É*Ç[ .h¢Hé"h?ÒÈÐ@~f÷Éž”ÞE®äô™Ó9GÏkN¼psPÁá¨,éHºhNϳWìÌš *ޱoÌ*ko‡®^˜tëãÚÇDnM®0£è6–¬»µ ÿª°‹»… ׿ÝþÓÇ-¬tø¯Ý#«–=V¾|• ´yࢀx€£ÊMžWœt‹ÕQÎqÁGÇ+æØŽåhíˆnx•y×4Ë6w%¸§µ*Ó¨ø¶ïâ¿“éÉ’iNUöT £b\«µ Kͦ·¶ {ñUiîÈÛ^…kÔªÿöÏT«þ©* ôJífûŸBsí¡ÒКám͆!ᇼÅz½¤rçù¢ ùÐG;î¥h+Æ  K_h4VÜpÙ¾þ'm%¬)kúÖ‰ÕåjEe9[!8oiá‚ñâbgû~[Ö¨W¬°³½”<—k‚µ×é…pÁÛ;D&lN´ýY-?ŠÓ=M1RgCù–2¨¶Naìõd>áµo0ªŸÖ”«1‡úr¹€ªi>–ô'¹nN©Ïcݼ"ºD‡.¯–fÃ~#bg’ùèz,ã?îCÕ~©þ =ú $‘øÎ#¥)­-ÝRÞ2àîï­º ËþcŽ£Î¬K­c ‘2‹ñ´qÜɧäâåkœÿ;ÙêÜ;öá£æ+KDÓê誇2=ñPÞN5-åÈ}EÊÉ¿ì²e¤•=Î`ΞK³á ÇJ×'ܧ¡ßËÊ΋…h¦ŒÃފ옂°òV@µƒ9ùFåùd2ìÑ³ŽŽG”IyrÁÚºÛÈw/·Y³¹s¸­=)]ÓîV–àx-Òô©¢"wä|2ÛYÿ![oiGCÌ1r­åw—ôÎý-,Ž|‚Ç&®ƒUZAZgŒf6‘T'lg²}+í}°´o¤¹u¾&m&ñfòÌ<ÎM4Çu‘{tõu$èY×Õ NVlw)y^ÍÊ~GÕuí’²äÕd¾g€£‹!iMü{Æ€¦J›Œ¾³uÿþÒ‹e‡Þ"tƒ&cžA§è)nòegUTÿÂáVù`LrQXà]aq]íIŽÝ ÎrC"¼>Ô梙–H.ÒõõøK¤–1<-» ¬Îî‡Çt:¤±~‰O·0L¬)ßÞïl§Î¯¯@Ò °ù€5vm×¤Ò ³y7A£c¹§³U{* ż¬F³D ±zÎ !_f#16Žä>K.³§\¶ÒX®WIÁ,õ>«@²a>ÏkÄSŸ6ª<ódôL‰”"cu²f‚ý,n˾«>‰Œ5+ñBy^ç#åöÜ¥kHbÔøvg¥ŸËÛÝößÅVï—W ¯ðØÅ€±‚”„ép]ÅC2½~¯v­«¶•WCdž䡳9IyÕ0ÙèÅŽOÑZ˜:r㦯§†=‡Šq°ÇQ#Á«qsü–r—_ýŽ’¨8çÛ)«|4=Ï›­/#X÷*sÃV(­c°v»(ó†8má?QÜàå \F<òÖ+lä IØ@EõFPØDy3ëùr_*_sÞ&J«yj›-=f8œKzµD^ŽfªO‡vƒp-]˜È“wºû#¡WòôÐ_‡å‹ú—¥gàªA-&˜1[™Ž3‰d1ŽýŽØÿ˜“-œýÿYg[íQd ÌgW‰ø‹Ú£#œMáb´=øÛN´§‰7·yB¦¤p e $Óhâf /¼XÇe{ܵ¬öùÖÝõÞºzò4 »¢¹“³a9£/ˆ´)æ(âãO´"rà@o:šèçiUûÀ™`¸¦Ú‚ h!¼Ÿ%?TM=+ßµÁQßÂéP¢J`z5Êû‚`€6n¦Æ5±Oób5GVµ_I§3œå—pBj6«Óa-‹2áW¶¥—Ͳrv_ÝÉggu pP8ïuÖà!ë˜;®Ã½®%âit‚Òé1ÒÄ$›ýf‹ r·ü»… ,óË_ÚƒÚ³p½G”׿à‰Iñ¡tµTGNÅ´x@ƒH*&¸ìÿz»YÿªG×;¼iw·²›øºý­ÿuw3Äþ×ÝíŸíîæC¾’a•óhe‡’‚Ä™j©^#ôp3XÖ›ý¼Öñ*”;ðÛvFêìöLñ?#t×úåÖì¾D¢·ûúÕáË¿öÞ¾dß;Ü>å¨h6Ž/ïþvtÜ9þæø©ñl ;¸Áðñ½NÊšÚBúü1†òéOæœësSÂ.1×]v𾜒£»¥1žŒ71Ó'ê¿7a½òQ6E¯'ÎÏUÞ \û“kû³··È#Þ„™ÁZî2z«Ög:ï÷?Åf}#ÛÇQ…!(‡I,7]œbpHÿýÉÎ}û5^/fÚ;t¡ˆËÑßO‚§8Rü»¢Ûæï_nô÷Õ÷·´ ÉÎß:ÿXC}Í(îÆ£P¤oˆ¨ô÷Èñ1£„?·¶8vBÂ}n‰¬ÛZ5Qv~ì¢L”@›×9[¢-k÷“••©["¹j²Ò‡îaÝìÛŠ"ö¬Òª¹à™2ŸÐiàÉ;ƒÔlþÊf·›×žòGE›+ƒ(4ñOç—úŒŒˆÅÀómÛ”Àcu̇&HÅ l\NŒK¿bû(#xEX=Å 'lû ÌV&µŸ¹ñ`å“uÏQç5 ÏÂùD]Ð/»…úãEõ4ƒl€€VÏ ›ÂewYhþö%vWD4úM<Nù?%9ƒ¿ÀÜ£pÔŸ(¬$Œ”йw¾"‚AE°†Òÿ¬žö­×1Ù‰šŸš[µ˜]´ÅÂÉ–Ø„G'½_–IuUé¹CS‰} {hN641WcÆaÒaìÛÖ ·^—–wFÕ [('Ûaù¡Ú‹ÚÝîA%'&ãHœ5¾ãDGФ¦¯-Ò¬g’)Ë©ŸBÜ¿ÂatŒ…ÃÑ ¢LÜ!žç>šß —Qx 5cQhç©ë»‹ˆF9ã»\[MÎtQ)zÜWžzKÚJêáL¬i¤ÊÚÈ~ÜûË//³ö5§ƒšóBؾ'SÛ©b_ñ§8Læ j;.‹9À™ÑTt7ƒ£IwSfe;¯G8jDƒOBÆÔ¦é…\:w÷ŠªÅÚÖÓ¦*cÀ¯€[Ã5RÞc©sM" s{ çwCçÃσ³ Æî`öžÊx¹Ë“ÁH_ô§úb]¹v×m íÅ'ôÒõ}óêèÊ7|j’F^JNùIU~”£`MÝg‹ù!n9ÔÀùkP ±¢eÍÿõC=)ótÕPx㻄#X(ü­ÊKÌãÖÈLjÛ<(†“š†þì4\aCDýÑýšŠôâ€UjȧXñèÁIwy~¿í>RßÃa £)”ÙÂáçÑn´?k%ÞÃxåöz(x7z½‹¼÷z®ì$d÷µµ^Aà{xk®ë‡”=¥ù3õµ÷¿’‰œè±~cLÿáýï¿AL?0Ó·lmßÄ Ó¨2K0¦%`ú_‚ævõrHëÛ¾Û~ÄÖLÕ-ƒ´–æîÑáZ|k©0û"뛢V›ŸUÀ±³æE9ÆDMa GùíÁ^ãm`8Yê_>ÉÜÁæúÜÎÊNÑi³8AÖ<{ã!Ú¬U‹¾ 'j -1mrvßáE¿ë8]ŒßσÏ*TzŸÍgEÑÏÇïÝÇò ŽÜÛK_QYMóAaWO£ƒè“ü¯þÖÛ¤±_Žý²o©óÞGÏGП‘_Ð#ò+lÛ78»^5üÔû oèô Ûtð"!-Œ*€ç-öÈûH¼’D^¤zíÒ4{õ—uâ .TÔwúô@Fuóp(kÕÆ§ôª€ZXÍúƒ(;½Z«á}FfCI¥á&æàÛyIŠ{Ë?ø¥a›»È7x4R!×É|ƒÌsD. ÄÚsI@":kì§ÆÑT"‘‘ß¡‹&œ8IÞ¥sÊAŒ0ëŒ)ÓËKþ ¦ˆ•õ9nÙ®!ÛfÉsaÂ6÷?¥×質àÚÝ{ z½¡ÓÞ˜Ãñm×{âðÝ™–ƒ÷˜ö‹(ˆèç¦ÆŸ÷Ôd’;„šÈž·ø†—`›fËÒ]³ðÄ_5õ~?ô´+ DoÊÍJ,Ÿöd÷?>× ‰M…wI1´F.oÉ ”¼§ýE“)ï%&!¼×Þߺö¢ýî»­ï~ÿýwtÑn›ŠpׯpÑæÓrå;–ƒ¬Ná8þg\¶·yW¦†€Ÿ»W …ZÃ;À>\På3+‡ÃB²/è\ |sö0¤Å.ŸtƒMíÕE•KH»‰›¥Þо§ïpä(jÕvî&/çÂׄÅhô{ú6§ýJRÊ¢O]‚“‡l®œ!Á¼ù6³¢$ÒýO–[Ÿ3Rù÷ññÙÓ#äÚ"üd Œ‡¥DsUëÌŠš ½€gù@ì`ðÇ*v=Tºeá³nùílIhd0GÕ‰M>„?‰ÐÈ:wþ¦vÃÖ sÍ„í 1‡Úé•V,.,Óœ‘:ˆ!¼š¾Ù+ÛwÛ+•Ë&ûËUGR Kߟ ÖE4@3·&œÁššX>é˜V^&}¾€Q¦`=mÚ*sÌ´L ó@ÍÔ<Ãø ´LYß0áR`%€+¸5ËH Oð=Š»~*ˆeá|zUË¥ÆJû™H\…+oÚ˜›£$÷¬>Q~õ[š­ O+N™b$5e+ñTHßß…WeWòÈ8ÓDÝkÙÌ5l¦râ‰;¶e.gîÀVB*^YI[ÙšÏ0wr¡ö–°HæÆ€$ÎiU*6MÒ=­[NXXå”5Ç–àŸ°ŽP@I|ÝùuÇÍí”ðd“W\³283B>5ùXc-–b¶¯“bn=|ôhk›‹<â‚[˜ëá R ‰·×É1$O'$ö 3#TUÎÏ/ºÙ”ÀFDFºYª|×Râί%êò£¥ïªFA”ÎÈTÿæ êŒ3”-Z7V3x ós#a+kÎ BjÑþ}RŒðì>¿†xü÷1ÿö§j¾À‹©³¨®†ª#òµVdnIzÃ¥ùÃÞ°›©4\°ÎÙÃýî®Ò÷›ðÿ[[Ùöv÷»îýïÿ+“¡fÁ¶]ݺ _n–TÐe=!¿¬àµ‚‰~û¼KS‹Þ¥C¿ig‡eu>ã¡igÿÓ'å²wã f}öàý²ñÓv`žy´¡ž3âÞUö¢˜³¾_,“‹é‚ ØÈÝX­¶ƒ*{°Ý…ùx´ùhû‘èr6²Ã×/^w³_ò÷hÍæ‚•Ÿž—ƒì¼X̨Z†.dpèTøÇÚx ¸4‡[Z,‘½A'Wu2µÊsxÑcÖe˜ÍËüªÚÀ»v* ”_cbféâf‚æ.Ø ]°ðaé£ÎY'k|óÍ7ló þ’}ÓXS9Ýä7Lú¶vÃ|kKrÀ )è¶ùÆÝ0¡·ò³¶ñ){­WC´Ç 9Yo­¡ÌÏ–00 ÏÄ2Îfp-Õ±µ 瑎)\Æ/nô2 X[àÉç/Úép†éòËÞ‹ ¶GEO€Wÿ±#©ÚÙ»ÝÊ‚Œ®LílwÇf%kg¯vw0­9üÞY[ë½~{øóëPî½ÛÅͰ“meg[ÈÙùíZò< Іqû´ÔÒ–é±hÙœÖ`¥Vp‚öu+¿l‚hÒä‘ôTC'È£ýÝWMP]+»@Þ离™¿üöo^²Ú^HΉe˱´¶´¶´¶¿Zku«óì ºFXé«Ùx²F;ËÔEtñFßöJ³²U·aiž]Z!I›g®]Óê~ГýÚž„¥yÞêz–æYm¯}^#?©‹|Ê*d†dYˆRÏ`ì׿ˆü¢9…¼$1ÒøqÞÅis:Žò3^‹ÏY®’ÅìÛº­#ÝÌÂ-ÞEFDÞ˜óïxÌáz¾µ›Hž[$ƒ™öþœÏª´rì`N¹Êl/ÚÃGŠß÷¹m‘¯ Ý(…U2j:ÀŒÊA®Ãƒ¬ðÇo®gchaŽYC”› ›!ÄeàZD6a©d(øj!z¹Üa*LÈîË—¬wÿsuHðù¨ä³ÇxÏLã9áš>“ô“,m >¦¯Qb+ ú $¯™ÌªàÙëU£É¼"c[³Ñ›=@9±Q^¸ñSO) ;Lã{sÌ^ZĘø4 $µÂ"=Í â2ÄNåæ§œûþCè´)Ò[FÎõ‰š^E†‹¡!Qïg†!’*þjƒ[å-=ªÊjUà¯ø‰-•Èj„ª4Ì0PÑl¢ëSüÏ7 «"Ýu D‚"*x‡@žÍ¶ö*s(ÛvÁþ$…©-f=ÓÜñæ§íöçߎ;ô~Ð9®ZøÇqÇ5úóÕô\R!c×!_ ­©nÚ±ëes­{§]ïô‚Ún4.ÿÊ·Mz©ãÐZ'à7'–<Ýù¶‰ÆÂœÕVÕ:>¸û”ĹùÕä&½¦G‘‰Ð´È½šzÐ âŽÚSok»|·|F°Šƒ²(Жu•ÎfÿÞ¶NÄ(ìò‘Ä |VWéQ8¶²p[¶5Ô¢°u/^4ºÇéŠ]/žÍÙù‘ƒ¶Sñc ÿTõj¸Zñ~ú6*ù)ú®!–g ®­*5uíÎÕuÝ©×úÓòîÕÞîë/³ßð?¿|ûüõÁK¯\2£bÝbY®ò=’C#:{„åf`wÍÐJ"e®j‡ÅÌܲa€&f<Íá©iÜIM:ÿlðåD\›ˆå#uê¢Ä gÏ.I(UPÄ‚É ¤Œd„V&êÓ´ùôÍãüHãXžÔ^a·Ä0ÎÓ5­9¾~óþ™Û”øý¸ZB 7²õlc´I¶,N±;ÌZfòV:²N=p=èxš@‘~è‹â$ÌNö‹xØsD¶ËÏfjFµXEüŽ2.úsOÍ(äøšÏ)éZ~Ž¿®ßþ¡pÐWž™á”8O€hj’Ór£ãÕïzp¼Öœ§~•ÎÓÄû¢5»é™ª·Ÿ>ý§Ã×ËOÄ`~×ÖLÏ­„èIø=þõxÈ 5Lc-ÔùœN öX4Í%oðE1+FW~´lóÊÁ¼F9&ËΟñ‹`cƒCs668Cqv9Ç €Z–(‹n]t‹Š ¶!¯8«Çï¡ò=#“ûFö¶À™“£~>xOVL6í” @32‡z—Þ`ÉX×ÔŠ2LÙÒ„ô(¿×šÛ—Ë8J”7ß-.<Ðúª8ZXg‰·žã_I‘9{Ó¼œõУ£Y†ê¼ÿD€*Õ‚‚¶×ì VR˜%*Ct ¡¤Ÿ“žÀd{1÷ŠÍZoP úÕ8°Ð¥C^¸;Ñ›¤Ã¥¹€‚aG˜ e0ÃÐ3¤„ú¼ª@46CÊw×,ñ¸1#4ŠôYo| ÇAž-(ç{©.öŠhb6|­!;‰FÁ2WŒboƒ¿±ê´ßcs;ê‘nWÔºèšl&qM$iÀ«7Û@Ø·Ãké­îmÖ(õø¨gp>/­ñ3üȼ#åFÀ¥cK“ÖGÕd.¦V\èZaŽÔmYð/GÊ’pPwÌãDå/æryš$ÚÍÆ© ŸÃ¾QpÞÚ·Â$¾—šªÁ‚üDÿ>ËÍ ¤ŒH²«DUpÐ7%,ÎrJ¶P 3”É`à)¼ð§Fºòç’D ëTpƒ%3à^$&ÏQ$§ƒÖâ@­;þ’‘IG‰i:Mv9ž}¦ódac"‰¼hž•ÝP±"<çN· £{¯‹ Ð@©ºQY:$!·ÖpUu3Ä¢Ò[i0‚&zÌõ‘6¹~?õ£l!‘äv ˆxl¤’f”€Œ·xI2×.¯¥š ×ÆåiÓŒ.%}ÅN²ßv¨ä­ç$>ý9®;Ãs¹Œ þ·äBIZW@†Ùpa†KÎx¥íV&µìÈÎk­@´è&ÔIþp%>%ö¥èUàWü×è‚YȾÛÚv„ñ1X9ÿ•'h¯w=ª¹{ܧݹC–ª(‘)|Éh,ÁǸ¹M”üY1§½…/‚îÓQø: …ÞDñø.,Þõ;|ˆÎ7‚qÀ ¡©Àh> !P¥L8Áßú¤°VëÖpœ>Á; NóKct rKm¡‹cÑÍŽžmþW¾ùßäÏÙ´•MPò¶´±D„=‡%⪷/{/÷÷÷Þì„ÓãÎqçîäNÆ‚¯ÞýòòíÞnXOŽ;í“§Çããaû¸³yrç¸óT×Ü{µw¸÷l¿æé{ÜùCðú5õží¿ùùYM­»a¥Û[Ã’ÏæV×ÌmC»oì>ôÕFo­¦HÃgaú¾P¡,‹†ˆ÷Ìs+GÉ<îåß Æ`ÙØv‹}í'¢×X†_D˜‹œÿf~NôbCᬸ˜à¼"„Æ¡`³'À…ùÐmÖ- ØÊžd[.á ~t´¹uBÁîÆ’\_Xsl&翽nÔ8Ý·<ò²2ù¸ÑeBœæ®Á ¡v^¬û¶Œç–hoа7å i+«Ñò+ˆ®»ovì¹äwÅG÷-ï ú}çÛªÍn]ÓY¸‚øÇ¥* ÷^@Vü€ÁÎèÝeÐ[½zo+h ÛjNÛÜÞ™ÿzh8[µo¯T‰)­|:ö{õ 1§ñ·U†üm•}[µ¨3{KÏÕ^¯cðzB/®©KâÓj›IhÛq{NðÍ—1Sþž’à2-`•"„\P]Ò1 ¶«_‡†)ð ^Z‚]4?{ÒˆŠ›+¥¦ÊËD•ZÉGê<‰nƾ‚oIÝ`UÒ}ô½¦Å¼eMFè?ò»øwÚÝxo,^rVGz1…ëãdà ÐεÎÛ6rÄõmuz‚¥æ)Á®–¯‰¸ÏÓü ß Uí±i‹ {ñ/Á·i×_†Z70²Ø)ïV¢Èq Bª½tIŒ¿_ ž†ŽÉ~qž('ÎIÎ`FrÑxVt+·Ð¯¡ïÔ­Þ@ÖÁÉ¥Á„Ãe”—ãÀT¸A° rIÕ–7;ž«[Uœ¹œ ±û;fØì1Œò_¡ôòâ8K\¾8ã°|6¨ÍŠbS¬œƒør’õ'“Qw¤€@’Ú“´[zjD¬äÁ¨dçU3‘SH/b ˆ0Å%åÖmësqZPg“â=œD« ÄÄ p’;-ÿ)Å”Ž Ò^™¥ èÆñ¸(°ðm ¥;x´OCíþP¥Êÿ–Û{±¸N+c~$*Úm¬¦%,ÆÃú¼6v°;ö7ÉFLÆ9gÒ3¡@Wœ'Û•xøé¾¨Ñi|KÝìãÚ-îëgV°Â@ÒA1ÄÌ«·º·Et+zü‚ÄÐ Ƚ./Ø3ÉkòKÆtÙ…¬¼Gsh1(+Ï臷á¶>y¤ðàŒBEJÒ˜SÆÖTk°i#øDhtìm•%¾ŠD ¨X¹²ÞÇxLÀ`РÅe0 eŸ³‘||e-–Œ@ÝÀë¡*¹ØÖŠ‘Öš‡”jÇ/7äò Íù¢H3d ½,²pj[U£‘/-8Qñ]ÕÌ»ªxK‘:‹‚ ‹ ½tꥭ®²o›öªše¨¯l]ãqJj%x.À•€DHÛMüžòes[£çæ«)Uý#‚ÏùJm³4 Þh†ÐÒVÑ~ØämâHàŽñ3Îøcu·g¤©Žíþ”tAº·Œ;ó}ï7j )Ëæø3H9eJ)URšŒ]°eT<‘I<Š:‰«­M¤ÑÕ ´µüè›}'>l¸LŸGÀÜ´›é”—Ñkuã;Ɍ˅Cæa§®šÚ9^/·ÿr1Qw·D×·À2ífðˆq¯—ý"GÛ†’¼éù s€],3¸¦¬™Ø0¡P~y;ûPÌú“ªØ!AÂT¾àE£×=[ï¨Ûßb9ɵaS³)ð)O³Nµ¸p8ˆx/‡µF÷@äñ¶Bó’ó4µ)µRƒ3×» ×ø½Šày_÷3•0ÒÝxb$¹i›Ê3ã¶§ ®'ÚPë*Ù½kœœìoœžž 1E ±ÉP0ütñü„Üáÿ’à—¬$áxÅ´>ÎÉÌ14.ŠJ(IE¬ }#pêméýOrî}î`ÂÕÎãàï¹Æ<ù¼l©´ûËï°V 6“6)âñ)g^rþ¢É‡5è–L“â/–ŠÅ­NbuÔØCØ™š¥r²u—,•¿Džg‘­­•WìÉçÉ­Kƒì4—ØÌs˜€‘ÚÒt˜•ŒXJ„³L›ûSu)òÑ`1â\%jicÌÔIt™Ä:Ä‚ ˜9j”ŸÑ6› €ÏhŠîpö92ý+ŠÈÐQ9þ¦ãõmgNÇ`iõL3ŠÏaÀµã¢°é7œµÓ söZK³ôeÓ±—^Û6./m{k¢ïóÎ2µñ®,Jº¢.íT¸öÍ+Å›B@µy.:EÁe²Kð²)õìY¢@¼½Çûî¶ß…(ä²?[¬‰¯o€H<{þüíË?SŽFR,曓SÉab¹*ϙވÁÓ2ÏÞO¯^£I¨öÞ¼|õlÿð?=µÍ] 7609 \B¿o§ÌúüÊm@Q¡â#ëùDBÃùxâT¥\s“˜ßòBôt“Å\nIí.ávŒàù³Ýÿxýã0‚G¦÷‚f#s„ìøKùþ¼(>4*Ä,ÛnùcPœip3l#»¯÷÷_ï>;Ü{ý Zø¾ó¯ÿj¹&ážlŽÊ÷ð~9Ÿ€ Â+©l. ”v³Ù̘˜ ø^cèÓö|ðòÕaò»½| M?¸¿zÃÆ›qf²×·\{¤:~yµ»ÿîÚ€÷{<!·Ð5U«ÐuQŒþ¹(·±„NòQ«qß› ^§‹'ŠŸM'3%fÁ9Ÿ«äžœ2äŠC4` •§pÈp ³_‹øô´ >Ì–\QWö(ò§‚9ïŸ4ñ^îdSâQLàêRÛÄ~NŸËîQÆ/+6 ÷^G°g5ev¤ MO+Jçw” Ø‚ÏÈœà††%áTS(šeT;ÿëôÃùËÞ+i·÷ãÛ—ÿâmysOðÁyFûdqÁ7Ñ8ªœStÐä"ÂÛû–{Ä÷ýâU„åÜ$¡só7m›¸U+9ó‹j"ú Cº¹f¾D ‡Ô>ƒ"xGß²éÐܳÛÔ0³Àá"ËÂWv vÔl‚«¡œA ½{§– ™¦¶ïçÛËÚì¬wÃêb 6a©Á†H}“ãpXŒò+àƽP¢S 弄0¢UÁ"Ðá d’ÜüÓ‰ïEÓ(Ú»túv=uÙsÒÙüxÊ÷¥˜™„‡âv#šT˜m؟ɑՉݣh‹:3ø}ê¦.gáóT´,†R_LN0ßÖHFË2^F53—+É|–€Ï—ºm—ºöwbP?ŸxzµóÒô-RΜ%šßev⥉¦'2Åë÷T`ŠC\•j1³Ðîxšç¾™V¾Ùa«­-?M™3êÊ*¦iy$Ù ÙÿüÖÆýVÜÙó[¿âf‚~Î1ÈxTÂ¥C™úPñ¦³Þ'l[ )’®ôùàk¤Ú©a Uá1î`<¯)ëºl ¯ÿ»çœ9—ä¡i"­Ý›µë(jÚœ¦m#ó¨¸Ï †öœœJ(jó3¼vQ*qO«^U©÷×ÂFgÞ$˜ÎuDN@“(ø>õzbbÑc2uvoYëì6–UÑÿ‘w„쀓ʾ½ÏZ÷åv¼gpåËñ"ð`ÙÈötô?Ëhy6£+T±çÓèL¤©…½EõaÚŒ—pºæ44Cô#FâƒâºCB8">»|d^+­p^^`ª_A2ài!œƒ<;/ÏÎõ YÖ`:}ý„Ù‡¹gƒ0cÞ¶Ó¶U;m±íBßîÛéñÐ:ÃÁ"/Ï<­ X¥ÛªøM;2ŽQ§“ËVÇ®û… †jiÓj<ެ~vî FiéÊñžm$–t¹!OÀ¯‘èßUà «î?ËI¥äÙ(^ÞF—áì€ © ~Fí£ù§·"õ7ÎDa3LìÕÑØn%Æë×\2\]F+”‰pö%ãV)‘õ°ïÒÈëD3!fHѼDFªÛÓ÷¼†A^póbpÛ^ÃøJë¡Yu> eã1`º³Í¡ êO^þþVo<çÌ^¦|]üÛ(.@Þ1pÁhçý‚G‰(Yh/}Çìßþ‡[š‡»%–÷ÑÜ@—­~öQ^‰1ot$/ÕåÁ‘]œž" 7ª5/flH4š6xÏ.„-Ïu-~ðܦÍZ“> ?ª6æFO7Ãúì 4{“}·ÉÆð0–ìŸúD!Cx,ì¹,öêÅ[o/ýçšð¬ÓYáàRLÞcÚx.ýµÔkFö]̾ºœÇºAÒÉ$IuÕ{ØßasIG‰›´hGÛM_ÝÉ­m¦Å|\³;] ºqå0rdJD³;É 9€Öï]ØÑFæÞ[¦¥6S. t%ý¾ Ò4 ðjó SéI8u«NKú+‚cUðB#±‘70u‰»AzTÓ4Ëò–-õë‚/³’«8°é˪ù¶hI]|ѵ«ÃsóUìF¥W']?»À=pûKpnñCôfž×Å;…·Hn#ðœÕèã$²Þ• M¸¯1(*yâ²PR«b(~El;4‰†=8Q{t†4ÏY’”ˆ¤òÈâ X3NzQ±3*4¡ÔnöÛ¹·ë-iSúC²¿1HÞï< hüjqvVžFdq%‡ Ÿ’³Ú)j™˜¥ä2tÔÞÿ¾4Öüõ¼Äà¾1!›ŽPñc\©‹Ð¿„€Y:úÚª´ü¸‘ Z^  ±ñvÂK¢CìZÞs%ZëÛ¡£QTä$XÂ.˜õ¤Ãuw!Ý ftí €xe–‚{ }lb£dý¼â{qhä]Lw´À©YÌÔPbCµÇˆ¥5Ï(Þ>X ‰DƒÚÏLúŠŠZbÒ†M%)1yÑÉÜÔšuêo§¨øÃÒ ü×GJ³ 2ó®Ù&$¾¿½NÚ^$"Ã#èí{oú ¬5¿Dˆþ[¦V{ˆø+ïÜ.*žž[3qY²»Áã «¶v¾ŽøcÓ#Ö º²_À >’Ij[w¼oi'aò¹ù¸ÿ$ !UmÈ ¾è1{¦íh´ÍigÕ]Ž3sò>ÎûO,2BåBT eM4Ð$ØMŽ…Ñ¨E!zq9AÒ6»!V®h¨yjÇ”5Q?£ ùkÃz œ™Y«&sz=±…ù†å:©© º¼º_q4²Å† zõ`V{nVc¥\ÜÏ»ÉöãÁ¤ëO;&XWS©f_5Cx‡ì\¾W&0n<œOsʾˆ'hc~‰ ´ü¤#Ö¸»:!7…§4°^vÁÙü¼KG,ôo>áÞNáÞ/Ü¥— !;§ëÇòúûd(Š¡Ëº”KçpyobüW•ŒÓvwŠÓ榿×M¹:2Ô¾ÊpHÅN†°Í¬‘Nm¤½uØ{3\6ýδ³Í{F,ƒì CßqC¼£:ÇuÒçЧŸ'%*cÚî"½V…§·É‰^´æuÏ Œ2h‹ÙYifO´V[Ñ·WAþ\â²—Jl•›ªêÜ€Ÿ^³¥hé\ÒGùËIÖÝ¢Ðc—ÎÌó "Ü[?}ªgœJ_ÜÕ&ÿ%•"99~„$>%÷¸ÑèaÝ32^I˜ §]Ð[–[Ûª"™·ÜÉ ¶=¶lA¥5±Ëõ5UÅhy4Œ¬ÇÇ'&Y2mnZrbÂÆP‘A Ä‹¡ZR÷Úµ©„Fg!”í"Öo_S<̲ÌcŠûÏÜ:´y24ƃç/ê½ÛmE/3ú!%û’Ê¿ì½ Ê ÍEÄ`·¢€Þ‡‡í¾}ŠÜš z#;çœc¸Èu¢ñOä+4v=—µ"q¢æÈrLnžr}óK¿½ª•+¬%пVhFn4¼—½à&Å«%Ã~ó*+>ÚMì¼sÒ$¨IJ†Kë|ré.Š‘ùâóPÍQVç—Oµ›º¸•aJ™I>—Yè·²{Ù+÷ý6…ÀÿÛ¿Å÷Õx1õί¦”Ÿ5‰hÜá猺9Ýjew“¦ÌTÑ'Yu«s¾NĆ÷7z]«Û_Ûêv+aW/ì5›¶_qÙcÍÍm {GQ0XÞi^LF¿Ÿ’– ÝP»€W[`@ó¦+ikï9@ìÄ‚ñ°Ìʾ¤¨7aüÙwtè¯PñèÙq‡Oë;Ff ¦÷ˆ „º»„]›M04Cnˆl)AÙlBKaiCiIôb §*ñF¼²(ámѢʂø[g+ø:ÇïåWƒq)³=¾¼—5_¹´ëâ⦱;ÀÛuü¿"û·â¶°«Íd/WjWÃþÖ·Ú·Ú^xSnœª€ÄÎŽ­x"soxî[±b\KO`ý¹µú,nÕ í®6€‡+­Jýqø¥K³Í;-{ÞuܳéæÜþúЧ±ä°´ån>F¥0Ä¢ˆž·e)¯ ñ@)Óa#)‡N¿2º ,„è ´X!pç N<‹lÖ,ƒèA Ç °×7‚j!•T’„-â@“2¯W6åX²¥=KZ±ÒÖëNž±¬ä¶WÒsÚÂ#–¨X\m«r‚X8ªß_´z˜U|É‹¹xƒ'bŽêÜU£$åô¡`{[€”g‡ðÅËÙl’p”Ù0gÁ‘á,¹"`|Ñwçx¥’öGÖÇnGÊ/ÎfÕ @¥VKi:¶Nî&?¾Y²°SKIm§ImǤDŸbzf²m©ž–Kóc“r…˜-koÇ;wÃEµÜVM¶Rh«Ê—ZÓ„êqʯé_»Æ˜V@µßù̘hᕼYM6íŽø0hŸbÄUMi*œÒô)Äod§îéåK˜k« ¾gGÚªqˆ“ìÎA¤Ö->ßMVŠM 3ûÝïG/4PïOÅz•û‘íS‡KÎQŸˆ p¨œsÞ7'àp"²û‰ PpªB§ºvJÃ:/ ½SÛT±•7Z =E¾ñ</g­nf7œ*M:GßjÖ¼åpù›¬£5ë;•ÆS&[‡Cð…oçÛ¿€Þ'“-é]¿úSsýÆW¸Ñ‹›ZYt—.MàE’jÁGyØQu†01^ îî"K9 ÁfƒkYô£×+LÜ$mÆ^‚³‹³WPØ© å%¡#v5½Á¶ôÚIß:ÉKÇ‘zœQêŠ3{ó½îßç+vù’Ä7h»ŽÞà2‰t“›h?ŽàñðS’ì²ë#ëß$ö1wH‚÷¶r0÷‡¶ ñ"V¶1G±ï‡¢Yˆù” !ø¼ælæNÁgÓÈ,Cr2±ë‹‹æV:,%餋uëàíæºÌMú¡ßÇ=Ê"º·í7{Ajªgn ØL@É!Êf‹qµ˜³%%@°šmïדrö¤“o7‘§?˜+zÌÝ`°Hj"Â<‘8„Ò¤P¤äÊx¼[ø8s. œ(jñ”\sÆWÙËf‘{éHæ+?ãÖÿû»V šFDqZ‘ÑÑ¢#`!z2 c€™`ìMfäÿ'Њy RgœÛ,½é~7˜}ó¥Ö’-ÅYv‰É–Ã.‡ˆtÑš®†VÁé§}¸eQ¯ͯ0´†>´Ô¨NÎËiAiÁñ%\q|FÄ’}ªæ³É•Ót;BìG¥:)ǃYA¹¼F Õ»ÆC’âc«kï^:.S'ʲ$P¨Ymâ&}ÑhH6Äæ~M[î]åðäörSèõp\ØË—ñ’ñku4,ä-k §$pæXö<á•gãž;û–2éOæõ#•M$¸FTг’;:ˆë¹ìŠux[¶r™¿Çqf5=Õ`4Õ4÷#üŠq4%Y1¶ˆ~åy©Ía‘9—ò¨Y:B“~›L ²R(Õ¨Œß:QMLЭ-è™UÖ/$¾0ÑyÈ]#h̓sɽ¤Qø`û¥_”òj •eJ-ÁhB‰µïŒçSÆîgUÒKÊ÷ØD4‚순$ªÑÉÊ `W;5”ðöàFŒ;fXºIöð©‘£ž$¹.IÂ#$Êêœr ×rqâ€6[J{@°h>}üq'D²s6›,¦MŒ†Ð7èm†›°Å—ò Š TD夬šPEI>?œˆ¦h[š…™ÒÊAs^·Œ‡M*ª686ÕÇtªä¥ ,ð÷4Äš¼hö¹Uª®[*ÖG’Öue. ìU»De ×M*…®à:h=I’{‘ ™‡küæóâb:'yCD[aÄGf–ݶE·O·[ÙóÊye÷ sFؼA,JafëþH±€ùÅtTt³õ&¾l¶:-zHmwÖÙEÁ#LéÀ²¼ê&“åeÙ‘!²ÞÎÖ™ü~⨸M‰YÔ–$ì¾–z ÉK/OêîžÒp¿W[°,!Êa¸N4¯hЯ¶Ž„P7RÏP^޾!îä­eã*6¤•1ö|tÇÒ¬0lveøeN“~;:U.jO 4 _Èp¿er¦F¥ÝL^›êQâÙ¿Éü8y¬æ8¼VxIÙk½½&AAˆiQ£ZÄH °ƒˆ¼Z0BâlL‘6¹¬ôxMÙÿ,Bª‚šN£€ÇHj|¹Ë`Pc¾ ÀŒÅ«è9øf [0!JõFÅY>¸º©¹ŠÜÔ1©`‹ÊK4f0¹5¤ùNÁÙQ==©ªí"ÉꀎHKLNw2rFeŠàã¤QÁ€?§™Â­Üx‰c©‹’‚§gÚï1 M›Z•¿üš%1 aû‚^ lºÒ_‘îÞÃ1w°ù yEÆÎ–J»œž²Ã:x&K Rꉷç®E#—>bHÅxèð6à §«!k˜\ʪgy7K·~>Å%Ôœ°‘š³•]ó+ý.mÒK^šÙ´H3nãø My¡Ù7áXÂãIùe°¨'•E¤NjG«Ü®|“Ø6+†%å«M©ëùþˆ}-vÊ[p}—tZ²Ç“µ,„+Üðµ`à`<+VHωMJí@·fÅ—ŽŸC®?­n$ŽãðåÁáÞ«ŸÜç˰6¨áâb“4š.Æ[p&u¬jeÀ [Çs’%ùÁØ´.Dm&ŠásS–c¯ƒ¹JgÌÓ@dè¨W âJÔ3þÄé`HÅnqürbqÎBA~ÆðjÏ Á¹fžiYH.i÷‚Ëú˜‚¼ˆG®ƒöUÍÁ¯‘ÉÍ>4€ó…L¬’üt.?“QMã-¢ü:Iªô3˜¼³Å(Çw "hTî©T"ÒyjHŽà¯½·/zù—7@vVtÐñºÍY㸺ÓHh¿ÕùÝpA+\3óú4Ô< §gžó-¹`*Þx ¼‚ÉðŸQ—89vDòà™‡²-£€ðêàŒÞe†Ë* |?@Ôa*Ä ‘Ä…:%#›co2°ù8O ×'š7µ 2€Fas“$~M*æ0„¿C tV'ÅêѼàSœž˜¥ŠP„,] e f–œGªIۚĨåp;’‘ ï2G ‚D!­Ltþw—ô [œ üü’@VÎ ‡ 9°ó÷I9ncÕ ŸÍ‹@™«øiváó1ji>@6Hj~ºÂ –I‚ñÿ‚HŸ½Á§Šþ2èI˜Æ ØôÀ‘_Ìæu3±C3QÇ—4Р;φ:ÆPБN"¬5¼'[Ù¸â˜Ðöñ•w‘…TŒ4Cí˜ÄT$-­Š{HŒ¾V¾Ã{¤Àp/ó$œŠôƒZWK'š0|ÑʈEœ#X¦XR—~ߪÈÛhè“{¹…žÊ¶,iq1Ÿ^µ1=æ Gà;eXUP^ã"¯’jdd5o\ÎXûÞübzºÌS§Óq§>e;Ù^ ͆_¬Ñ|`ùý«Œ§šì`çŠfãx|6¾b~ÆÌ,íªËÅ5ͱLÌˇ¯÷Л£W1¨S2ŒN }WžgºÕ§1–„½Ølr­H]`ö‰Å»$/Fœ–â65:”³6›u¼5q*?³…w°<œkxê–s²99æµ1ªQu³Ë7Ã0 ¶\$œšç`hô“¹lºS˜Õ¸ º”Fý0G÷Ò¸„_Å—¯­Á׿0™›qÎö$Á8ÍçkI.~¢ê[–ǰIGŽ|i3‘σ<@ì/®ÏèÊmæÊl{J@dñ¥) 5Wž†·XZìÿŒÝ¼Âv¦ûbE>Šï¯hÝý’â6÷ÂOÃ*9ûÔ…@U]›‡I¦6¨-µ”öÚy³û÷£Ëü*\0ï| ¬è8+kgÝìß;Ùó|pÞJlåÄLZ°xVäû|˜Üå780&k ê9/N0Ð~è÷^Xý7½RBîôž'µ.ÙQO>ÎêYŽWß3wJ…Hˆ [C¦D-¥í -„z߯‚kv©ú¼' åWòÊ3ðÁ˜ÆŸMÊ/úCX™®o1l>íÏ~ûëqu·Õ@³Vç—wû‡{û{¯^¶:Õ¢ßlÀ§U«3+ÈrJª×vFêT$?—È7ˆ¦÷MêC릘Ü W}^õѵKf®iJé@ .G^t>ö‡7Š'ØÁ.D:t™®æãúJ ÝÃŒözt×öz¸¨½žÿ ’S%=-8üÑ©æ yv*;yÿ{ú?óóðþƒ­û÷¿ûîáýðgKþÙzðÝw[ßmo?¢?=’}÷p»Cúúñhþ~“BmF£bFÞ3†Ì{³â¬ø8íL¯þe#{•g‚`¢Æ@ÌžŒÞ—ó® ,¬Ö6Ö@ˆžL¯Ø<Þ„ µ ­mnC²Wû‡ÿ ^ à‚bϰ§fÝìåð-ûŸO»÷hB:l^î8+:ãbŽ…~$‡ < ínÂ&Õý½Ý—¯^vÿr¸¶†›ÇM?†%ŠØÆÀŽƒýaþfï±é¯9›~eÀüÆâ¡/¶¼JБ—­tÊn5S♃ýX6›æ·jVôÈ#§2åјÍíWÖ„Rp®£Ï?2œ³5C–R ézQ ’ªK… Üæ¦êþd1/Gî#‰}Käì5íoq˜¨5°‹EÝ®éB¦i«™ÅÜÎ#qs)… Ç >4™eÈ[ƒ¦›)Ü`Ê©Â;Æ…ÛýÇ!ºcÀFk¶>Ó‘KŸÑ0à“6üLÆg²×¯™{hŽ %=6l!îfMÐ3 ¶Ð9&eác+¨tIT©$lggùT¢€¬1²‡úWtâ%‚Ó1©WµÃz÷jo÷õ‹—ÙoÞ5À¾x}'xmVDëDfÙôªû‹[\ëºûI†õSô¡ÿ×i¾ÍÛê‹(Ë®ñÖ¡p_^Ö_ŽóÝO8;Ÿ ¼¸ Ks÷MÕga܈ ýÊ6à®e!'ÞÀœÀ \þDкB¸ mÀŒ|vßÒÅæ¾><·emÓä¦<pŽý‡"…"˜jˆ‘Tå§ UÏi ‡8Gؽ>\QÿøKç®»ÒpM;5Š'MâϵSøƒ¸§ ý òum¹µ7êà€Âyq[æC0²×8²Öu—p˪‡>»‹ëk–88ba=Ÿ¯ê×›w˜~ð¤Wÿë^ŠÜîjã"ß$b€Á•ýh²Þæ"Ö¾5}é—à[\á‘&Ë 6qtÿ$(D+–Ú KÑçImãÛê7Ò˜f*DÝ6Öºi"ž ÐÖI«褖köôz˜“Üâ ¤¯ zçš2?x+7„’á—\PæçÚ‹Ê–~†wjR$ZU-.Œ'Æì½Ûy{c, Ï”)Ûpx»…Ïì)†”V¤[)+N T$¨)Þêä.êx™t3†ÿh›:³#b‚Tpwž{w„­»§áÕ´{‰1`àMCªàÀ‚îx&i¾ð-½Ã›*#cÓY[Á˜ÙxõYÔR”ìº:8A»Ÿð–=䬀oݶýœ¬­ <ÍbÌjïÄáo"ðô⛂ƒÒ&—ʨøÕB³sH%q,5q…8§ÚÖðГ±¤Qô+A€v¢DX$6€ÜªŒ‘«6îþÝý°¢m0@–§0´¿Ñ^àâîä OqÂ×Âz+ ƒÛ‹óêŒÏäz`e8}ÒR×?aàæTˆUÉ,PB:305§¤•wÖž³#­±¿º—§³ »kd(­›hXªYA/Gë3K:°•|Ò‘ñŒ>Ì·‹Ps¿·]þk¶SDÖÍZ›µÖjûvëÙ¬¹3±Ýú&6k¢q#»µ²¦ëžPÝÁ!½8‡yÿÿÎÒ ½÷ÞÂÏÕÊÐ2«ôòì }ùú5ž²´T‹|ä#“³Ì3\ç¶ñûÌ2?–ÈEÀà>)«¨øy¨E¦.#¢xˆï>ѼPõT±£·^ãü¥ëQz;û;6[Œä… S4>+š[œÏ¡º»Õ a×ÙI +^ó>Ø÷ì[‘Iá÷Ù¢âè³l›þ —éH@• ï Ö3£–f÷3܉&ŽH)NœVåÚPký«vbDlÔhêä¡ïjt³=ØB?IA»¼X´­~Ž£"û;¼JÊaöß&¤?KfŽØ$ƒôèsW`Ju§×'VP Ø‘Í*ôÕ§nÙ)è3M„•Ÿ3þ”s§#/›¤7X'ì5bçb—Gzå£#¯î¶Éªd»Õ—Qo“S$#ÆÂj¤NcÑÊð’¯Åe°Ÿ ù4nbû['ÏqÆ H“ ñ´ý.aϱœ²M»Z팧•Hj(ÞËRU•¼h!ÉëÞ³Ú‡ŠTó}Ãp%'a¸²º5l7ЪvoÈî¥Úo}´™ªîòÒðŽœPê 2¶…é'§Þ9`m†½Ú±³Þð§Ð4}4r‘ÄÂ’§Ù¨3“\"NF`•Ë eüžÅ­ ae鯣ͭ“«¤+£˜N´Ì^8™k<ªf|!0ÒÝÊWI݈Ac¿\$ç.»À›¸"œq*Ý’€›N󱚽”ã¯i é®, 4«»ÇãÖkúË+Èó0Ÿ %—ðƒE«Mº±D ï®Uh|ÿpëþ#ÖgPi Y¿¿Jè96 O3Äš»ÎÉJRÉ/ÑfüT ’ {w]UNïKÖ‰x*…ËË˽h¡è½›é¢xk(yn~Ákz¿Lœ´¶ö|ÿõîôv_ÿòæÙÛ½ƒ×¯ÚÙŸ_ï>{þnÿÙÛÿìí½:|ûúÅ»]Aheñm»µ¶¿ÛÎ~ÞÕŸ¼xùã³wû‡½ƒ_^¿>üyïÕOöË­–Íãë&r¹ºO $·‚‘Ú\›z˜ò©×ë0utG•6¤"l“Wô¨†+ø Ñ6G—«S‘K1ƒ7'Åͳi9-è5Ϫ«RÑȱ… ]=…£ $ô©zzØ‹£'°˜Æ˜WT/üVá6áäÖŸ‘¹5S~£^œ¥'fà oj®/µ“«0ãÈãTbÔñD°y` yÌ \ö¿#רÄwÁr>’õ£|ó¿7Ùñøx~²ÞÎÚ^mº3´ÚÌé>ë˜ãÉûªø‡«ÌÚ5ŒUfÿÔ§‹o`ùóÉhr†¢NòÅÛc`¶/Ø>ä®^=ì ‡e¾8Iq—4ym–Az©]EÀÖ1Ë,éá,ñf)MÔÄ=TµwªÌæÍ°wwn~›†] 2Æ1 Ó³É ,{‡d¶Tb˪Â'uù.oÎ)Ô\môWœÒ×Lèˆ|1o%¬©;à×n#§_³Z—Öñ`P¾¹Êv'“÷}øßzØö>¡ÊM2itòÙ,¿RÂáQ÷šH(aä9ð+)ƒÐeÍbú \eäv+RêjIì-Iž%öÄË›ë1*’í—½W=xP<ûéí³7?Cÿ·îßWU P˜Š[?ÊŽçdzãÓã'wŽÇõ)$ƒe1ƒXˆøæÁИžÉ zd5 :‹)IlÒP$£MûÆ™SQ~ì~E.}:KÏ Û»ããÆ›+í!©$¥ZÝGšq^ˆ#Òu#e*rûãÒ0ó¥û(xÀ+™Â­°`Ñâ®_Þ]—~y«ÅÐu æµcf´IEMxL›kÚIŽ\Žè1~ 3Ô,ï]¶}ºGe·¼{™° ÷2'íÜg2_©´Z'jaê•¡±gŠB›ÍCF­Ô.Õ'ÏgRKÀšPÿñ¯Ѩ_¹•ñõŸ\(^­(ƒCÞ®1ŸCEì ~—©-Ey9~Tº†H#ðY¯§ô Š¸b^*×Áü'~ J¢tôHJ^‡×7“mTïÑ׋ÍxóÌ—å2;˜O¦{ìà8IÄEþBÉó}ýÕ$´VÙ%fe?Ei“Î.X º³ÍæèÎy×ÑÓÖz’š¿¬„¤Ù#vAÝW›ýüðµ[ñÈ’“Š?—ç¤É”žÄÓœ®„?7Xø%Uãð0üYmµðg㼜ƒ?ŸO.’ߣh}}œÉéCV,ñG?å°.¼å&ó|ïÕÅxΣJÖ+OUéèÇÙøfGONýä/!áQ¸\Ç/ï¼9CÔä£ÂG¥Õ _+ û/os™-¶·j"íÒ—}ýÀ¼ÖЯ óiÖGÊÙâUD)Éjéc¾ÿ^hrº˜é¤Ú¡½ýåº6¯ë;ÙõS[ÿs”˜ß“¯è™ÛpI“éŠ?–ñ¿†ˆÙ~;jó}%5™£¯›ò`ÂÓܼ¶¤FÌ¥¡H¥¸Q‰>)m4Ë>žNºî=£xæÖIˆ¤ÒÐßÄiòœ‘÷–ñTÜG÷]¸¤îI"F=ÿp& 8¯Ë÷Pîó릿«ù°øÀêþ«¦´Qµz¦}ˆöw»Á’`ÌýûálSµqo[iÁâ#ii5¥ÿľQ&tœŸÿ.§M_…Ëòn4hÏlïHtª‰6î}7Ã,Z”0Þ|}>tª»á÷„{ßö´|müTAçSØn¹†s8gñtô9˜ënKålÊOlINJêô†-ž0âï*Ú=Émà½ñ7J;Õ¯šüç&}Ý‚7åÃú}ŒU¼^os·¡ßéK&5Äûá¾T*wõTôlLìøœØ‰»ùÑGæòJäpqØ{„™p–OAöìöe„%EßU·ûÜÆSy¤ Ê~¬¼›·PlTÜ‘$œz`.Ó{8ܽƒ¾œ]†AâÏ'#(2[Œ$Wýâ¢ß¼¸â_”‰Ï¬K(‡¥`nBkº ×^º²©üœFo#|H³‡t`<3yúdا>Ÿ¡¯”ç¥ïX8žì¢7W€Â˜ä Ú‚Gè6<:©3…ùª¥ ’Ï»œ€s~>+ ";+†8UcJÚìÍ3eé$¶ÏNÐ{˜°ÿ.fß´¯^ÄØd ,kzG¥Ï¥Ñ÷›%ÈEQý㵌šrR8Z&8Ât7ñ¿'AÔß'¦Nt„”h»]èP‚Š?ÙaÂé=jڌğz’ÞRÞ7³›ö¾ËÝ¿¦ï³ú¾ÓW;_ßsý±S¸À<3we\›ÙöäȘ•¯?tê K±>¬­ŽˆvlLöÃW†kQ4aŒûεØúö`*!q¨.ðbNñ»ëÛâ• £ÆXDm3J®üŽÓÖ£œ”°kÚôÿ¶~LÎ:Q"Yo™8ÍØRìMB¸˜Šë¨Ú¯s(¿i3aΨíŽùq«oO\€t›lð¾¾Ý(O.uÈx%n(c©îÓ“›‹át^Û&î°ÖT`ë/Ã0T鑺ÙmÍe’Àö‰G{Èfé] {èB¶ã Z’É|Jknú—/ŽÀÐBbP¶é8©F—’èZ\õÚš” ¹_¯òXþf'*µ“&ãê[ ö+‘ù‡I9̆ §rpkmKAU£{`úñ0úz9þjþ©¢L@L 'à”O:攢uÀ)+•Q†X7ŽfÙ†dB*Ç^rGÙqfˆˆob}@qÜa«¾H|çi$Rß_£Xþà·/ù”g¹÷0GjÅÑ_íã¬WÈZ(üõy¦"€9ž—ðÝÌ­#.Åxt•s6 ~\}ÎëEžj9ñ½i^<šÃy ¿þ&cedI¹ÅY][Ûx“^`üˆñ·›©˜¸jPÂ;ƒâŒmøÞAyÝ= ã-E%Š]øcÛ™xw¶¶ä¯Æ)0Ãí(1AÓ;*Ÿç6ô^\Uhmô.YÛlĈàÔ"ˆ CÜšÉhAÏ„¹±.|Ž#Jöoê{G虿‘Üt†à‰b" *oZŸ,æše†#õà#ÇgH>õzKSAï\éQ ‚ ò|§1# ÀNfwþ•<Ëñ×> Ó³Óï£|ðþ"û(tXß­èhdPD`¹/&hªäñm{/SÃÓ忯X çÆƒ@°”ù< © ¹¹ÝÞnßïzÝÇ “O·îÂbÇô·Zwà{úújÇl ÑÐ`Ì(ìÐIW¼Yû$óжÒ\˜?Í|Ø¿eNÌßÌòXˆãm*ì0b’ìš×/^w5o:°ÐYPõ-œ“¥B9ò2òñ0@&²¯@AS3ïf;cà"Ôü(Ù,´ ¶3³ÿ " a«Ê¶R£UG&g®Çši—5Àxr ñS˜%Ãhö˳3£©P›2m¹&?ˆ X>ºr(ö˜gG_ÂÚ'Kñ«'% Ìü—´`ÆUí0ÇÌzGÛwP™¸ùñHÍå¾ëÛÛøü¶‰ÿínª›ÖïlƒòXé"·4B ^‚ôçïGwDHOP·Ót-´C Žò…ÒËâC>j6¸RC|áî6TUÄ –=ço‹æå=¸½ÍV»jSXsݛⲤWj"üQßÝ:1ÝpOE‡¨ˆï}³×…‹õgàV>˜^ò>Ÿ.óT8¶ [ã@2É&î–"Õ询ݙå—ÍÖQ—bïù\µ«ª=l÷wæó0¿¦t>ŠQsýÀåþDΫ(!ébWR ¿= ¥‚÷5†…6F˜tíŒ*í¬Z‰k¹jædÆúÕµõ‡X}hk¿ ål¢Æë¨j}¨Ö×ßà6³H¡Rå|r‰.G_ˆùýÍÒs˜ª¶<\>A2ý¾:n ù»äÛ„2 ëSÄ?R¡ÀRæÏÊޝ9¬4…pèE¿1ØdL0§F"¥Æ-ázl|’´4›<"³ª*óìmµ”5Ç~§Í€²Bþ°˜•ÌS L(f_`©U˲‡¬¸*YrUÜ£9¹2”Ç‚Ò1caà˜<·ó*[À7_oÛÑœæ•\X(Áû†™«63ª—|Ú|ò~ÏàûÕyÑ)“¿‚ŽÀö+ôo’zŸÙÙ +ºwŠ=ãÚKð¢õ1<Ï9¡Ú ‡°€î‡"5ÊDIr.ØxådHùrsz ÈŽ,rÉ8öå´ÍM¢àë÷ ï1ÁË…°©Ü|žÀ1l0W¨Õ`*ýþ¬øPæ2óÅ| yWbnð×0}¼…,ïøaîÙ¾` Ô2fù0ŸÚ'éÛI¿€Þý’öÆó«Yш’×ï¾~uøöEÖ£½ò¼­7Ÿ–­f§Õ„õú­1+~k|(~ƒÕù­QýÖ¸ø­1l÷×ë|•#BÇý&pH« ã»i½Ð…«â¦µ~*/ZÍ‹›W›`'ó›W›·šóWÛ/¾¨“¿Lf0'ã›V;l5Ëê *]æ7®õkî¦ñ$âµI^Ãjçù°ÕÂÿ®n<—P– Õœëf­šjyd?j%g‰ ö6GàKÊAn&‘ñ¢ßœ5Ž·²ãíFÊýx Ý+ÒÍŽXÒ¶ÆFv ¼ÛQMŸÍ·™Úz“Ò5vŽÇ›Ç÷ÚØÅw#ƒy¿BóK,ŠKb*ó'‰5ÄyR‹îA#Wáš^7ó7_#„1ðÖHÍ ÉaÔݵx–áEŠuMÑMúЖ²¼Ñu¼…šªïEױɻõpûÁÖöýG|??ÜVE–]Ç“Qòñ_¢Ö¶o¦ûÎÖüþúÖî“îõ*1*ŸqšŠ/Â…ý\äãqöø þèåôÇŸ*>àfOßüòÅÍÔ_¿L†è Éy>¤ÃírVÎmš-§èЬ™¡¡mµÏÉõÈi>'PõÓUº ™:§ØŠVìÁÍVLéáÇß?¸~Åäßÿ]°ULÖhB·b›R *^FNÀ°÷Ú¬£ù{Í& T-v^2êÓ,-üþÓ% 8'ß·³ƒE_~W4Èb/ÕÞäóó7ÂÔjgÿUN„žªÏ,.Ô³ ˆ%Î4ƒ‘³ éTRM͇Jñhóèð:*Üä4QXèVçá=+$ìà¨ÚYHÒãÉÄ’@g¿–;DB½9t•p ¡š¹ªo¦žC®c o†ÌÉå%;™å—=rÄc§eôoYó«Ì–;|^°óžz¤:ît^¸¸‰Ù0©3u¤Rá/8&„„dë+˜£·+osŠŽ–ï„HB`‡€Ù &†~ÀÛ’µ)C†¤ˆ¢ 8³¢*f(E:½Ø¸Û:®î´ž6;wZpl‚˜²ç¯1+EþŽDM­º@RÓ|î£mèf¿Í lsqZœž¶ž6:´¡Šfc1?ý׆ò‘[…TÃË+!‡ <窇áÊÙf4™LÙ“S… 朱ÅÇ–”ƒ–)˜ÍsŠÀ–ün¹XŒæ%ê§\V Þ„€“¼ÚmãŠCÈŠdìùܹ<`†'õgµMõUé÷3þ¨Gl@¨Ïè•¢Jßö\n [£dÙ¦oÆ‹‹(¬áYÛñÄj+Gñá¨êÆc—}ÕádãÀ:u &ÑSûÂfE1Ó’8:èt.¤£¯W™SüIûq«ÚÆ£ÚQhÕ,Š7¯7™=¦Õ-yWȉ¥-Æ—³Üüá Zð'jžªçcm`áw¬ø¡8öõ~íAoØ¡ß&W’&ó‘lýf)35§ê´ ¯?¦·û©’~‘?‡ òät­2CŽðŠ/¾S(ic>Ð!qLÝV®%o¦» ï+óò±ý ƒ\ÕÁœ9%9qnH âÁ$Hþ½EéÇyŽm2Fs ʲÙš:ÛxP ‡£r}²[#3Ä#$êf¤Ã´'nV Îáý"§1ˆ]ì'@àŸv?ÁGðñg×±rŒYãæFê1Y=ƒ®Ic±Hc—S³o7;(ÈÞ-¼‡¢vÜ„F{—½+{wøãæ¿ò°‹aÇ›ª½³ñ“Ძ&Ž`“è ~ÀW1|Jús¿ûÉî@¾?“øÁ™¶v?ùýÿìüêa‡ç˜ƒXÓâã†G݉xÓŸ ì Æ—dõÇpâfŽ–¿º‚ޝ2ƒðÂÒ•,¦øšä¥k”ã Å´õ[Z¸ùI-!ü¤„µÝO\Š'k÷•ýL‹r>{Ϻi†9ÌõÆ’Ó)X[u²×¸}.ÑÚ`}3`Õoœ[ì,F£õ²þGqUä‚þ‚-$¦µŽ“ù@‹3Lêt·á[¨érú¬Z½Z‘1Ã*Ñm _Ö‘u6ï)|'àèˆl è9ü·ÃŒHžæ¢‹úH¬ˆ”B=1–*à¿«U¡‘Ó ’!€’[¶£å¾‰¨ð-WCG`R£%ìM³ŽJ” ¾Äco—H§]Nø<Æsn‰ndb^Áô»³ÃÍ)#h‹³5FZr/~~“=›CÀ×)KÁ.Ùx&Ò÷½ÈçyÓׇµºvôôÚ6˜Žp©ÀXDZÊîÜy™Ï΂ülR(‰N,Øíç‹1)aCRþÕT—(2<èÜϲ‡÷á?o'óÉ{8÷_Øë.(,P\LΫ'‹Ù‹ò¬œÿgÀTù•F³÷yî6­²?wžùŸ!ŠÄYðÙî¾÷Y‰ìø%M<š¨0šélTDí@ å¶æŒ¿”!“›£Š`9Ð÷d1UÑY¸n›%J>ÆEníY®"˜¬<Þ2[©¿ü²RzN•Òp¿XMñ,µ„”Bþ.£y¾|4†©—ÉrøM†ôøžÏRáÓH÷Jƒ?ÌfΫ¹xíÒ[“ 'ŠTpZr$¶4ª†dv£…ŒôNœŠYÔár(yqÕ{m1îP*ð#F‚ÐÑPò§Ô¾øìïfÿQ\‘ï1üµ`ˆzÊÅE´÷?ù—cLjuŸÃ¦„ØûˆØ0>xŒò¶Úøžü±‡6‚S8TT+æQAËDxú²aÀYöÜÎx32¶­PZÔî³Á ¡êœùM ò½@·64߀öésmIn?(SŽå>®"À -G/ +IËt'.j¢Çì©ßŒ`›¡³|ƒ3X6D $ª†¤I†Ý!&J‰ê®•à™Tz5¸dã&R·W_¦3·àMk„¨¡iÉâHoüʼnËÑzÔAhz¼°Û@ÀóÓ©›²åSOÑò©ñ¾õ866EÜíäFô¶ÉV-R#†e{XÎÍF)eÆV > e_7™Hg /"ÿ 98/YÀ%"6W6K*Ú¦v®pØ …s'Y„ª:à42ÏQÕÎÛâìåÇégFÛ› 6»Yxwš™^ÒEW„’Z¢>ˆOoºÎ]¤¾Mé©“£—M¦\MlQŬêŸH$숲ûÉI_(ów¨ëeŸ¯dv뙺­Å<‡¤ìä6·Ç²9‰©nvj¦tìÉ~)>Nß`ÙYÓ**ì1d~1ÇýW ô 0¨¡°[ ß£³Y]uw–ˆt;@•PûWŸ¡fn.ÆÓ U[è䣑»uC­LÆTFpÔÄóÃ÷ÿ g>CÁ_YToظG§a_ß<É T3Óà·Êž .$“UÙß®'#™Q0OòɰUý™Š>Ñ%Ý YLÍé©Ð?~&Ž ¬®ã‚›S5 3m˜JЦ;Ütƒú5ÃVÕið̳Ùà"µ§¨—|µ)iÛoßÞ«’{©¼ç´=¦jS¬³tO¬¦ê.—ö°¹Ïm-ÞÒgMÄŠiø”.&ÖãS\’ZŽ÷»'Í@ uÄVæqE”!tË€ªœ5 αµÑìÔ½@®5“kmT½˜b ¯dÛìØJJ. o¦·1ñû(²’h¿ªûÌô^Ÿº‘«cxàÚM#*<˜Áý jͲ¬‰»‡;bÇ"ï'Ú­ü?$•õW˜YÇ G”ãî“IJ«¥¥1CâÒ£?QÔ„ÏNrx‘.ánÌFS"§Ç@¿ÖbN®Àj™ªƒ߀“cQŽWR¤»MñòNÛ¼ŠõHFžeÂŒ.݆ÔdÛííq3U±º!,kNjvlÐuœdâu8š£¡1®'g«½Èj"bzcØ.Ó¿H[·KSu±eUL_j[j´#2ì¥B×±™Î­l}™”U®j¼?6u_ÛAÿ¦õ®†JX8B×J ûKšîœ›‹¦ar!aÝHõcé. zâÿŒ<øXHŒ0ªmÎe£"á,Ô²\"ËYé™ð†ìDŽ2çÀ‹œºaùÏ*ŒBŽvÓG[X%š÷kSU+ö‹ßÚúîç5¿[k>@:',:L£]IbœýÛv­!ZvâAÈ«|\¾Ëû§£lrY¡E‹à†ä’#ýǧö7ž |„&5« ¤H]>oº¾  ˜(é·/üÖc¬¸òôn åbîo0O¡ÐEÐAúø¤cÓ¥¼¥2ɣз۴*K À@kÇ]Èg`ÞÓ^ˆÃ>ëŸÅ(Jµÿ¡ ¶=4¼tpùzxF”>œÃBp;~?ºb„zŒ!²MœcâʽÛšj=§ìep曂Þ­6˜CZS‘¶soöíÆðÊ4±|Ó†˜ô–\©›h©ðm8AÖ¡À.ÿå,@üÑ7üc>Shrrc4 Z†%¦'XÉG í߇,º]¬tùu±Å‡w+®`A :kø«hÂjq§Â3GáØ3«U Ž±$î~|Hª}]Z( bß©EÖ|bn jI±^ÀdZXÇRZ(hÙúutQ’ã~I¡ŠK¹¨ä‚Sˆ ³|"s/óæ„Òõ¦úÌ̲gjܶ»™½¡繞vÞòÞ¼»é=y1JGTŸBU©Ÿ˜<+vX³ ‡¶Zyš{ä€Z¦jÿ>r {Ñ]Þs>KêV-û n¥FóìõüÙ?ió‰*HIÌzÁÂE—¢¿ÖWù"[…SŠOÓŸ_hC¯Æ—ÈVì؇^âÝGW÷Ø~Ùåµn[ ¡<×¾±½ù•,ÂkÀKFd]uµL‡œ mÉñI›Õ;^GìCÒÜr~ÛÛÍd7Ù~;ñÓ3/‡ÚNãtÿ[úö…F>]àŠ®ƒØ€\ØÐßE!C¶¶0 Á‡}(.à’(4X— ÃËYfC¡˜¢&ÕåïÍÍv‚ùl×@=ÖF*à[V'ô`FÇCÍ&+‘ù¨'ÛõzãƒÙZÞ) Óo Æ' ’eÙcÿ¬\ÌæbÁÚm¨’ üø«“㽻Ϣïäš­Ûy;q *Öž=ÜŽÇ,Ó¥ ™§@4òæØÝSEMXíPK«‚ü>ŒÞ3;·f“ÊàÁèýê†mkkvQÜLYÙh¯¦¸;v¼xòü×<šÿ±c!FiÃÅ;!Ô ¬öß®ÄfÓûÙu¯±¼¹&e¨(µ€ ÙéÍ]•T©1˜î|sÐЊ3HCÌßO œæ´6›Ÿ½ÊÎs3E‹éƒi6ÍgGŇ¼Ñtègå¾1 LºRçr3u1‡W½+øÔÈóû_ßbó«ïlŽéµÿíÁ×ß®ˆÙ“t[ & Õ0öI°yù¬ŠS>¦ûà”ü½™iáiòðT¾šª£E§–³w¹½8ÐÂ'ˆŽœÔ¨E‚sè˜ižÃÑHþÏ„BŽŽVÓ6nCS¸sÜßÇX¾Q¹;3ø(‹Û9† :üÓ€69‘þQì+Œä”%ûhŠœÝpÂé¬*I:)'m$Œ¤o#óM(Ê ¨4‚aõ^Ðʼn¹ý FÍ ¡%óB"ñgv¦)J(¹á_NÊ«IÂîqûˆ&²®æ w/ö¿&²Û<ÉNAü‰\éÝBf©¢ëV[o+C¬+×ËŸ<•>@\}ÊsB¥ÍèeyõôbZzäðK’„·Û/_½ÅD¡o·ÿb¾ñ,ArÓ5™R S'þö‘bZV†ê¿¡Pp&®˜á¨ åƒôÉ¥!À##Á á@àJlVžkòÝ«T^°'öšN^|,nì¾r+h0^™“ÄdˆÃiSà 65¥¾rµŸAˆÎ§77bûàÃ*ÏÔxM†7-4a²|ÕÞM¡s‘Vá±í4@Jñ¬“wÌž5Ks´lºÃVRvgRv`Ê‹N‹$,ãFgÇðòäæ·lå(ÁÌœ˜û¦÷æÏùÓét‚ùýÒô¼¥@mÑÀÜþÍÉ1Ö:1•ޱüÉM,ðÞ$¿ê÷ÓÁÈ]°©úå  Ö4 …¡^0Ñ–-M³žÈLG5ÓLv¬mÆÀ0!ažOáKM稅Ìi¾”ˆêL aèŒ9ƒ !Ü›ë)ÙÁ¥Û÷À¦ãý”2 fÚd( ¡f±Ü®Á ?ÛÚH•z@fÐa›±)‚Ä«q¬Ré™ HC÷3[ÞÐC3zøÀ¬]dU6ŸÏT%úÂÙ°®±\¿‘ K›Öj$9Ðéq¢Z ™4®1_w˜£m&p¬æ¶C¸q +Õ®õÆö¦Ç NÌI6뇮‰u£ZÚ²Ùªâ âk{ð á¹1ó úU˜Ž ;Q‰“"5‰ £S,¨Y¶%kÞt¢Ë±“|Ÿ28ü’qv  òó CL' J‰¾E„Ã#A]vt?§»ž,àÄ|x)§ø¯ç³ãfJÆÆˆJ+̺ë sGç8ñú`d §i&ñ¯è¶4#aã­Ì†ÿ?YÈÿ¢¨ ¤W†…¹ ‚x¼‰iþã‚”Á©¸c窞‰'&¢Sÿ‘ô©Ï•„[ëý=Äã ííO#zÃÁ˜ÝÓ»—“>øŒA0;|lf¥¨€mà¸EÕ€+ ”ÿÓŽÞðõ2,!žŒ‚R¾[°öA3Ó#ÀôKö~'Ò²wì/í•MØ=O°„m@×Í¿ð¨êAáÕÇTkW÷iù˜6Ô§Þ’N… «NöDyî˵>=Šw©Ö¬îÒ—ž§GKæ©Ö°;­7±—Ÿä—³·³M\·±ý<^Œì(Þém0J ¼[Aò¦ 8iClBMßz]f_4èZ>²©^Ô»»9 CgpÛ‚™2KSèië]u››E Rítà­ÏßWjOQ{ÃÃC8w”Ÿ…œ·ÏõÆ¡¯¾ €âÊŽ‘]ë½CÛZ¿öþÊÚÇy ï4×±8^^ù˜¾îwO‚dk}Ï×'¡îàñy3½²‘Ïp܈¡²/j¢ sµ¶”sÕ=†jŸ†a¨‹ó ʼ:A0ŒõV’WVà„bÂñ2â«èWT+çÁP+É}¾Ü&‰Cÿ—¯ÜFæùðÓÖBbÙ¹i]cê•¢»häõ{þ’GÀ&WQH‰0h¿»p¢tÔ(ÝÄ@$HÒoä_|aùÈHÌ\°Fù€—Zk¿ð~ÍÂq Ê€—) \ÿ$¿ Ù£c`ÀÖa>žMŒÿÓƒžðØó÷skÜá‰]ËŸŽë×͉á#˨ Ó£l>ç ·¯9|üåjK¼ÅC]ózB’¹aAÛ1­?'sYgÃSŽ ºÆÃ…ÖïrD TV°® ¯‘‘è¬,lŽöÚ2âú¦CŠ:i'ÕÎÃEŠÕµ¦БŠ{ãï$Áš.oä éÅô/Þû¿Ed]·v91Ó*x^%$­ ˆºÆ"Z)0¨šŽ8,ã8{_Œc¨‹#kø‘ tt¼P&zÖ ä>‚ësÇÂXõÚ;‘vÌ£4|ÌòŠ/÷ZÛjmÐûµÝ¶ŸìÖª;œƒ¥°Ì­Ü±€¨õÓYŽßË‘ìÉ'ùƒ¦Ã:H½›ü(«¹ûuZ‚Yþ Äx„GKäwNÑä‘=³6Ö°9uÝm% ێ黫-T NÞšsÄð6؈wE|k—¸O®É>ºVËõ–ipZ´š»”‚S÷ ØuÐ&2tM >˜BY¬»Î!£š-åðK4Ã.C“!¨«DçP=@ŒQjÌi|–“G[îdÃæ+aúµRëɤ'ŽÄi¢×€ÂR¢æR+EbW2€ }•ÖN#:@•p 4$Iò‹(˜²wÓÚ ¹Booš\‹W»^Ô«%ðÜ1*«\º­™ÈJ6Ì.1Ì¢ª#i%t˜+ªÃ6,ŒçÝýƒ¸ZȨÄ˶œ“ô•YØýäð´¡Éá¤IÈDÍ£òÙõ$dèÀ«¤ýȃÐQÑ>ùåÉáGg1sã£Á ‘™b–ê"‰Ù§¯ÔPõ0á»y5äô¸.²wtðs2;㉙ @ßp]„AšëÚH•ùëp³•ðù‚!Zhñ›_„A¥>í MÕ(W§GPV¡gy§Û5²×L2z1ŽEË*¨KóvyÖ®¦yî>ÙyÕü÷ß!TŽÒ*ÎÑ õen¶•€%X‹ášÿƒÛpØ;xÜ2ƒÑQ_ŸµKÄKëšU™w`ý›öü[ e)|î”yp2ô(pæ§=a÷ƒNÍм>l‰1ŸžôzÞ=á¸8¹‰²% ]Ê Â礠n•œåW’»…‡²»A7`ÿ¨ëXèáÞž c‚‰ÊÃd¯‘wÐKÌœ}J$|ÚÖâ]ÞP9æ /~rÌk”jªA)µ±ˆ £…§ì„¡ŒA®0AÆrÕ%øj…R7Ú©•FÌå í0ƒU­‘ÙÄPEÙälÆ9óR³½ßJè¿%G. Ñr_XËà„…]àì€'s{ã–51¡bmõ tïÀµ”œÃÛÉF3~KÀ„Þ@ ûJàÒJT²åOÝb£òÊzÚ‹Y¾úå‘£\í…´kÜ^ÁÎt ½g~³êžƒ.ÿa°4Ó¥.;‘Ô´ee’Ïgy6—‡µ½t?|eöµ *uçlžü[—íiŽ^Òä„B™Öxl¦Ô°@bÖÑ»m6ï«m§LXLI@'ÃÛë˜BíýèÈÈS„P,C1û͸¨7÷É‚½áu8ãÂf…Š®B®MÊ ¢*šm^ZªŒy²\»àUíŽÅ lNõÆÚ¤o¤Ý¹ƒ‹r\]^÷)xjàËU,C¤^²öüÛ­Ä\÷>¼Èf—å;ö£kA¼Ô¹}´Ç·äCˆ`cêý¼-±œåÉß·—l~´|bû`RÇå°8»i¥hª„¶NRI­Ð~Çe†—:†X èFh:Œáa@fÿðåÁÕZµÛÉS4”{˜­°ý%X\éûF@õ! ÓÄ`°ÜFLE-¬/Nnl0Æ´:Ë hÑti˜ mœ*ΊA‘¶Wʓ䠓ð’¤0!À6“j\šû:ËCPo‚ÆX¶ß­}ÕIþXΊÀ3Œ±Ý4Ç¢¶·ºRízŒ8ÕM^“üÒÝóS:2I%‰E>·XaK ¹øZ2Ñ@û+œ„-;ÜíÊì_·Ü│nÙI;y;)|§L)É÷­ Žé“Áh1$Qèj=Õ-“2ùÙ·³â.º½é"͵êaºçueR&²Øj=jÑm¬ìŒÝÈÝä±öDóZX5w6‡³ì8@²fl7v‰ñ? ΫeëÙÔ‹tã •ÀÓý’×]SŒSÈcØpCÐÈósqÔÀ–FsD+=˜ÂÓ¸r= ó0&,;‡V± {£[Z2Ù^Òo’ »\£á¶Úmô]SuMÎ)×DÜ'šöYLúË¢•ù Ó)iÂç„|2äÁî:bn¿þÁáVuÞÄNS¸‹Ð©R}ÁZ—ÊèÔ@%É¢Çâ™ó£éÓj JT¡*„w¹"¨)å Îߛ˺'Ò5´8g“縢Is"Q¨íò¦ù”£Q6Åøì`Ò"?qÒN§£å·‘°…nQ»É£ ÃÓCîS?·aékûÑ´<§iŒÅ( nH»õA—ÕHú— ‘~9ašëXd¡U_þ^ïËjBaC)ó–Q“Áа'!ãB¾`´VJ´©û±[ï‡jB…¶¼+ÅŠÓ [s]r²„H)Ú¡ó 2‚÷ñ¥XóÃW?Iž÷ð5…sæ§0‘·’£C®ê¶ „1'ï :ÅÒ"ïÔf^ínä]ÉNÞcõR4x£ðrÐòviHÏŸ,èÐu¶¡qxrUÎ.]`õѹ¡Zó‹q%wer¯§,µAÉÂÁƒ1 ªV ñD¯w’tå`n×\E+¼Árç”7ë§ß°þ— úP¥Úu}j&tscWz ý937È æˆ£Øm¶³âÛ™äڭчݤKÞP ZÀOÐr€u´‹É×ñLØDXÜ,N’¼bIð‘’'4›ø5Á‘µbJ…Fé~¯ Ÿ ¥WŽ/^èxèïš^9$Ng_pÙÓ ¼Ê'“ä¯GBJp Ál ¤À¢j‘Òisêò3ïúž7?¢žEÍH¨¥ó‡º|Jd×DX<6QG¤s‚©n£î§3t©r¼Ü-̧\ŽbmЖ/Ù­ÄzÖm@±“£±q'Šbccä–Éb—:Sé‹¿Ä+2xŒ´`Ž´Ø, Òœ7$?üh€‚'0ÑVuË%r(Â]qê|ÉÓ™sJ³×ÖV°þ¦ŸÝäy`ô#7²ÈGnyH=ÍÿÍ…tp™Ï«^#m6Zô’2+‘(—À Ѓ`­ÐKn †˜³ ×ÀÕò•8m>žÎ¯ûÀ‰;Qy½¬IFT<Úüqh†‰™6Iõ†’8¨ê™{` \VÝ"«$p—D1¡ë¹Ò¦GIúò•ùÏüó§òbÒl&é_Ìï¿$³…aØ›ÍúK¸ý鯈ˆó$\]"Áýx­º ¼8Ÿ_´–€u:ÿÂøéÙ`‰¯åè‚ò5٦Ȕ‚©8uŒ Žvã2‹cEÙS¿Å•6'ʬ@¦/Ð3Û–|<ü¨iä 'ÔDò&@UÎQã!5œ:AÁ"ì2š£Á´‚¯óóÅ(ÔÒXä‡ßù£Ä€Pi‰sƹuû ¦ä©ÿýu|’ÀZ”·ƒ´ãE…A5̘ølÔT-MÚj±àgäÕfsÅ!$÷ÃÚÔ­¬óì,rîïl™«+ù‰OŠŠ©zByVA6ÃÇY(Yö¤FÀÚçÙ°S“îˆ"Þ®Uœ%ùЕ6è¼;s5ìá{sûtY<Ý(=ãE¾…<“¡ w«‹ÝªB)¼¾ˆñ‘ŽØCá@ Î jp‹J ÅÆKO+ ¯ ÕÓm&©U'$P7‹7Œº9Ђ0 ÈË&X¬´ÃLRØZ~dîl'¸ ÃwY§o«†cßVÛ{¦ µ?l“V²—=ë†d†Ù7iæpRçåe>)>äjyÝlB Çþ©¹ûAž‘>¤Þ”Æü"–úPAþ A^ÍøòjMó”`5[‰{Ä`›Mi9[rçÑ%zIãøïo+Șu²‹y-Vô¨~¹ZÙWâÂùíÏ‚›{ÕÛê~z¯jþîç{ÕÏðAzÍûð}2ïµæSý¦^û¿f#`gåâü‚ÐC‡Ìq6’î"cë;á =;!¶Ê#®ÿø¤yb@ŠUlxFÇ¢†¦TBb<Oã,ÐÐ=@q,-¦J£©f|¿E8Yë–ô@ÌÏuIˆ_ à K=„Ÿ@Ô‹Úù±yÑà’|*ùØãg¦úñþI$U†|0î1h˜Â&5ûßJÀü í€ÃM¯±Rxc?÷»' ÇÌÝtL 1Z€(ÑÏ„î-¥µ¤¥…3ËÎïSº¬˜YtDå¥ê ]Xgfã¨ËÉlÇ]iטr¦0ѪËóîÝt­uliyÛ2+TøÐ@Ûû0Rë9ªÂ­®ËsÑ;k±$v÷ZSÿ i‘,Ÿ5"ÊîuïS|´ÞpQE·(pSyr^‚nò^ǘ¥®ô C×µƒåô %–xUûªÌŠÎ{øšá%=²Á#_ÅÅ  ÙÝmÐç¶âã²±‹§"-ÁîAs—=p‰\Ñ‚•Ú=ÕÁë|nEš¢hçQdt—Þ'Ûø`‚ ­hDËtÉ\pÎ}¼Kà ZJ„¹Æ5| Yì‚]Æ”ýå°N»Îlºü}ÿŸs #¤'}V¼¦XÀ(ì­ ¹H÷ÇSóôt ý<{“ÿ-ù'ù"š ™áÖ;.h5X¨;ÇÄ…¡ëKÛIÑIj7¶ ¬¸a­¤‡übVõ/&ÒJ%*5•µ2É•¹¬2Ýšºžb¡]ŠçØe/ÌOJÄYÍ! j'n|û]rÜyfî Ç—øï<ÁNð7üã;ÿ8õÍ?iпðjôÄBfx‹Š Èÿ\‡ŽLg~ú¨Õ€G’K ó ¾+ùÄ—ÎÅ|<º!Ý‘é/.­ð^0ÞIn ø«Õ!\.ø™lG$JÖ¬A³ÝHv=v‹ù¨o£|TÚ8îàæD2G©»»:³\÷Áªs¢_.$‚mÁ‹‘¶Þ ûY”µÖp@P7NQk jBºN ˆ€YÀZÇ}Œóà?YïÔ¯?Å?¬Is hÊR‚¨n÷ :6û7tŒ³y¦Ž©ÂãOM°"À»áù*ôy˜£[šïÚ&T$o1/Áq€Q'!m+ˆÕÆ”ÝVl.WH@¶®¬/¡-ˆ9Á‡£V‚6¶,ú‰. ºHfCd&2‰ÿæB¾ÈW¥µ¤cI!€5x m®ÅúR%á{þQOÇ•_ÚL.')GëÑ-þ ®¼ Á"h"£³)®pí° ,‘Už'š±mËvu‘Qb¯ò,bœ®ÆðÄ~wH¦¸I >» ¹µù¬«?Là¤W®> Ä7a½‚2M›ÃV:â•ávÏ}ð÷´1Žaí wÂÀ’Ô–@Èl$uDO„áÅÕÑØb:Ÿ8§ƒ‚fÓ_ž£ëP j’ þ*gÞîV°*@#¨ädtFŸµQ+Åé yä*¹£`¸£[ØÊtnc© ÿ}”ØâÝýžVr0å`›žÐ(€!¿oúpßV0×[Îi&O„1ªpÀuÂÐÊ™ÛH_,â1' ¸´ik8»îÏ“À»ì_Þ‘¦OL6äèûbœM²sn²ÇèðؘB9.dVW)¢í{ø«BjUÿ$B¯’r™Ó¼ݳœ 7rN’«ðLƒ$ýY%çåo“‚"`:ÛŒ© y|P%FeöŒëøA_TOØgžaœR¢;;(Š¡ux8#i˜ðFE‘w©9kžø*âÝkÕƒïW)7ß;Ýæ{²k§ùaï èF-¡½¹w);­Ô°¡j(˜ÃÔí†+›f-=lG#øà[ÒÒ+ãà´Ä ž×jb˜ß@µ(vÆ,lŠh ÙÁ¼½x¯rTËÇ9c“ë3ºc§äÞО25l±ºÎýÖ± áò¶Y3Mbt' 3„ªÓT…våÁ€ÄâD<ÀÇüßR6B28Rœ™× ÀùÁEĈ0ëh(ÒLß{†LPLãb²Ä0Šê=Fó=S‰Íù×9ÏF(Èv§b|Æ®‡ýøïÙù6ªï}VßÔw¿r‡Ýϼµ ^k oÿ}”àSqdc'Ø NNwœáðMŸÏWl¢! ºÈX%­UÀwÏ$F¯ ŠÇÙeÎ5³©¥~ˆÈ˜jv$Í>v´eÏm ¤v0ÒÍG)Âk5/§^¢‰z‚)J-ˆq).ÔÓ ôI¨¼–½:;É¡Ù3›ïÀ2¢rUô¬l”Ô÷&U5¹$ ±¯Á˜ÁKØ&²N>ÉÁ:EØí'9Ybë>ãNázl!ø5ød·\‘éŠ.š Ò*®xµÌTFøIÀš¾îö\üã…—EV.èƒ Óˆ¯²:š’Ü©5.´±|ñâ Çu¢xG‘é ý2«]±´,ÙÊ”jI§ïGÑZÝ“ÀÒü–LkqìÜ7 p`þûJ·êþV§cSÔlÍ„†;u‚VKÒ÷… Pp éËÃì Ž½Î2Ã#4*}A ñ?¹=¥\ñ¤åŽl’ØöyÓ ÿ"õêð†¯*A•QгweqB{ÄHþM¶Å„mc0ÑÕ}gÈ"`Ì^Š¢â^}ÃuŒ¯Õêû/.YüŒ+¹»ôJN£ø´ÊI-ÁGÌœS,"³:Rbç~¡ç”w#ð†"ž”Iæ_áBQ`X3`›qb€s½o¦á~Ò¡6ÍKÞ9ïPH+cmùG(1àP¾íÌ/I™<+&tÇd™ Î*䃑Pý¹:9È;áìo~Õe+êí{úg.šˆ½”A(:ã…gSÍ + ”=Ï—Å®ÚkT¦7Wβœá"ì$4÷ðPJõC1.’‹ê£¨iÛö²¥€\áLê+ms[Âÿ¢ „JpŠ®v€bA(wFÜqhƒk¨†ùÑÉÁbÏ|W‚µŽ‰XAfÔQcŸü‘jg±š}&räïvàµöVY§Q›j4èy)‹>qÂîx¦Þí|¸ËÙ †·ÎÌ‚¾àî$“Îi§KÎ9ÿë÷W0ñôš²,óë]nÀ°ÙüúxS² BsP8´{Aѧ廞Q,2"âàÐ}âËMåísÈ]×¶ø$'åMhV{• çpÇOµa¹A-ÑØç b,70ôÔt–2T4ˆ®c—]®9 `Ñ:Èüf0,hæ@ÔÄÑWN…O€Ò4ÄçO L\5º—KÀ¹M¡’|>è´Ðw:Ù{÷˜Ñ&é¥}b.HÒ_#½×dµpðÃl4˳!2é&÷I4~_wA†hUꔫùJ´'Wi†åât”·æfÄjLh¦¬˜èöÝÐPµ5I(Î}ËÌV²üòçÇg=w¢ ¨Ÿz³Ôò¦HkQËÊõUÒá;7Z­i]$So»ºÃ€kIÖ ž]Sl›…vN¢ÎXeͲå.òa Ìã˜Ad¶¢Ýs²aÕEbEÆ¡â«×.›@9ö.b«ŒI#M"r>v±ãAICnZökŸr5ÂÈÔçä=cìùGtquOáIY€/œõ kžf¨õMr¬ñPÔêÛ…AJ °Ÿµ|ã fÖÙ@ŒÐ®ˆâ4žâFœå°ß35ñµö3§.áRœÌÏv#Iy鲊4¯Ó‘Õ‹'W6–ò&žh µ}øÑY%Þ('åäÁS[Ü÷ûX݈Ù-­²³-ãrf‡m㥆e;¬ÛxaüÁ¤~t:Ø æ•ÜÈj‘pnêæw“!iã¬^ ¯ú¥X¹³”¼pi=?*Œéæ/YrtÎâLBéŽà†{“RðÔó ¦óúSL—Ü,­@[·àP糌ZËøŠèN‘o ÁuÃ{XMaB;%3ÇÞJ…«î­Ûjë'Q—[Y}7:¢ÙÐ’¶Gâ´& ½£|B5ñc“V~ÕF©Éf8J=ÖC¼65Í¥³‚É“5ŒZ˜5濫ǺF{¿–•“·íncÎ9îºÃ/;2öñ¡=LR¹ûØ­åx½m¸»«†Ká–WhwÅW5R/œ¾7žN½™f²7'R³>¥­f3@A8’ïÚaxïr½ÇŸµ”ÕÚ¾²ñé0ã=¦+ƒ´vÐós”Õ¨ï|Mê«L}ÔóðõËx¬±‰Ëý\LT.*IKm+~Ä»dGðMý v0þ>¾Ñò›%˜åÙö$‡˜Ù¬]ÛÜÂKjò½ANl›)GrPƒ1p`4cWwç¿ATQbr<ûˆÞp:7‹™æÁh W›oý2>ߚʬ·ì“\µîQ¡ZóÞÛxûÞ¶ÿ”Pò« ]Ì+mŠ{^Þ’æœ2CÿD·A³èTV´`(°¸Û¸ÜÅNB¦%nÔû I:ݲåÕ;§†¦IQmˆúe|¨¾pÝŸÙY>¢8‡EvÓâÆéÍŽŒ òÓ=Ð{Ì’.ÀílOv’ã÷fSf£Â¬f»ÍÆûÝHØì+d°·Ó0¡¢{±A ç¯ZÓÂq'9Ê #zM6›Ê0˜8žºŒÔ» ‡Å•ì #^X.#Îùiñ€Qb#¨ÛÔk>fàÝø?ÍÌÛ†o”ÖÅÓ^{Øì£fšË½'QÌ+b3vK(êCsÛD((.ïŽrÒÖ’Æ26€> N„§ëø*«gÂî:)We%«FW[8±42&¼½6(q"¡ w£±r\ICw3ÐÂåó%è’ü:©«0ÿ>{†(E˜I¢¡ÚÝù_ òm»ï*²î.ò¾EHTR"£ÖäDU\PTYIQUI|-/Šx©Ýæqv›ØH‹ŒžÙ¼“,|Yáw›H± ¼­ x±Q.ŠÁlæÀP²Âe58´slW—hQiÎÚÂ!Áµ¹¹«\ˆX ôÿª\(ˆYczù؆l·,Œ%:Ö¿‹ÈR”^Èî~Ù>² ɉj=†si¥Ÿ¿-Õ¿0ü¶Ùzoš:~Ó˜ðÅ–#ÚFäs¹0&µÍêF,¡\Æ6Íú2@x%Ž ²¸AÓ&O $áÅH1 Î¹j©¸m‡½7ãWE0--Ï,¼ð~¬…1žèG%ðÁ³Vœ 5(µdºd'µB24ȱ®®+kixl2+[±Æ …G7|~/aÜ`bnnÌ?.Z©VÊVªú¼ºC ,µ˜¥ .ãh€–åNÌåÝÂáŒã¤FÌ<‰ÄÀ“Þ¸ÜÆÄ)YnJÍWœ/ÊEµjv½ÖÜÒ,c›æöôµ˜‚% Ä“ë¡ ç¬C¿M-'û'­¯‹¦êË翾mý<1ÕÐý²V°.‘ÿ.áòûEÖž®$q¸–X7 rÈCB!ô~] ZÄl€Žžà4PVÆž;›_@>ñy1âÀÆœn" ¹0`ÏÚO¦Iœ05äŸZ¢ûŽûѾ–ÌcŽ‘vÖ7ÀDÝ<*¥˜×k/$þF Ä$¡‹\ó)¢§××bOªnogrFþU|ÎåÃÚ»îAÎÓ~ SwX³Â`w(ÝÇ9‹*%åDõ”VN´ÙB–ƒ¥-‡`Y¬ÀeÊ>§n"ƒåÙ)®Ç¡Ú>,ãS½»½0@ÿ×ÅôæÔ‚dö¯·"•sÑu/PT19 ÙY£³C²VÑx B/ûêå)³]iÈÁ`éƒRl.wûú’¢¹ÂZQ…gà¸$¦4¸Ë _Í»ÜjbS@ô?2Kk¯wË0lùç?¢{P£åŸ$¿‡+Óñ”ÚÛ; !»4A¥º˜¢ °3£½ –K&Ð䀪€`9Ü¿ô×·õ«uâá ì¸Iny*9þÀ©¡,—+ö–?eDÜüEê%êèð)ʉ”u5µŠ¾LÒõàú*±¿•ê#oØ,¤?€5χë ú}Êæ/\ÔÐ×V…¸LJN(¾á=C›7²Tÿîà\Ñ~¯-l Æ^×°° a1³ðér× …iÜAéivZÀn(› É6Òº\&FOýüEñþÙÄ[žI~eÆ ß–/N+¹r©õ/¯²Ùy‘º‘AeØH5XÁ4S+>Y^ÚdóîØ³¼ƒŸLþó=qUêsð»«B$¤ö#T- 1ÄÔKäš’B €ÇBrUÎ.é ®Vt-»Ôp¡ÔŒÇóV¹píI:íI*_ž]…öÃèÒ¦{EΩ,AÐÙ[zßî-‡<Oí¼¢×‹ÎrôdëÅ<$ŽbÏNÕ ì¢©ªöD¤7ù?—v¦~äc q?’¦ÉÛçAÔ #;¤:ÖëéŽy}i¿¸ è–À|Ân† >ð‚\{hî 5FHNõòFùŽœžLygq„éb¯¶¦ð‰˜3Ú,gƒròNÜ3uÿè)R•wÙh©¿¬yu÷²D¢—C&C ®<ž'«á+@"M,%»ú#¸5ª ’ÎØ6ƒ°—8Að"œ;GŸq¿³·FfSóD$½D¥ Mùi[¨C!»ô¯;–wêÿ‰ƒÉ›ØÿœPî( ‰?Ÿ,ÐÿœNÿ9þs:}¹ÓiƒgÓ"6€¹Q+J«M0ñ3$òæŠÝ=âBaÂKˆ4C!N|…9VΫ¯œ×eÀë ç$Àí‚]ÝÂ#‚m¬0Å0ŸYÿtŠê³Åd@Õw~ïœ×ÓÆO Ùb] dôhIþtÈœN9Ô]þtŠç•HŽîF—›ß÷B`ùù óæ‘iÁi6¹”ô~@§f¢Ù}jê²™—С€Cb-imN bƒ•pRøô¥ùçOåŤI¹áÓ¿$³Å¤j6%`§J7‡šVÓc°‰Õ}4õm‡ë‹^êéÙ4’>i–-,䟈…¥Óq¦Å$ý›rÌà È5bÜ L‡íùs”ž—Ù¨ûòUúGƒbÝ—æüøÛßš?o“»§YÖ5Oÿ¹È0ÈE÷IvÚýÛÏøî/ÏÌ«½ôÕÓ×Ï~|røøÍÓ?üøúš¿|À¼ë@gy§šŽŠy:k¤Çióçä:sîSûŽBß‚*6ý3Žb³^ÒHƒ´÷ò¶½ÒÒEÝ÷–.ƒuv’éb4Y·Y ¥º•¢³¦ýF·Ø ð¹s‘†•n‡lªÕâ£Á©ªQºÉ-ŸÈK/k‡¢/Á=ìë⇩®·n'»R»}pb~l'úÑ>>jnû³â¯ÊÏKVh4œÃlØ>#VÏK¶ÇK¯IˆOŽƒê a…²‚f‹Sƒdß9þûÛêäþÛ Ò]"ªQdŽÕåæ„Ú7d;·Uù¹þîn²âÊK0Ÿ†Xe×< k˜íBîÓ#Jl#Lš9µ0Y<œºÒñÞ‰Grl%åˆê•ß_R¾Ê!ìl½BÇ,æùÅ,²ssû/ËÑe1ï‰{#ÅÉ¿mË0WÉa9½&»ãô°™˜æÛæŸï’gù¹AÊ?̲á°ëkóÜÔy¼0ì¬ Í>}o0úvš<<•¯¿T£E§ÇláÝ]¦¸JîŸ?;|úòèiçÍßÞlmwò8ú›ÄÏ^„å6…_zn.mãlÖtT]úÐùÌöFMxÆF+›P7ÕëÜô»œ0€\Å@"âBù™Æi`V`F‚¹‘ÌØp*Må\ø°wi,ÁJo(é)Àº‚Sp'× É%ÓÕE$º5fØ–"‚ÄÕ°Ïj~"Ft÷döƒÅ,mœ Îôø4Ÿ_åäÂ06@žRµnW:nάâ÷}^C»í°]³zðynzhŸ¸[.}'Þ'øéÛ*’Ÿ“·îÍóßÛzz,•¾7×û$y’@×GøÓ€üùáaûÉ#Ákúù}û𔪉©Àoƒ_«ÊÂ?Oä6|ˆSwÐ{ïÇ–`Ëœ>£t‹6v#¡ž> Zœ‰Wô:ÃlF³ëQ˜;ÎlGC óQ%ÉRÌyáûîgê"uµc—;J¥^d³Ëòm‰öˤ—”"WZç.û´©q;™.fS(dý/5ð¢"ï  ËByˆ÷!€MâN¯]BÊj ”)cÜòŒr\¼ï°¿â).Oe€Å3Û‚ƒäþ Ž@®Úæ&[ÉðÚ0Þæ fÚÃóZ¡:†¨Ø²} Y ]Çô»¯ïÁ ¾ûͽ$Ï lpþiB¹Ïó ˆ˜¹dwËfÃ4=Ö_â…+¿.'ì9ÄÍl˜—ggz¸ ô [YòÎ_0Þ^ Ó4åÎßókL¢BX” ‹ËjGÒ«)èœÌ]¢EÑu§y ‡Ë‚²àаdQ qèe™ÙÁÓGÏY²>80 r÷–ýþcsIVŸ¥§LíÁ–,þƒŸßªõ_yêÔ”ï f>Iž&?Ô¡öÓöÔcx*¿ácïm'S­O«;ÿà‘ÕétV>“ÊCJ–QmyƒˆE…›=éèŠgèÌ)ä àøà‰ù~CaâB1(RØ‚å%“d_ oóÄ.ª®a㉉ãoæ9Q6`ì^~1Ç2|ûrŒ‘èõ+Žê-\QÅ ã`@êÞ ×f‹ËOÌxÿ<Ê ¢Ø‡3Cg^:ŽÇ¥¹ëÕL¶#•6×üŸ&@Œxͤ#œÍ¶[ÉãÃçí=C,u‹Ï£«üïñÙÄLé¨<¿~ðjï«öþÞ7_[Üÿºcg†Yfq*n"éŸ/bõÎV¼‡X.šRKXkë=~6t31·Í7“Å’†¦èbÈÕûl1ª_ÂéH›…œ_ŒÙí’,Ñ·ÞÁVrÏ“ër!宊Šà˜NÊŠYñÁ±ë'9jÓË“ŸÕYÂ[ðñî÷–ØýôÀí$ØOÿ+‰M¶¬2§CŒ;itе`1‰4±xļƒj™yƒ ß?0Á£¨t-Ü<öÑ›"vêý¼-ÓòäïÛ¬pÛ§»Å8Çk]mº§–ãØí~_¯ã.ÄXiRÚæú3»U>.Ú #6˃E1ñ¸Ò¾ µ!Ü3ÌbÇ|RŸµ¤ÑɵçÊ@=à±À“tŽ >–µþæ7¿!É „ /6´ÌÌÞά+$JPÚcVo’ (=œ7Ä'çJ&“OçmR&yáDÉâX,16ID¬âPvâ3@€ -eáõBÁü[›’Œ—B®s9Ê9~^,#oSæ‘l%%$Ì Ý!P[ ¿¡¼Ž’âäèmc¢µx0Le^UO‰g%`21<0R…Ùd}¥ˆAv‚$ñÉwïÍÂU°`âÇTDDHøõ^koo/A[˜E"Å1`ŸÃüö’cÞ)ètŒBõöQ6`G¹+ú+°‘s¦dµ7•`|R ÓÄ,K¶‚u¬Åœw€îÔ¹xï=½=såw Ÿ’™ž—¤ÀY³2 ÕµÓÁ’Ö-̯z{ ¼Jýï¸>ûŸ°S÷ª‡÷ªGÛ`ìHF+Ùno“a;I×=¬m‡!u†º¤IX7=f0ëàˆ]5ÆvÂÚÑ`ªÍýÍÉuï2C®†™¤± `* y {<#Ièv}j,>°£ØKƦ èÝ ®{!̇:b°)–É£ä`‰—çK$`7k0ëèdz‚§ i¤BãøúïuOÐŽ”Rᆙ¢~Úõn1SX[‡¼:«_)t)çLYaí·teƒ KRbš”cÒž“ñ’Ø¡ü LÚÝ áïUˆò!Õ!ªðß­ÄqÑ5'_z\캣Bw×РfKo„& PÈ.ÐuUyJQwÌ=6$îdyÄ^YD™d$g`ÖÏ0럸eÖ†g Rã«)j–ÀÍÝÞI³Û.ÂIû¢SD Z‰7WÍ%šS½z±>íÌÚ…–Ü*ÿ·˜ô—³€ùû©¡êÄÆÃpù^‹xfy°ëX¾ûÒFö% Ùq†gòÊtû /=N¶(B=6¨ŸËÇw8?ç0v›ºi¤ v™' ;o g# \Ô8Ï8ÞK ÕQYMä-_hÉ Ï _&êKP_ÌùíE‹@bèéO8è}P ‘ Š<ãö˜í@^ÔŽ¦fÜ©j™‰nïÇÏ7Ë¡T(t?DKŽm‰:ÁGµ…Ùt;ÉPÚ †"8aWY%ºBÔ5©E¢~˜yÞ#V0L(wú<±VrA³²CHDŽ/ 3£§¹¡a“_P½Q-¯[œéù«¥€ &ͰöZÌÅ)”_xŸ ïG /'Ö ‚Ó™ óÇ" "“âBQLÊ+šÁÅŒ£>™U¬JoV•,_.[†7#cÊ¢ÈgñÎ{\² é‹ãÔu—ì9XçLáÀ0ýF,cíõ¨ÄªÇ~?îªÄNßÈÅOÄÖmX—ÄêtPćŠÐ–L¦=5âHZœéÚ+¦ç#8£í”(»É~÷ÄÛ…cþè ·j|–Ì8‹#UƒÓYá ]ØWŠ¥<¾í"îòðÕG¦[œJ¾¦”üªÚ.FÃV˜·JjGr½65YÀsV ó° d6ÑÞ4èä¿MÈ·RÀ‡"B>Ú ®›ç£k:hEMßWè±ÅB¢ „;å A%ùfæ‡çLLbc“Z1 ; BŽ}–³f\$#.ç»éç®­ìÉ‚FÑ®Qqeá-N%Ñ•…K²-º¡p¥›4+IjP+ƒˆ»D1M½à‚¢]噳t`ùîÒ}òÊ0;íò¬}4ÍóÁ:4]’ŸkNbŠ‘êâ«Àñú{øñ´,M&Ñ!¦¯®Ðµ³hqM.×c¸ £ÐKÙ„Wùd’üõèOÎL–Íø9Ê£õ_ŠG¢ÃŸpo~|•´éè¨)ð‡ŸÙHà>Å&µlÐ0¯ˆ7êN$|œ€Af‰éžy3î%²OŸÐ0¥ …HD¨Ó& ðŽÂ_~ÎyÍüÃ{È/ýC=,fݵÕÊ0ŘyÍ;Ä'\bïÉc`€ËdjjFRŠÓÙ öEµZ-[/"Ëð„h–Øïò±uRJ¥[nœvôg'‘4É Ð/άëp¡xMs­(ÓX™‰ÐHóŒh~ë‰I¾3¸ÆiƒÏj+Uöóš¶6î"µõI^Q“” ôÅøRÇ-µîEÕñÓJ gÙ•§š‚˜x«òKjg$gÝòÝã\ ½( ó| ¿µ*EäGÔw¬TÙ¹€–ŠÎ·SCuÍãôßñŸ<›ìò¦ùþeòÏbGÒÇOÀùéõ÷ wª´{úê•ÂŒôÙKƒ\šM_¾ò°ÌºF]dr¯z•è¼M½‚ƒ8Û‰7A«ä\¥ìö†€ >ÐQb›U–õ¿ì:Á.*¢ê"»Hjvr6¶Æ1 u¦9¨n¥doú.ѱ¡Lœ9ïØà”mI Îfá`iErï@ë%tŲ1ž²»ËF’“ ß‚‘sÀ°N| ‰7ÌÎ鈻j멹¾E‡³DF*â}Öý]AUL?õ‚¢ÉGo)yt žm‰¢ÒZUþ.Ùn)âÕ êÑuÃÛàNÈAóZ‰^=7îOpڎ̤éçv|Žá¿·#¾6ßÞækóõÁWæŸoö¿ÅòâsðÍþr_›Å¼ÝæaóÓŒD¯ÑNܽf¿}`ú’¼|þæÏàÎYÊ£æhŽi’¾/fÃäa^4UnP®ç£Sð¤édu¦á3qT ý–_3û ˜øÊþ0Øz5˦ò{z=,òã´¨` ü«¬ØÖ4ùs3A•œ¾E5B˼ÁðbX†ð\ Ð/e©‚[vîNo¼j Ó˜*„*4árOl3jh² i6è•nN\hhQ€Ž×üÝpRÆÄ´<ý…P‚ÕkÀ·sd2¥LA„d>ƒ ä{61¬ HGÙ‡k¾TâMœÍÍ%ˆ„«‚–SD„G†ÿZ„CˆôýÁTúuúÀI;VCÔ¯Ê0 ¦ÏU`ç>Öd"Þ…˜`‚( 1q4~·Ønƒ’!•|#E<ì˜ÍÊ 8ƒyã* Âfˆ†Ä¯àhL}3—”ác²ÈuÉóÜœ•f2 f ˆ™ ¬cHZ+á XõÚvJáÑÞ FÕÌ~¹¼j ƒW¯ š"æ;·éÝSxGž/拹Hõà™ÏÃYØîp‘ĸsà›”h†—?l¤GMìwý»Š¡{°ˆ{• ÜFkÿ6½!›a¨±’áÕš\>?({£2 êœ$N Æ®P7¸¦çÂYû@T¾Âœ( ešÌ{$i'ÕU OYåÿ\H‚ ¬Õ¸ÑYpè»ß47Ü÷¿Ù½Ož?Ý$D¤(æ0.†£<¼Ñ¹°7¾XH¢é@R“©PS(d\LÅ„¼ˆ.È2/‘ó®GŸ³bÀ¡Æ‚²~ GpBAãe8s'šÞüÖlob¿¼V¹šJ„qw}¨@ЬÍ$á¡®²(׺$!¶A6Œ¶ç˜^ˆÑáÇêº2TÂÜlÊÓƒÜþ¬˜V—åÐô ÅÍ®% Š5ùC̆úa6˜Œ@L]¹(ÖGú›ÚFPD$øÓi ÂHÞz8l«¨hzwöêúè"›ÆÜW¯Ÿ¾yó?æÏ³—ož½üÃÆ‘x:KÁç E©Mz{­$Ÿ uÌsËLáüš)$hƒ<@]:-à ±¥% ð¸KNgèÕ`¸`˜—ìr%Q®IÍâ3LÎ-Jv‹åÌÝìrè)ôÜa{GË@Öú¹WÙ ªš/±Šø^U“±óÝÓÑË¥' § ®Ü<¡É€¤³—ìªÎ/zßíÝ2X¾E)`ÛŒ ähÖ¯.ÌÈ«Ó6ݲT xzÛÍÞÅ»zoû&Ôkåà.ï(N#0†KÅœùsr8üHÀoŠá…™Ø˜œÊˆhÄ@y¥ÜRPX4)ĺÍ0bȰïˆstÅ6Ð 'QIT·ù¨eÒîÖ k… ”ŒRTzäE<<Íá_ÀP’²Q@0­-¤†9:øåÚh±ÉLÚÍÎÐMª&)ÚÅæ1£ %ÒLv6çð+[aPA3–=öŸÚX“\ß—Hšæ¥Î[J:U>§o)?x~Øüü9D¶•¨Ogæ„FßB´Ó›ó¶68ó1¥ñû‹I1@ãWÒÎcó¦#p“õõ'‚€gÆO "0Ù½–UŠàPZÍÆb~Ö6“;·ÄNÊÓA%³Œñø¨,Q‘4kçÚIƒW+)}9" òž.«‡j„À6"D‰§u2êC85X8ytøã“§GOß°šŸ¶~òØÞÅébN3áàZ °VkçvU›ÖNHýZtÉÂïÍÏhޝZjuÇ­¬é€Ú)æZ8ߊvÅ–qQÃMJüÉEuRͪt&伩HXªÊ€Ì=¢ÅrÔ!|X=dÀ™4BíGí¤àmcð7K¢ (ÿ¨ˆ£¾ùêö±L°Þ@­äyY^.¦áüzsŒ•kV\.½%/ÒUƒØ=ݶÓe4~šPÐ9G'ïÆÚ!â8 ƒçÛ¥JîU†u/“$Õ8†Å)LUt‘ÑÜ~28iƸS+B戬ÍKH84"À!{výôÐFZH‡¼äë(Œq¥9f?ÖF³û>ur–‘Àd= ¢\9†(A½ 0{ý~#@= E™ÏÆ G2®ê¾Ci+n1V¥6ñk|0½4µìÌ+¶é•&{8ôa¬d«nÜœÆÓ L‚ÉŒyoHÊ`TBÐ<²£Ç>Uh¤ˆ9–'“u/…Æ ^æðÞTŠO‰rm'ûÜÉû÷î3„ò.÷²Y/‡l4XŒÄÊ´>TºÌøsBüN9¥0#¶!—f"@”v&Ê:.pøË ;<'H¦ñÛ`@$æ±TÙ–4ÏJü24~¼Î '1+ª‹‰9òÓýßüæ¿›ÉöÓ3ð.„[ÎסC•…Wm× p$ùekbçòö•E@(„a8‡¡Ô EËü¦ò%](X“?eï²£Á¬˜Î ½ÂUœíôwEÕ|˜VXòçj~=Ê›û¿{dþK>x»ÿ¨ ¶©Ëô­cåðÔôŽ»Ì-C°CS,bÔþ€M w[¬±n†¬ÊaxàÇN3/ía ijkÙ~“ÎV=¨®Ç§å(fô8Y°™6Z¥‘»$Šå´}5LKè¿ÁÓöXYsäj¥©ôè‘ôûx¿uÐúªõuë››Uã²àÀÏV’ÂßVò5|…¿­ä›¦ 3õZoã Á¤Õ |ƒ".TY³u@O=·sDŽ™›#t¢›ÑIf£‰Ã H b»kŒè@O>%,t#”áÉØà/Ðüý¦…²%‰pcíQh–Ù¥ßd‡dÛ#qê‘:\ýõ처¯æç ËEMu³Ê’ùí‚{9§U‘ºJ‰EÚSŒC¡ ºÊq7\;§·@Ç2qðV½ >ížiü.†?g„.(|³~½lIÜ š(‡³Éup…zL¦=ŸÑ‰XµùÂlŬÉÉÎå²ìì‘ã@šºö[Íä~’NÚû˜3šÊ55(•/; Ë‘—(T‚¥j¡Qß%ô»—@|©½Ú\ÛRI;™@Œ ¸ãØBÇE·Øœ4Ãðc6 *^‹&Þ¿ïåB¿¡cpŸLé¤;Š0¬EB2gû±.¡ûÒÁ}íÐøíÁYÁÕÚ׎¶µ’"›ÚìZ ¼O^lÛ¡%«½þ©æºò¯Xoií³Üô¹+þUmÅ‹%ã­ÀŸ˜s-k5šÜ‚+dyg„> g& aÈtö_Î-Sú/ô^èV^ȮŠ~H1P<²8¿$n‰n·Dš¾v¶K1%y¤µp)Ñy è˜&¤JÌ9qÆCÈ*„þ‚åËBÁÐâ‰x..è^ó‘Ԗ쥸÷‘,>Ó[áw6JmcÌØ§pc3v ­ýåòaël.MõÃ÷ ¶Î V€%à×B4±|ÒFñdc|œÏÆ}ALùÒÜçãÊWkãÊel;ÉÀ¿˜YzòeŒ-È3…y‚æÎ1%â“+Ø£\‘ÙÀt’ýËüŒÒð|gs *Ú®˜iø@LΙ>Ê…®wAi ÆäLÿáÔ©—æ1zÓŸxXºzOˆÀ"å†òm­Š .¬¢ÿkƇí{ßÝZÙz<´žàØXÒÂ׊….?ª{+a qJ¡€“-|1·á6#šÛõû+¨†·–K)Ìñ‰CÜi9… â??Å Œ@o­ˆ5 ™5ôœ·§Ú™;Qü¢à27«F’V;(e0¡¨ÝYVŒÊÓ„ÉÈÊ"‘¶{‹©Yí\ŸóAoêìûèÁÀX¿§ëô–©]œ@Ž3&ç`*ç@mL£l˜ÑçÙ‡ëäˆYâMÙj úü&Í H)yÐ…·«I’qa´šJ,(ØÛP8wHôTÚlz¥aPšJ²ao3Œêãs>úñáu¡JÚmŠJL/I¦†ÿ6Õ `ç|•ƒ+Þo“ÓÅœê> Á©Ë~®ü]0•%Ø{@âÕét¤L>G~¿ ap±ÇŒž}¤¸Â˜°û†!¸&¼‚̱…JªÅV!u-Ø–€)+O Âó Ș1¤Ä½Õ„8×\ïlcùÎÌÞ„´žTL‹|Û„`\/µÃ¢ºTóíX@òf†È)P0SnñbŽV÷ùsˆ’Õ­7’´|1áTëœ"-Á¨¦Eî¹^šâq6Ìe4‚¼#¼PS¸êVstuÌÉÕã5 ½¿/rÎ4aú´E›˜û5ÈМòˆSŠ 2b*¦,‡¬ìxÙ½Õ`®ÄŒG‹SÄnB]‰7>$7Ja|UJên% € GÏ?öû†é0œfóF=dôìC‹i3pO"v–j„FŠÀç5¹—Å•£T°‰¨¬<ˆaqf£kB¦ÂŒ i;ÑvèdyYΟ°cŒf‡tÈ4œp¸pï+ž²FpÉðÆMƒ¥¬¢ÙàZФ¦Ò‘9¼1»#"2v=fî¢úÜã)µ`ÀæÍãXEüëQôÏf‰$0‚·…0p„tBÈS—¿ü|×ïŸÅíØtø±¸Dj8ŸˆIIò2?ÏКÛÐç‚oofB»è´4Mp¤:!DaŸ|®²` œíl§š—S`²TB;â[…Gœ„ä Ø*fk'ù£ ÈÚƒA†\G‘y¥,ïŠi£y¯§.ŽÂb„Õ5‰áE—pÀìç÷{Ì+u¦œ^KÌ[Ì‚pé.M¶Úþ8*\3D/=+tÿŸ Ê*„”“ Tx:»€ñðâÉ)úA½~M;ܱ97^›­_Zÿ°FÿÅã¿õ_?}õºô쟚ûížîs>Ÿ5ö{=îu¹¡ !:žLjÐ>G<ŠqÁ¡0Óî¿*€CSÊ\s%¬¤™·0~~»§| ÌEbDP®› Áu8¾Zø6ä|.bßx„J/{7Âi¹J´À ”¹ªrÜÞ?i&»ÉAHÔ¸ð#–øk%9gÆ’$¹µîøž¹_w:“†çÐÍíwÛ:£ÞR @‰ð©åxºŠ*Ô®,rò#cHøÉ;@::»1mˆy)™ré £mºcà‹ÊÎÎÞ×PÌVq-I{¿ãAù\`¿Æ%ÌOS6®5èORÚ”Ô¼Q½0÷¼«›¬$‰…÷Ïݑ΢>th*Â7oæzdý=AEo‘ÿ]QŽ0 ¸ÚE˜ŠŠÚXÖÅ’ëT$æ6ÑÛ4&íb±u³ëæ4¤A«fPcÌEV]ÜFf‚“»›ê5& 2;‹ €9̪K£æîUKT¯m(ûaÙzÌ"„/¼7Å&±f‰Q¹ bˆA×VÑɓ'I¼>ä“dªS[›ŒMrR°Uƒ³úáîP„XFƒb²ò’jãd1•+CÇ dðË‹g/å$ÙßÛ“w(14?^Œ –~ [6’öÑpFxïu—vJ Ò±3Y‚ÃáŒ6qËû±ÍôÇLò…Ó>ü(]DF[Çfì”È#RvEž ía«x’_üŒ ö*NÏ­ÆqúP$jœ]øp6¨!ïDuóVÜzÞ%¯VŽ ˆVåh1w"†vÛm{ÇïKlûÚ+{Šü9Çu×È b¡¬(P\ïRƒ÷:ÉÈ'§è&¾=w8Â5OàöÂiçH!ÜJþíÀ’—«£É ¿M· ‡‰YŽŽ¬qô8ÓሩOþ=–äm-µ¹Æ!I¤¤£"ªƒ÷Œ6V# QÞw=.¾ÅA÷ÿ¾#ïèNfþhª›zjA¬ØÖ-xêïÛä“r s—8EdžwU+KCæ{¾,¦ßvU4Íõ¹Þµ¨®/£‡ø^Du}.ÜÅH¦C”h/‚PÕ+ƒÂyæÙOIîÞJæõ“DÞàÀJ—Eâ xh¢<ò@@ß#\ÊU$P\Ûõ4Ÿ_嘄ø ܬGÚCQô.à ½.… Ul‚/l¯Eòí—g},]ÃØe3ô E¤qÃwïÜŠœœƒÐ«ÚLöÔÓˆ*o å¥ÕgÖ0×ÂB>¼ë‡iò6„*wûNÀä ïå¸{a(T h ¯¥eàâ=NjÓ¡?dí÷ßoXñcub®ϫ©îzÎ¥ü{0N\é}´ÕlH/ò¿xjÛé–²U´†rméœ8=…|ÁRëî®"ä<~Q§«²‘ÿD.òlI 5E» þJ”Ïä0áX)FU'iAc8€²ÅèÚÉ›Q7}].Ð(Ât€æ]xMC{O3üâÔ*öU&k¬Sð©£2¿¨w¸AuÓªihl…ø¼aT¡²øæ—çÉ«ëù…ù5A¸oà›¥E¬øqœ¡¸>¢ïwQø@?®â©[ Ф›î™¨8¢`C–4gÎy¦êt:M¯r¹‚1aI¦ZÏ SÅC ¾º‘´H¤m/Ä=°ˆÂ4J!e£¹ßY/[²€@#oìÝ Ô™ˆ$¤ú 6«m89ˆ»h¡µ‹éU¥2¸B(S ·lf*Ÿal1ó((ÝØÓä3kýR¼+†ÐGÖÌ!:—¹Ùšå,‘gÃ*~ œhÉA°O`ÎbØ#Æ´¥¬±„Eô˜’•+æBãí“k‘îcÞ»óëi1¹¹OÐ% .?X݉V‘*@.ö|»YKg9dGº9Åâƒ_ (²2ðRDñ×±‘‘Px4¡˜™f6~R‘[ƒXÚTòBw\QQ› ê@ze4s™Ì1‹=;¸ç±¢€ ™+8šõò6áw6&ÍnçTe+¶’ûˆ¼à`Vé¬8_&q"Ï©GþŽ„©Q×kÆæyœwLfñÚ‹í"°UqÖ à€ÄóöUãâ d–ŸåHØM|L}¬ÑaWöòŠ³Ëƒ±O6j7y’C GÌ3b Å0±•×£ÓkÖnñŽì¸,o½ošÑÉdÉ"õÒ¿4£4’F±0Ž1Dl‡*°5>dޤ,Á}>Ž9«øÆÞ4`éÌ+$»·nFÀ³ñ‚ü8Ü‹xy¼|XóQ¦\•pѾ1j<§WÔ‹uë Å#pL¢ñu‡M$_‹±ù‚Ø‚IMq…„ä¢fj)XL%Á2ºÇŠ—ÏkÑÁœA¦‘ìttík´ ¨jÒ˜› ›)„,“¡ÖµÐøl§zØWÅò–òúõ¤êÞs“TVq[Yãuúµòš^Ðv’#¢÷8+]doˆ}"^,~uÍî“åŽ7Ôú]Ôq”®:ä±÷mJ‚®É‡J‡Òi`î º²Ö7Ilx“rÒŽMÉ£ƒô;Ež´àd^ï,˜ÑÔ{ìû˜nSy²Ÿq¦1Ýp`v5‡¼l¬o!+˜«VLn¯(£…wJZ&„jt'kO”=(<ý£|ØÕ.Ñ…»PGŒ²í¬[ÊùSvªÐ™¹>åõÊÎÃÜ›™3ÕoÓq(êåiV96ÜŽß÷¥H¢ëN* Z÷÷V¼ÕkbÖ–ñt­¥vs©Ñ7>媕e­È&3Üg2sûÌ×Fx\@¬f7Æ¥µ"[Ò“•éÏça|¾†,1ô¬Ëé´1%Éx¢•hFoÙŽ»¢‰²Ôx!þ8$ÜjЛ±¯d™#ØXz²‡¨¥+üI¶–‡˜ Çò5¡—bvXg©)7@·Ó†…VÛbR™îî±I£Kà>0El!î_ܶÓïñOèÃs繫·îßX¾i IÔ ºýT©ŽxzAïaQ “Šéú°„t fþ–ˆõ¢$h— TRÖ×…×j©»Jcb9dû@ß}ÍP€6͇ÏA:¢³³ó+'F»a¹”„*à˜JYØÄª‹\=tÕ¦­ö:ö¿Å4­õb]aY´HRçìLÈ^µ=¿ nPDx(^â|åsª¬”H^8 ‡ÜøZÎFUÈ~“Š—¥kNá‹×JT½{)°ì¥B¤„l¸—Ÿ|² ÐÌæ-‚B–˜ |‚B-8÷;mÊ¥ç–ùr"+Õ”%}®üðCñIòÃå²CrS¾Upø¿a1Ÿf9ñœ·–x.Îy3¹E$ÄzµÍr5ÚÑ5Úô©9¾»ŒlPŽOiO(ׂ3•ÏK`/$gIf6Aì•U€îÌJŸÄñma‡Ï)üpÅi\TD/‡}Úˆ@üýµCu¬I[€QÚ&Ü»²°¾mhÃXc¦ZE óÆÓò%iÊ 0&vÞ*@»²kaK¡bÂuW”ògRI/XK ‚_B?9×À}ÐåtQ£c»y§k¿½[õlKKkÙJtFÉrRþyj®è»—¢;U)¾wݳ¯˜Üíì{j¯ ¼¿?ó qçLÆ>©%¡òYÙD\Z³B§¸NÛI«‚Q4¥<µ-Nį•œ(°Îª³a­yžxg‰ê‘õW»ã©Â– :Jäxñe˜ªØ¹l+ßO8Þ¼}±ƒ ¬¤ûð£½[n^y¥¯­:ø›¼·GÐÓzÁe‡ÀÄ|ÚaDD·v´Ã(†Æ–…‘8Vš¬D:…~ÁÇÇ Þø–¤šGðô¨æÝØ‘ï1T.<@ÞSDc!MfÆmõÚmác†×Ï«~qðíѬ&–˜¤ÍÍϹXxàí¡EÆæ",|_`"ÎEŽ\ÑæTÜX¨PP\` 47½¾2 ÕbFû㨋¢£yn¨$‡£†õ+ìP¼Sø‡Ãrà•´÷ñ†¿=áŒÀÑÌꨛÉ)Ø£ç-{GV²“¨±„sé°$²b*Öéâ¿[ Ïé)¹ R–>ЉSÃCiX§5‡t“Xºq|ÇÅèJlà ŽSÎf©d nøÞ™é$N<>$.YeÎÆÅj€%½N`~‡½¤¬:Ü u HÓü&Í[›Ù'_>†«<¿{ƒfßÎóèÎÚŠýRµE#±|b^l ¸¦ 7æÑ¸€žñš÷öõ¸‚Œ®—­«™6󶙃šÇSTAòïV‡¾o=É)5¡µ›\6‚$"Ù¤ Â"‡Ê—PD‚ÅÁg:+!Ы ƒ ¨yþ~AP€ÚZ£A†uÐùóÅ9:Žÿ9Çûce¦ôù«ÖáóV ˜k¨E1÷2Ž˜G ƒ7Å j™‹–`y ­i‹𵺞̳÷-]Z`×Ûz5Êæ &ì&?½üóËÿúrëÐZ´‚aæ»|TNQçþ¢JºÝäÃ^¼B¿`èØƒ# 8çÕ{ƶwÉãŰ@‘Q×3kt{Ù§8åß»µè³ ôŸäIoòÁŤ•ç×·×;àׯsbM¼ŒRPîÇ£gÑ  еHwTžÍ¯à†ËE½ê?NQúgPå躂éÕð`Ì*&]ÜL&ДÕZô4Uƒº°¿¾soî\ã[¯Æ›rZ ìLÁµ~ðàéÄ`[žƒà.eqÞføÌá`= sóW0\Œ µx‘/™#¤ÙYª³;ÂÑhóØì®ëªXã ¤ú|e7åZ…àÑÅh"ûªò £±µË?cgèµ+<·¡~âìßvâ|uðÝ×ß÷ åmÅo¿ý.zâ¼~úøÉ‹§ùûùÝFx“DŸ[|TÞ‘‘<ŒŸO Ï”äa>DRó{ƒó°åçAñÿw}½qÛ0|ÏþÎî!É8X»­XÖv ®}‡kŠ Šƒíó.^Û°lgÁ°ï>þHÊ–s§4),ÁY¤þK$EŠ”‰:Ú|%rºÙæC™è÷/tL#”(N£îúuÌ«úNÞH:˜†ÔûoÁž…ÖÓøB`¼/Q(ÍÈÇ•O¢çó·ï?ð`¨D.qQý, †!~«¬Ãy‚yw‚ÅéZw¬¬F‚ì†ìÁÊV›Ž*ÚdƆë“x8VCHG®µCÔõ~#ÆECˆ7Uʹ^¤Á!Ú˜­9a|cHk-Œ\/ºªn}6†õ2-ºµ¤À1œSÐæ³ÅùPmŸ'€#™ÚAºÛ,1yë“6RáPPv oOŒã-b¹²¢)[á^«]hÃh„ؤ¥!µÞp œ4é SÜG €‰Ý8ºØV&/¡)HÈF˜ŽÔ𥔄¤šjLªHº¹¥m*~iÐRב£×Ç4ß ‚§í@!ìE4­stQ“T¿ˆïHŸ,¢õÀª#˜ ÞÑT0ß<<«Ýíùžë"¨G·1ÑùÖÄÀ¼Ê¨`¦ˆçàtên±Í–%­§ÔxyñœðØ4}UPOìIG—À¦©ºÚD›ªÚYD2“¤0Ij=æÞ‘’ÎÕ6xS•£•0üñ[|-ž†ìÂÇ}B ”i,Ÿðº 'õð8 `]Œ\+6ÁêsªëE|å³þ¡K¬ÑVÒhB6C•=¡-´•vbžthõ©u*‘àBEºâëcŒÂ_LG¸¦þ8“×]„`Ì&±HvˆûŒëøAÝÍ^eEM{áç0\hÉB='Ó7Ü Jîð*¹Žve XAØçtŠˆïqqäÐÚ×ÖñK¼¢cNMD-–Ù:7Î80\áóV0`Оb»$—¤¢(w¬*›^Ú<ÙôÞ2æl‹¥q™s mYŸg­¶§*ßW%¿©Þ@t£/’㛼—<¿V ‘ÀçTÿÇ’-d¬4–:­‡_ÕÄb¾–àÚn+àÿe4àŒ’1‘ F³‡=“…pü‰I²Dxz2(DÑa”&c'°Õ¦!Jp›…;ô~asö…Ò¦“ïaqs~\^œ†/wÑ4@%g^þv6~¼Œ‘à†ÃPW倯2Ðf’'Iƒ§ÙÇ%O”¥¡U21ŠZþPÏ×GªíT3€øfQ±ùŸå<¯¸½Ñj¥ «•z÷6Å£›Žû$«i}ŠTÖC,üZ7#‚M±6rGìPœ1mkÅj®¸ ±9b^¢#ž|O¡÷Ëšâ8^Ê#Š“hñfD•D¼—ÃÇkg*¾h},T¡öÆ. Õ°­øðò*ü›µªû§áï¢^ý3f=)®kŸ|+ïýOÿüWí¸°z´ì:#mØV3K“âºöÂR¸œú]é/6M7~˜ÌÐYágÀ‘xô?PK]´¾nâ[/P/™ùÛ@TJN>^ :Õ4tóÃIØõI@*¼PhIý›ìÆkBkï…‰"Ö_+$ΤúË?¢- "; Ÿ$é0ØæÌÏ·xò„3ßOuÑ/—gço—{{{_ýïÿÞ T¹B<nltk-2.0~b9/nltk-2.0b9.pkg/Contents/Archive.bom0000644000175000017500000013731411423120504020714 0ustar bhavanibhavaniBOMStoreCi<U#"<nfotreePaths 43=<@?76CB10"!%$('+*.-:9=<a`dcvuŽ ŸÄÃÙØÜÛßÞâáNMQPTSWVlk{z~}‡†Š‰Œ«ª®­±°ÌËáàíìðïóò#"&%SRVUts•”  ('104376:9@?CBFEIHLKONRQUTXW[Z^]gfjimlposryx|{~‚…„ˆ‡‹Š‘”“—–š™œ£¢¦¥©¨¬«¯®²±µ´¸·»º¾½ÁÀÇÆÊÉÍÌÐÏÓÒÖÕåäèçëêKJîíñðôó÷öúùýüÿ   ! $#'&*)-,0/326598<;?>BAEDHGZY]\`_cbfeihrqutxwon€„ƒ“’–•™˜œ›Ÿž¢¡¥¤¨§´³·¶º¹½¼À¿ÃÂÆÅÉÈÏÎÒÑÕÔØ×ÛÚÞÝäãçæêéöõùøüûÿþ    )(,+/.215487;:>=A@DCGFJIMLPOYX\[_^baedhgkjnmqpwvzy}|€ƒ‚†…‰ˆŒ‹Ž’‘˜—›šž¡ ¤£§¦ª©­¬°¯³²¶µ¹¸¼»¿¾ÂÁÅÄÈÇËÊÎÍÑÐÔÓ×ÖÚÙÝÜàßãâæåéèìëïîòñõôø÷ûúþý  "!%$+*.-treetree€. dextree AíLL¡AfIndexBomInfoPathsHLIndexVIndex Size64CýLL¡AftmpAýLL¡A2nltk-installer´JRåB²¼¹n‚INSTALL.txtAýLL¡Aªjavasrc´KÏñÀûÙyòMakefileAýLL¡AforgAýLL¡Afnltk AýLL¡Aªmallet #´K^TA6í„L# CRFInfo.java &´K^T„¨m RunCRF.java )´KÏñÀ"¯qVg TrainCRF.java ,´K^T¢m°² README.txt /´K^T1zÙé|LICENSE.txt2AýLL¡Anltk5´LL™NîláÑÜ__init__.py8AýLL¡Aºapp;´K^S| 1ê±¶__init__.py>´K^S|Oëkmchartparser_app.pyA´K^S|Ø›9Zxchunkparser_app.pyD´K^S|48U…2écollocations_app.pyGýK^S|Z "concordance_app.pyJýKD/ ›C«½nemo_app.pyM´K^S|`6•6rdparser_app.pyP´K^S|€Œ?ò®srparser_app.pyS´K^S|a=ß“Xwordfreq_app.pyV´K^S|©”WÏYwordnet_app.pyY´K^S|ŠndtÜwxwordnet_app.py\´Khªö Ç‚L"ëbook.py_AýLL¡Aîccgb´K^S|ƒþ'yn__init__.pye´K^S|$º̨9ýapi.py h´K^S|/°— ¥chart.py!k´K^S|(Ü fCcombinator.py"n´K^S|f'~,lexicon.py#qAýLL¡A2chat$t´K^S€Ð•hÕÏ$__init__.py%w´K^S€={œ$eliza.py&z´K^S€èÃ:u$iesha.py'}´K^S€ p:ÇNo$rude.py(€´K^S€Oþªó\$suntsu.py)ƒ´K^S€B8Â`«$util.py*†´K^S€&ìÚ¼l$zen.py+‰AýLL¡Aîchunk,Œ´K^S~cTב,__init__.py-´K^S~>²[Ëù,api.py.’´K^S~(æ7ÏA¨,named_entity.py/•´K^S~ÖE½‚×,regexp.py0˜´K^S~P2:x©,util.py1›AýLL¡Aºclassify2ž´K^S‚ŽúÁ]2__init__.py3¡´K^S‚ ­£"î2api.py4¤´L2Ò´.‚M©÷2decisiontree.py5§´K^S‚ ¨)Šð2mallet.py6ª´KðŠ¢ªI‹™Ö2maxent.py7­´KðŠ¢¹$!ç 2megam.py8°´K^S‚&#ðcqÁ2naivebayes.py9³´K^S‚ žÙcÅ2rte_classify.py:¶´K^S‚ o: ëä2tadm.py;¹´KÏñÀ%äVYâ~2util.py<¼´K±6T0’€™Á2weka.py=¿AýLL¡Acluster>´LL™Ny(?`™>__init__.py?Å´K^S|øyGé>api.py@È´K^S|%d˜DH>em.pyAË´K^S|4[é¸>gaac.pyBδK^S|~ÅÕÈ.>kmeans.pyCÑ´KÏñ¾%Lÿ`¿U>util.pyDÔ´K^S‚'[ÙÐÛ¯collocations.pyE×´K^S‚ C¡šÎcompat.pyFÚ´K^S‚¿Î =containers.pyGÝAýLL¡AÌcorpusHà´KðŠ¢&þb×€6H__init__.pyIã´K^S€ŒNoßHeuroparl_raw.pyJæAýLL¡AbHreaderKé´K^S€ÄX;ÒtK__init__.pyLì´Kü_G¡¥ØæKapi.pyMï´K^S€$×âK¸YKverbnet.pyf:´K^S€÷ŸËÁ1Kwordlist.pyg=´KÏñ¾ñ©Zá¯QKwordnet.pyh@´K^S€?ÕlKxmldocs.pyiC´KÏñ¾,•0Ñ¡!Kycoe.pyjF´K^S€ z¶uHHutil.pykI´KèÒ@°17Îçdata.pylLýI€ó”o44decorators.pymO´K^S‚WÎƤVdownloader.pynRAýLL¡AdrawoU´K^S|% d7Üo__init__.pypX´K^S|s(ê, `ocfg.pyq[´K^S|H±7N¶odispersion.pyr^´K^S~®O \otable.pysa´K^S|’D[0Qotree.pytd´K^S|TBþVõoutil.pyugAýLL¡AÌetreevj´I€óDÎÄ…‡v__init__.pywm´I€ó®tñ¢ÄvElementInclude.pyxp´I€ó²bbÉ4vElementPath.pyys´I€ó `2ZvElementTree.pyzv´K^S‚ !uÉevaluate.py{yAýLL¡Aˆexamples||´KFÿÿÿÿ|__init__.py}´KhªžÕlÕ¬|pt.py~‚´K^S‚ø¶¾%featstruct.py…´K±6TÔë›í¶ˆgrammar.py€ˆ´K^S‚OÍLØhelp.py‹AýLL¡ATinference‚Ž´K^S~3±àׂ__init__.pyƒ‘´KDMéwZk#‚api.py„”´Ks VR"i•‚discourse.py…—´Kü_-]ÊÜd‚mace.py†š´K^S~G¡Ü î<‚nonmonotonic.py‡´Khªú3!O N‚prover9.pyˆ ýK^S~cnkÐÖ‚resolution.py‰£´KgtÔcIåx‚tableau.pyЦ´KðŠ rÒú’˜óinternals.py‹©´J$gäöPx#Tlazyimport.pyŒ¬AýLL¡ATmetrics¯´K^S‚'TØnF__init__.py޲´K^S‚0ýÛsO¶agreement.pyµ´K^S‚(þxB$µassociation.py¸´K^S‚á•Ç|€confusionmatrix.py‘»´KðŠ¢øMÆ­¿distance.py’¾´K^S‚(~J>ýscores.py“Á´K^S‚^É–spearman.py”Ä´K^S‚Ðè|ýwindowdiff.py•ÇAýLL¡Amisc–Ê´K^S‚n¯¬¯W–__init__.py—Í´I Í¾ïLkü–babelfish.py˜Ð´I€óW*vÇ^–chomsky.py™Ó´K^S‚ >ëÇ`–minimalset.pyšÖ´K^S‚Ì_{Ø>–sort.py›Ù´K^S‚ùƒgØ–wordfinder.pyœÜAýLL¡Aªmodelß´K^S~~Çêã__init__.pyžâ´KgtzC:“Lapi.pyŸå´L2Ò´îáqêngram.py è´I’½|3Ém±˜2nltk.jar¡ë´K^S‚Nþv»”olac.py¢îAýLL¡ABparse£ñ´K^S~¿<$v¬£__init__.py¤ô´K^S~Øu䤣api.py¥÷´K±6T.¥hÑU£chart.py¦ú´K^S~Yrw½4ð£dependencygraph.py§ý´K±6TFôØ&o£earleychart.py¨´K^S~Tª‹§O£featurechart.py©´K^S~ŽÔ•ëU£generate.pyª´KguÄñ%R£malt.py« ´K^S~iŠ4 £nonprojectivedependencyparser.py¬ ´K^S~IK÷Þ…O£pchart.py­´K^S~`6¥§ÛÝ£projectivedependencyparser.py®´K^S~c~RˆO£rd.py¯´K^S~ANo‰b£sr.py°´Kgu Lý |f£util.py±´K^S~CÞ«õ"£viterbi.py²´LL™NUI„6probability.py³!AýLL¡A sem´$´Kü_©}$é´__init__.pyµ'´Kü_Qês™™´boxer.py¶*´Kguf ÷¬¿´chat80.py·-´K^S€ßN9дcooper_storage.py¸0´Kü_Œ/¼±‰þ´drt.py¹3´Kgu"G‚açÑ´drt_glue_demo.pyº6´K^S‚WJlT6û´evaluate.py»9´KguNi¹ãŸ’´glue.py¼<´Kü_5? ;:´hole.py½?´Kgub9VZÛ´lfg.py¾B´Kü_>—l§‰m´linearlogic.py¿E´Kü_úïìc´logic.pyÀH´K^S€:é)¡´relextract.pyÁK´Kü_>\TÑÒ ´util.pyÂN´KðŠ Õ,×*sourcedstring.pyÃQAýLL¡AvstemÄT´LL™N TÇëÄ__init__.pyÅW´K^S‚¼~lÜtÄapi.pyÆZ´K^S‚Ùtokenize.doctestùó´Kgp'(B1¸Ùtoolbox.doctestúö´KgpÓm"vÙtree.doctestûù´Kgp еŒ­Ùtreetransforms.doctestüü´KðŠ¢ óoNfÙutil.doctestýÿ´KÏñ¾ýLLŸ 0á×±Tsetup.pyA;ÁE ªi 66q K"¢€"q#^"†`f3^3u3}3…3œ3¯3·3Ò3â3ê4 4 440 4=4E4\4d4l4ƒ 4Œ4”4« 4¶4¾4Ù4ê4ò5 55%5@5R5Z5u5„5Œ5§5·5¿5Ö 5ß5ç66661696A6\6l6t66¦6®6É6à6è777#7>7U7]7x7ˆ77«7¿7Ç7â7ö7þ88-858P8c8k8†8›8£8¾ 8Ê8Ò8é8ñ8ù99$9,9G 9R9Z9u 9‚9Š9¥9·9¿9Ú9é9ñ: :::4:D:L:g :t:|:— :¤:¬:Ç :Ó:Û:ö;; ;' ;3;;;V ;a;i;€ ;Š;’;­;½;Å;à ;ë;ó<<"<* >>0>D>L>g >s>{>– >¢>ª>Å >Ñ>Ù>ð >ü???/?7?R ?]?e?€ ?Š?’?­ ?¹?Á?Ü?ê?ò@ @@!@<@P@X@s@@‰@¤@¶@¾@Õ @à@èAAAA6AJARAi AtA|A—A§A¯AÊ AÕAÝAø BB B&B;BCB^BmBuBBŸB§B BÏB×BòCC C' C3C;CVCdClC‡C•CC¸CÇCÏCêCúDDD+D3DND_DgD‚D’DšDµDÅDÍDè DóDûEE&E.EIE`EhEƒEšE¢E½EÐEØEóFF F$ F1F9FTFcFkF† F’FšFµFÄFÌFçF÷FÿGG)G1GLG[GcG~ GŠG’G­ G¹GÁGÜ GèGðH HH%H@HRHZHq HzH‚HH­HµHÐ HÛHãHþIII3 I@IHIc IoIwI’ IžI¦I½ IÇIÏIêIúJJJ3J;JVJiJqJŒJŸJ§JÂJÒJÚJñ JþKK!K1K9KT K^KfKK“K›K¶KÅKÍKè KôKüLL!L)LDLTL\Lw L‚LŠL¥L¶L¾LÙ LåLíMMM$M?MNMVMqMƒM‹M¦MµM½MØMéMñN NN&N= NINQNlN|N„NŸN°N¸NÓNæNîO O O(OCOSO[OvO„OŒO§O·O¿OÚOìOôP PPP7PGPOPjP{PƒPžP­PµPÐPâPêQ QQQ4QFQNQe QoQwQ’Q¢QªQÅ QÐQØQó RRR# R0R8RS R_RgR~ RˆRR«R»RÃRÞ RéRñS SS!S<SSS[SvS‰S‘S¬SÀSÈSãSóSûT T"T*TE%TjTrTT›T£T¾"TàTèU U UU0 U:UBU] UiUqUŒU›U£U¾UÑUÙUðUøVVV+V3VN V[VcV~VŒV”V¯VÅVÍVè VóVûWW+W3WNW^WfW WW•W° W¼WÄWß WêWòX X X(XC XPXXXsX…XX¨ X´X¼X×XìXôY YYY7YGYOYj YuY}Y˜ Y¤Y¬YÇYØYàYûZ ZZ,Z:ZBZ] ZiZqZŒZœZ¤Z¿ZÎZÖZíZõZý[[([0 [V[^ [†[Ž[© [´[¼[× [â[ê\\\\6\H\P\k\{\ƒ\ž \©\±\Ì \Ø\à\÷ ]]]#]3];]V]i]q]Œ]ž]¦]Á]Ö]Þ]ù^^^5^H^P^k^|^„^Ÿ^¶^¾^Ù^ï^÷__"_*_E_Z_b_}_”_œ_·_Ñ_Ù_ô```+`I`Q`l`‚`Š`¥`»`Ã`Þ`ð`øaa'a/aJa[aca~a—aŸaºaÌaÔaïb bb,bDbLbgb~b†b¡b¸bÀbÛbñbùcc'c/cJcdclc‡c˜c c»cËcÓcîdd d&d:dBd]dndvd‘d¬d´dÏdàdèeeee: eFeNee ereze•e¥e­eÈ eÓeÛeö ff f&f4f<fW fdflf‡f•ff¸fÊfÒfífýgg g,g4gK gWg_gzgŠg’g­g¼gÄgß gëgóhh$h,hG hSh[hvh†hŽh© h¶h¾hÙhèhði in "1 nltk-2.0~b9/nltk/toolbox/toolbox.py0000644000175000017500000004447311327451575017302 0ustar bhavanibhavani# Natural Language Toolkit: Toolbox Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Greg Aumann # URL: # For license information, see LICENSE.TXT """ Module for reading, writing and manipulating Toolbox databases and settings files. """ import os, re, codecs from StringIO import StringIO from nltk.etree.ElementTree import TreeBuilder, Element, SubElement from nltk.data import PathPointer, ZipFilePathPointer import nltk class StandardFormat(object): """ Class for reading and processing standard format marker files and strings. """ def __init__(self, filename=None, encoding=None): self._encoding = encoding if filename is not None: self.open(filename) def open(self, sfm_file): """Open a standard format marker file for sequential reading. @param sfm_file: name of the standard format marker input file @type sfm_file: C{string} """ if isinstance(sfm_file, PathPointer): # [xx] We don't use 'rU' mode here -- do we need to? # (PathPointer.open doesn't take a mode option) self._file = sfm_file.open(self._encoding) else: self._file = codecs.open(sfm_file, 'rU', self._encoding) def open_string(self, s): """Open a standard format marker string for sequential reading. @param s: string to parse as a standard format marker input file @type s: C{string} """ self._file = StringIO(s) def raw_fields(self): """Return an iterator for the fields in the standard format marker file. @return: an iterator that returns the next field in a (marker, value) tuple. Linebreaks and trailing white space are preserved except for the final newline in each field. @rtype: iterator over C{(marker, value)} tuples """ join_string = '\n' line_regexp = r'^%s(?:\\(\S+)\s*)?(.*)$' # discard a BOM in the first line first_line_pat = re.compile(line_regexp % u'(?:\ufeff)?'.encode('utf8')) line_pat = re.compile(line_regexp % '') # need to get first line outside the loop for correct handling # of the first marker if it spans multiple lines file_iter = iter(self._file) line = file_iter.next() mobj = re.match(first_line_pat, line) mkr, line_value = mobj.groups() value_lines = [line_value,] self.line_num = 0 for line in file_iter: self.line_num += 1 mobj = re.match(line_pat, line) line_mkr, line_value = mobj.groups() if line_mkr: yield (mkr, join_string.join(value_lines)) mkr = line_mkr value_lines = [line_value,] else: value_lines.append(line_value) self.line_num += 1 yield (mkr, join_string.join(value_lines)) def fields(self, strip=True, unwrap=True, encoding=None, errors='strict', unicode_fields=None): """Return an iterator for the fields in the standard format marker file. @param strip: strip trailing whitespace from the last line of each field @type strip: C{boolean} @param unwrap: Convert newlines in a field to spaces. @type unwrap: C{boolean} @param encoding: Name of an encoding to use. If it is specified then the C{fields} method returns unicode strings rather than non unicode strings. @type encoding: C{string} or C{None} @param errors: Error handling scheme for codec. Same as the C{decode} inbuilt string method. @type errors: C{string} @param unicode_fields: Set of marker names whose values are UTF-8 encoded. Ignored if encoding is None. If the whole file is UTF-8 encoded set C{encoding='utf8'} and leave C{unicode_fields} with its default value of None. @type unicode_fields: set or dictionary (actually any sequence that supports the 'in' operator). @return: an iterator that returns the next field in a C{(marker, value)} tuple. C{marker} and C{value} are unicode strings if an C{encoding} was specified in the C{fields} method. Otherwise they are nonunicode strings. @rtype: iterator over C{(marker, value)} tuples """ if encoding is None and unicode_fields is not None: raise ValueError, 'unicode_fields is set but not encoding.' unwrap_pat = re.compile(r'\n+') for mkr, val in self.raw_fields(): if encoding: if unicode_fields is not None and mkr in unicode_fields: val = val.decode('utf8', errors) else: val = val.decode(encoding, errors) mkr = mkr.decode(encoding, errors) if unwrap: val = unwrap_pat.sub(' ', val) if strip: val = val.rstrip() yield (mkr, val) def close(self): """Close a previously opened standard format marker file or string.""" self._file.close() try: del self.line_num except AttributeError: pass class ToolboxData(StandardFormat): def parse(self, grammar=None, **kwargs): if grammar: return self._chunk_parse(grammar=grammar, **kwargs) else: return self._record_parse(**kwargs) def _record_parse(self, key=None, **kwargs): """ Returns an element tree structure corresponding to a toolbox data file with all markers at the same level. Thus the following Toolbox database:: \_sh v3.0 400 Rotokas Dictionary \_DateStampHasFourDigitYear \lx kaa \ps V.A \ge gag \gp nek i pas \lx kaa \ps V.B \ge strangle \gp pasim nek after parsing will end up with the same structure (ignoring the extra whitespace) as the following XML fragment after being parsed by ElementTree::

<_sh>v3.0 400 Rotokas Dictionary <_DateStampHasFourDigitYear/>
kaa V.A gag nek i pas kaa V.B strangle pasim nek @param key: Name of key marker at the start of each record. If set to None (the default value) the first marker that doesn't begin with an underscore is assumed to be the key. @type key: C{string} @param kwargs: Keyword arguments passed to L{StandardFormat.fields()} @type kwargs: keyword arguments dictionary @rtype: C{ElementTree._ElementInterface} @return: contents of toolbox data divided into header and records """ builder = TreeBuilder() builder.start('toolbox_data', {}) builder.start('header', {}) in_records = False for mkr, value in self.fields(**kwargs): if key is None and not in_records and mkr[0] != '_': key = mkr if mkr == key: if in_records: builder.end('record') else: builder.end('header') in_records = True builder.start('record', {}) builder.start(mkr, {}) builder.data(value) builder.end(mkr) if in_records: builder.end('record') else: builder.end('header') builder.end('toolbox_data') return builder.close() def _tree2etree(self, parent): from nltk.parse import Tree root = Element(parent.node) for child in parent: if isinstance(child, Tree): root.append(self._tree2etree(child)) else: text, tag = child e = SubElement(root, tag) e.text = text return root def _chunk_parse(self, grammar=None, top_node='record', trace=0, **kwargs): """ Returns an element tree structure corresponding to a toolbox data file parsed according to the chunk grammar. @type grammar: C{string} @param grammar: Contains the chunking rules used to parse the database. See L{chunk.RegExp} for documentation. @type top_node: C{string} @param top_node: The node value that should be used for the top node of the chunk structure. @type trace: C{int} @param trace: The level of tracing that should be used when parsing a text. C{0} will generate no tracing output; C{1} will generate normal tracing output; and C{2} or higher will generate verbose tracing output. @type kwargs: C{dictionary} @param kwargs: Keyword arguments passed to L{toolbox.StandardFormat.fields()} @rtype: C{ElementTree._ElementInterface} @return: Contents of toolbox data parsed according to the rules in grammar """ from nltk import chunk from nltk.parse import Tree cp = chunk.RegexpParser(grammar, top_node=top_node, trace=trace) db = self.parse(**kwargs) tb_etree = Element('toolbox_data') header = db.find('header') tb_etree.append(header) for record in db.findall('record'): parsed = cp.parse([(elem.text, elem.tag) for elem in record]) tb_etree.append(self._tree2etree(parsed)) return tb_etree _is_value = re.compile(r"\S") def to_sfm_string(tree, encoding=None, errors='strict', unicode_fields=None): """Return a string with a standard format representation of the toolbox data in tree (tree can be a toolbox database or a single record). @param tree: flat representation of toolbox data (whole database or single record) @type tree: C{ElementTree._ElementInterface} @param encoding: Name of an encoding to use. @type encoding: C{string} @param errors: Error handling scheme for codec. Same as the C{encode} inbuilt string method. @type errors: C{string} @param unicode_fields: @type unicode_fields: C{dictionary} or C{set} of field names @rtype: C{string} @return: C{string} using standard format markup """ if tree.tag == 'record': root = Element('toolbox_data') root.append(tree) tree = root if tree.tag != 'toolbox_data': raise ValueError, "not a toolbox_data element structure" if encoding is None and unicode_fields is not None: raise ValueError, \ "if encoding is not specified then neither should unicode_fields" l = [] for rec in tree: l.append('\n') for field in rec: mkr = field.tag value = field.text if encoding is not None: if unicode_fields is not None and mkr in unicode_fields: cur_encoding = 'utf8' else: cur_encoding = encoding if re.search(_is_value, value): l.append((u"\\%s %s\n" % (mkr, value)).encode(cur_encoding, errors)) else: l.append((u"\\%s%s\n" % (mkr, value)).encode(cur_encoding, errors)) else: if re.search(_is_value, value): l.append("\\%s %s\n" % (mkr, value)) else: l.append("\\%s%s\n" % (mkr, value)) return ''.join(l[1:]) class ToolboxSettings(StandardFormat): """This class is the base class for settings files.""" def __init__(self): super(ToolboxSettings, self).__init__() def parse(self, encoding=None, errors='strict', **kwargs): """Parses a settings file using ElementTree. @param encoding: encoding used by settings file @type encoding: C{string} @param errors: Error handling scheme for codec. Same as C{.decode} inbuilt method. @type errors: C{string} @param kwargs: Keyword arguments passed to L{StandardFormat.fields()} @type kwargs: keyword arguments dictionary @rtype: C{ElementTree._ElementInterface} @return: contents of toolbox settings file with a nested structure """ builder = TreeBuilder() for mkr, value in self.fields(encoding=encoding, errors=errors, **kwargs): # Check whether the first char of the field marker # indicates a block start (+) or end (-) block=mkr[0] if block in ("+", "-"): mkr=mkr[1:] else: block=None # Build tree on the basis of block char if block == "+": builder.start(mkr, {}) builder.data(value) elif block == '-': builder.end(mkr) else: builder.start(mkr, {}) builder.data(value) builder.end(mkr) return builder.close() def to_settings_string(tree, encoding=None, errors='strict', unicode_fields=None): # write XML to file l = list() _to_settings_string(tree.getroot(), l, encoding=encoding, errors=errors, unicode_fields=unicode_fields) return ''.join(l) def _to_settings_string(node, l, **kwargs): # write XML to file tag = node.tag text = node.text if len(node) == 0: if text: l.append('\\%s %s\n' % (tag, text)) else: l.append('\\%s\n' % tag) else: if text: l.append('\\+%s %s\n' % (tag, text)) else: l.append('\\+%s\n' % tag) for n in node: _to_settings_string(n, l, **kwargs) l.append('\\-%s\n' % tag) return def remove_blanks(elem): """Remove all elements and subelements with no text and no child elements. @param elem: toolbox data in an elementtree structure @type elem: ElementTree._ElementInterface """ out = list() for child in elem: remove_blanks(child) if child.text or len(child) > 0: out.append(child) elem[:] = out def add_default_fields(elem, default_fields): """Add blank elements and subelements specified in default_fields. @param elem: toolbox data in an elementtree structure @type elem: ElementTree._ElementInterface @param default_fields: fields to add to each type of element and subelement @type default_fields: dictionary of tuples """ for field in default_fields.get(elem.tag, []): if elem.find(field) is None: SubElement(elem, field) for child in elem: add_default_fields(child, default_fields) def sort_fields(elem, field_orders): """Sort the elements and subelements in order specified in field_orders. @param elem: toolbox data in an elementtree structure @type elem: ElementTree._ElementInterface @param field_orders: order of fields for each type of element and subelement @type field_orders: dictionary of tuples """ order_dicts = dict() for field, order in field_orders.items(): order_dicts[field] = order_key = dict() for i, subfield in enumerate(order): order_key[subfield] = i _sort_fields(elem, order_dicts) def _sort_fields(elem, orders_dicts): """sort the children of elem""" try: order = orders_dicts[elem.tag] except KeyError: pass else: tmp = [((order.get(child.tag, 1e9), i), child) for i, child in enumerate(elem)] tmp.sort() elem[:] = [child for key, child in tmp] for child in elem: if len(child): _sort_fields(child, orders_dicts) def add_blank_lines(tree, blanks_before, blanks_between): """Add blank lines before all elements and subelements specified in blank_before. @param elem: toolbox data in an elementtree structure @type elem: ElementTree._ElementInterface @param blank_before: elements and subelements to add blank lines before @type blank_before: dictionary of tuples """ try: before = blanks_before[tree.tag] between = blanks_between[tree.tag] except KeyError: for elem in tree: if len(elem): add_blank_lines(elem, blanks_before, blanks_between) else: last_elem = None for elem in tree: tag = elem.tag if last_elem is not None and last_elem.tag != tag: if tag in before and last_elem is not None: e = last_elem.getiterator()[-1] e.text = (e.text or "") + "\n" else: if tag in between: e = last_elem.getiterator()[-1] e.text = (e.text or "") + "\n" if len(elem): add_blank_lines(elem, blanks_before, blanks_between) last_elem = elem def demo(): from itertools import islice # zip_path = nltk.data.find('corpora/toolbox.zip') # lexicon = ToolboxData(ZipFilePathPointer(zip_path, 'toolbox/rotokas.dic')).parse() file_path = nltk.data.find('corpora/toolbox/rotokas.dic') lexicon = ToolboxData(file_path).parse() print 'first field in fourth record:' print lexicon[3][0].tag print lexicon[3][0].text print '\nfields in sequential order:' for field in islice(lexicon.find('record'), 10): print field.tag, field.text print '\nlx fields:' for field in islice(lexicon.findall('record/lx'), 10): print field.text from nltk.etree.ElementTree import ElementTree settings = ToolboxSettings() file_path = nltk.data.find('corpora/toolbox/MDF/MDF_AltH.typ') settings.open(file_path) # settings.open(ZipFilePathPointer(zip_path, entry='toolbox/MDF/MDF_AltH.typ')) tree = settings.parse(unwrap=False, encoding='cp1252') print tree.find('expset/expMDF/rtfPageSetup/paperSize').text settings_tree = ElementTree(tree) print to_settings_string(settings_tree).encode('utf8') if __name__ == '__main__': demo() nltk-2.0~b9/nltk/toolbox/__init__.py0000644000175000017500000000047011327451575017340 0ustar bhavanibhavani# Natural Language Toolkit: Toolbox Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Greg Aumann # URL: # For license information, see LICENSE.TXT """ Module for reading, writing and manipulating Toolbox databases and settings files. """ from toolbox import * nltk-2.0~b9/nltk/tokenize/util.py0000644000175000017500000000343011366165256016720 0ustar bhavanibhavani# Natural Language Toolkit: Simple Tokenizers # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT from re import finditer def string_span_tokenize(s, sep): """ Identify the tokens in the string, as defined by the token delimiter, and generate (start, end) offsets. @param s: the string to be tokenized @type s: C{str} @param sep: the token separator @type sep: C{str} @rtype: C{iter} of C{tuple} of C{int} """ if len(sep) == 0: raise ValueError, "Token delimiter must not be empty" left = 0 while True: try: right = s.index(sep, left) if right != 0: yield left, right except ValueError: if left != len(s): yield left, len(s) break left = right + len(sep) def regexp_span_tokenize(s, regexp): """ Identify the tokens in the string, as defined by the token delimiter regexp, and generate (start, end) offsets. @param s: the string to be tokenized @type s: C{str} @param regexp: the token separator regexp @type regexp: C{str} @rtype: C{iter} of C{tuple} of C{int} """ left = 0 for m in finditer(regexp, s): right, next = m.span() if right != 0: yield left, right left = next yield left, len(s) def spans_to_relative(spans): """ Convert absolute token spans to relative spans. @param spans: the (start, end) offsets of the tokens @type s: C{iter} of C{tuple} of C{int} @rtype: C{iter} of C{tuple} of C{int} """ prev = 0 for left, right in spans: yield left - prev, right - left prev = right nltk-2.0~b9/nltk/tokenize/treebank.py0000644000175000017500000000523411327451575017541 0ustar bhavanibhavani# Natural Language Toolkit: Tokenizers # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT """ A regular-expression based word tokenizer that tokenizes sentences using the conventions used by the Penn Treebank. """ import re from api import * ###################################################################### #{ Regexp-based treebank tokenizer ###################################################################### # (n.b., this isn't derived from RegexpTokenizer) class TreebankWordTokenizer(TokenizerI): """ A word tokenizer that tokenizes sentences using the conventions used by the Penn Treebank. Contractions, such as "can't", are split in to two tokens. E.g.: - can't S{->} ca n't - he'll S{->} he 'll - weren't S{-} were n't This tokenizer assumes that the text has already been segmented into sentences. Any periods -- apart from those at the end of a string -- are assumed to be part of the word they are attached to (e.g. for abbreviations, etc), and are not separately tokenized. """ # List of contractions adapted from Robert MacIntyre's tokenizer. CONTRACTIONS2 = [re.compile(r"(?i)(.)('ll|'re|'ve|n't|'s|'m|'d)\b"), re.compile(r"(?i)\b(can)(not)\b"), re.compile(r"(?i)\b(D)('ye)\b"), re.compile(r"(?i)\b(Gim)(me)\b"), re.compile(r"(?i)\b(Gon)(na)\b"), re.compile(r"(?i)\b(Got)(ta)\b"), re.compile(r"(?i)\b(Lem)(me)\b"), re.compile(r"(?i)\b(Mor)('n)\b"), re.compile(r"(?i)\b(T)(is)\b"), re.compile(r"(?i)\b(T)(was)\b"), re.compile(r"(?i)\b(Wan)(na)\b")] CONTRACTIONS3 = [re.compile(r"(?i)\b(Whad)(dd)(ya)\b"), re.compile(r"(?i)\b(Wha)(t)(cha)\b")] def tokenize(self, text): for regexp in self.CONTRACTIONS2: text = regexp.sub(r'\1 \2', text) for regexp in self.CONTRACTIONS3: text = regexp.sub(r'\1 \2 \3', text) # Separate most punctuation text = re.sub(r"([^\w\.\'\-\/,&])", r' \1 ', text) # Separate commas if they're followed by space. # (E.g., don't separate 2,500) text = re.sub(r"(,\s)", r' \1', text) # Separate single quotes if they're followed by a space. text = re.sub(r"('\s)", r' \1', text) # Separate periods that come before newline or end of string. text = re.sub('\. *(\n|$)', ' . ', text) return text.split() nltk-2.0~b9/nltk/tokenize/texttiling.py0000644000175000017500000004030211374105240020117 0ustar bhavanibhavani# Natural Language Toolkit: TextTiling # # Copyright (C) 2001-2010 NLTK Project # Author: George Boutsioukis # # URL: # For license information, see LICENSE.TXT import re import math import numpy from api import TokenizerI BLOCK_COMPARISON, VOCABULARY_INTRODUCTION = range(2) LC, HC = range(2) DEFAULT_SMOOTHING = range(1) class TextTilingTokenizer(TokenizerI): """A section tokenizer based on the TextTiling algorithm. The algorithm detects subtopic shifts based on the analysis of lexical co-occurence patterns. The process starts by tokenizing the text into pseudosentences of a fixed size w. Then, depending on the method used, similarity scores are assigned at sentence gaps. The algorithm proceeds by detecting the peak differences between these scores and marking them as boundaries. The boundaries are normalized to the closest paragraph break and the segmented text is returned. @type w: number @param w: Pseudosentence size @type k: number @param k: Size(in sentences) of the block used in the block comparison method @type similarity_method: constant @param similarity_method: The method used for determining similarity scores @type stopwords: list @param stopwords: A list of stopwords that are filtered out @type smoothing_method: constant @param smoothing_method: The method used for smoothing the score plot @type smoothing_width: number @param smoothing_width: The width of the window used by the smoothing method @type smoothing_rounds: number @param smoothing_rounds: The number of smoothing passes @type cutoff_policy: constant @param cutoff_policy: The policy used to determine the number of boundaries """ def __init__(self, w=20, k=10, similarity_method=BLOCK_COMPARISON, stopwords=None, smoothing_method=DEFAULT_SMOOTHING, smoothing_width=2, smoothing_rounds=1, cutoff_policy=HC, demo_mode=False): if stopwords is None: from nltk.corpus import stopwords stopwords = stopwords.words('english') self.__dict__.update(locals()) del self.__dict__['self'] def tokenize(self, text): "The main function. Follows a pipeline structure." lowercase_text = text.lower() paragraph_breaks = self._mark_paragraph_breaks(text) text_length = len(lowercase_text) # Tokenization step starts here #remove punctuation nopunct_text = ''.join([c for c in lowercase_text if re.match("[a-z\-\' \n\t]", c)]) nopunct_par_breaks = self._mark_paragraph_breaks(nopunct_text) tokseqs = self._divide_to_tokensequences(nopunct_text) # The morphological stemming step mentioned in the TextTile # paper is not implemented. A comment in the original C # implementation states that it offers no benefit to the # process. It might be interesting to test the existing # stemmers though. #words = _stem_words(words) #filter stopwords for ts in tokseqs: ts.wrdindex_list = filter(lambda wi: wi[0] not in self.stopwords, ts.wrdindex_list) token_table = self._create_token_table(tokseqs, nopunct_par_breaks) # End of the Tokenization step # Lexical score Determination if self.similarity_method == BLOCK_COMPARISON: gap_scores = self._block_comparison(tokseqs, token_table) elif self.similarity_method == VOCABULARY_INTRODUCTION: raise NotImplementedError("Vocabulary introduction not implemented") if self.smoothing_method == DEFAULT_SMOOTHING: smooth_scores = self._smooth_scores(gap_scores) # End of Lexical score Determination # Boundary identification depth_scores = self._depth_scores(smooth_scores) segment_boundaries = self._identify_boundaries(depth_scores) normalized_boundaries = self._normalize_boundaries(text, segment_boundaries, paragraph_breaks) # End of Boundary Identification segmented_text = [] prevb = 0 for b in normalized_boundaries: if b == 0: continue segmented_text.append(text[prevb:b]) prevb = b if not segmented_text: segmented_text = [text] if self.demo_mode: return gap_scores, smooth_scores, depth_scores, segment_boundaries return segmented_text def _block_comparison(self, tokseqs, token_table): "Implements the block comparison method" def blk_frq(tok, block): ts_occs = filter(lambda o: o[0] in block, token_table[tok].ts_occurences) freq = sum([tsocc[1] for tsocc in ts_occs]) return freq gap_scores = [] numgaps = len(tokseqs)-1 for curr_gap in range(numgaps): score_dividend, score_divisor_b1, score_divisor_b2 = 0.0, 0.0, 0.0 score = 0.0 #adjust window size for boundary conditions if curr_gap < self.k-1: window_size = curr_gap + 1 elif curr_gap > numgaps-self.k: window_size = numgaps - curr_gap else: window_size = self.k b1 = [ts.index for ts in tokseqs[curr_gap-window_size+1 : curr_gap+1]] b2 = [ts.index for ts in tokseqs[curr_gap+1 : curr_gap+window_size+1]] for t in token_table.keys(): score_dividend += blk_frq(t, b1)*blk_frq(t, b2) score_divisor_b1 += blk_frq(t, b1)**2 score_divisor_b2 += blk_frq(t, b2)**2 try: score = score_dividend/math.sqrt(score_divisor_b1* score_divisor_b2) except ZeroDivisionError: pass # score += 0.0 gap_scores.append(score) return gap_scores def _smooth_scores(self, gap_scores): "Wraps the smooth function from the SciPy Cookbook" return list(smooth(numpy.array(gap_scores[:]), window_len = self.smoothing_width+1)) def _mark_paragraph_breaks(self, text): """Identifies indented text or line breaks as the beginning of paragraphs""" MIN_PARAGRAPH = 100 pattern = re.compile("[ \t\r\f\v]*\n[ \t\r\f\v]*\n[ \t\r\f\v]*") matches = pattern.finditer(text) last_break = 0 pbreaks = [0] for pb in matches: if pb.start()-last_break < MIN_PARAGRAPH: continue else: pbreaks.append(pb.start()) last_break = pb.start() return pbreaks def _divide_to_tokensequences(self, text): "Divides the text into pseudosentences of fixed size" w = self.w wrdindex_list = [] matches = re.finditer("\w+", text) for match in matches: wrdindex_list.append((match.group(), match.start())) return [TokenSequence(i/w, wrdindex_list[i:i+w]) for i in range(0, len(wrdindex_list), w)] def _create_token_table(self, token_sequences, par_breaks): "Creates a table of TokenTableFields" token_table = {} current_par = 0 current_tok_seq = 0 pb_iter = par_breaks.__iter__() current_par_break = pb_iter.next() if current_par_break == 0: try: current_par_break = pb_iter.next() #skip break at 0 except StopIteration: raise ValueError( "No paragraph breaks were found(text too short perhaps?)" ) for ts in token_sequences: for word, index in ts.wrdindex_list: try: while index > current_par_break: current_par_break = pb_iter.next() current_par += 1 except StopIteration: #hit bottom pass if word in token_table.keys(): token_table[word].total_count += 1 if token_table[word].last_par != current_par: token_table[word].last_par = current_par token_table[word].par_count += 1 if token_table[word].last_tok_seq != current_tok_seq: token_table[word].last_tok_seq = current_tok_seq token_table[word]\ .ts_occurences.append([current_tok_seq,1]) else: token_table[word].ts_occurences[-1][1] += 1 else: #new word token_table[word] = TokenTableField(first_pos=index, ts_occurences= \ [[current_tok_seq,1]], total_count=1, par_count=1, last_par=current_par, last_tok_seq= \ current_tok_seq) current_tok_seq += 1 return token_table def _identify_boundaries(self, depth_scores): """Identifies boundaries at the peaks of similarity score differences""" boundaries = [0 for x in depth_scores] avg = sum(depth_scores)/len(depth_scores) numpy.stdev = numpy.std(depth_scores) if self.cutoff_policy == LC: cutoff = avg-numpy.stdev/2.0 else: cutoff = avg-numpy.stdev/2.0 depth_tuples = zip(depth_scores, range(len(depth_scores))) depth_tuples.sort() depth_tuples.reverse() hp = filter(lambda x:x[0]>cutoff, depth_tuples) for dt in hp: boundaries[dt[1]] = 1 for dt2 in hp: #undo if there is a boundary close already if dt[1] != dt2[1] and abs(dt2[1]-dt[1]) < 4 \ and boundaries[dt2[1]] == 1: boundaries[dt[1]] = 0 return boundaries def _depth_scores(self, scores): """Calculates the depth of each gap, i.e. the average difference between the left and right peaks and the gap's score""" depth_scores = [0 for x in scores] #clip boundaries: this holds on the rule of thumb(my thumb) #that a section shouldn't be smaller than at least 2 #pseudosentences for small texts and around 5 for larger ones. clip = min(max(len(scores)/10, 2), 5) index = clip # SB: next three lines are redundant as depth_scores is already full of zeros for i in range(clip): depth_scores[i] = 0 depth_scores[-i-1] = 0 for gapscore in scores[clip:-clip]: lpeak = gapscore for score in scores[index::-1]: if score >= lpeak: lpeak = score else: break rpeak = gapscore for score in scores[:index:]: if score >= rpeak: rpeak=score else: break depth_scores[index] = lpeak + rpeak - 2*gapscore index += 1 return depth_scores def _normalize_boundaries(self, text, boundaries, paragraph_breaks): """Normalize the boundaries identified to the original text's paragraph breaks""" norm_boundaries = [] char_count, word_count, gaps_seen = 0, 0, 0 seen_word = False for char in text: char_count += 1 if char in " \t\n" and seen_word: seen_word = False word_count += 1 if char not in " \t\n" and not seen_word: seen_word=True if gaps_seen < len(boundaries) and word_count > \ (max(gaps_seen*self.w, self.w)): if boundaries[gaps_seen] == 1: #find closest paragraph break best_fit = len(text) for br in paragraph_breaks: if best_fit > abs(br-char_count): best_fit = abs(br-char_count) bestbr = br else: break if bestbr not in norm_boundaries: #avoid duplicates norm_boundaries.append(bestbr) gaps_seen += 1 return norm_boundaries class TokenTableField(object): """A field in the token table holding parameters for each token, used later in the process""" def __init__(self, first_pos, ts_occurences, total_count=1, par_count=1, last_par=0, last_tok_seq=None): self.__dict__.update(locals()) del self.__dict__['self'] class TokenSequence(object): "A token list with its original length and its index" def __init__(self, index, wrdindex_list, original_length=None): original_length=original_length or len(wrdindex_list) self.__dict__.update(locals()) del self.__dict__['self'] #Pasted from the SciPy cookbook: http://www.scipy.org/Cookbook/SignalSmooth def smooth(x,window_len=11,window='flat'): """smooth the data using a window with requested size. This method is based on the convolution of a scaled window with the signal. The signal is prepared by introducing reflected copies of the signal (with the window size) in both ends so that transient parts are minimized in the begining and end part of the output signal. input: x: the input signal window_len: the dimension of the smoothing window; should be an odd integer window: the type of window from 'flat', 'hanning', 'hamming', 'bartlett', 'blackman' flat window will produce a moving average smoothing. output: the smoothed signal example: t=linspace(-2,2,0.1) x=sin(t)+randn(len(t))*0.1 y=smooth(x) see also: numpy.hanning, numpy.hamming, numpy.bartlett, numpy.blackman, numpy.convolve scipy.signal.lfilter TODO: the window parameter could be the window itself if an array instead of a string """ if x.ndim != 1: raise ValueError, "smooth only accepts 1 dimension arrays." if x.size < window_len: raise ValueError, "Input vector needs to be bigger than window size." if window_len<3: return x if not window in ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']: raise ValueError, "Window is on of 'flat', 'hanning', 'hamming', 'bartlett', 'blackman'" s=numpy.r_[2*x[0]-x[window_len:1:-1],x,2*x[-1]-x[-1:-window_len:-1]] #print(len(s)) if window == 'flat': #moving average w=numpy.ones(window_len,'d') else: w=eval('numpy.'+window+'(window_len)') y=numpy.convolve(w/w.sum(),s,mode='same') return y[window_len-1:-window_len+1] def demo(text=None): from nltk.corpus import brown import pylab tt=TextTilingTokenizer(demo_mode=True) if text is None: text=brown.raw()[:10000] s,ss,d,b=tt.tokenize(text) pylab.xlabel("Sentence Gap index") pylab.ylabel("Gap Scores") pylab.plot(range(len(s)), s, label="Gap Scores") pylab.plot(range(len(ss)), ss, label="Smoothed Gap scores") pylab.plot(range(len(d)), d, label="Depth scores") pylab.stem(range(len(b)),b) pylab.legend() pylab.show() if __name__ == '__main__': demo() nltk-2.0~b9/nltk/tokenize/simple.py0000644000175000017500000001013011366153324017220 0ustar bhavanibhavani# Natural Language Toolkit: Simple Tokenizers # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # Trevor Cohn # URL: # For license information, see LICENSE.TXT """ Tokenizers that divide strings into substrings using the string C{split()} method. These tokenizers follow the standard L{TokenizerI} interface, and so can be used with any code that expects a tokenizer. For example, these tokenizers can be used to specify the tokenization conventions when building a L{CorpusReader}. But if you are tokenizing a string yourself, consider using string C{split()} method directly instead. """ from api import * from util import * class SpaceTokenizer(StringTokenizer): r""" A tokenizer that divides a string into substrings by treating any single space character as a separator. If you are performing the tokenization yourself (rather than building a tokenizer to pass to some other piece of code), consider using the string C{split()} method instead: >>> words = s.split(' ') """ _string = ' ' class TabTokenizer(StringTokenizer): r""" A tokenizer that divides a string into substrings by treating any single tab character as a separator. If you are performing the tokenization yourself (rather than building a tokenizer to pass to some other piece of code), consider using the string C{split()} method instead: >>> words = s.split('\t') """ _string = '\t' class CharTokenizer(StringTokenizer): r""" A tokenizer that produces individual characters. If you are performing the tokenization yourself (rather than building a tokenizer to pass to some other piece of code), consider iterating over the characters of the string directly instead: for char in string """ def tokenize(self, s): return list(s) def span_tokenize(self, s): for i, j in enumerate(range(1, len(s+1))): yield i, j class LineTokenizer(TokenizerI): r""" A tokenizer that divides a string into substrings by treating any single newline character as a separator. Handling of blank lines may be controlled using a constructor parameter. """ def __init__(self, blanklines='discard'): """ @param blanklines: Indicates how blank lines should be handled. Valid values are: - C{'discard'}: strip blank lines out of the token list before returning it. A line is considered blank if it contains only whitespace characters. - C{'keep'}: leave all blank lines in the token list. - C{'discard-eof'}: if the string ends with a newline, then do not generate a corresponding token C{''} after that newline. """ valid_blanklines = ('discard', 'keep', 'discard-eof') if blanklines not in valid_blanklines: raise ValueError('Blank lines must be one of: %s' % ' '.join(valid_blanklines)) self._blanklines = blanklines def tokenize(self, s): lines = s.split('\n') # If requested, strip off blank lines. if self._blanklines == 'discard': lines = [l for l in lines if l.rstrip()] elif self._blanklines == 'discard-eof': if lines and not lines[-1].strip(): lines.pop() return lines # discard-eof not implemented def span_tokenize(self, s): if self._blanklines == 'keep': for span in string_span_tokenize(s, r'\n'): yield span else: for span in regexp_span_tokenize(s, r'\n(\s+\n)*'): yield span ###################################################################### #{ Tokenization Functions ###################################################################### def line_tokenize(text, blanklines='discard'): return LineTokenizer(blanklines).tokenize(text) nltk-2.0~b9/nltk/tokenize/sexpr.py0000644000175000017500000001042711327451575017107 0ustar bhavanibhavani# Natural Language Toolkit: Tokenizers # # Copyright (C) 2001-2010 NLTK Project # Author: Yoav Goldberg # Steven Bird (minor edits) # URL: # For license information, see LICENSE.TXT """ A tokenizer that divides strings into s-expressions. E.g.: >>> sexpr_tokenize('(a b (c d)) e f (g)') ['(a b (c d))', 'e', 'f', '(g)'] """ import re from api import * class SExprTokenizer(TokenizerI): """ A tokenizer that divides strings into X{s-expressions}. An s-expresion can be either: - A parenthasized expression, including any nested parenthasized expressions. - A sequence of non-whitespace non-parenthasis characters. For example, the string C{'(a (b c)) d e (f)'} consists of four s-expressions: C{'(a (b c))'}, C{'d'}, C{'e'}, and C{'(f)'}. """ def __init__(self, parens='()', strict=True): """ Construct a new SExpr tokenizer. By default, the characters C{'('} and C{')'} are treated as open and close parenthases; but alternative strings may be specified. @param parens: A two-element sequence specifying the open and close parenthases that should be used to find sexprs. This will typically be either a two-character string, or a list of two strings. @type parens: C{str} or C{list} @param strict: If true, then raise an exception when tokenizing an ill-formed sexpr. """ if len(parens) != 2: raise ValueError('parens must contain exactly two strings') self._strict = strict self._open_paren = parens[0] self._close_paren = parens[1] self._paren_regexp = re.compile('%s|%s' % (re.escape(parens[0]), re.escape(parens[1]))) def tokenize(self, text): """ Tokenize the text into s-expressions. For example: >>> SExprTokenizer().tokenize('(a b (c d)) e f (g)') ['(a b (c d))', 'e', 'f', '(g)'] All parenthases are assumed to mark sexprs. In particular, no special processing is done to exclude parenthases that occur inside strings, or following backslash characters. If the given expression contains non-matching parenthases, then the behavior of the tokenizer depends on the C{strict} parameter to the constructor. If C{strict} is C{True}, then raise a C{ValueError}. If C{strict} is C{False}, then any unmatched close parenthases will be listed as their own s-expression; and the last partial sexpr with unmatched open parenthases will be listed as its own sexpr: >>> SExprTokenizer(strict=False).tokenize('c) d) e (f (g') ['c', ')', 'd', ')', 'e', '(f (g'] @param text: the string to be tokenized @type text: C{string} or C{iter(string)} @return: An iterator over tokens (each of which is an s-expression) """ result = [] pos = 0 depth = 0 for m in self._paren_regexp.finditer(text): paren = m.group() if depth == 0: result += text[pos:m.start()].split() pos = m.start() if paren == self._open_paren: depth += 1 if paren == self._close_paren: if self._strict and depth == 0: raise ValueError('Un-matched close paren at char %d' % m.start()) depth = max(0, depth-1) if depth == 0: result.append(text[pos:m.end()]) pos = m.end() if self._strict and depth > 0: raise ValueError('Un-matched open paren at char %d' % pos) if pos < len(text): result.append(text[pos:]) return result sexpr_tokenize = SExprTokenizer().tokenize def demo(): from nltk import tokenize example = "d (d ((e) ((f) ss) a a c) d) r (t i) (iu a" example = "d [d [[e] [[f] ss] a a c] d] r [t i]" print 'Input text:' print example print print 'Tokenize s-expressions:' for x in SExprTokenizer('[]').tokenize(example): print x if __name__ == '__main__': demo() nltk-2.0~b9/nltk/tokenize/regexp.py0000644000175000017500000001564211366162250017234 0ustar bhavanibhavani# Natural Language Toolkit: Tokenizers # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # Trevor Cohn # URL: # For license information, see LICENSE.TXT """ Tokenizers that divide strings into substrings using regular expressions that can match either tokens or separators between tokens. """ import re import sre_constants from nltk.internals import convert_regexp_to_nongrouping, Deprecated from api import * from util import * class RegexpTokenizer(TokenizerI): """ A tokenizer that splits a string into substrings using a regular expression. The regular expression can be specified to match either tokens or separators between tokens. Unlike C{re.findall()} and C{re.split()}, C{RegexpTokenizer} does not treat regular expressions that contain grouping parenthases specially. """ def __init__(self, pattern, gaps=False, discard_empty=True, flags=re.UNICODE | re.MULTILINE | re.DOTALL): """ Construct a new tokenizer that splits strings using the given regular expression C{pattern}. By default, C{pattern} will be used to find tokens; but if C{gaps} is set to C{False}, then C{patterns} will be used to find separators between tokens instead. @type pattern: C{str} @param pattern: The pattern used to build this tokenizer. This pattern may safely contain grouping parenthases. @type gaps: C{bool} @param gaps: True if this tokenizer's pattern should be used to find separators between tokens; False if this tokenizer's pattern should be used to find the tokens themselves. @type discard_empty: C{bool} @param discard_empty: True if any empty tokens (C{''}) generated by the tokenizer should be discarded. Empty tokens can only be generated if L{_gaps} is true. @type flags: C{int} @param flags: The regexp flags used to compile this tokenizer's pattern. By default, the following flags are used: C{re.UNICODE | re.MULTILINE | re.DOTALL}. """ # If they gave us a regexp object, extract the pattern. pattern = getattr(pattern, 'pattern', pattern) self._pattern = pattern """The pattern used to build this tokenizer.""" self._gaps = gaps """True if this tokenizer's pattern should be used to find separators between tokens; False if this tokenizer's pattern should be used to find the tokens themselves.""" self._discard_empty = discard_empty """True if any empty tokens (C{''}) generated by the tokenizer should be discarded. Empty tokens can only be generated if L{_gaps} is true.""" self._flags = flags """The flags used to compile this tokenizer's pattern.""" self._regexp = None """The compiled regular expression used to tokenize texts.""" # Remove grouping parentheses -- if the regexp contains any # grouping parentheses, then the behavior of re.findall and # re.split will change. nongrouping_pattern = convert_regexp_to_nongrouping(pattern) try: self._regexp = re.compile(nongrouping_pattern, flags) except re.error, e: raise ValueError('Error in regular expression %r: %s' % (pattern, e)) def tokenize(self, text): # If our regexp matches gaps, use re.split: if self._gaps: if self._discard_empty: return [tok for tok in self._regexp.split(text) if tok] else: return self._regexp.split(text) # If our regexp matches tokens, use re.findall: else: return self._regexp.findall(text) def span_tokenize(self, text): if self._gaps: for left, right in regexp_span_tokenize(text, self._regexp): if not (self._discard_empty and left == right): yield left, right else: for m in finditer(self._regexp, s): yield m.span() def __repr__(self): return ('%s(pattern=%r, gaps=%r, discard_empty=%r, flags=%r)' % (self.__class__.__name__, self._pattern, self._gaps, self._discard_empty, self._flags)) class WhitespaceTokenizer(RegexpTokenizer): r""" A tokenizer that divides a string into substrings by treating any sequence of whitespace characters as a separator. Whitespace characters are space (C{' '}), tab (C{'\t'}), and newline (C{'\n'}). If you are performing the tokenization yourself (rather than building a tokenizer to pass to some other piece of code), consider using the string C{split()} method instead: >>> words = s.split() """ def __init__(self): RegexpTokenizer.__init__(self, r'\s+', gaps=True) class BlanklineTokenizer(RegexpTokenizer): """ A tokenizer that divides a string into substrings by treating any sequence of blank lines as a separator. Blank lines are defined as lines containing no characters, or containing only space (C{' '}) or tab (C{'\t'}) characters. """ def __init__(self): RegexpTokenizer.__init__(self, r'\s*\n\s*\n\s*', gaps=True) class WordPunctTokenizer(RegexpTokenizer): r""" A tokenizer that divides a text into sequences of alphabetic and non-alphabetic characters. E.g.: >>> WordPunctTokenizer().tokenize("She said 'hello'.") ['She', 'said', "'", 'hello', "'."] """ def __init__(self): RegexpTokenizer.__init__(self, r'\w+|[^\w\s]+') class WordTokenizer(RegexpTokenizer, Deprecated): """ B{If you want to tokenize words, you should probably use TreebankWordTokenizer or word_tokenize() instead.} A tokenizer that divides a text into sequences of alphabetic characters. Any non-alphabetic characters are discarded. E.g.: >>> WordTokenizer().tokenize("She said 'hello'.") ['She', 'said', 'hello'] """ def __init__(self): RegexpTokenizer.__init__(self, r'\w+') ###################################################################### #{ Tokenization Functions ###################################################################### def regexp_tokenize(text, pattern, gaps=False, discard_empty=True, flags=re.UNICODE | re.MULTILINE | re.DOTALL): """ Split the given text string, based on the given regular expression pattern. See the documentation for L{RegexpTokenizer.tokenize()} for descriptions of the arguments. """ tokenizer = RegexpTokenizer(pattern, gaps, discard_empty, flags) return tokenizer.tokenize(text) blankline_tokenize = BlanklineTokenizer().tokenize wordpunct_tokenize = WordPunctTokenizer().tokenize nltk-2.0~b9/nltk/tokenize/punkt.py0000644000175000017500000015476411414551264017115 0ustar bhavanibhavani# Natural Language Toolkit: Punkt sentence tokenizer # # Copyright (C) 2001-2010 NLTK Project # Algorithm: Kiss & Strunk (2006) # Author: Willy (original Python port) # Steven Bird (additions) # Edward Loper (rewrite) # Joel Nothman (almost rewrite) # URL: # For license information, see LICENSE.TXT # # $Id: probability.py 4865 2007-07-11 22:6:07Z edloper $ """ The Punkt sentence tokenizer. The algorithm for this tokenizer is described in Kiss & Strunk (2006):: Kiss, Tibor and Strunk, Jan (2006): Unsupervised Multilingual Sentence Boundary Detection. Computational Linguistics 32: 485-525. """ # TODO: Make orthographic heuristic less susceptible to overtraining # TODO: Frequent sentence starters optionally exclude always-capitalised words # FIXME: Problem with ending string with e.g. '!!!' -> '!! !' import re import math from nltk.compat import defaultdict from nltk.probability import FreqDist from api import TokenizerI ###################################################################### #{ Orthographic Context Constants ###################################################################### # The following constants are used to describe the orthographic # contexts in which a word can occur. BEG=beginning, MID=middle, # UNK=unknown, UC=uppercase, LC=lowercase, NC=no case. _ORTHO_BEG_UC = 1 << 1 """Orthogaphic context: beginning of a sentence with upper case.""" _ORTHO_MID_UC = 1 << 2 """Orthogaphic context: middle of a sentence with upper case.""" _ORTHO_UNK_UC = 1 << 3 """Orthogaphic context: unknown position in a sentence with upper case.""" _ORTHO_BEG_LC = 1 << 4 """Orthogaphic context: beginning of a sentence with lower case.""" _ORTHO_MID_LC = 1 << 5 """Orthogaphic context: middle of a sentence with lower case.""" _ORTHO_UNK_LC = 1 << 6 """Orthogaphic context: unknown position in a sentence with lower case.""" _ORTHO_UC = _ORTHO_BEG_UC + _ORTHO_MID_UC + _ORTHO_UNK_UC """Orthogaphic context: occurs with upper case.""" _ORTHO_LC = _ORTHO_BEG_LC + _ORTHO_MID_LC + _ORTHO_UNK_LC """Orthogaphic context: occurs with lower case.""" _ORTHO_MAP = { ('initial', 'upper'): _ORTHO_BEG_UC, ('internal', 'upper'): _ORTHO_MID_UC, ('unknown', 'upper'): _ORTHO_UNK_UC, ('initial', 'lower'): _ORTHO_BEG_LC, ('internal', 'lower'): _ORTHO_MID_LC, ('unknown', 'lower'): _ORTHO_UNK_LC, } """A map from context position and first-letter case to the appropriate orthographic context flag.""" #} (end orthographic context constants) ###################################################################### ###################################################################### #{ Language-dependent variables ###################################################################### class PunktLanguageVars(object): """ Stores variables, mostly regular expressions, which may be language-dependent for correct application of the algorithm. An extension of this class may modify its properties to suit a language other than English; an instance can then be passed as an argument to PunktSentenceTokenizer and PunktTrainer constructors. """ __slots__ = ('_re_period_context', '_re_word_tokenizer') def __getstate__(self): # All modifications to the class are performed by inheritance. # Non-default parameters to be pickled must be defined in the inherited # class. return 1 def __setstate__(self, state): return 1 sent_end_chars = ('.', '?', '!') """Characters which are candidates for sentence boundaries""" @property def _re_sent_end_chars(self): return '[%s]' % re.escape(''.join(self.sent_end_chars)) internal_punctuation = ',:;' # might want to extend this.. """sentence internal punctuation, which indicates an abbreviation if preceded by a period-final token.""" re_boundary_realignment = re.compile(r'["\')\]}]+?(?:\s+|(?=--)|$)', re.MULTILINE) """Used to realign punctuation that should be included in a sentence although it follows the period (or ?, !).""" _re_word_start = r"[^\(\"\`{\[:;&\#\*@\)}\]\-,]" """Excludes some characters from starting word tokens""" _re_non_word_chars = r"(?:[?!)\";}\]\*:@\'\({\[])" """Characters that cannot appear within words""" _re_multi_char_punct = r"(?:\-{2,}|\.{2,}|(?:\.\s){2,}\.)" """Hyphen and ellipsis are multi-character punctuation""" _word_tokenize_fmt = r'''( %(MultiChar)s | (?=%(WordStart)s)\S+? # Accept word characters until end is found (?= # Sequences marking a word's end \s| # White-space $| # End-of-string %(NonWord)s|%(MultiChar)s| # Punctuation ,(?=$|\s|%(NonWord)s|%(MultiChar)s) # Comma if at end of word ) | \S )''' """Format of a regular expression to split punctuation from words, excluding period.""" def _word_tokenizer_re(self): """Compiles and returns a regular expression for word tokenization""" try: return self._re_word_tokenizer except AttributeError: self._re_word_tokenizer = re.compile( self._word_tokenize_fmt % { 'NonWord': self._re_non_word_chars, 'MultiChar': self._re_multi_char_punct, 'WordStart': self._re_word_start, }, re.UNICODE | re.VERBOSE ) return self._re_word_tokenizer def word_tokenize(self, s): """Tokenize a string to split of punctuation other than periods""" return self._word_tokenizer_re().findall(s) _period_context_fmt = r""" \S* # some word material %(SentEndChars)s # a potential sentence ending (?=(?P %(NonWord)s # either other punctuation | \s+(?P\S+) # or whitespace and some other token ))""" """Format of a regular expression to find contexts including possible sentence boundaries. Matches token which the possible sentence boundary ends, and matches the following token within a lookahead expression.""" def period_context_re(self): """Compiles and returns a regular expression to find contexts including possible sentence boundaries.""" try: return self._re_period_context except: self._re_period_context = re.compile( self._period_context_fmt % { 'NonWord': self._re_non_word_chars, 'SentEndChars': self._re_sent_end_chars, }, re.UNICODE | re.VERBOSE) return self._re_period_context _re_non_punct = re.compile(r'[^\W\d]', re.UNICODE) """Matches token types that are not merely punctuation. (Types for numeric tokens are changed to ##number## and hence contain alpha.)""" #} ###################################################################### ###################################################################### #{ Punkt Word Tokenizer ###################################################################### class PunktWordTokenizer(TokenizerI): # Retained for backward compatibility def __init__(self, lang_vars=PunktLanguageVars()): self._lang_vars = lang_vars def tokenize(self, text): return self._lang_vars.word_tokenize(text) #} ###################################################################### #//////////////////////////////////////////////////////////// #{ Helper Functions #//////////////////////////////////////////////////////////// def _pair_iter(it): """ Yields pairs of tokens from the given iterator such that each input token will appear as the first element in a yielded tuple. The last pair will have None as its second element. """ it = iter(it) prev = it.next() for el in it: yield (prev, el) prev = el yield (prev, None) ###################################################################### #{ Punkt Parameters ###################################################################### class PunktParameters(object): """Stores data used to perform sentence boundary detection with punkt.""" def __init__(self): self.abbrev_types = set() """A set of word types for known abbreviations.""" self.collocations = set() """A set of word type tuples for known common collocations where the first word ends in a period. E.g., ('S.', 'Bach') is a common collocation in a text that discusses 'Johann S. Bach'. These count as negative evidence for sentence boundaries.""" self.sent_starters = set() """A set of word types for words that often appear at the beginning of sentences.""" self.ortho_context = defaultdict(int) """A dictionary mapping word types to the set of orthographic contexts that word type appears in. Contexts are represented by adding orthographic context flags: ...""" def clear_abbrevs(self): self.abbrev_types = set() def clear_collocations(self): self.collocations = set() def clear_sent_starters(self): self.sent_starters = set() def clear_ortho_context(self): self.ortho_context = defaultdict(int) def add_ortho_context(self, typ, flag): self.ortho_context[typ] |= flag ###################################################################### #{ PunktToken ###################################################################### class PunktToken(object): """Stores a token of text with annotations produced during sentence boundary detection.""" _properties = [ 'parastart', 'linestart', 'sentbreak', 'abbr', 'ellipsis' ] __slots__ = ['tok', 'type', 'period_final'] + _properties def __init__(self, tok, **params): self.tok = tok self.type = self._get_type(tok) self.period_final = tok.endswith('.') for p in self._properties: setattr(self, p, None) for k, v in params.iteritems(): setattr(self, k, v) #//////////////////////////////////////////////////////////// #{ Regular expressions for properties #//////////////////////////////////////////////////////////// # Note: [A-Za-z] is approximated by [^\W\d] in the general case. _RE_ELLIPSIS = re.compile(r'\.\.+$') _RE_NUMERIC = re.compile(r'^-?[\.,]?\d[\d,\.-]*\.?$') _RE_INITIAL = re.compile(r'[^\W\d]\.$', re.UNICODE) _RE_ALPHA = re.compile(r'[^\W\d]+$', re.UNICODE) #//////////////////////////////////////////////////////////// #{ Derived properties #//////////////////////////////////////////////////////////// def _get_type(self, tok): """Returns a case-normalized representation of the token.""" return self._RE_NUMERIC.sub('##number##', tok.lower()) @property def type_no_period(self): """ The type with its final period removed if it has one. """ if len(self.type) > 1 and self.type[-1] == '.': return self.type[:-1] return self.type @property def type_no_sentperiod(self): """ The type with its final period removed if it is marked as a sentence break. """ if self.sentbreak: return self.type_no_period return self.type @property def first_upper(self): """True if the token's first character is uppercase.""" return self.tok[0].isupper() @property def first_lower(self): """True if the token's first character is lowercase.""" return self.tok[0].islower() @property def first_case(self): if self.first_lower: return 'lower' elif self.first_upper: return 'upper' return 'none' @property def is_ellipsis(self): """True if the token text is that of an ellipsis.""" return self._RE_ELLIPSIS.match(self.tok) @property def is_number(self): """True if the token text is that of a number.""" return self.type.startswith('##number##') @property def is_initial(self): """True if the token text is that of an initial.""" return self._RE_INITIAL.match(self.tok) @property def is_alpha(self): """True if the token text is all alphabetic.""" return self._RE_ALPHA.match(self.tok) @property def is_non_punct(self): """True if the token is either a number or is alphabetic.""" return _re_non_punct.search(self.type) #//////////////////////////////////////////////////////////// #{ String representation #//////////////////////////////////////////////////////////// def __repr__(self): """ A string representation of the token that can reproduce it with eval(), which lists all the token's non-default annotations. """ if self.type != self.tok: typestr = ' type=%s,' % repr(self.type) else: typestr = '' propvals = ', '.join( '%s=%s' % (p, repr(getattr(self, p))) for p in self._properties if getattr(self, p) ) return '%s(%s,%s %s)' % (self.__class__.__name__, repr(self.tok), typestr, propvals) def __str__(self): """ A string representation akin to that used by Kiss and Strunk. """ res = self.tok if self.abbr: res += '
' if self.ellipsis: res += '' if self.sentbreak: res += '' return res ###################################################################### #{ Punkt base class ###################################################################### class _PunktBaseClass(object): """ Includes common components of PunktTrainer and PunktSentenceTokenizer. """ def __init__(self, lang_vars=PunktLanguageVars(), token_cls=PunktToken, params=PunktParameters()): self._params = params self._lang_vars = lang_vars self._Token = token_cls """The collection of parameters that determines the behavior of the punkt tokenizer.""" #//////////////////////////////////////////////////////////// #{ Word tokenization #//////////////////////////////////////////////////////////// def _tokenize_words(self, plaintext): """ Divide the given text into tokens, using the punkt word segmentation regular expression, and generate the resulting list of tokens augmented as three-tuples with two boolean values for whether the given token occurs at the start of a paragraph or a new line, respectively. """ parastart = False for line in plaintext.split('\n'): if line.strip(): line_toks = iter(self._lang_vars.word_tokenize(line)) yield self._Token(line_toks.next(), parastart=parastart, linestart=True) parastart = False for t in line_toks: yield self._Token(t) else: parastart = True #//////////////////////////////////////////////////////////// #{ Annotation Procedures #//////////////////////////////////////////////////////////// def _annotate_first_pass(self, tokens): """ Perform the first pass of annotation, which makes decisions based purely based on the word type of each word: - '?', '!', and '.' are marked as sentence breaks. - sequences of two or more periods are marked as ellipsis. - any word ending in '.' that's a known abbreviation is marked as an abbreviation. - any other word ending in '.' is marked as a sentence break. Return these annotations as a tuple of three sets: - sentbreak_toks: The indices of all sentence breaks. - abbrev_toks: The indices of all abbreviations. - ellipsis_toks: The indices of all ellipsis marks. """ for aug_tok in tokens: self._first_pass_annotation(aug_tok) yield aug_tok def _first_pass_annotation(self, aug_tok): """ Performs type-based annotation on a single token. """ tok = aug_tok.tok if tok in self._lang_vars.sent_end_chars: aug_tok.sentbreak = True elif aug_tok.is_ellipsis: aug_tok.ellipsis = True elif aug_tok.period_final and not tok.endswith('..'): if (tok[:-1].lower() in self._params.abbrev_types or tok[:-1].lower().split('-')[-1] in self._params.abbrev_types): aug_tok.abbr = True else: aug_tok.sentbreak = True return ###################################################################### #{ Punkt Trainer ###################################################################### class PunktTrainer(_PunktBaseClass): """Learns parameters used in Punkt sentence boundary detection.""" def __init__(self, train_text=None, verbose=False, lang_vars=PunktLanguageVars(), token_cls=PunktToken): _PunktBaseClass.__init__(self, lang_vars=lang_vars, token_cls=token_cls) self._type_fdist = FreqDist() """A frequency distribution giving the frequency of each case-normalized token type in the training data.""" self._num_period_toks = 0 """The number of words ending in period in the training data.""" self._collocation_fdist = FreqDist() """A frequency distribution giving the frequency of all bigrams in the training data where the first word ends in a period. Bigrams are encoded as tuples of word types. Especially common collocations are extracted from this frequency distribution, and stored in L{_params}.L{collocations }.""" self._sent_starter_fdist = FreqDist() """A frequency distribution giving the frequency of all words that occur at the training data at the beginning of a sentence (after the first pass of annotation). Especially common sentence starters are extracted from this frequency distribution, and stored in L{_params}.L{sent_starters }. """ self._sentbreak_count = 0 """The total number of sentence breaks identified in training, used for calculating the frequent sentence starter heuristic.""" self._finalized = True """A flag as to whether the training has been finalized by finding collocations and sentence starters, or whether finalize_training() still needs to be called.""" if train_text: self.train(train_text, verbose, finalize=True) def get_params(self): """ Calculates and returns parameters for sentence boundary detection as derived from training.""" if not self._finalized: self.finalize_training() return self._params #//////////////////////////////////////////////////////////// #{ Customization Variables #//////////////////////////////////////////////////////////// ABBREV = 0.3 """cut-off value whether a 'token' is an abbreviation""" IGNORE_ABBREV_PENALTY = False """allows the disabling of the abbreviation penalty heuristic, which exponentially disadvantages words that are found at times without a final period.""" ABBREV_BACKOFF = 5 """upper cut-off for Mikheev's(2002) abbreviation detection algorithm""" COLLOCATION = 7.88 """minimal log-likelihood value that two tokens need to be considered as a collocation""" SENT_STARTER = 30 """minimal log-likelihood value that a token requires to be considered as a frequent sentence starter""" INCLUDE_ALL_COLLOCS = False """this includes as potential collocations all word pairs where the first word ends in a period. It may be useful in corpora where there is a lot of variation that makes abbreviations like Mr difficult to identify.""" INCLUDE_ABBREV_COLLOCS = False """this includes as potential collocations all word pairs where the first word is an abbreviation. Such collocations override the orthographic heuristic, but not the sentence starter heuristic. This is overridden by INCLUDE_ALL_COLLOCS, and if both are false, only collocations with initials and ordinals are considered.""" """""" MIN_COLLOC_FREQ = 1 """this sets a minimum bound on the number of times a bigram needs to appear before it can be considered a collocation, in addition to log likelihood statistics. This is useful when INCLUDE_ALL_COLLOCS is True.""" #//////////////////////////////////////////////////////////// #{ Training.. #//////////////////////////////////////////////////////////// def train(self, text, verbose=False, finalize=True): """ Collects training data from a given text. If finalize is True, it will determine all the parameters for sentence boundary detection. If not, this will be delayed until get_params() or finalize_training() is called. If verbose is True, abbreviations found will be listed. """ # Break the text into tokens; record which token indices correspond to # line starts and paragraph starts; and determine their types. self._train_tokens(self._tokenize_words(text), verbose) if finalize: self.finalize_training(verbose) def train_tokens(self, tokens, verbose=False, finalize=True): """ Collects training data from a given list of tokens. """ self._train_tokens((self._Token(t) for t in tokens), verbose) if finalize: self.finalize_training(verbose) def _train_tokens(self, tokens, verbose): self._finalized = False # Ensure tokens are a list tokens = list(tokens) # Find the frequency of each case-normalized type. (Don't # strip off final periods.) Also keep track of the number of # tokens that end in periods. for aug_tok in tokens: self._type_fdist.inc(aug_tok.type) if aug_tok.period_final: self._num_period_toks += 1 # Look for new abbreviations, and for types that no longer are unique_types = self._unique_types(tokens) for abbr, score, is_add in self._reclassify_abbrev_types(unique_types): if score >= self.ABBREV: if is_add: self._params.abbrev_types.add(abbr) if verbose: print (' Abbreviation: [%6.4f] %s' % (score, abbr)) else: if not is_add: self._params.abbrev_types.remove(abbr) if verbose: print (' Removed abbreviation: [%6.4f] %s' % (score, abbr)) # Make a preliminary pass through the document, marking likely # sentence breaks, abbreviations, and ellipsis tokens. tokens = list(self._annotate_first_pass(tokens)) # Check what contexts each word type can appear in, given the # case of its first letter. self._get_orthography_data(tokens) # We need total number of sentence breaks to find sentence starters self._sentbreak_count += self._get_sentbreak_count(tokens) # The remaining heuristics relate to pairs of tokens where the first # ends in a period. for aug_tok1, aug_tok2 in _pair_iter(tokens): if not aug_tok1.period_final or not aug_tok2: continue # Is the first token a rare abbreviation? if self._is_rare_abbrev_type(aug_tok1, aug_tok2): self._params.abbrev_types.add(aug_tok1.type_no_period) if verbose: print (' Rare Abbrev: %s' % aug_tok1.type) # Does second token have a high likelihood of starting a sentence? if self._is_potential_sent_starter(aug_tok2, aug_tok1): self._sent_starter_fdist.inc(aug_tok2.type) # Is this bigram a potential collocation? if self._is_potential_collocation(aug_tok1, aug_tok2): self._collocation_fdist.inc( (aug_tok1.type_no_period, aug_tok2.type_no_sentperiod)) def _unique_types(self, tokens): return set(aug_tok.type for aug_tok in tokens) def finalize_training(self, verbose=False): """ Uses data that has been gathered in training to determine likely collocations and sentence starters. """ self._params.clear_sent_starters() for typ, ll in self._find_sent_starters(): self._params.sent_starters.add(typ) if verbose: print (' Sent Starter: [%6.4f] %r' % (ll, typ)) self._params.clear_collocations() for (typ1, typ2), ll in self._find_collocations(): self._params.collocations.add( (typ1,typ2) ) if verbose: print (' Collocation: [%6.4f] %r+%r' % (ll, typ1, typ2)) self._finalized = True #//////////////////////////////////////////////////////////// #{ Overhead reduction #//////////////////////////////////////////////////////////// def freq_threshold(self, ortho_thresh=2, type_thresh=2, colloc_thres=2, sentstart_thresh=2): """ Allows memory use to be reduced after much training by removing data about rare tokens that are unlikely to have a statistical effect with further training. Entries occurring above the given thresholds will be retained. """ if ortho_thresh > 1: old_oc = self._params.ortho_context self._params.clear_ortho_context() for tok, count in self._type_fdist.iteritems(): if count >= ortho_thresh: self._params.ortho_context[tok] = old_oc[tok] self._type_fdist = self._freq_threshold(self._type_fdist, type_thresh) self._collocation_fdist = self._freq_threshold( self._collocation_fdist, colloc_thres) self._sent_starter_fdist = self._freq_threshold( self._sent_starter_fdist, sentstart_thresh) def _freq_threshold(self, fdist, threshold): """ Returns a FreqDist containing only data with counts below a given threshold, as well as a mapping (None -> count_removed). """ # We assume that there is more data below the threshold than above it # and so create a new FreqDist rather than working in place. res = FreqDist() num_removed = 0 for tok, count in fdist.iteritems(): if count < threshold: num_removed += 1 else: res.inc(tok, count) res.inc(None, num_removed) return res #//////////////////////////////////////////////////////////// #{ Orthographic data #//////////////////////////////////////////////////////////// def _get_orthography_data(self, tokens): """ Collect information about whether each token type occurs with different case patterns (i) overall, (ii) at sentence-initial positions, and (iii) at sentence-internal positions. """ # 'initial' or 'internal' or 'unknown' context = 'internal' tokens = list(tokens) for aug_tok in tokens: # If we encounter a paragraph break, then it's a good sign # that it's a sentence break. But err on the side of # caution (by not positing a sentence break) if we just # saw an abbreviation. if aug_tok.parastart and context != 'unknown': context = 'initial' # If we're at the beginning of a line, then err on the # side of calling our context 'initial'. if aug_tok.linestart and context == 'internal': context = 'unknown' # Find the case-normalized type of the token. If it's a # sentence-final token, strip off the period. typ = aug_tok.type_no_sentperiod # Update the orthographic context table. flag = _ORTHO_MAP.get((context, aug_tok.first_case), 0) if flag: self._params.add_ortho_context(typ, flag) # Decide whether the next word is at a sentence boundary. if aug_tok.sentbreak: if not (aug_tok.is_number or aug_tok.is_initial): context = 'initial' else: context = 'unknown' elif aug_tok.ellipsis or aug_tok.abbr: context = 'unknown' else: context = 'internal' #//////////////////////////////////////////////////////////// #{ Abbreviations #//////////////////////////////////////////////////////////// def _reclassify_abbrev_types(self, types): """ (Re)classifies each given token if - it is period-final and not a known abbreviation; or - it is not period-final and is otherwise a known abbreviation by checking whether its previous classification still holds according to the heuristics of section 3. Yields triples (abbr, score, is_add) where abbr is the type in question, score is its log-likelihood with penalties applied, and is_add specifies whether the present type is a candidate for inclusion or exclusion as an abbreviation, such that: - (is_add and score >= 0.3) suggests a new abbreviation; and - (not is_add and score < 0.3) suggests excluding an abbreviation. """ # (While one could recalculate abbreviations from all .-final tokens at # every iteration, in cases requiring efficiency, the number of tokens # in the present training document will be much less.) for typ in types: # Check some basic conditions, to rule out words that are # clearly not abbrev_types. if not _re_non_punct.search(typ) or typ == '##number##': continue if typ.endswith('.'): if typ in self._params.abbrev_types: continue typ = typ[:-1] is_add = True else: if typ not in self._params.abbrev_types: continue is_add = False # Count how many periods & nonperiods are in the # candidate. num_periods = typ.count('.') + 1 num_nonperiods = len(typ) - num_periods + 1 # Let be the candidate without the period, and # be the period. Find a log likelihood ratio that # indicates whether occurs as a single unit (high # value of ll), or as two independent units and # (low value of ll). count_with_period = self._type_fdist[typ + '.'] count_without_period = self._type_fdist[typ] ll = self._dunning_log_likelihood( count_with_period + count_without_period, self._num_period_toks, count_with_period, self._type_fdist.N()) # Apply three scaling factors to 'tweak' the basic log # likelihood ratio: # F_length: long word -> less likely to be an abbrev # F_periods: more periods -> more likely to be an abbrev # F_penalty: penalize occurances w/o a period f_length = math.exp(-num_nonperiods) f_periods = num_periods f_penalty = (int(self.IGNORE_ABBREV_PENALTY) or math.pow(num_nonperiods, -count_without_period)) score = ll * f_length * f_periods * f_penalty yield typ, score, is_add def find_abbrev_types(self): """ Recalculates abbreviations given type frequencies, despite no prior determination of abbreviations. This fails to include abbreviations otherwise found as "rare". """ self._params.clear_abbrevs() tokens = (typ for typ in self._type_fdist if typ and typ.endswith('.')) for abbr, score, is_add in self._reclassify_abbrev_types(tokens): if score >= self.ABBREV: self._params.abbrev_types.add(abbr) # This function combines the work done by the original code's # functions `count_orthography_context`, `get_orthography_count`, # and `get_rare_abbreviations`. def _is_rare_abbrev_type(self, cur_tok, next_tok): """ A word type is counted as a rare abbreviation if... - it's not already marked as an abbreviation - it occurs fewer than ABBREV_BACKOFF times - either it is followed by a sentence-internal punctuation mark, *or* it is followed by a lower-case word that sometimes appears with upper case, but never occurs with lower case at the beginning of sentences. """ if cur_tok.abbr or not cur_tok.sentbreak: return False # Find the case-normalized type of the token. If it's # a sentence-final token, strip off the period. typ = cur_tok.type_no_sentperiod # Proceed only if the type hasn't been categorized as an # abbreviation already, and is sufficiently rare... count = self._type_fdist[typ] + self._type_fdist[typ[:-1]] if (typ in self._params.abbrev_types or count >= self.ABBREV_BACKOFF): return False # Record this token as an abbreviation if the next # token is a sentence-internal punctuation mark. # [XX] :1 or check the whole thing?? if next_tok.tok[:1] in self._lang_vars.internal_punctuation: return True # Record this type as an abbreviation if the next # token... (i) starts with a lower case letter, # (ii) sometimes occurs with an uppercase letter, # and (iii) never occus with an uppercase letter # sentence-internally. # [xx] should the check for (ii) be modified?? elif next_tok.first_lower: typ2 = next_tok.type_no_sentperiod typ2ortho_context = self._params.ortho_context[typ2] if ( (typ2ortho_context & _ORTHO_BEG_UC) and not (typ2ortho_context & _ORTHO_MID_UC) ): return True #//////////////////////////////////////////////////////////// #{ Log Likelihoods #//////////////////////////////////////////////////////////// # helper for _reclassify_abbrev_types: @staticmethod def _dunning_log_likelihood(count_a, count_b, count_ab, N): """ A function that calculates the modified Dunning log-likelihood ratio scores for abbreviation candidates. The details of how this works is available in the paper. """ p1 = float(count_b) / N p2 = 0.99 null_hypo = (float(count_ab) * math.log(p1) + (count_a - count_ab) * math.log(1.0 - p1)) alt_hypo = (float(count_ab) * math.log(p2) + (count_a - count_ab) * math.log(1.0 - p2)) likelihood = null_hypo - alt_hypo return (-2.0 * likelihood) @staticmethod def _col_log_likelihood(count_a, count_b, count_ab, N): """ A function that will just compute log-likelihood estimate, in the original paper it's decribed in algorithm 6 and 7. This *should* be the original Dunning log-likelihood values, unlike the previous log_l function where it used modified Dunning log-likelihood values """ import math p = 1.0 * count_b / N p1 = 1.0 * count_ab / count_a p2 = 1.0 * (count_b - count_ab) / (N - count_a) summand1 = (count_ab * math.log(p) + (count_a - count_ab) * math.log(1.0 - p)) summand2 = ((count_b - count_ab) * math.log(p) + (N - count_a - count_b + count_ab) * math.log(1.0 - p)) if count_a == count_ab: summand3 = 0 else: summand3 = (count_ab * math.log(p1) + (count_a - count_ab) * math.log(1.0 - p1)) if count_b == count_ab: summand4 = 0 else: summand4 = ((count_b - count_ab) * math.log(p2) + (N - count_a - count_b + count_ab) * math.log(1.0 - p2)) likelihood = summand1 + summand2 - summand3 - summand4 return (-2.0 * likelihood) #//////////////////////////////////////////////////////////// #{ Collocation Finder #//////////////////////////////////////////////////////////// def _is_potential_collocation(self, aug_tok1, aug_tok2): """ Returns True if the pair of tokens may form a collocation given log-likelihood statistics. """ return ((self.INCLUDE_ALL_COLLOCS or (self.INCLUDE_ABBREV_COLLOCS and aug_tok1.abbr) or (aug_tok1.sentbreak and (aug_tok1.is_number or aug_tok1.is_initial))) and aug_tok1.is_non_punct and aug_tok2.is_non_punct) def _find_collocations(self): """ Generates likely collocations and their log-likelihood. """ for types, col_count in self._collocation_fdist.iteritems(): try: typ1, typ2 = types except TypeError: # types may be None after calling freq_threshold() continue if typ2 in self._params.sent_starters: continue typ1_count = self._type_fdist[typ1]+self._type_fdist[typ1+'.'] typ2_count = self._type_fdist[typ2]+self._type_fdist[typ2+'.'] if (typ1_count > 1 and typ2_count > 1 and self.MIN_COLLOC_FREQ < col_count <= min(typ1_count, typ2_count)): ll = self._col_log_likelihood(typ1_count, typ2_count, col_count, self._type_fdist.N()) # Filter out the not-so-collocative if (ll >= self.COLLOCATION and (float(self._type_fdist.N())/typ1_count > float(typ2_count)/col_count)): yield (typ1, typ2), ll #//////////////////////////////////////////////////////////// #{ Sentence-Starter Finder #//////////////////////////////////////////////////////////// def _is_potential_sent_starter(self, cur_tok, prev_tok): """ Returns True given a token and the token that preceds it if it seems clear that the token is beginning a sentence. """ # If a token (i) is preceeded by a sentece break that is # not a potential ordinal number or initial, and (ii) is # alphabetic, then it is a a sentence-starter. return ( prev_tok.sentbreak and not (prev_tok.is_number or prev_tok.is_initial) and cur_tok.is_alpha ) def _find_sent_starters(self): """ Uses collocation heuristics for each candidate token to determine if it frequently starts sentences. """ for (typ, typ_at_break_count) in self._sent_starter_fdist.iteritems(): if not typ: continue typ_count = self._type_fdist[typ]+self._type_fdist[typ+'.'] if typ_count < typ_at_break_count: # needed after freq_threshold continue ll = self._col_log_likelihood(self._sentbreak_count, typ_count, typ_at_break_count, self._type_fdist.N()) if (ll >= self.SENT_STARTER and float(self._type_fdist.N())/self._sentbreak_count > float(typ_count)/typ_at_break_count): yield typ, ll def _get_sentbreak_count(self, tokens): """ Returns the number of sentence breaks marked in a given set of augmented tokens. """ return sum(1 for aug_tok in tokens if aug_tok.sentbreak) ###################################################################### #{ Punkt Sentence Tokenizer ###################################################################### class PunktSentenceTokenizer(_PunktBaseClass,TokenizerI): """ A sentence tokenizer which uses an unsupervised algorithm to build a model for abbreviation words, collocations, and words that start sentences; and then uses that model to find sentence boundaries. This approach has been shown to work well for many European languages. """ def __init__(self, train_text=None, verbose=False, lang_vars=PunktLanguageVars(), token_cls=PunktToken): """ train_text can either be the sole training text for this sentence boundary detector, or can be a PunktParameters object. """ _PunktBaseClass.__init__(self, lang_vars=lang_vars, token_cls=token_cls) if train_text: self._params = self.train(train_text, verbose) def train(self, train_text, verbose=False): """ Derives parameters from a given training text, or uses the parameters given. Repeated calls to this method destroy previous parameters. For incremental training, instantiate a separate PunktTrainer instance. """ if type(train_text) not in (type(''), type(u'')): return train_text return PunktTrainer(train_text, lang_vars=self._lang_vars, token_cls=self._Token).get_params() #//////////////////////////////////////////////////////////// #{ Tokenization #//////////////////////////////////////////////////////////// def tokenize(self, text, realign_boundaries=False): """ Given a text, returns a list of the sentences in that text. """ return list(self.sentences_from_text(text, realign_boundaries)) def span_tokenize(self, text): """ Given a text, returns a list of the (start, end) spans of sentences in the text. """ return [(sl.start, sl.stop) for sl in self._slices_from_text(text)] def sentences_from_text(self, text, realign_boundaries=False): """ Given a text, generates the sentences in that text by only testing candidate sentence breaks. If realign_boundaries is True, includes in the sentence closing punctuation that follows the period. """ sents = [text[sl] for sl in self._slices_from_text(text)] if realign_boundaries: sents = self._realign_boundaries(sents) return sents def _slices_from_text(self, text): last_break = 0 for match in self._lang_vars.period_context_re().finditer(text): context = match.group() + match.group('after_tok') if self.text_contains_sentbreak(context): yield slice(last_break, match.end()) if match.group('next_tok'): # next sentence starts after whitespace last_break = match.start('next_tok') else: # next sentence starts at following punctuation last_break = match.end() yield slice(last_break, len(text)) def _realign_boundaries(self, sents): """ Attempts to realign punctuation that falls after the period but should otherwise be included in the same sentence. For example: "(Sent1.) Sent2." will otherwise be split as:: ["(Sent1.", ") Sent1."]. This method will produce:: ["(Sent1.)", "Sent2."]. """ realign = 0 for s1, s2 in _pair_iter(sents): s1 = s1[realign:] if not s2: if s1: yield s1 continue m = self._lang_vars.re_boundary_realignment.match(s2) if m: yield s1 + m.group(0).strip() realign = m.end() else: realign = 0 if s1: yield s1 def text_contains_sentbreak(self, text): """ Returns True if the given text includes a sentence break. """ found = False # used to ignore last token for t in self._annotate_tokens(self._tokenize_words(text)): if found: return True if t.sentbreak: found = True return False def sentences_from_text_legacy(self, text): """ Given a text, generates the sentences in that text. Annotates all tokens, rather than just those with possible sentence breaks. Should produce the same results as L{sentences_from_text}. """ tokens = self._annotate_tokens(self._tokenize_words(text)) return self._build_sentence_list(text, tokens) def sentences_from_tokens(self, tokens): """ Given a sequence of tokens, generates lists of tokens, each list corresponding to a sentence. """ tokens = iter(self._annotate_tokens(self._Token(t) for t in tokens)) sentence = [] for aug_tok in tokens: sentence.append(aug_tok.tok) if aug_tok.sentbreak: yield sentence sentence = [] if sentence: yield sentence def _annotate_tokens(self, tokens): """ Given a set of tokens augmented with markers for line-start and paragraph-start, returns an iterator through those tokens with full annotation including predicted sentence breaks. """ # Make a preliminary pass through the document, marking likely # sentence breaks, abbreviations, and ellipsis tokens. tokens = self._annotate_first_pass(tokens) # Make a second pass through the document, using token context # information to change our preliminary decisions about where # sentence breaks, abbreviations, and ellipsis occurs. tokens = self._annotate_second_pass(tokens) ## [XX] TESTING #tokens = list(tokens) #self.dump(tokens) return tokens def _build_sentence_list(self, text, tokens): """ Given the original text and the list of augmented word tokens, construct and return a tokenized list of sentence strings. """ # Most of the work here is making sure that we put the right # pieces of whitespace back in all the right places. # Our position in the source text, used to keep track of which # whitespace to add: pos = 0 # A regular expression that finds pieces of whitespace: WS_REGEXP = re.compile(r'\s*') sentence = '' for aug_tok in tokens: tok = aug_tok.tok # Find the whitespace before this token, and update pos. ws = WS_REGEXP.match(text, pos).group() pos += len(ws) # Some of the rules used by the punkt word tokenizer # strip whitespace out of the text, resulting in tokens # that contain whitespace in the source text. If our # token doesn't match, see if adding whitespace helps. # If so, then use the version with whitespace. if text[pos:pos+len(tok)] != tok: pat = '\s*'.join(re.escape(c) for c in tok) m = re.compile(pat).match(text,pos) if m: tok = m.group() # Move our position pointer to the end of the token. assert text[pos:pos+len(tok)] == tok pos += len(tok) # Add this token. If it's not at the beginning of the # sentence, then include any whitespace that separated it # from the previous token. if sentence: sentence += ws + tok else: sentence += tok # If we're at a sentence break, then start a new sentence. if aug_tok.sentbreak: yield sentence sentence = '' # If the last sentence is emtpy, discard it. if sentence: yield sentence # [XX] TESTING def dump(self, tokens): print 'writing to /tmp/punkt.new...' out = open('/tmp/punkt.new', 'w') for aug_tok in tokens: if aug_tok.parastart: out.write('\n\n') elif aug_tok.linestart: out.write('\n') else: out.write(' ') out.write(str(aug_tok)) out.close() #//////////////////////////////////////////////////////////// #{ Customization Variables #//////////////////////////////////////////////////////////// PUNCTUATION = tuple(';:,.!?') #//////////////////////////////////////////////////////////// #{ Annotation Procedures #//////////////////////////////////////////////////////////// def _annotate_second_pass(self, tokens): """ Performs a token-based classification (section 4) over the given tokens, making use of the orthographic heuristic (4.1.1), collocation heuristic (4.1.2) and frequent sentence starter heuristic (4.1.3). """ for t1, t2 in _pair_iter(tokens): self._second_pass_annotation(t1, t2) yield t1 def _second_pass_annotation(self, aug_tok1, aug_tok2): """ Performs token-based classification over a pair of contiguous tokens returning an updated augmented token for the first of them. """ # Is it the last token? We can't do anything then. if not aug_tok2: return tok = aug_tok1.tok if not aug_tok1.period_final: # We only care about words ending in periods. return typ = aug_tok1.type_no_period next_tok = aug_tok2.tok next_typ = aug_tok2.type_no_sentperiod tok_is_initial = aug_tok1.is_initial # [4.1.2. Collocational Heuristic] If there's a # collocation between the word before and after the # period, then label tok as an abbreviation and NOT # a sentence break. Note that collocations with # frequent sentence starters as their second word are # excluded in training. if (typ, next_typ) in self._params.collocations: aug_tok1.sentbreak = False aug_tok1.abbr = True return # [4.2. Token-Based Reclassification of Abbreviations] If # the token is an abbreviation or an ellipsis, then decide # whether we should *also* classify it as a sentbreak. if ( (aug_tok1.abbr or aug_tok1.ellipsis) and (not tok_is_initial) ): # [4.1.1. Orthographic Heuristic] Check if there's # orthogrpahic evidence about whether the next word # starts a sentence or not. is_sent_starter = self._ortho_heuristic(aug_tok2) if is_sent_starter == True: aug_tok1.sentbreak = True return # [4.1.3. Frequent Sentence Starter Heruistic] If the # next word is capitalized, and is a member of the # frequent-sentence-starters list, then label tok as a # sentence break. if ( aug_tok2.first_upper and next_typ in self._params.sent_starters): aug_tok1.sentbreak = True return # [4.3. Token-Based Detection of Initials and Ordinals] # Check if any initials or ordinals tokens that are marked # as sentbreaks should be reclassified as abbreviations. if tok_is_initial or typ == '##number##': # [4.1.1. Orthographic Heuristic] Check if there's # orthogrpahic evidence about whether the next word # starts a sentence or not. is_sent_starter = self._ortho_heuristic(aug_tok2) if is_sent_starter == False: aug_tok1.sentbreak = False aug_tok1.abbr = True return # Special heuristic for initials: if orthogrpahic # heuristc is unknown, and next word is always # capitalized, then mark as abbrev (eg: J. Bach). if ( is_sent_starter == 'unknown' and tok_is_initial and aug_tok2.first_upper and not (self._params.ortho_context[next_typ] & _ORTHO_LC) ): aug_tok1.sentbreak = False aug_tok1.abbr = True return return def _ortho_heuristic(self, aug_tok): """ Decide whether the given token is the first token in a sentence. """ # Sentences don't start with punctuation marks: if aug_tok.tok in self.PUNCTUATION: return False ortho_context = self._params.ortho_context[aug_tok.type_no_sentperiod] # If the word is capitalized, occurs at least once with a # lower case first letter, and never occurs with an upper case # first letter sentence-internally, then it's a sentence starter. if ( aug_tok.first_upper and (ortho_context & _ORTHO_LC) and not (ortho_context & _ORTHO_MID_UC) ): return True # If the word is lower case, and either (a) we've seen it used # with upper case, or (b) we've never seen it used # sentence-initially with lower case, then it's not a sentence # starter. if ( aug_tok.first_lower and ((ortho_context & _ORTHO_UC) or not (ortho_context & _ORTHO_BEG_LC)) ): return False # Otherwise, we're not sure. return 'unknown' def main(text, tok_cls=PunktSentenceTokenizer, train_cls=PunktTrainer): """Builds a punkt model and applies it to the same text""" cleanup = lambda s: re.compile(r'(?:\r|^\s+)', re.MULTILINE).sub('', s).replace('\n', ' ') trainer = train_cls() trainer.INCLUDE_ALL_COLLOCS = True trainer.train(text) sbd = tok_cls(trainer.get_params()) for l in sbd.sentences_from_text(text, realign_boundaries=True): print cleanup(l) if __name__ == '__main__': import sys main(sys.stdin.read()) nltk-2.0~b9/nltk/tokenize/api.py0000644000175000017500000000412011366164777016520 0ustar bhavanibhavani# Natural Language Toolkit: Tokenizer Interface # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT """ Tokenizer Interface """ from nltk.internals import overridden from util import string_span_tokenize class TokenizerI(object): """ A processing interface for I{tokenizing} a string, or dividing it into a list of substrings. Subclasses must define: - either L{tokenize()} or L{batch_tokenize()} (or both) """ def tokenize(self, s): """ Divide the given string into a list of substrings. @return: C{list} of C{str} """ if overridden(self.batch_tokenize): return self.batch_tokenize([s])[0] else: raise NotImplementedError() def span_tokenize(self, s): """ Identify the tokens using integer offsets (start_i, end_i), where s[start_i:end_i] is the corresponding token. @return: C{iter} of C{tuple} of C{int} """ raise NotImplementedError() def batch_tokenize(self, strings): """ Apply L{self.tokenize()} to each element of C{strings}. I.e.: >>> return [self.tokenize(s) for s in strings] @rtype: C{list} of C{list} of C{str} """ return [self.tokenize(s) for s in strings] def batch_span_tokenize(self, strings): """ Apply L{self.span_tokenize()} to each element of C{strings}. I.e.: >>> return [self.span_tokenize(s) for s in strings] @rtype: C{iter} of C{list} of C{tuple} of C{int} """ for s in strings: yield list(self.span_tokenize(s)) class StringTokenizer(TokenizerI): r""" A tokenizer that divides a string into substrings by splitting on the specified string (defined in subclasses). """ def tokenize(self, s): return s.split(self._string) def span_tokenize(self, s): for span in string_span_tokenize(s, self._string): yield span nltk-2.0~b9/nltk/tokenize/__init__.py0000644000175000017500000000335611332362502017473 0ustar bhavanibhavani# Natural Language Toolkit: Tokenizers # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird (minor additions) # URL: # For license information, see LICENSE.TXT """ Functions for X{tokenizing}, i.e., dividing text strings into substrings. """ from simple import * from regexp import * from punkt import * from sexpr import * from treebank import * import nltk __all__ = ['WhitespaceTokenizer', 'SpaceTokenizer', 'TabTokenizer', 'LineTokenizer', 'RegexpTokenizer', 'BlanklineTokenizer', 'WordPunctTokenizer', 'WordTokenizer', 'blankline_tokenize', 'wordpunct_tokenize', 'regexp_tokenize', 'word_tokenize', 'SExprTokenizer', 'sexpr_tokenize', 'line_tokenize', 'PunktWordTokenizer', 'PunktSentenceTokenizer', 'TreebankWordTokenizer', 'sent_tokenize', 'word_tokenize', ] try: import numpy except ImportError: pass else: from texttiling import * __all__ += ['TextTilingTokenizer'] # Standard sentence tokenizer. def sent_tokenize(text): """ Use NLTK's currently recommended sentence tokenizer to tokenize sentences in the given text. Currently, this uses L{PunktSentenceTokenizer}. """ tokenizer = nltk.data.load('tokenizers/punkt/english.pickle') return tokenizer.tokenize(text) # Standard word tokenizer. _word_tokenize = TreebankWordTokenizer().tokenize def word_tokenize(text): """ Use NLTK's currently recommended word tokenizer to tokenize words in the given sentence. Currently, this uses L{TreebankWordTokenizer}. This tokenizer should be fed a single sentence at a time. """ return _word_tokenize(text) nltk-2.0~b9/nltk/test/wordnet.doctest0000644000175000017500000003616211363770677017607 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ================= WordNet Interface ================= WordNet is accessed just another NLTK corpus reader, and can be imported like this: >>> from nltk.corpus import wordnet For more compact code, we recommend: >>> from nltk.corpus import wordnet as wn ----- Words ----- Look up a word using ``synsets()``; this function has an optional ``pos`` argument which lets you constrain the part of speech of the word: >>> wn.synsets('dog') # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [Synset('dog.n.01'), Synset('frump.n.01'), Synset('dog.n.03'), Synset('cad.n.01'), Synset('frank.n.02'), Synset('pawl.n.01'), Synset('andiron.n.01'), Synset('chase.v.01')] >>> wn.synsets('dog', pos=wn.VERB) [Synset('chase.v.01')] The other parts of speech are ``NOUN``, ``ADJ`` and ``ADV``. A synset is identified with a 3-part name of the form: word.pos.nn: >>> wn.synset('dog.n.01') Synset('dog.n.01') >>> wn.synset('dog.n.01').definition 'a member of the genus Canis (probably descended from the common wolf) that has been domesticated by man since prehistoric times; occurs in many breeds' >>> wn.synset('dog.n.01').examples ['the dog barked all night'] >>> wn.synset('dog.n.01').lemmas [Lemma('dog.n.01.dog'), Lemma('dog.n.01.domestic_dog'), Lemma('dog.n.01.Canis_familiaris')] >>> [lemma.name for lemma in wn.synset('dog.n.01').lemmas] ['dog', 'domestic_dog', 'Canis_familiaris'] >>> wn.lemma('dog.n.01.dog').synset Synset('dog.n.01') ------- Synsets ------- `Synset`: a set of synonyms that share a common meaning. >>> dog = wn.synset('dog.n.01') >>> dog.hypernyms() [Synset('domestic_animal.n.01'), Synset('canine.n.02')] >>> dog.hyponyms() # doctest: +ELLIPSIS [Synset('puppy.n.01'), Synset('great_pyrenees.n.01'), Synset('basenji.n.01'), ...] >>> dog.member_holonyms() [Synset('pack.n.06'), Synset('canis.n.01')] >>> dog.root_hypernyms() [Synset('entity.n.01')] Each synset contains one or more lemmas, which represent a specific sense of a specific word. Note that some relations are defined by WordNet only over Lemmas: >>> good = wn.synset('good.a.01') >>> good.antonyms() Traceback (most recent call last): File "", line 1, in AttributeError: 'Synset' object has no attribute 'antonyms' >>> good.lemmas[0].antonyms() [Lemma('bad.a.01.bad')] The relations that are currently defined in this way are `antonyms`, `derivationally_related_forms` and `pertainyms`. ------ Lemmas ------ >>> eat = wn.lemma('eat.v.03.eat') >>> eat Lemma('feed.v.06.eat') >>> eat.key 'eat%2:34:02::' >>> eat.count() 4 >>> wn.lemma_from_key(eat.key) Lemma('feed.v.06.eat') >>> wn.lemma_from_key(eat.key).synset Synset('feed.v.06') >>> wn.lemma_from_key('feebleminded%5:00:00:retarded:00') Lemma('backward.s.03.feebleminded') >>> for lemma in wn.synset('eat.v.03').lemmas: ... print lemma, lemma.count() ... Lemma('feed.v.06.feed') 3 Lemma('feed.v.06.eat') 4 >>> for lemma in wn.lemmas('eat', 'v'): ... print lemma, lemma.count() ... Lemma('eat.v.01.eat') 61 Lemma('eat.v.02.eat') 13 Lemma('feed.v.06.eat') 4 Lemma('eat.v.04.eat') 0 Lemma('consume.v.05.eat') 0 Lemma('corrode.v.01.eat') 0 Lemmas can also have relations between them: >>> vocal = wn.lemma('vocal.a.01.vocal') >>> vocal.derivationally_related_forms() [Lemma('vocalize.v.02.vocalize')] >>> vocal.pertainyms() [Lemma('voice.n.02.voice')] >>> vocal.antonyms() [Lemma('instrumental.a.01.instrumental')] The three relations above exist only on lemmas, not on synsets. ----------- Verb Frames ----------- >>> wn.synset('think.v.01').frame_ids [5, 9] >>> for lemma in wn.synset('think.v.01').lemmas: ... print lemma, lemma.frame_ids ... print lemma.frame_strings ... Lemma('think.v.01.think') [5, 9] ['Something think something Adjective/Noun', 'Somebody think somebody'] Lemma('think.v.01.believe') [5, 9] ['Something believe something Adjective/Noun', 'Somebody believe somebody'] Lemma('think.v.01.consider') [5, 9] ['Something consider something Adjective/Noun', 'Somebody consider somebody'] Lemma('think.v.01.conceive') [5, 9] ['Something conceive something Adjective/Noun', 'Somebody conceive somebody'] >>> wn.synset('stretch.v.02').frame_ids [8] >>> for lemma in wn.synset('stretch.v.02').lemmas: ... print lemma, lemma.frame_ids ... print lemma.frame_strings ... Lemma('stretch.v.02.stretch') [8, 2] ['Somebody stretch something', 'Somebody stretch'] Lemma('stretch.v.02.extend') [8] ['Somebody extend something'] ---------- Similarity ---------- >>> dog = wn.synset('dog.n.01') >>> cat = wn.synset('cat.n.01') ``synset1.path_similarity(synset2):`` Return a score denoting how similar two word senses are, based on the shortest path that connects the senses in the is-a (hypernym/hypnoym) taxonomy. The score is in the range 0 to 1, except in those cases where a path cannot be found (will only be true for verbs as there are many distinct verb taxonomies), in which case None is returned. A score of 1 represents identity i.e. comparing a sense with itself will return 1. >>> dog.path_similarity(cat) 0.20000000000000001 ``synset1.lch_similarity(synset2):`` Leacock-Chodorow Similarity: Return a score denoting how similar two word senses are, based on the shortest path that connects the senses (as above) and the maximum depth of the taxonomy in which the senses occur. The relationship is given as -log(p/2d) where p is the shortest path length and d the taxonomy depth. >>> dog.lch_similarity(cat) 2.0281482472922856 ``synset1.wup_similarity(synset2):`` Wu-Palmer Similarity: Return a score denoting how similar two word senses are, based on the depth of the two senses in the taxonomy and that of their Least Common Subsumer (most specific ancestor node). Note that at this time the scores given do _not_ always agree with those given by Pedersen's Perl implementation of Wordnet Similarity. The LCS does not necessarily feature in the shortest path connecting the two senses, as it is by definition the common ancestor deepest in the taxonomy, not closest to the two senses. Typically, however, it will so feature. Where multiple candidates for the LCS exist, that whose shortest path to the root node is the longest will be selected. Where the LCS has multiple paths to the root, the longer path is used for the purposes of the calculation. >>> dog.wup_similarity(cat) 0.8571428571428571 ``wordnet_ic`` Information Content: Load an information content file from the wordnet_ic corpus. >>> from nltk.corpus import wordnet_ic >>> brown_ic = wordnet_ic.ic('ic-brown.dat') >>> semcor_ic = wordnet_ic.ic('ic-semcor.dat') Or you can create an information content dictionary from a corpus (or anything that has a words() method). >>> from nltk.corpus import genesis >>> genesis_ic = wn.ic(genesis, False, 0.0) ``synset1.res_similarity(synset2, ic):`` Resnik Similarity: Return a score denoting how similar two word senses are, based on the Information Content (IC) of the Least Common Subsumer (most specific ancestor node). Note that for any similarity measure that uses information content, the result is dependent on the corpus used to generate the information content and the specifics of how the information content was created. >>> dog.res_similarity(cat, brown_ic) 7.9116665090365768 >>> dog.res_similarity(cat, genesis_ic) 7.2040239913748305 ``synset1.jcn_similarity(synset2, ic):`` Jiang-Conrath Similarity Return a score denoting how similar two word senses are, based on the Information Content (IC) of the Least Common Subsumer (most specific ancestor node) and that of the two input Synsets. The relationship is given by the equation 1 / (IC(s1) + IC(s2) - 2 * IC(lcs)). >>> dog.jcn_similarity(cat, brown_ic) 0.44977552855167391 >>> dog.jcn_similarity(cat, genesis_ic) 0.28539390848096963 ``synset1.lin_similarity(synset2, ic):`` Lin Similarity: Return a score denoting how similar two word senses are, based on the Information Content (IC) of the Least Common Subsumer (most specific ancestor node) and that of the two input Synsets. The relationship is given by the equation 2 * IC(lcs) / (IC(s1) + IC(s2)). >>> dog.lin_similarity(cat, semcor_ic) 0.88632886280862277 --------------------- Access to all Synsets --------------------- Iterate over all the noun synsets: >>> for synset in list(wn.all_synsets('n'))[:10]: ... print synset ... Synset('entity.n.01') Synset('physical_entity.n.01') Synset('abstraction.n.06') Synset('thing.n.12') Synset('object.n.01') Synset('whole.n.02') Synset('congener.n.03') Synset('living_thing.n.01') Synset('organism.n.01') Synset('benthos.n.02') Get all synsets for this word, possibly restricted by POS: >>> wn.synsets('dog') # doctest: +ELLIPSIS [Synset('dog.n.01'), Synset('frump.n.01'), Synset('dog.n.03'), Synset('cad.n.01'), ...] >>> wn.synsets('dog', pos='v') [Synset('chase.v.01')] Walk through the noun synsets looking at their hypernyms: >>> from itertools import islice >>> for synset in islice(wn.all_synsets('n'), 5): ... print synset, synset.hypernyms() ... Synset('entity.n.01') [] Synset('physical_entity.n.01') [Synset('entity.n.01')] Synset('abstraction.n.06') [Synset('entity.n.01')] Synset('thing.n.12') [Synset('physical_entity.n.01')] Synset('object.n.01') [Synset('physical_entity.n.01')] ------ Morphy ------ Look up forms not in WordNet, with the help of Morphy: >>> wn.morphy('denied', wn.NOUN) >>> wn.morphy('denied', wn.VERB) 'deny' >>> wn.synsets('denied', wn.NOUN) [] >>> wn.synsets('denied', wn.VERB) # doctest: +NORMALIZE_WHITESPACE [Synset('deny.v.01'), Synset('deny.v.02'), Synset('deny.v.03'), Synset('deny.v.04'), Synset('deny.v.05'), Synset('traverse.v.03'), Synset('deny.v.07')] Morphy uses a combination of inflectional ending rules and exception lists to handle a variety of different possibilities: >>> wn.morphy('dogs') 'dog' >>> wn.morphy('churches') 'church' >>> wn.morphy('aardwolves') 'aardwolf' >>> wn.morphy('abaci') 'abacus' >>> wn.morphy('book', wn.NOUN) 'book' >>> wn.morphy('hardrock', wn.ADV) >>> wn.morphy('book', wn.ADJ) >>> wn.morphy('his', wn.NOUN) >>> --------------- Synset Closures --------------- Compute transitive closures of synsets >>> dog = wn.synset('dog.n.01') >>> hypo = lambda s: s.hyponyms() >>> hyper = lambda s: s.hypernyms() >>> list(dog.closure(hypo, depth=1)) == dog.hyponyms() True >>> list(dog.closure(hyper, depth=1)) == dog.hypernyms() True >>> list(dog.closure(hypo)) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [Synset('puppy.n.01'), Synset('great_pyrenees.n.01'), Synset('basenji.n.01'), Synset('newfoundland.n.01'), Synset('lapdog.n.01'), Synset('poodle.n.01'), Synset('leonberg.n.01'), Synset('toy_dog.n.01'), Synset('spitz.n.01'), ...] >>> list(dog.closure(hyper)) # doctest: +NORMALIZE_WHITESPACE [Synset('domestic_animal.n.01'), Synset('canine.n.02'), Synset('animal.n.01'), Synset('carnivore.n.01'), Synset('organism.n.01'), Synset('placental.n.01'), Synset('living_thing.n.01'), Synset('mammal.n.01'), Synset('whole.n.02'), Synset('vertebrate.n.01'), Synset('object.n.01'), Synset('chordate.n.01'), Synset('physical_entity.n.01'), Synset('entity.n.01')] ---------------- Regression Tests ---------------- Bug 85: morphy returns the base form of a word, if it's input is given as a base form for a POS for which that word is not defined: >>> wn.synsets('book', wn.NOUN) [Synset('book.n.01'), Synset('book.n.02'), Synset('record.n.05'), Synset('script.n.01'), Synset('ledger.n.01'), Synset('book.n.06'), Synset('book.n.07'), Synset('koran.n.01'), Synset('bible.n.01'), Synset('book.n.10'), Synset('book.n.11')] >>> wn.synsets('book', wn.ADJ) [] >>> wn.morphy('book', wn.NOUN) 'book' >>> wn.morphy('book', wn.ADJ) Bug 160: wup_similarity breaks when the two synsets have no common hypernym >>> t = wn.synsets('picasso')[0] >>> m = wn.synsets('male')[1] >>> t.wup_similarity(m) 0.63157894736842102 >>> t = wn.synsets('titan')[1] >>> s = wn.synsets('say', wn.VERB)[0] >>> print t.wup_similarity(s) None Bug 21: "instance of" not included in LCS (very similar to bug 160) >>> a = wn.synsets("writings")[0] >>> b = wn.synsets("scripture")[0] >>> brown_ic = wordnet_ic.ic('ic-brown.dat') >>> a.jcn_similarity(b, brown_ic) 0.17546021191621833 Bug 221: Verb root IC is zero >>> from nltk.corpus.reader.wordnet import information_content >>> s = wn.synsets('say', wn.VERB)[0] >>> information_content(s, brown_ic) 4.6237121100177792 Bug 161: Comparison between WN keys/lemmas should not be case sensitive >>> k = wn.synsets("jefferson")[0].lemmas[0].key >>> wn.lemma_from_key(k) Lemma('jefferson.n.01.Jefferson') >>> wn.lemma_from_key(k.upper()) Lemma('jefferson.n.01.Jefferson') Bug 99: WordNet root_hypernyms gives incorrect results >>> from nltk.corpus import wordnet as wn >>> for s in wn.all_synsets(wn.NOUN): ... if s.root_hypernyms()[0] != wn.synset('entity.n.01'): ... print s, s.root_hypernyms() ... >>> Bug 382: JCN Division by zero error >>> tow = wn.synset('tow.v.01') >>> shlep = wn.synset('shlep.v.02') >>> from nltk.corpus import wordnet_ic >>> brown_ic = wordnet_ic.ic('ic-brown.dat') >>> tow.jcn_similarity(shlep, brown_ic) 1.0000000000000001e+300 Bug 428: Depth is zero for instance nouns >>> s = wn.synset("lincoln.n.01") >>> s.max_depth() > 0 True Bug 429: Information content smoothing used old reference to all_synsets >>> genesis_ic = wn.ic(genesis, True, 1.0) Bug 430: all_synsets used wrong pos lookup when synsets were cached >>> for ii in wn.all_synsets(): pass >>> for ii in wn.all_synsets(): pass Bug 470: shortest_path_distance ignored instance hypernyms >>> google = wordnet.synsets("google")[0] >>> earth = wordnet.synsets("earth")[0] >>> google.wup_similarity(earth) 0.10000000000000001 Bug 484: similarity metrics returned -1 instead of None for no LCS >>> t = wn.synsets('fly', wn.VERB)[0] >>> s = wn.synsets('say', wn.VERB)[0] >>> print s.shortest_path_distance(t) None >>> print s.path_similarity(t) None >>> print s.lch_similarity(t) None >>> print s.wup_similarity(t) None Bug 427: "pants" does not return all the senses it should >>> from nltk.corpus import wordnet >>> wordnet.synsets("pants",'n') [Synset('bloomers.n.01'), Synset('pant.n.01'), Synset('trouser.n.01'), Synset('gasp.n.01')] Bug 482: Some nouns not being lemmatised by WordNetLemmatizer().lemmatize >>> from nltk.stem.wordnet import WordNetLemmatizer >>> WordNetLemmatizer().lemmatize("eggs", pos="n") 'egg' >>> WordNetLemmatizer().lemmatize("legs", pos="n") 'leg' nltk-2.0~b9/nltk/test/util.doctest0000644000175000017500000000561711374105242017061 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ================= Utility functions ================= >>> from nltk.util import * >>> from nltk.tree import Tree >>> print_string("This is a long string, therefore it should break", 25) This is a long string, therefore it should break >>> re_show("[a-z]+", "sdf123") {sdf}123 >>> tree = Tree(5, ... [Tree(4, [Tree(2, [1, 3])]), ... Tree(8, [Tree(6, [7]), 9])]) >>> lst = [x for x in breadth_first(tree)] >>> for l in lst: ... if type(l) == int: print l ... else: print l.node 5 4 8 2 6 9 1 3 7 >>> invert_dict({1: 2}) defaultdict(, {2: 1}) >>> invert_dict({1: [3, 4, 5]}) defaultdict(, {3: [1], 4: [1], 5: [1]}) Testing HTML cleaning --------------------- >>> html = """ ...
...
... ...
... ... ... """ >>> [link.strip() for link in re.split("\n+", clean_html(html))] ['Skip Links', 'AOL', 'My AOL', 'Mail', '', '', 'Get The All-Amer... Ringtones'] >>> clean_html("

Heading

Test

") 'Heading Test' >>> clean_html(" aaa

bbb ") 'aaa bbb' nltk-2.0~b9/nltk/test/treetransforms.doctest0000644000175000017500000001132011331670012021141 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ------------------------------------------- Unit tests for the TreeTransformation class ------------------------------------------- >>> from copy import deepcopy >>> from nltk.tree import * >>> from nltk.treetransforms import * >>> sentence = "(TOP (S (S (VP (VBN Turned) (ADVP (RB loose)) (PP (IN in) (NP (NP (NNP Shane) (NNP Longman) (POS 's)) (NN trading) (NN room))))) (, ,) (NP (DT the) (NN yuppie) (NNS dealers)) (VP (AUX do) (NP (NP (RB little)) (ADJP (RB right)))) (. .)))" >>> tree = bracket_parse(sentence) >>> print tree (TOP (S (S (VP (VBN Turned) (ADVP (RB loose)) (PP (IN in) (NP (NP (NNP Shane) (NNP Longman) (POS 's)) (NN trading) (NN room))))) (, ,) (NP (DT the) (NN yuppie) (NNS dealers)) (VP (AUX do) (NP (NP (RB little)) (ADJP (RB right)))) (. .))) Make a copy of the original tree and collapse the subtrees with only one child >>> collapsedTree = deepcopy(tree) >>> collapse_unary(collapsedTree) >>> print collapsedTree (TOP (S (S+VP (VBN Turned) (ADVP (RB loose)) (PP (IN in) (NP (NP (NNP Shane) (NNP Longman) (POS 's)) (NN trading) (NN room)))) (, ,) (NP (DT the) (NN yuppie) (NNS dealers)) (VP (AUX do) (NP (NP (RB little)) (ADJP (RB right)))) (. .))) >>> collapsedTree2 = deepcopy(tree) >>> collapse_unary(collapsedTree2, collapsePOS=True, collapseRoot=True) >>> print collapsedTree2 (TOP+S (S+VP (VBN Turned) (ADVP+RB loose) (PP (IN in) (NP (NP (NNP Shane) (NNP Longman) (POS 's)) (NN trading) (NN room)))) (, ,) (NP (DT the) (NN yuppie) (NNS dealers)) (VP (AUX do) (NP (NP+RB little) (ADJP+RB right))) (. .)) Convert the tree to Chomsky Normal Form i.e. each subtree has either two subtree children or a single leaf value. This conversion can be performed using either left- or right-factoring. >>> cnfTree = deepcopy(collapsedTree) >>> chomsky_normal_form(cnfTree, factor='left') >>> print cnfTree (TOP (S (S| (S| (S| (S+VP (S+VP| (VBN Turned) (ADVP (RB loose))) (PP (IN in) (NP (NP| (NP (NP| (NNP Shane) (NNP Longman)) (POS 's)) (NN trading)) (NN room)))) (, ,)) (NP (NP| (DT the) (NN yuppie)) (NNS dealers))) (VP (AUX do) (NP (NP (RB little)) (ADJP (RB right))))) (. .))) >>> cnfTree = deepcopy(collapsedTree) >>> chomsky_normal_form(cnfTree, factor='right') >>> print cnfTree (TOP (S (S+VP (VBN Turned) (S+VP| (ADVP (RB loose)) (PP (IN in) (NP (NP (NNP Shane) (NP| (NNP Longman) (POS 's))) (NP| (NN trading) (NN room)))))) (S|<,-NP-VP-.> (, ,) (S| (NP (DT the) (NP| (NN yuppie) (NNS dealers))) (S| (VP (AUX do) (NP (NP (RB little)) (ADJP (RB right)))) (. .)))))) Employ some Markov smoothing to make the artificial node labels a bit more readable. See the treetransforms.py documentation for more details. >>> markovTree = deepcopy(collapsedTree) >>> chomsky_normal_form(markovTree, horzMarkov=2, vertMarkov=1) >>> print markovTree (TOP (S^ (S+VP^ (VBN Turned) (S+VP|^ (ADVP^ (RB loose)) (PP^ (IN in) (NP^ (NP^ (NNP Shane) (NP|^ (NNP Longman) (POS 's))) (NP|^ (NN trading) (NN room)))))) (S|<,-NP>^ (, ,) (S|^ (NP^ (DT the) (NP|^ (NN yuppie) (NNS dealers))) (S|^ (VP^ (AUX do) (NP^ (NP^ (RB little)) (ADJP^ (RB right)))) (. .)))))) Convert the transformed tree back to its original form >>> un_chomsky_normal_form(markovTree) >>> tree == markovTree True nltk-2.0~b9/nltk/test/tree.doctest0000644000175000017500000011672311331670025017043 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT =============================== Unit tests for nltk.tree.Tree =============================== >>> from nltk.tree import * >>> print Tree(1, [2, 3, 4]) (1 2 3 4) >>> print Tree('S', [Tree('NP', ['I']), ... Tree('VP', [Tree('V', ['saw']), ... Tree('NP', ['him'])])]) (S (NP I) (VP (V saw) (NP him))) One exception to "any iterable": in order to avoid confusion, strings are *not* accepted as children lists: >>> print Tree('NP', 'Bob') Traceback (most recent call last): . . . TypeError: Tree() argument 2 should be a list, not a string A single level can contain both leaves and subtrees: >>> print Tree(1, [2, Tree(3, [4]), 5]) (1 2 (3 4) 5) Some trees to run tests on: >>> dp1 = Tree('dp', [Tree('d', ['the']), Tree('np', ['dog'])]) >>> dp2 = Tree('dp', [Tree('d', ['the']), Tree('np', ['cat'])]) >>> vp = Tree('vp', [Tree('v', ['chased']), dp2]) >>> tree = Tree('s', [dp1, vp]) >>> print tree (s (dp (d the) (np dog)) (vp (v chased) (dp (d the) (np cat)))) The node value is stored using the `node` attribute: >>> dp1.node, dp2.node, vp.node, tree.node ('dp', 'dp', 'vp', 's') This attribute can be modified directly: >>> dp1.node = 'np' >>> dp2.node = 'np' >>> print tree (s (np (d the) (np dog)) (vp (v chased) (np (d the) (np cat)))) Children can be accessed with indexing, just as with normal lists: >>> print tree[0] (np (d the) (np dog)) >>> print tree[1][1] (np (d the) (np cat)) Children can be modified directly, as well: >>> tree[0], tree[1][1] = tree[1][1], tree[0] >>> print tree (s (np (d the) (np cat)) (vp (v chased) (np (d the) (np dog)))) The `Tree` class adds a new method of indexing, using tuples rather than ints. ``t[a,b,c]`` is equivalant to ``t[a][b][c]``. The sequence ``(a,b,c)`` is called a "tree path". >>> print tree[1,1][0] (d the) >>> # Switch the cat & dog back the way they were. >>> tree[1,1], tree[0] = tree[0], tree[1,1] >>> print tree (s (np (d the) (np dog)) (vp (v chased) (np (d the) (np cat)))) >>> path = (1,1,1,0) >>> print tree[path] cat The length of a tree is the number of children it has. >>> len(tree), len(dp1), len(dp2), len(dp1[0]) (2, 2, 2, 1) >>> len(Tree('x', [])) 0 The `leaves` method returns a list of a tree's leaves: >>> print tree.leaves() ['the', 'dog', 'chased', 'the', 'cat'] The `height` method returns the height of the tree. A tree with no children is considered to have a height of 1; a tree with only children is considered to have a height of 2; and any other tree's height is one plus the maximum of its children's heights: >>> print tree.height() 5 >>> print tree[1,1,1].height() 2 >>> print tree[0].height() 3 The `treepositions` method returns a list of the tree positions of subtrees and leaves in a tree. By default, it gives the position of every tree, subtree, and leaf, in prefix order: >>> print tree.treepositions() [(), (0,), (0, 0), (0, 0, 0), (0, 1), (0, 1, 0), (1,), (1, 0), (1, 0, 0), (1, 1), (1, 1, 0), (1, 1, 0, 0), (1, 1, 1), (1, 1, 1, 0)] The order can also be specified explicitly. Four orders are currently supported: # Prefix order >>> print tree.treepositions('preorder') [(), (0,), (0, 0), (0, 0, 0), (0, 1), (0, 1, 0), (1,), (1, 0), (1, 0, 0), (1, 1), (1, 1, 0), (1, 1, 0, 0), (1, 1, 1), (1, 1, 1, 0)] # Postfix order >>> print tree.treepositions('postorder') [(0, 0, 0), (0, 0), (0, 1, 0), (0, 1), (0,), (1, 0, 0), (1, 0), (1, 1, 0, 0), (1, 1, 0), (1, 1, 1, 0), (1, 1, 1), (1, 1), (1,), ()] # Both prefix & postfix order (subtrees listed twice, leaves once) >>> print tree.treepositions('bothorder') [(), (0,), (0, 0), (0, 0, 0), (0, 0), (0, 1), (0, 1, 0), (0, 1), (0,), (1,), (1, 0), (1, 0, 0), (1, 0), (1, 1), (1, 1, 0), (1, 1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 1, 1, 0), (1, 1, 1), (1, 1), (1,), ()] # Leaves only (in order) >>> print tree.treepositions('leaves') [(0, 0, 0), (0, 1, 0), (1, 0, 0), (1, 1, 0, 0), (1, 1, 1, 0)] `treepositions` can be useful for modifying a tree. For example, we could upper-case all leaves with: >>> for pos in tree.treepositions('leaves'): ... tree[pos] = tree[pos].upper() >>> print tree (s (np (d THE) (np DOG)) (vp (v CHASED) (np (d THE) (np CAT)))) In addition to `str` and `repr`, several methods exist to convert a tree object to one of several standard tree encodings: >>> print tree.pprint_latex_qtree() \Tree [.s [.np [.d THE ] [.np DOG ] ] [.vp [.v CHASED ] [.np [.d THE ] [.np CAT ] ] ] ] Trees can be parsed from treebank strings with the static `Tree.parse()` method: >>> tree2 = Tree.parse('(S (NP I) (VP (V enjoyed) (NP my cookie)))') >>> print tree2 (S (NP I) (VP (V enjoyed) (NP my cookie))) If the `Tree` constructor is called with a single string argument, then it simply delegates to `Tree.parse()`. >>> print Tree('(S (NP I) (VP (V enjoyed) (NP my cookie)))') (S (NP I) (VP (V enjoyed) (NP my cookie))) Trees can be compared for equality: >>> tree == bracket_parse(str(tree)) True >>> tree2 == bracket_parse(str(tree2)) True >>> tree == tree2 False >>> tree == bracket_parse(str(tree2)) False >>> tree2 == bracket_parse(str(tree)) False >>> tree != bracket_parse(str(tree)) False >>> tree2 != bracket_parse(str(tree2)) False >>> tree != tree2 True >>> tree != bracket_parse(str(tree2)) True >>> tree2 != bracket_parse(str(tree)) True >>> tree < tree2 or tree > tree2 True Tree Parsing ============ The class method `Tree.parse()` can be used to parse trees: >>> tree = Tree.parse('(S (NP I) (VP (V enjoyed) (NP my cookie)))') >>> print tree (S (NP I) (VP (V enjoyed) (NP my cookie))) When called on a subclass of `Tree`, it will create trees of that type: >>> tree = ImmutableTree.parse('(VP (V enjoyed) (NP my cookie))') >>> print tree (VP (V enjoyed) (NP my cookie)) >>> print type(tree) The ``brackets`` parameter can be used to specify two characters that should be used as brackets: >>> print Tree.parse('[S [NP I] [VP [V enjoyed] [NP my cookie]]]', ... brackets='[]') (S (NP I) (VP (V enjoyed) (NP my cookie))) >>> print Tree.parse(' >>', ... brackets='<>') (S (NP I) (VP (V enjoyed) (NP my cookie))) If ``brackets`` is not a string, or is not exactly two characters, then `Tree.parse` raises an exception: >>> Tree.parse(' >', brackets='') Traceback (most recent call last): . . . TypeError: brackets must be a length-2 string >>> Tree.parse(' >', brackets='<<>>') Traceback (most recent call last): . . . TypeError: brackets must be a length-2 string >>> Tree.parse(' >', brackets=12) Traceback (most recent call last): . . . TypeError: brackets must be a length-2 string >>> Tree.parse('<>', brackets=('<<','>>')) Traceback (most recent call last): . . . TypeError: brackets must be a length-2 string (We may add support for multi-character brackets in the future, in which case the ``brackets=('<<','>>')`` example would start working.) Whitespace brackets are not permitted: >>> Tree.parse('(NP my cookie\n', brackets='(\n') Traceback (most recent call last): . . . TypeError: whitespace brackets not allowed If an invalid tree is given to Tree.parse, then it raises a ValueError, with a description of the problem: >>> Tree.parse('(NP my cookie) (NP my milk)') Traceback (most recent call last): . . . ValueError: Tree.parse(): expected 'end-of-string' but got '(NP' at index 15. "...y cookie) (NP my mil..." ^ >>> Tree.parse(')NP my cookie(') Traceback (most recent call last): . . . ValueError: Tree.parse(): expected '(' but got ')' at index 0. ")NP my coo..." ^ >>> Tree.parse('(NP my cookie))') Traceback (most recent call last): . . . ValueError: Tree.parse(): expected 'end-of-string' but got ')' at index 14. "...my cookie))" ^ >>> Tree.parse('my cookie)') Traceback (most recent call last): . . . ValueError: Tree.parse(): expected '(' but got 'my' at index 0. "my cookie)" ^ >>> Tree.parse('(NP my cookie') Traceback (most recent call last): . . . ValueError: Tree.parse(): expected ')' but got 'end-of-string' at index 13. "... my cookie" ^ >>> Tree.parse('') Traceback (most recent call last): . . . ValueError: Tree.parse(): expected '(' but got 'end-of-string' at index 0. "" ^ Trees with no children are supported: >>> print Tree.parse('(S)') (S ) >>> print Tree.parse('(X (Y) (Z))') (X (Y ) (Z )) Trees with an empty node and no children are supported: >>> print Tree.parse('()') ( ) >>> print Tree.parse('(X () ())') (X ( ) ( )) Trees with an empty node and children are supported, but only if the first child is not a leaf (otherwise, it will be treated as the node value). >>> print Tree.parse('((A) (B) (C))') ( (A ) (B ) (C )) >>> print Tree.parse('((A) leaf)') ( (A ) leaf) >>> print Tree.parse('(((())))') ( ( ( ( )))) The optional arguments `parse_node` and `parse_leaf` may be used to transform the string values of nodes or leaves. >>> print Tree.parse('(A b (C d e) (F (G h i)))', ... parse_node=lambda s: '<%s>' % s, ... parse_leaf=lambda s: '"%s"' % s) ( "b" ( "d" "e") ( ( "h" "i"))) These transformation functions are typically used when the node or leaf values should be parsed to a non-string value (such as a feature structure). If node values and leaf values need to be able to include whitespace, then you must also use the optional `node_pattern` and `leaf_pattern` arguments. >>> from nltk.featstruct import FeatStruct >>> tree = Tree.parse('([cat=NP] [lex=the] [lex=dog])', ... parse_node=FeatStruct, parse_leaf=FeatStruct) >>> tree.node = tree.node.unify(FeatStruct('[num=singular]')) >>> print tree ([cat='NP', num='singular'] [lex='the'] [lex='dog']) The optional argument ``remove_empty_top_bracketing`` can be used to remove any top-level empty bracketing that occurs. >>> print Tree.parse('((S (NP I) (VP (V enjoyed) (NP my cookie))))', ... remove_empty_top_bracketing=True) (S (NP I) (VP (V enjoyed) (NP my cookie))) It will not remove a top-level empty bracketing with multiple children: >>> print Tree.parse('((A a) (B b))') ( (A a) (B b)) Parented Trees ============== `ParentedTree` is a subclass of `Tree` that automatically maintains parent pointers for single-parented trees. Parented trees can be created directly from a node value and a list of children: >>> ptree = ( ... ParentedTree('VP', [ ... ParentedTree('VERB', ['saw']), ... ParentedTree('NP', [ ... ParentedTree('DET', ['the']), ... ParentedTree('NOUN', ['dog'])])])) >>> print ptree (VP (VERB saw) (NP (DET the) (NOUN dog))) Parented trees can be created from strings using the classmethod `ParentedTree.parse`: >>> ptree = ParentedTree.parse('(VP (VERB saw) (NP (DET the) (NOUN dog)))') >>> print ptree (VP (VERB saw) (NP (DET the) (NOUN dog))) >>> print type(ptree) Parented trees can also be created by using the classmethod `ParentedTree.convert` to convert another type of tree to a parented tree: >>> tree = Tree.parse('(VP (VERB saw) (NP (DET the) (NOUN dog)))') >>> ptree = ParentedTree.convert(tree) >>> print ptree (VP (VERB saw) (NP (DET the) (NOUN dog))) >>> print type(ptree) .. clean-up: >>> del tree `ParentedTree`\ s should never be used in the same tree as `Tree`\ s or `MultiParentedTree`\ s. Mixing tree implementations may result in incorrect parent pointers and in `TypeError` exceptions: >>> # Inserting a Tree in a ParentedTree gives an exception: >>> ParentedTree('NP', [ ... Tree('DET', ['the']), Tree('NOUN', ['dog'])]) Traceback (most recent call last): . . . TypeError: Can not insert a non-ParentedTree into a ParentedTree >>> # inserting a ParentedTree in a Tree gives incorrect parent pointers: >>> broken_tree = Tree('NP', [ ... ParentedTree('DET', ['the']), ParentedTree('NOUN', ['dog'])]) >>> print broken_tree[0].parent None Parented Tree Properties ------------------------ In addition to all the methods defined by the `Tree` class, the `ParentedTree` class adds six new properties whose values are automatically updated whenver a parented tree is modified: `parent`, `parent_index`, `left_sibling`, `right_sibling`, `root`, and `treeposition`. The `parent` property contains a `ParentedTree`\ 's parent, if it has one; and ``None`` otherwise. `ParentedTree`\ s that do not have parents are known as "root trees." >>> for subtree in ptree.subtrees(): ... print subtree ... print ' Parent = %s' % subtree.parent (VP (VERB saw) (NP (DET the) (NOUN dog))) Parent = None (VERB saw) Parent = (VP (VERB saw) (NP (DET the) (NOUN dog))) (NP (DET the) (NOUN dog)) Parent = (VP (VERB saw) (NP (DET the) (NOUN dog))) (DET the) Parent = (NP (DET the) (NOUN dog)) (NOUN dog) Parent = (NP (DET the) (NOUN dog)) The `parent_index` property stores the index of a tree in its parent's child list. If a tree does not have a parent, then its `parent_index` is ``None``. >>> for subtree in ptree.subtrees(): ... print subtree ... print ' Parent Index = %s' % subtree.parent_index ... assert (subtree.parent is None or ... subtree.parent[subtree.parent_index] is subtree) (VP (VERB saw) (NP (DET the) (NOUN dog))) Parent Index = None (VERB saw) Parent Index = 0 (NP (DET the) (NOUN dog)) Parent Index = 1 (DET the) Parent Index = 0 (NOUN dog) Parent Index = 1 Note that ``ptree.parent.index(ptree)`` is *not* equivalent to ``ptree.parent_index``. In particular, ``ptree.parent.index(ptree)`` will return the index of the first child of ``ptree.parent`` that is equal to ``ptree`` (using ``==``); and that child may not be ``ptree``: >>> on_and_on = ParentedTree('CONJP', [ ... ParentedTree('PREP', ['on']), ... ParentedTree('COJN', ['and']), ... ParentedTree('PREP', ['on'])]) >>> second_on = on_and_on[2] >>> print second_on.parent_index 2 >>> print second_on.parent.index(second_on) 0 The properties `left_sibling` and `right_sibling` can be used to get a parented tree's siblings. If a tree does not have a left or right sibling, then the corresponding property's value is ``None``: >>> for subtree in ptree.subtrees(): ... print subtree ... print ' Left Sibling = %s' % subtree.left_sibling ... print ' Right Sibling = %s' % subtree.right_sibling (VP (VERB saw) (NP (DET the) (NOUN dog))) Left Sibling = None Right Sibling = None (VERB saw) Left Sibling = None Right Sibling = (NP (DET the) (NOUN dog)) (NP (DET the) (NOUN dog)) Left Sibling = (VERB saw) Right Sibling = None (DET the) Left Sibling = None Right Sibling = (NOUN dog) (NOUN dog) Left Sibling = (DET the) Right Sibling = None A parented tree's root tree can be accessed using the `root` property. This property follows the tree's parent pointers until it finds a tree without a parent. If a tree does not have a parent, then it is its own root: >>> for subtree in ptree.subtrees(): ... print subtree ... print ' Root = %s' % subtree.root (VP (VERB saw) (NP (DET the) (NOUN dog))) Root = (VP (VERB saw) (NP (DET the) (NOUN dog))) (VERB saw) Root = (VP (VERB saw) (NP (DET the) (NOUN dog))) (NP (DET the) (NOUN dog)) Root = (VP (VERB saw) (NP (DET the) (NOUN dog))) (DET the) Root = (VP (VERB saw) (NP (DET the) (NOUN dog))) (NOUN dog) Root = (VP (VERB saw) (NP (DET the) (NOUN dog))) The `treeposition` property can be used to find a tree's treeposition relative to its root: >>> for subtree in ptree.subtrees(): ... print subtree ... print ' Tree Position = %s' % (subtree.treeposition,) ... assert subtree.root[subtree.treeposition] is subtree (VP (VERB saw) (NP (DET the) (NOUN dog))) Tree Position = () (VERB saw) Tree Position = (0,) (NP (DET the) (NOUN dog)) Tree Position = (1,) (DET the) Tree Position = (1, 0) (NOUN dog) Tree Position = (1, 1) Whenever a parented tree is modified, all of the properties described above (`parent`, `parent_index`, `left_sibling`, `right_sibling`, `root`, and `treeposition`) are automatically updated. For example, if we replace ``ptree``\ 's subtree for the word "dog" with a new subtree for "cat," the properties for both the "dog" subtree and the "cat" subtree get automatically updated: >>> # Replace the dog with a cat >>> dog = ptree[1,1] >>> cat = ParentedTree('NOUN', ['cat']) >>> ptree[1,1] = cat >>> # the noun phrase is no longer the dog's parent: >>> print dog.parent, dog.parent_index, dog.left_sibling None None None >>> # dog is now its own root. >>> print dog.root (NOUN dog) >>> print dog.treeposition () >>> # the cat's parent is now the noun phrase: >>> print cat.parent (NP (DET the) (NOUN cat)) >>> print cat.parent_index 1 >>> print cat.left_sibling (DET the) >>> print cat.root (VP (VERB saw) (NP (DET the) (NOUN cat))) >>> print cat.treeposition (1, 1) ParentedTree Regression Tests ----------------------------- Keep track of all trees that we create (including subtrees) using this variable: >>> all_ptrees = [] Define a helper funciton to create new parented trees: >>> def make_ptree(s): ... ptree = ParentedTree.convert(bracket_parse(s)) ... all_ptrees.extend(t for t in ptree.subtrees() ... if isinstance(t, Tree)) ... return ptree Define a test function that examines every subtree in all_ptrees; and checks that all six of its properties are defined correctly. If any ptrees are passed as arguments, then they are printed. >>> def pcheck(*print_ptrees): ... for ptree in all_ptrees: ... # Check ptree's properties. ... if ptree.parent is not None: ... i = ptree.parent_index ... assert ptree.parent[i] is ptree ... if i > 0: ... assert ptree.left_sibling is ptree.parent[i-1] ... if i < (len(ptree.parent)-1): ... assert ptree.right_sibling is ptree.parent[i+1] ... assert len(ptree.treeposition) > 0 ... assert (ptree.treeposition == ... ptree.parent.treeposition + (ptree.parent_index,)) ... assert ptree.root is not ptree ... assert ptree.root is not None ... assert ptree.root is ptree.parent.root ... assert ptree.root[ptree.treeposition] is ptree ... else: ... assert ptree.parent_index is None ... assert ptree.left_sibling is None ... assert ptree.right_sibling is None ... assert ptree.root is ptree ... assert ptree.treeposition == () ... # Check ptree's children's properties: ... for i, child in enumerate(ptree): ... if isinstance(child, Tree): ... # pcheck parent & parent_index properties ... assert child.parent is ptree ... assert child.parent_index == i ... # pcheck sibling properties ... if i == 0: ... assert child.left_sibling is None ... else: ... assert child.left_sibling is ptree[i-1] ... if i == len(ptree)-1: ... assert child.right_sibling is None ... else: ... assert child.right_sibling is ptree[i+1] ... if print_ptrees: ... print 'ok!', ... for ptree in print_ptrees: print ptree ... else: ... print 'ok!' Run our test function on a variety of newly-created trees: >>> pcheck(make_ptree('(A)')) ok! (A ) >>> pcheck(make_ptree('(A (B (C (D) (E f)) g) h)')) ok! (A (B (C (D ) (E f)) g) h) >>> pcheck(make_ptree('(A (B) (C c) (D d d) (E e e e))')) ok! (A (B ) (C c) (D d d) (E e e e)) >>> pcheck(make_ptree('(A (B) (C (c)) (D (d) (d)) (E (e) (e) (e)))')) ok! (A (B ) (C (c )) (D (d ) (d )) (E (e ) (e ) (e ))) Run our test function after performing various tree-modification operations: **__delitem__()** >>> ptree = make_ptree('(A (B (C (D) (E f) (Q p)) g) h)') >>> e = ptree[0,0,1] >>> del ptree[0,0,1]; pcheck(ptree); pcheck(e) ok! (A (B (C (D ) (Q p)) g) h) ok! (E f) >>> del ptree[0,0,0]; pcheck(ptree) ok! (A (B (C (Q p)) g) h) >>> del ptree[0,1]; pcheck(ptree) ok! (A (B (C (Q p))) h) >>> del ptree[-1]; pcheck(ptree) ok! (A (B (C (Q p)))) >>> del ptree[-100] Traceback (most recent call last): . . . IndexError: index out of range >>> del ptree[()] Traceback (most recent call last): . . . IndexError: The tree position () may not be deleted. >>> # With slices: >>> ptree = make_ptree('(A (B c) (D e) f g (H i) j (K l))') >>> b = ptree[0] >>> del ptree[0:0]; pcheck(ptree) ok! (A (B c) (D e) f g (H i) j (K l)) >>> del ptree[:1]; pcheck(ptree); pcheck(b) ok! (A (D e) f g (H i) j (K l)) ok! (B c) >>> del ptree[-2:]; pcheck(ptree) ok! (A (D e) f g (H i)) >>> del ptree[1:3]; pcheck(ptree) ok! (A (D e) (H i)) >>> ptree = make_ptree('(A (B c) (D e) f g (H i) j (K l))') >>> del ptree[5:1000]; pcheck(ptree) ok! (A (B c) (D e) f g (H i)) >>> del ptree[-2:1000]; pcheck(ptree) ok! (A (B c) (D e) f) >>> del ptree[-100:1]; pcheck(ptree) ok! (A (D e) f) >>> del ptree[1:2:3] Traceback (most recent call last): . . . ValueError: slices with steps are not supported by ParentedTree **__setitem__()** >>> ptree = make_ptree('(A (B (C (D) (E f) (Q p)) g) h)') >>> d, e, q = ptree[0,0] >>> ptree[0,0,0] = 'x'; pcheck(ptree); pcheck(d) ok! (A (B (C x (E f) (Q p)) g) h) ok! (D ) >>> ptree[0,0,1] = make_ptree('(X (Y z))'); pcheck(ptree); pcheck(e) ok! (A (B (C x (X (Y z)) (Q p)) g) h) ok! (E f) >>> ptree[1] = d; pcheck(ptree) ok! (A (B (C x (X (Y z)) (Q p)) g) (D )) >>> ptree[-1] = 'x'; pcheck(ptree) ok! (A (B (C x (X (Y z)) (Q p)) g) x) >>> ptree[-100] = 'y' Traceback (most recent call last): . . . IndexError: index out of range >>> ptree[()] = make_ptree('(X y)') Traceback (most recent call last): . . . IndexError: The tree position () may not be assigned to. >>> # With slices: >>> ptree = make_ptree('(A (B c) (D e) f g (H i) j (K l))') >>> b = ptree[0] >>> ptree[0:0] = ('x', make_ptree('(Y)')); pcheck(ptree) ok! (A x (Y ) (B c) (D e) f g (H i) j (K l)) >>> ptree[2:6] = (); pcheck(ptree); pcheck(b) ok! (A x (Y ) (H i) j (K l)) ok! (B c) >>> ptree[-2:] = ('z', 'p'); pcheck(ptree) ok! (A x (Y ) (H i) z p) >>> ptree[1:3] = [make_ptree('(X)') for x in range(10)]; pcheck(ptree) ok! (A x (X ) (X ) (X ) (X ) (X ) (X ) (X ) (X ) (X ) (X ) z p) >>> ptree[5:1000] = []; pcheck(ptree) ok! (A x (X ) (X ) (X ) (X )) >>> ptree[-2:1000] = ['n']; pcheck(ptree) ok! (A x (X ) (X ) n) >>> ptree[-100:1] = [make_ptree('(U v)')]; pcheck(ptree) ok! (A (U v) (X ) (X ) n) >>> ptree[-1:] = (make_ptree('(X)') for x in range(3)); pcheck(ptree) ok! (A (U v) (X ) (X ) (X ) (X ) (X )) >>> ptree[1:2:3] = ['x'] Traceback (most recent call last): . . . ValueError: slices with steps are not supported by ParentedTree **append()** >>> ptree = make_ptree('(A (B (C (D) (E f) (Q p)) g) h)') >>> ptree.append('x'); pcheck(ptree) ok! (A (B (C (D ) (E f) (Q p)) g) h x) >>> ptree.append(make_ptree('(X (Y z))')); pcheck(ptree) ok! (A (B (C (D ) (E f) (Q p)) g) h x (X (Y z))) **extend()** >>> ptree = make_ptree('(A (B (C (D) (E f) (Q p)) g) h)') >>> ptree.extend(['x', 'y', make_ptree('(X (Y z))')]); pcheck(ptree) ok! (A (B (C (D ) (E f) (Q p)) g) h x y (X (Y z))) >>> ptree.extend([]); pcheck(ptree) ok! (A (B (C (D ) (E f) (Q p)) g) h x y (X (Y z))) >>> ptree.extend(make_ptree('(X)') for x in range(3)); pcheck(ptree) ok! (A (B (C (D ) (E f) (Q p)) g) h x y (X (Y z)) (X ) (X ) (X )) **insert()** >>> ptree = make_ptree('(A (B (C (D) (E f) (Q p)) g) h)') >>> ptree.insert(0, make_ptree('(X (Y z))')); pcheck(ptree) ok! (A (X (Y z)) (B (C (D ) (E f) (Q p)) g) h) >>> ptree.insert(-1, make_ptree('(X (Y z))')); pcheck(ptree) ok! (A (X (Y z)) (B (C (D ) (E f) (Q p)) g) (X (Y z)) h) >>> ptree.insert(-4, make_ptree('(X (Y z))')); pcheck(ptree) ok! (A (X (Y z)) (X (Y z)) (B (C (D ) (E f) (Q p)) g) (X (Y z)) h) >>> # Note: as with ``list``, inserting at a negative index that >>> # gives a position before the start of the list does *not* >>> # raise an IndexError exception; it just inserts at 0. >>> ptree.insert(-400, make_ptree('(X (Y z))')); pcheck(ptree) ok! (A (X (Y z)) (X (Y z)) (X (Y z)) (B (C (D ) (E f) (Q p)) g) (X (Y z)) h) **pop()** >>> ptree = make_ptree('(A (B (C (D) (E f) (Q p)) g) h)') >>> ptree[0,0].pop(1); pcheck(ptree) ParentedTree('E', ['f']) ok! (A (B (C (D ) (Q p)) g) h) >>> ptree[0].pop(-1); pcheck(ptree) 'g' ok! (A (B (C (D ) (Q p))) h) >>> ptree.pop(); pcheck(ptree) 'h' ok! (A (B (C (D ) (Q p)))) >>> ptree.pop(-100) Traceback (most recent call last): . . . IndexError: index out of range **remove()** >>> ptree = make_ptree('(A (B (C (D) (E f) (Q p)) g) h)') >>> e = ptree[0,0,1] >>> ptree[0,0].remove(ptree[0,0,1]); pcheck(ptree); pcheck(e) ok! (A (B (C (D ) (Q p)) g) h) ok! (E f) >>> ptree[0,0].remove(make_ptree('(Q p)')); pcheck(ptree) ok! (A (B (C (D )) g) h) >>> ptree[0,0].remove(make_ptree('(Q p)')) Traceback (most recent call last): . . . ValueError: list.index(x): x not in list >>> ptree.remove('h'); pcheck(ptree) ok! (A (B (C (D )) g)) >>> ptree.remove('h'); Traceback (most recent call last): . . . ValueError: list.index(x): x not in list >>> # remove() removes the first subtree that is equal (==) to the >>> # given tree, which may not be the identical tree we give it: >>> ptree = make_ptree('(A (X x) (Y y) (X x))') >>> x1, y, x2 = ptree >>> ptree.remove(ptree[-1]); pcheck(ptree) ok! (A (Y y) (X x)) >>> print x1.parent; pcheck(x1) None ok! (X x) >>> print x2.parent (A (Y y) (X x)) Test that a tree can not be given multiple parents: >>> ptree = make_ptree('(A (X x) (Y y) (Z z))') >>> ptree[0] = ptree[1] Traceback (most recent call last): . . . ValueError: Can not insert a subtree that already has a parent. >>> pcheck() ok! [more to be written] MultiParentedTree Regression Tests ---------------------------------- Keep track of all trees that we create (including subtrees) using this variable: >>> all_mptrees = [] Define a helper funciton to create new parented trees: >>> def make_mptree(s): ... mptree = MultiParentedTree.convert(bracket_parse(s)) ... all_mptrees.extend(t for t in mptree.subtrees() ... if isinstance(t, Tree)) ... return mptree Define a test function that examines every subtree in all_mptrees; and checks that all six of its properties are defined correctly. If any mptrees are passed as arguments, then they are printed. >>> def mpcheck(*print_mptrees): ... def has(seq, val): # uses identity comparison ... for item in seq: ... if item is val: return True ... return False ... for mptree in all_mptrees: ... # Check mptree's properties. ... if len(mptree.parents) == 0: ... assert len(mptree.left_siblings) == 0 ... assert len(mptree.right_siblings) == 0 ... assert len(mptree.roots) == 1 ... assert mptree.roots[0] is mptree ... assert mptree.treepositions(mptree) == [()] ... left_siblings = right_siblings = () ... roots = {id(mptree): 1} ... else: ... roots = dict((id(r), 0) for r in mptree.roots) ... left_siblings = mptree.left_siblings ... right_siblings = mptree.right_siblings ... for parent in mptree.parents: ... for i in mptree.parent_indices(parent): ... assert parent[i] is mptree ... # check left siblings ... if i > 0: ... for j in range(len(left_siblings)): ... if left_siblings[j] is parent[i-1]: ... del left_siblings[j] ... break ... else: ... assert 0, 'sibling not found!' ... # check ight siblings ... if i < (len(parent)-1): ... for j in range(len(right_siblings)): ... if right_siblings[j] is parent[i+1]: ... del right_siblings[j] ... break ... else: ... assert 0, 'sibling not found!' ... # check roots ... for root in parent.roots: ... assert id(root) in roots, 'missing root' ... roots[id(root)] += 1 ... # check that we don't have any unexplained values ... assert len(left_siblings)==0, 'unexpected sibling' ... assert len(right_siblings)==0, 'unexpected sibling' ... for v in roots.values(): assert v>0, roots #'unexpected root' ... # check treepositions ... for root in mptree.roots: ... for treepos in mptree.treepositions(root): ... assert root[treepos] is mptree ... # Check mptree's children's properties: ... for i, child in enumerate(mptree): ... if isinstance(child, Tree): ... # mpcheck parent & parent_index properties ... assert has(child.parents, mptree) ... assert i in child.parent_indices(mptree) ... # mpcheck sibling properties ... if i > 0: ... assert has(child.left_siblings, mptree[i-1]) ... if i < len(mptree)-1: ... assert has(child.right_siblings, mptree[i+1]) ... if print_mptrees: ... print 'ok!', ... for mptree in print_mptrees: print mptree ... else: ... print 'ok!' Run our test function on a variety of newly-created trees: >>> mpcheck(make_mptree('(A)')) ok! (A ) >>> mpcheck(make_mptree('(A (B (C (D) (E f)) g) h)')) ok! (A (B (C (D ) (E f)) g) h) >>> mpcheck(make_mptree('(A (B) (C c) (D d d) (E e e e))')) ok! (A (B ) (C c) (D d d) (E e e e)) >>> mpcheck(make_mptree('(A (B) (C (c)) (D (d) (d)) (E (e) (e) (e)))')) ok! (A (B ) (C (c )) (D (d ) (d )) (E (e ) (e ) (e ))) >>> subtree = make_mptree('(A (B (C (D) (E f)) g) h)') Including some trees that contain multiple parents: >>> mpcheck(MultiParentedTree('Z', [subtree, subtree])) ok! (Z (A (B (C (D ) (E f)) g) h) (A (B (C (D ) (E f)) g) h)) Run our test function after performing various tree-modification operations (n.b., these are the same tests that we ran for `ParentedTree`, above; thus, none of these trees actually *uses* multiple parents.) **__delitem__()** >>> mptree = make_mptree('(A (B (C (D) (E f) (Q p)) g) h)') >>> e = mptree[0,0,1] >>> del mptree[0,0,1]; mpcheck(mptree); mpcheck(e) ok! (A (B (C (D ) (Q p)) g) h) ok! (E f) >>> del mptree[0,0,0]; mpcheck(mptree) ok! (A (B (C (Q p)) g) h) >>> del mptree[0,1]; mpcheck(mptree) ok! (A (B (C (Q p))) h) >>> del mptree[-1]; mpcheck(mptree) ok! (A (B (C (Q p)))) >>> del mptree[-100] Traceback (most recent call last): . . . IndexError: index out of range >>> del mptree[()] Traceback (most recent call last): . . . IndexError: The tree position () may not be deleted. >>> # With slices: >>> mptree = make_mptree('(A (B c) (D e) f g (H i) j (K l))') >>> b = mptree[0] >>> del mptree[0:0]; mpcheck(mptree) ok! (A (B c) (D e) f g (H i) j (K l)) >>> del mptree[:1]; mpcheck(mptree); mpcheck(b) ok! (A (D e) f g (H i) j (K l)) ok! (B c) >>> del mptree[-2:]; mpcheck(mptree) ok! (A (D e) f g (H i)) >>> del mptree[1:3]; mpcheck(mptree) ok! (A (D e) (H i)) >>> mptree = make_mptree('(A (B c) (D e) f g (H i) j (K l))') >>> del mptree[5:1000]; mpcheck(mptree) ok! (A (B c) (D e) f g (H i)) >>> del mptree[-2:1000]; mpcheck(mptree) ok! (A (B c) (D e) f) >>> del mptree[-100:1]; mpcheck(mptree) ok! (A (D e) f) >>> del mptree[1:2:3] Traceback (most recent call last): . . . ValueError: slices with steps are not supported by MultiParentedTree **__setitem__()** >>> mptree = make_mptree('(A (B (C (D) (E f) (Q p)) g) h)') >>> d, e, q = mptree[0,0] >>> mptree[0,0,0] = 'x'; mpcheck(mptree); mpcheck(d) ok! (A (B (C x (E f) (Q p)) g) h) ok! (D ) >>> mptree[0,0,1] = make_mptree('(X (Y z))'); mpcheck(mptree); mpcheck(e) ok! (A (B (C x (X (Y z)) (Q p)) g) h) ok! (E f) >>> mptree[1] = d; mpcheck(mptree) ok! (A (B (C x (X (Y z)) (Q p)) g) (D )) >>> mptree[-1] = 'x'; mpcheck(mptree) ok! (A (B (C x (X (Y z)) (Q p)) g) x) >>> mptree[-100] = 'y' Traceback (most recent call last): . . . IndexError: index out of range >>> mptree[()] = make_mptree('(X y)') Traceback (most recent call last): . . . IndexError: The tree position () may not be assigned to. >>> # With slices: >>> mptree = make_mptree('(A (B c) (D e) f g (H i) j (K l))') >>> b = mptree[0] >>> mptree[0:0] = ('x', make_mptree('(Y)')); mpcheck(mptree) ok! (A x (Y ) (B c) (D e) f g (H i) j (K l)) >>> mptree[2:6] = (); mpcheck(mptree); mpcheck(b) ok! (A x (Y ) (H i) j (K l)) ok! (B c) >>> mptree[-2:] = ('z', 'p'); mpcheck(mptree) ok! (A x (Y ) (H i) z p) >>> mptree[1:3] = [make_mptree('(X)') for x in range(10)]; mpcheck(mptree) ok! (A x (X ) (X ) (X ) (X ) (X ) (X ) (X ) (X ) (X ) (X ) z p) >>> mptree[5:1000] = []; mpcheck(mptree) ok! (A x (X ) (X ) (X ) (X )) >>> mptree[-2:1000] = ['n']; mpcheck(mptree) ok! (A x (X ) (X ) n) >>> mptree[-100:1] = [make_mptree('(U v)')]; mpcheck(mptree) ok! (A (U v) (X ) (X ) n) >>> mptree[-1:] = (make_mptree('(X)') for x in range(3)); mpcheck(mptree) ok! (A (U v) (X ) (X ) (X ) (X ) (X )) >>> mptree[1:2:3] = ['x'] Traceback (most recent call last): . . . ValueError: slices with steps are not supported by MultiParentedTree **append()** >>> mptree = make_mptree('(A (B (C (D) (E f) (Q p)) g) h)') >>> mptree.append('x'); mpcheck(mptree) ok! (A (B (C (D ) (E f) (Q p)) g) h x) >>> mptree.append(make_mptree('(X (Y z))')); mpcheck(mptree) ok! (A (B (C (D ) (E f) (Q p)) g) h x (X (Y z))) **extend()** >>> mptree = make_mptree('(A (B (C (D) (E f) (Q p)) g) h)') >>> mptree.extend(['x', 'y', make_mptree('(X (Y z))')]); mpcheck(mptree) ok! (A (B (C (D ) (E f) (Q p)) g) h x y (X (Y z))) >>> mptree.extend([]); mpcheck(mptree) ok! (A (B (C (D ) (E f) (Q p)) g) h x y (X (Y z))) >>> mptree.extend(make_mptree('(X)') for x in range(3)); mpcheck(mptree) ok! (A (B (C (D ) (E f) (Q p)) g) h x y (X (Y z)) (X ) (X ) (X )) **insert()** >>> mptree = make_mptree('(A (B (C (D) (E f) (Q p)) g) h)') >>> mptree.insert(0, make_mptree('(X (Y z))')); mpcheck(mptree) ok! (A (X (Y z)) (B (C (D ) (E f) (Q p)) g) h) >>> mptree.insert(-1, make_mptree('(X (Y z))')); mpcheck(mptree) ok! (A (X (Y z)) (B (C (D ) (E f) (Q p)) g) (X (Y z)) h) >>> mptree.insert(-4, make_mptree('(X (Y z))')); mpcheck(mptree) ok! (A (X (Y z)) (X (Y z)) (B (C (D ) (E f) (Q p)) g) (X (Y z)) h) >>> # Note: as with ``list``, inserting at a negative index that >>> # gives a position before the start of the list does *not* >>> # raise an IndexError exception; it just inserts at 0. >>> mptree.insert(-400, make_mptree('(X (Y z))')); mpcheck(mptree) ok! (A (X (Y z)) (X (Y z)) (X (Y z)) (B (C (D ) (E f) (Q p)) g) (X (Y z)) h) **pop()** >>> mptree = make_mptree('(A (B (C (D) (E f) (Q p)) g) h)') >>> mptree[0,0].pop(1); mpcheck(mptree) MultiParentedTree('E', ['f']) ok! (A (B (C (D ) (Q p)) g) h) >>> mptree[0].pop(-1); mpcheck(mptree) 'g' ok! (A (B (C (D ) (Q p))) h) >>> mptree.pop(); mpcheck(mptree) 'h' ok! (A (B (C (D ) (Q p)))) >>> mptree.pop(-100) Traceback (most recent call last): . . . IndexError: index out of range **remove()** >>> mptree = make_mptree('(A (B (C (D) (E f) (Q p)) g) h)') >>> e = mptree[0,0,1] >>> mptree[0,0].remove(mptree[0,0,1]); mpcheck(mptree); mpcheck(e) ok! (A (B (C (D ) (Q p)) g) h) ok! (E f) >>> mptree[0,0].remove(make_mptree('(Q p)')); mpcheck(mptree) ok! (A (B (C (D )) g) h) >>> mptree[0,0].remove(make_mptree('(Q p)')) Traceback (most recent call last): . . . ValueError: list.index(x): x not in list >>> mptree.remove('h'); mpcheck(mptree) ok! (A (B (C (D )) g)) >>> mptree.remove('h'); Traceback (most recent call last): . . . ValueError: list.index(x): x not in list >>> # remove() removes the first subtree that is equal (==) to the >>> # given tree, which may not be the identical tree we give it: >>> mptree = make_mptree('(A (X x) (Y y) (X x))') >>> x1, y, x2 = mptree >>> mptree.remove(mptree[-1]); mpcheck(mptree) ok! (A (Y y) (X x)) >>> print [str(p) for p in x1.parents] [] >>> print [str(p) for p in x2.parents] ['(A (Y y) (X x))'] Squashed Bugs ============= This used to cause an infinite loop (fixed in svn 6269): >>> tree = Tree.parse('(VP (VERB saw) (NP (DET the) (NOUN cat)))') >>> tree < None False This used to discard the ``(B b)`` subtree (fixed in svn 6270): >>> print bracket_parse('((A a) (B b))') ( (A a) (B b)) nltk-2.0~b9/nltk/test/toolbox.doctest0000644000175000017500000002345011331670035017565 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT =============================== Unit test cases for ``toolbox`` =============================== >>> from nltk import toolbox -------------------------- ``toolbox.StandardFormat`` -------------------------- >>> f = toolbox.StandardFormat() ``toolbox.StandardFormat.open()`` --------------------------------- >>> import os, tempfile >>> (fd, fname) = tempfile.mkstemp() >>> tf = os.fdopen(fd, "w") >>> tf.write('\\lx a value\n\\lx another value\n') >>> tf.close() >>> f = toolbox.StandardFormat() >>> f.open(fname) >>> list(f.fields()) [('lx', 'a value'), ('lx', 'another value')] >>> f.close() >>> os.unlink(fname) ``toolbox.StandardFormat.open_string()`` ---------------------------------------- >>> f = toolbox.StandardFormat() >>> f.open_string('\\lx a value\n\\lx another value\n') >>> list(f.fields()) [('lx', 'a value'), ('lx', 'another value')] >>> f.close() ``toolbox.StandardFormat.close()`` ---------------------------------- >>> f = toolbox.StandardFormat() >>> f.open_string('\\lx a value\n\\lx another value\n') >>> list(f.fields()) [('lx', 'a value'), ('lx', 'another value')] >>> f.close() ``toolbox.StandardFormat.line_num`` --------------------------------------- ``StandardFormat.line_num`` contains the line number of the last line returned: >>> f = toolbox.StandardFormat() >>> f.open_string('\\lx a value\n\\lx another value\n\\lx a third value\n') >>> line_nums = [] >>> for l in f.raw_fields(): ... line_nums.append(f.line_num) >>> line_nums [1, 2, 3] ``StandardFormat.line_num`` contains the line number of the last line returned: >>> f = toolbox.StandardFormat() >>> f.open_string('\\lx two\nlines\n\\lx three\nlines\n\n\\lx two\nlines\n') >>> line_nums = [] >>> for l in f.raw_fields(): ... line_nums.append(f.line_num) >>> line_nums [2, 5, 7] ``StandardFormat.line_num`` doesn't exist before openning or after closing a file or string: >>> f = toolbox.StandardFormat() >>> f.line_num Traceback (most recent call last): ... AttributeError: 'StandardFormat' object has no attribute 'line_num' >>> f.open_string('\\lx two\nlines\n\\lx three\nlines\n\n\\lx two\nlines\n') >>> line_nums = [] >>> for l in f.raw_fields(): ... line_nums.append(f.line_num) >>> line_nums [2, 5, 7] >>> f.close() >>> f.line_num Traceback (most recent call last): ... AttributeError: 'StandardFormat' object has no attribute 'line_num' ``toolbox.StandardFormat.raw_fields()`` --------------------------------------- ``raw_fields()`` returns an iterator over tuples of two strings representing the marker and its value. The marker is given without the backslash and the value without its trailing newline: >>> f = toolbox.StandardFormat() >>> f.open_string('\\lx a value\n\\lx another value\n') >>> list(f.raw_fields()) [('lx', 'a value'), ('lx', 'another value')] an empty file returns nothing: >>> f = toolbox.StandardFormat() >>> f.open_string('') >>> list(f.raw_fields()) [] file with only a newline returns WHAT SHOULD IT RETURN???: >>> f = toolbox.StandardFormat() >>> f.open_string('\n') >>> list(f.raw_fields()) [(None, '')] file with only one field should be parsed ok: >>> f = toolbox.StandardFormat() >>> f.open_string('\\lx one value\n') >>> list(f.raw_fields()) [('lx', 'one value')] file without a trailing newline should be parsed ok: >>> f = toolbox.StandardFormat() >>> f.open_string('\\lx a value\n\\lx another value') >>> list(f.raw_fields()) [('lx', 'a value'), ('lx', 'another value')] trailing white space is preserved except for the final newline: >>> f = toolbox.StandardFormat() >>> f.open_string('\\lx trailing space \n\\lx trailing tab\t\n\\lx extra newline\n\n') >>> list(f.raw_fields()) [('lx', 'trailing space '), ('lx', 'trailing tab\t'), ('lx', 'extra newline\n')] line wrapping is preserved: >>> f = toolbox.StandardFormat() >>> f.open_string('\\lx a value\nmore of the value\nand still more\n\\lc another val\n') >>> list(f.raw_fields()) [('lx', 'a value\nmore of the value\nand still more'), ('lc', 'another val')] file beginning with a multiline record should be parsed ok: >>> f = toolbox.StandardFormat() >>> f.open_string('\\lx a value\nmore of the value\nand still more\n\\lc another val\n') >>> list(f.raw_fields()) [('lx', 'a value\nmore of the value\nand still more'), ('lc', 'another val')] file ending with a multiline record should be parsed ok: >>> f = toolbox.StandardFormat() >>> f.open_string('\\lc a value\n\\lx another value\nmore of the value\nand still more\n') >>> list(f.raw_fields()) [('lc', 'a value'), ('lx', 'another value\nmore of the value\nand still more')] file beginning with a BOM should be parsed ok: >>> f = toolbox.StandardFormat() >>> f.open_string(u'\ufeff\\lx a value\n\\lx another value\n'.encode('utf8')) >>> list(f.raw_fields()) [('lx', 'a value'), ('lx', 'another value')] file beginning with two BOMs should ignore only the first one: >>> f = toolbox.StandardFormat() >>> f.open_string(u'\ufeff\ufeff\\lx a value\n\\lx another value\n'.encode('utf8')) >>> list(f.raw_fields()) [(None, '\xef\xbb\xbf\\lx a value'), ('lx', 'another value')] should not ignore a BOM not at the beginning of the file: >>> f = toolbox.StandardFormat() >>> f.open_string(u'\\lx a value\n\ufeff\\lx another value\n'.encode('utf8')) >>> list(f.raw_fields()) [('lx', 'a value\n\xef\xbb\xbf\\lx another value')] ``toolbox.StandardFormat.fields()`` ----------------------------------- trailing white space is not preserved: >>> f = toolbox.StandardFormat() >>> f.open_string('\\lx trailing space \n\\lx trailing tab\t\n\\lx extra newline\n\n') >>> list(f.fields()) [('lx', 'trailing space'), ('lx', 'trailing tab'), ('lx', 'extra newline')] multiline fields are unwrapped: >>> f = toolbox.StandardFormat() >>> f.open_string('\\lx a value\nmore of the value\nand still more\n\\lc another val\n') >>> list(f.fields()) [('lx', 'a value more of the value and still more'), ('lc', 'another val')] markers ------- A backslash in the first position on a new line indicates the start of a marker. The backslash is not part of the marker: >>> f = toolbox.StandardFormat() >>> f.open_string('\\mk a value\n') >>> list(f.fields()) [('mk', 'a value')] If the backslash occurs later in the line it does not indicate the start of a marker: >>> f = toolbox.StandardFormat() >>> f.open_string('\\mk a value\n \\mk another one\n') >>> list(f.raw_fields()) [('mk', 'a value\n \\mk another one')] There is no specific limit to the length of a marker: >>> f = toolbox.StandardFormat() >>> f.open_string('\\this_is_an_extremely_long_marker value\n') >>> list(f.fields()) [('this_is_an_extremely_long_marker', 'value')] A marker can contain any non white space character: >>> f = toolbox.StandardFormat() >>> f.open_string('\\`~!@#$%^&*()_-=+[{]}\|,<.>/?;:"0123456789 value\n') >>> list(f.fields()) [('`~!@#$%^&*()_-=+[{]}\\|,<.>/?;:"0123456789', 'value')] A marker is terminated by any white space character: >>> f = toolbox.StandardFormat() >>> f.open_string('\\mk a value\n\\mk\tanother one\n\\mk\rthird one\n\\mk\ffourth one') >>> list(f.fields()) [('mk', 'a value'), ('mk', 'another one'), ('mk', 'third one'), ('mk', 'fourth one')] Consecutive whitespace characters (except newline) are treated the same as one: >>> f = toolbox.StandardFormat() >>> f.open_string('\\mk \t\r\fa value\n') >>> list(f.fields()) [('mk', 'a value')] ----------------------- ``toolbox.ToolboxData`` ----------------------- >>> db = toolbox.ToolboxData() ``toolbox.ToolboxData.parse()`` ------------------------------- check that normal parsing works: >>> from nltk.etree import ElementTree >>> td = toolbox.ToolboxData() >>> s = """\\_sh v3.0 400 Rotokas Dictionary ... \\_DateStampHasFourDigitYear ... ... \\lx kaa ... \\ps V.A ... \\ge gag ... \\gp nek i pas ... ... \\lx kaa ... \\ps V.B ... \\ge strangle ... \\gp pasim nek ... """ >>> td.open_string(s) >>> tree = td.parse(key='lx') >>> tree.tag 'toolbox_data' >>> ElementTree.tostring(tree.getchildren()[0]) '

<_sh>v3.0 400 Rotokas Dictionary<_DateStampHasFourDigitYear />
' >>> ElementTree.tostring(tree.getchildren()[1]) 'kaaV.Agagnek i pas' >>> ElementTree.tostring(tree.getchildren()[2]) 'kaaV.Bstranglepasim nek' check that guessing the key marker works: >>> from nltk.etree import ElementTree >>> td = toolbox.ToolboxData() >>> s = """\\_sh v3.0 400 Rotokas Dictionary ... \\_DateStampHasFourDigitYear ... ... \\lx kaa ... \\ps V.A ... \\ge gag ... \\gp nek i pas ... ... \\lx kaa ... \\ps V.B ... \\ge strangle ... \\gp pasim nek ... """ >>> td.open_string(s) >>> tree = td.parse() >>> ElementTree.tostring(tree.getchildren()[0]) '
<_sh>v3.0 400 Rotokas Dictionary<_DateStampHasFourDigitYear />
' >>> ElementTree.tostring(tree.getchildren()[1]) 'kaaV.Agagnek i pas' >>> ElementTree.tostring(tree.getchildren()[2]) 'kaaV.Bstranglepasim nek' ----------------------- ``toolbox`` functions ----------------------- ``toolbox.to_sfm_string()`` ------------------------------- nltk-2.0~b9/nltk/test/tokenize.doctest0000644000175000017500000003514311366164570017743 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ========== Tokenizers ========== (See Chapter 3 of the NLTK book for more detailed information about tokenization.) Overview ~~~~~~~~ Tokenizers divide strings into lists of substrings. For example, tokenizers can be used to find the list of sentences or words in a string. >>> from nltk import word_tokenize, wordpunct_tokenize >>> s = ("Good muffins cost $3.88\nin New York. Please buy me\n" ... "two of them.\n\nThanks.") >>> word_tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good', 'muffins', 'cost', '$', '3.88', 'in', 'New', 'York.', 'Please', 'buy', 'me', 'two', 'of', 'them', '.', 'Thanks', '.'] >>> wordpunct_tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good', 'muffins', 'cost', '$', '3', '.', '88', 'in', 'New', 'York', '.', 'Please', 'buy', 'me', 'two', 'of', 'them', '.', 'Thanks', '.'] When tokenizing text containing Unicode characters, be sure to tokenize the Unicode string, and not (say), the UTF8-encoded version: >>> wordpunct_tokenize("das ist ein t\xc3\xa4ller satz".decode('utf8')) [u'das', u'ist', u'ein', u't\xe4ller', u'satz'] >>> wordpunct_tokenize("das ist ein t\xc3\xa4ller satz") ['das', 'ist', 'ein', 't\xc3', '\xa4', 'ller', 'satz'] There are numerous ways to tokenize text. If you need more control over tokenization, see the methods described below. Simple Tokenizers ~~~~~~~~~~~~~~~~~ The following tokenizers, defined in `nltk.tokenize.simple`, just divide the string using the string ``split()`` method. >>> from nltk.tokenize import * >>> # same as s.split(): >>> WhitespaceTokenizer().tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good', 'muffins', 'cost', '$3.88', 'in', 'New', 'York.', 'Please', 'buy', 'me', 'two', 'of', 'them.', 'Thanks.'] >>> # same as s.split(' '): >>> SpaceTokenizer().tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good', 'muffins', 'cost', '$3.88\nin', 'New', 'York.', '', 'Please', 'buy', 'me\ntwo', 'of', 'them.\n\nThanks.'] >>> # same as s.split('\n'): >>> LineTokenizer(blanklines='keep').tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good muffins cost $3.88', 'in New York. Please buy me', 'two of them.', '', 'Thanks.'] >>> # same as [l for l in s.split('\n') if l.strip()]: >>> LineTokenizer(blanklines='discard').tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good muffins cost $3.88', 'in New York. Please buy me', 'two of them.', 'Thanks.'] >>> # same as s.split('\t'): >>> TabTokenizer().tokenize('a\tb c\n\t d') # doctest: +NORMALIZE_WHITESPACE ['a', 'b c\n', ' d'] The simple tokenizers are *not* available as separate functions; instead, you should just use the string ``split()`` method directly: >>> s.split() # doctest: +NORMALIZE_WHITESPACE ['Good', 'muffins', 'cost', '$3.88', 'in', 'New', 'York.', 'Please', 'buy', 'me', 'two', 'of', 'them.', 'Thanks.'] >>> s.split(' ') # doctest: +NORMALIZE_WHITESPACE ['Good', 'muffins', 'cost', '$3.88\nin', 'New', 'York.', '', 'Please', 'buy', 'me\ntwo', 'of', 'them.\n\nThanks.'] >>> s.split('\n') # doctest: +NORMALIZE_WHITESPACE ['Good muffins cost $3.88', 'in New York. Please buy me', 'two of them.', '', 'Thanks.'] The simple tokenizers are mainly useful because they follow the standard ``TokenizerI`` interface, and so can be used with any code that expects a tokenizer. For example, these tokenizers can be used to specify the tokenization conventions when building a `CorpusReader`. Regular Expression Tokenizers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `RegexpTokenizer` splits a string into substrings using a regular expression. By default, any substrings matched by this regexp will be returned as tokens. For example, the following tokenizer selects out only capitalized words, and throws everything else away: >>> capword_tokenizer = RegexpTokenizer('[A-Z]\w+') >>> capword_tokenizer.tokenize(s) ['Good', 'New', 'York', 'Please', 'Thanks'] The following tokenizer forms tokens out of alphabetic sequences, money expressions, and any other non-whitespace sequences: >>> tokenizer = RegexpTokenizer('\w+|\$[\d\.]+|\S+') >>> tokenizer.tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good', 'muffins', 'cost', '$3.88', 'in', 'New', 'York', '.', 'Please', 'buy', 'me', 'two', 'of', 'them', '.', 'Thanks', '.'] ``RegexpTokenizers`` can be told to use their regexp pattern to match separators between tokens, using ``gaps=True``: >>> tokenizer = RegexpTokenizer('\s+', gaps=True) >>> tokenizer.tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good', 'muffins', 'cost', '$3.88', 'in', 'New', 'York.', 'Please', 'buy', 'me', 'two', 'of', 'them.', 'Thanks.'] The `nltk.tokenize.regexp` module contains several subclasses of ``RegexpTokenizer`` that use pre-defined regular expressions: >>> # Uses '\w+|[^\w\s]+': >>> WordPunctTokenizer().tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good', 'muffins', 'cost', '$', '3', '.', '88', 'in', 'New', 'York', '.', 'Please', 'buy', 'me', 'two', 'of', 'them', '.', 'Thanks', '.'] >>> # Uses '\s*\n\s*\n\s*': >>> BlanklineTokenizer().tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good muffins cost $3.88\nin New York. Please buy me\ntwo of them.', 'Thanks.'] All of the regular expression tokenizers are also available as simple functions: >>> regexp_tokenize(s, pattern='\w+|\$[\d\.]+|\S+') # doctest: +NORMALIZE_WHITESPACE ['Good', 'muffins', 'cost', '$3.88', 'in', 'New', 'York', '.', 'Please', 'buy', 'me', 'two', 'of', 'them', '.', 'Thanks', '.'] >>> wordpunct_tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good', 'muffins', 'cost', '$', '3', '.', '88', 'in', 'New', 'York', '.', 'Please', 'buy', 'me', 'two', 'of', 'them', '.', 'Thanks', '.'] >>> blankline_tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good muffins cost $3.88\nin New York. Please buy me\ntwo of them.', 'Thanks.'] .. warning:: The function ``regexp_tokenize()`` takes the text as its first argument, and the regular expression pattern as its second argument. This differs from the conventions used by Python's ``re`` functions, where the pattern is always the first argument. But ``regexp_tokenize()`` is primarily a tokenization function, so we chose to follow the convention among other tokenization functions that the text should always be the first argument. Treebank Tokenizer ~~~~~~~~~~~~~~~~~~ The Treebank tokenizer uses regular expressions to tokenize text as in Penn Treebank. This is the method that is invoked by nltk.word_tokenize(). It assumes that the text has already been segmented into sentences. >>> TreebankWordTokenizer().tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good', 'muffins', 'cost', '$', '3.88', 'in', 'New', 'York.', 'Please', 'buy', 'me', 'two', 'of', 'them', '.', 'Thanks', '.'] S-Expression Tokenizers ~~~~~~~~~~~~~~~~~~~~~~~ `SExprTokenizer` is used to find parenthesized expressions in a string. In particular, it divides a string into a sequence of substrings that are either parenthesized expressions (including any nested parenthesized expressions), or other whitespace-separated tokens. >>> SExprTokenizer().tokenize('(a b (c d)) e f (g)') ['(a b (c d))', 'e', 'f', '(g)'] By default, `SExprTokenizer` will raise a ``ValueError`` exception if used to tokenize an expression with non-matching parentheses: >>> SExprTokenizer().tokenize('c) d) e (f (g') Traceback (most recent call last): ... ValueError: Un-matched close paren at char 1 But the ``strict`` argument can be set to False to allow for non-matching parentheses. Any unmatched close parentheses will be listed as their own s-expression; and the last partial sexpr with unmatched open parentheses will be listed as its own sexpr: >>> SExprTokenizer(strict=False).tokenize('c) d) e (f (g') ['c', ')', 'd', ')', 'e', '(f (g'] The characters used for open and close parentheses may be customized using the ``parens`` argument to the `SExprTokenizer` constructor: >>> SExprTokenizer(parens='{}').tokenize('{a b {c d}} e f {g}') ['{a b {c d}}', 'e', 'f', '{g}'] The s-expression tokenizer is also available as a function: >>> sexpr_tokenize('(a b (c d)) e f (g)') ['(a b (c d))', 'e', 'f', '(g)'] Punkt Tokenizer ~~~~~~~~~~~~~~~ The `PunktSentenceTokenizer` divides a text into a list of sentences, by using an unsupervised algorithm to build a model for abbreviation words, collocations, and words that start sentences. It must be trained on a large collection of plaintext in the taret language before it can be used. The algorithm for this tokenizer is described in Kiss & Strunk (2006):: Kiss, Tibor and Strunk, Jan (2006): Unsupervised Multilingual Sentence Boundary Detection. Computational Linguistics 32: 485-525. The NLTK data package includes a pre-trained Punkt tokenizer for English. >>> import nltk.data >>> text = """ ... Punkt knows that the periods in Mr. Smith and Johann S. Bach ... do not mark sentence boundaries. And sometimes sentences ... can start with non-capitalized words. i is a good variable ... name. ... """ >>> sent_detector = nltk.data.load('tokenizers/punkt/english.pickle') >>> print '\n-----\n'.join(sent_detector.tokenize(text.strip())) Punkt knows that the periods in Mr. Smith and Johann S. Bach do not mark sentence boundaries. ----- And sometimes sentences can start with non-capitalized words. ----- i is a good variable name. (Note that whitespace from the original text, including newlines, is retained in the output.) Punctuation following sentences can be included with the realign_boundaries flag: >>> text = """ ... (How does it deal with this parenthesis?) "It should be part of the ... previous sentence." ... """ >>> print '\n-----\n'.join( ... sent_detector.tokenize(text.strip(), realign_boundaries=True)) (How does it deal with this parenthesis?) ----- "It should be part of the previous sentence." The `nltk.tokenize.punkt` module also defines `PunktWordTokenizer`, which uses a regular expression to divide a text into tokens, leaving all periods attached to words, but separating off other punctuation: >>> PunktWordTokenizer().tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good', 'muffins', 'cost', '$3.88', 'in', 'New', 'York.', 'Please', 'buy', 'me', 'two', 'of', 'them.', 'Thanks.'] Span Tokenizers ~~~~~~~~~~~~~~~ NLTK tokenizers can produce token-spans, i.e. pairs of integers representing the offsets of tokens in the original input stream. This supports efficient comparison of tokenizers, since it saves having to allocate and compare strings. Note that these methods are implemented as generators. >>> input = "Wome... is your fwiend!\tTo pwove our fwiendship, ... " >>> list(SpaceTokenizer().span_tokenize(input)) [(0, 7), (8, 8), (9, 11), (12, 16), (17, 27), (28, 33), (34, 37), (38, 49), (50, 53)] >>> list(TabTokenizer().span_tokenize(input)) [(0, 24), (25, 54)] >>> list(WhitespaceTokenizer().span_tokenize(input)) [(0, 7), (9, 11), (12, 16), (17, 24), (25, 27), (28, 33), (34, 37), (38, 49), (50, 53)] >>> list(RegexpTokenizer(r"\.+ +", gaps=True).span_tokenize(input)) [(0, 4), (9, 50)] Note that empty token spans are not returned when the delimiter appears at the start or end of the string. The offsets are interpreted in the same way as slices, i.e. the end offset is one more than the index of the last character of the token. This means we use can slice notation to extract the corresponding tokens from the input: >>> tokens = ["{" + input[left:right] + "}" ... for left, right in SpaceTokenizer().span_tokenize(input)] >>> "".join(tokens) '{Wome...}{}{is}{your}{fwiend!\tTo}{pwove}{our}{fwiendship,}{...}' A utility function supports access to relative offsets: >>> from nltk.tokenize.util import spans_to_relative >>> list(spans_to_relative(SpaceTokenizer().span_tokenize(input))) [(0, 7), (1, 0), (1, 2), (1, 4), (1, 10), (1, 5), (1, 3), (1, 11), (1, 3)] Regression Tests: Regexp Tokenizer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Some additional test strings. >>> s = ("Good muffins cost $3.88\nin New York. Please buy me\n" ... "two of them.\n\nThanks.") >>> s2 = ("Alas, it has not rained today. When, do you think, " ... "will it rain again?") >>> s3 = ("

Although this is not the case here, we must " ... "not relax our vigilance!

") >>> print regexp_tokenize(s2, r'[,\.\?!"]\s*', gaps=False) [', ', '. ', ', ', ', ', '?'] >>> print regexp_tokenize(s2, r'[,\.\?!"]\s*', gaps=True) ... # doctest: +NORMALIZE_WHITESPACE ['Alas', 'it has not rained today', 'When', 'do you think', 'will it rain again'] Make sure that grouping parentheses don't confuse the tokenizer: >>> print regexp_tokenize(s3, r'', gaps=False) ['

', '', '', '

'] >>> print regexp_tokenize(s3, r'', gaps=True) ... # doctest: +NORMALIZE_WHITESPACE ['Although this is ', 'not', ' the case here, we must not relax our vigilance!'] Make sure that named groups don't confuse the tokenizer: >>> print regexp_tokenize(s3, r'b|p)>', gaps=False) ['

', '', '', '

'] >>> print regexp_tokenize(s3, r'b|p)>', gaps=True) ... # doctest: +NORMALIZE_WHITESPACE ['Although this is ', 'not', ' the case here, we must not relax our vigilance!'] Make sure that nested groups don't confuse the tokenizer: >>> print regexp_tokenize(s2, r'(h|r|l)a(s|(i|n0))', gaps=False) ['las', 'has', 'rai', 'rai'] >>> print regexp_tokenize(s2, r'(h|r|l)a(s|(i|n0))', gaps=True) ... # doctest: +NORMALIZE_WHITESPACE ['A', ', it ', ' not ', 'ned today. When, do you think, will it ', 'n again?'] The tokenizer should reject any patterns with backreferences: >>> print regexp_tokenize(s2, r'(.)\1') ... # doctest: +NORMALIZE_WHITESPACE Traceback (most recent call last): ... ValueError: Regular expressions with back-references are not supported: '(.)\\1' >>> print regexp_tokenize(s2, r'(?P)(?P=foo)') ... # doctest: +NORMALIZE_WHITESPACE Traceback (most recent call last): ... ValueError: Regular expressions with back-references are not supported: '(?P)(?P=foo)' A simple sentence tokenizer '\.(\s+|$)' >>> print regexp_tokenize(s, pattern=r'\.(\s+|$)', gaps=True) ... # doctest: +NORMALIZE_WHITESPACE ['Good muffins cost $3.88\nin New York', 'Please buy me\ntwo of them', 'Thanks'] nltk-2.0~b9/nltk/test/tag.doctest0000644000175000017500000004170511365743231016663 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ======= Taggers ======= Overview ~~~~~~~~ The ``nltk.tag`` module defines functions and classes for manipulating *tagged tokens*, which combine a basic token value with a tag. *Tags* are case-sensitive strings that identify some property of a token, such as its part of speech. Tagged tokens are encoded as tuples ``(tag, token)``. For example, the following tagged token combines the word ``'fly'`` with a noun part of speech tag (``'NN'``): >>> tagged_tok = ('fly', 'NN') An off-the-shelf tagger is available. It uses the Penn Treebank tagset: >>> from nltk import pos_tag, word_tokenize >>> pos_tag(word_tokenize("John's big idea isn't all that bad.")) # doctest: +NORMALIZE_WHITESPACE [('John', 'NNP'), ("'s", 'POS'), ('big', 'JJ'), ('idea', 'NN'), ('is', 'VBZ'), ("n't", 'RB'), ('all', 'DT'), ('that', 'DT'), ('bad', 'JJ'), ('.', '.')] String Representation for Tagged Tokens ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tagged tokens are often written using the form ``'fly/NN'``. The `nltk.tag` module provides utility functions to convert between this string representation and the tuple representation: >>> import nltk.tag >>> print nltk.tag.tuple2str(tagged_tok) fly/NN >>> print nltk.tag.str2tuple('the/DT') ('the', 'DT') To convert an entire sentence from the string format to the tuple format, we simply tokenize the sentence and then apply ``str2tuple`` to each word: >>> sent = 'The/DT cat/NN sat/VBD on/IN the/DT mat/NN ./.' >>> [nltk.tag.str2tuple(w) for w in sent.split()] # doctest: +NORMALIZE_WHITESPACE [('The', 'DT'), ('cat', 'NN'), ('sat', 'VBD'), ('on', 'IN'), ('the', 'DT'), ('mat', 'NN'), ('.', '.')] Similarly, we can convert from a list of tagged tuples to a single string by combining ``tuple2str`` with the string ``join`` method: >>> sent = [('The', 'DT'), ('cat', 'NN'), ('yawned', 'VBD')] >>> ' '.join([nltk.tag.tuple2str(w) for w in sent]) 'The/DT cat/NN yawned/VBD' Taggers ~~~~~~~ The ``nltk.tag`` module defines several *taggers*, which take a token list (typically a sentence), assign a tag to each token, and return the resulting list tagged of tagged tokens. Most of the taggers defined in the ``nltk.tag`` module are built automatically based on a training corpus. For example, the unigram tagger tags each word *w* by checking what the most frequent tag for *w* was in a training corpus: >>> # Load the brown corpus. >>> from nltk.corpus import brown >>> brown_news_tagged = brown.tagged_sents(categories='news') >>> brown_news_text = brown.sents(categories='news') >>> tagger = nltk.UnigramTagger(brown_news_tagged[:500]) >>> tagger.tag(brown_news_text[501]) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [('Mitchell', 'NP'), ('decried', None), ('the', 'AT'), ('high', 'JJ'), ('rate', 'NN'), ('of', 'IN'), ('unemployment', None), ...] Note that words that the tagger has not seen before, such as *decried*, receive a tag of ``None``. In the examples below, we'll look at developing automatic part-of-speech taggers based on the Brown Corpus. Here are the training & test sets we'll use: >>> brown_train = brown_news_tagged[100:] >>> brown_test = brown_news_tagged[:100] >>> test_sent = nltk.tag.untag(brown_test[0]) (Note that these are on the small side, to make the tests run faster -- for real-world use, you would probably want to train on more data.) Default Tagger -------------- The simplest tagger is the ``DefaultTagger``, which just applies the same tag to all tokens: >>> default_tagger = nltk.DefaultTagger('XYZ') >>> default_tagger.tag('This is a test'.split()) [('This', 'XYZ'), ('is', 'XYZ'), ('a', 'XYZ'), ('test', 'XYZ')] Since ``'NN'`` is the most frequent tag in the Brown corpus, we can use a tagger that assigns 'NN' to all words as a baseline. >>> default_tagger = nltk.DefaultTagger('NN') >>> default_tagger.tag(test_sent) # doctest: +NORMALIZE_WHITESPACE [('The', 'NN'), ('Fulton', 'NN'), ('County', 'NN'), ('Grand', 'NN'), ('Jury', 'NN'), ('said', 'NN'), ('Friday', 'NN'), ('an', 'NN'), ('investigation', 'NN'), ('of', 'NN'), ("Atlanta's", 'NN'), ('recent', 'NN'), ('primary', 'NN'), ('election', 'NN'), ('produced', 'NN'), ('``', 'NN'), ('no', 'NN'), ('evidence', 'NN'), ("''", 'NN'), ('that', 'NN'), ('any', 'NN'), ('irregularities', 'NN'), ('took', 'NN'), ('place', 'NN'), ('.', 'NN')] Using this baseline, we achieve about a fairly low accuracy: >>> print 'Accuracy: %4.1f%%' % ( ... 100.0 * default_tagger.evaluate(brown_test)) Accuracy: 14.6% Regexp Tagger ------------- The `RegexpTagger` class assigns tags to tokens by comparing their word strings to a series of regular expressions. The following tagger uses word suffixes to make guesses about the correct Brown Corpus part of speech tag: >>> regexp_tagger = nltk.RegexpTagger( ... [(r'^-?[0-9]+(.[0-9]+)?$', 'CD'), # cardinal numbers ... (r'(The|the|A|a|An|an)$', 'AT'), # articles ... (r'.*able$', 'JJ'), # adjectives ... (r'.*ness$', 'NN'), # nouns formed from adjectives ... (r'.*ly$', 'RB'), # adverbs ... (r'.*s$', 'NNS'), # plural nouns ... (r'.*ing$', 'VBG'), # gerunds ... (r'.*ed$', 'VBD'), # past tense verbs ... (r'.*', 'NN') # nouns (default) ... ]) >>> regexp_tagger.tag(test_sent) # doctest: +NORMALIZE_WHITESPACE [('The', 'AT'), ('Fulton', 'NN'), ('County', 'NN'), ('Grand', 'NN'), ('Jury', 'NN'), ('said', 'NN'), ('Friday', 'NN'), ('an', 'AT'), ('investigation', 'NN'), ('of', 'NN'), ("Atlanta's", 'NNS'), ('recent', 'NN'), ('primary', 'NN'), ('election', 'NN'), ('produced', 'VBD'), ('``', 'NN'), ('no', 'NN'), ('evidence', 'NN'), ("''", 'NN'), ('that', 'NN'), ('any', 'NN'), ('irregularities', 'NNS'), ('took', 'NN'), ('place', 'NN'), ('.', 'NN')] This gives us a higher score than the default tagger, but accuracy is still fairly low: >>> print 'Accuracy: %4.1f%%' % ( ... 100.0 * regexp_tagger.evaluate(brown_test)) Accuracy: 33.1% Unigram Tagger -------------- As mentioned above, the `UnigramTagger` class finds the most likely tag for each word in a training corpus, and then uses that information to assign tags to new tokens. >>> unigram_tagger = nltk.UnigramTagger(brown_train) >>> unigram_tagger.tag(test_sent) # doctest: +NORMALIZE_WHITESPACE [('The', 'AT'), ('Fulton', None), ('County', 'NN-TL'), ('Grand', 'JJ-TL'), ('Jury', 'NN-TL'), ('said', 'VBD'), ('Friday', 'NR'), ('an', 'AT'), ('investigation', 'NN'), ('of', 'IN'), ("Atlanta's", 'NP$'), ('recent', 'JJ'), ('primary', 'NN'), ('election', 'NN'), ('produced', 'VBD'), ('``', '``'), ('no', 'AT'), ('evidence', 'NN'), ("''", "''"), ('that', 'CS'), ('any', 'DTI'), ('irregularities', None), ('took', 'VBD'), ('place', 'NN'), ('.', '.')] This gives us a significantly higher accuracy score than the default tagger or the regexp tagger: >>> print 'Accuracy: %4.1f%%' % ( ... 100.0 * unigram_tagger.evaluate(brown_test)) Accuracy: 85.4% As was mentioned above, the unigram tagger will assign a tag of ``None`` to any words that it never saw in the training data. We can avoid this problem by providing the unigram tagger with a *backoff tagger*, which will be used whenever the unigram tagger is unable to choose a tag: >>> unigram_tagger_2 = nltk.UnigramTagger(brown_train, backoff=regexp_tagger) >>> print 'Accuracy: %4.1f%%' % ( ... 100.0 * unigram_tagger_2.evaluate(brown_test)) Accuracy: 88.0% Using a backoff tagger has another advantage, as well -- it allows us to build a more compact unigram tagger, because the unigram tagger doesn't need to explicitly store the tags for words that the backoff tagger would get right anyway. We can see this by using the `size()` method, which reports the number of words that a unigram tagger has stored the most likely tag for. >>> print unigram_tagger.size() 14262 >>> print unigram_tagger_2.size() 8722 Bigram Tagger ------------- The bigram tagger is similar to the unigram tagger, except that it finds the most likely tag for each word, *given the preceding tag*. (It is called a "bigram" tagger because it uses two pieces of information -- the current word, and the previous tag.) When training, it can look up the preceding tag directly. When run on new data, it works through the sentence from left to right, and uses the tag that it just generated for the preceding word. >>> bigram_tagger = nltk.BigramTagger(brown_train, backoff=unigram_tagger_2) >>> print bigram_tagger.size() 3394 >>> print 'Accuracy: %4.1f%%' % ( ... 100.0 * bigram_tagger.evaluate(brown_test)) Accuracy: 89.4% Trigram Tagger & N-Gram Tagger ------------------------------ Similarly, the trigram tagger finds the most likely tag for a word, *given the preceding two tags*; and the n-gram tagger finds the most likely tag for a word, *given the preceding n-1 tags*. However, these higher-order taggers are only likely to improve performance if there is a large amount of training data available; otherwise, the sequences that they consider do not occur often enough to gather reliable statistics. >>> trigram_tagger = nltk.TrigramTagger(brown_train, backoff=bigram_tagger) >>> print trigram_tagger.size() 1493 >>> print 'Accuracy: %4.1f%%' % ( ... 100.0 * trigram_tagger.evaluate(brown_test)) Accuracy: 88.8% Brill Tagger ------------ The Brill Tagger starts by running an initial tagger, and then improves the tagging by applying a list of transformation rules. These transformation rules are automatically learned from the training corpus, based on one or more "rule templates." >>> from nltk.tag.brill import * >>> templates = [ ... SymmetricProximateTokensTemplate(ProximateTagsRule, (1,1)), ... SymmetricProximateTokensTemplate(ProximateTagsRule, (2,2)), ... SymmetricProximateTokensTemplate(ProximateTagsRule, (1,2)), ... SymmetricProximateTokensTemplate(ProximateTagsRule, (1,3)), ... SymmetricProximateTokensTemplate(ProximateWordsRule, (1,1)), ... SymmetricProximateTokensTemplate(ProximateWordsRule, (2,2)), ... SymmetricProximateTokensTemplate(ProximateWordsRule, (1,2)), ... SymmetricProximateTokensTemplate(ProximateWordsRule, (1,3)), ... ProximateTokensTemplate(ProximateTagsRule, (-1, -1), (1,1)), ... ProximateTokensTemplate(ProximateWordsRule, (-1, -1), (1,1)), ... ] >>> trainer = FastBrillTaggerTrainer(initial_tagger=unigram_tagger_2, ... templates=templates, trace=3, ... deterministic=True) >>> brill_tagger = trainer.train(brown_train, max_rules=10) # doctest: +NORMALIZE_WHITESPACE Training Brill tagger on 4523 sentences... Finding initial useful rules... Found 75359 useful rules. B | S F r O | Score = Fixed - Broken c i o t | R Fixed = num tags changed incorrect -> correct o x k h | u Broken = num tags changed correct -> incorrect r e e e | l Other = num tags changed incorrect -> incorrect e d n r | e ------------------+------------------------------------------------------- 354 354 0 3 | TO -> IN if the tag of the following word is 'AT' 111 173 62 3 | NN -> VB if the tag of the preceding word is 'TO' 110 110 0 4 | TO -> IN if the tag of the following word is 'NP' 83 157 74 4 | NP -> NP-TL if the tag of the following word is | 'NN-TL' 73 77 4 0 | VBD -> VBN if the tag of words i-2...i-1 is 'BEDZ' 71 116 45 3 | TO -> IN if the tag of words i+1...i+2 is 'NNS' 65 65 0 3 | NN -> VB if the tag of the preceding word is 'MD' 63 63 0 0 | VBD -> VBN if the tag of words i-3...i-1 is 'HVZ' 59 62 3 2 | CS -> QL if the text of words i+1...i+3 is 'as' 55 57 2 0 | VBD -> VBN if the tag of words i-3...i-1 is 'HVD' >>> print 'Accuracy: %4.1f%%' % ( ... 100.0 * brill_tagger.evaluate(brown_test)) Accuracy: 89.1% HMM Tagger ---------- The HMM tagger uses a hidden markov model to find the most likely tag sequence for each sentence. (Note: this requires numpy.) >>> from nltk.tag.hmm import * Demo code lifted more or less directly from the HMM class. >>> symbols = ['up', 'down', 'unchanged'] >>> states = ['bull', 'bear', 'static'] >>> def probdist(values, samples): ... d = {} ... for value, item in zip(values, samples): ... d[item] = value ... return DictionaryProbDist(d) >>> def conditionalprobdist(array, conditions, samples): ... d = {} ... for values, condition in zip(array, conditions): ... d[condition] = probdist(values, samples) ... return DictionaryConditionalProbDist(d) >>> A = array([[0.6, 0.2, 0.2], [0.5, 0.3, 0.2], [0.4, 0.1, 0.5]], float64) >>> A = conditionalprobdist(A, states, states) >>> B = array([[0.7, 0.1, 0.2], [0.1, 0.6, 0.3], [0.3, 0.3, 0.4]], float64) >>> B = conditionalprobdist(B, states, symbols) >>> pi = array([0.5, 0.2, 0.3], float64) >>> pi = probdist(pi, states) >>> model = HiddenMarkovModelTagger(symbols=symbols, states=states, ... transitions=A, outputs=B, priors=pi) >>> test = ['up', 'down', 'up'] >>> sequence = [(t, None) for t in test] >>> print '%.3f' % (model.probability(sequence)) 0.051 Check the test sequence by hand -- calculate the joint probability for each possible state sequence, and verify that they're equal to what the model gives; then verify that their total is equal to what the model gives for the probability of the sequence w/ no states specified. >>> seqs = [zip(test, [a,b,c]) for a in states ... for b in states for c in states] >>> total = 0 >>> for seq in seqs: ... # Calculate the probability by hand: ... expect = pi.prob(seq[0][1]) ... for (o,s) in seq: ... expect *= B[s].prob(o) ... for i in range(1, len(seq)): ... expect *= A[seq[i-1][1]].prob(seq[i][1]) ... # Check that it matches the model: ... assert abs(expect-model.probability(seq)) < 1e-10 ... total += model.probability(seq) >>> assert abs(total-model.probability(sequence)) < 1e-10 Find the most likely set of tags for the test sequence. >>> model.tag(test) [('up', 'bull'), ('down', 'bear'), ('up', 'bull')] Find some entropy values. These are all in base 2 (i.e., bits). >>> print '%.3f' % (model.entropy(sequence)) 3.401 >>> print '%.3f' % (model._exhaustive_entropy(sequence)) 3.401 >>> model.point_entropy(sequence) array([ 0.99392864, 1.54508687, 0.97119001]) >>> model._exhaustive_point_entropy(sequence) array([ 0.99392864, 1.54508687, 0.97119001]) Regression Tests ~~~~~~~~~~~~~~~~ TaggerI Interface ----------------- The `TaggerI` interface defines two methods: `tag` and `batch_tag`: >>> nltk.usage(nltk.TaggerI) TaggerI supports the following operations: - self.batch_tag(sentences) - self.evaluate(gold) - self.tag(tokens) The `TaggerI` interface should not be directly instantiated: >>> nltk.TaggerI().tag(test_sent) Traceback (most recent call last): . . . NotImplementedError Sequential Taggers ------------------ Add tests for: - make sure backoff is being done correctly. - make sure ngram taggers don't use previous sentences for context. - make sure ngram taggers see 'beginning of the sentence' as a unique context - make sure regexp tagger's regexps are tried in order - train on some simple examples, & make sure that the size & the generated models are correct. - make sure cutoff works as intended - make sure that ngram models only exclude contexts covered by the backoff tagger if the backoff tagger gets that context correct at *all* locations. Brill Tagger ------------ - test that fast & normal trainers get identical results when deterministic=True is used. - check on some simple examples to make sure they're doing the right thing. Make sure that get_neighborhoods is implemented correctly -- in particular, given *index*, it should return the indices *i* such that applicable_rules(token, i, ...) depends on the value of the *index*\ th token. There used to be a bug where this was swapped -- i.e., it calculated the values of *i* such that applicable_rules(token, index, ...) depended on *i*. >>> t = ProximateTokensTemplate(ProximateWordsRule, (2,3)) >>> for i in range(10): ... print sorted(t.get_neighborhood('abcdefghijkl', i)) [0] [1] [0, 2] [0, 1, 3] [1, 2, 4] [2, 3, 5] [3, 4, 6] [4, 5, 7] [5, 6, 8] [6, 7, 9] nltk-2.0~b9/nltk/test/stem.doctest0000644000175000017500000001100311423114516017035 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ========== Stemmers ========== Overview ~~~~~~~~ Stemmers remove morphological affixes from words, leaving only the word stem. >>> from nltk.stem import * Unit tests for the Porter stemmer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ >>> from nltk.stem.porter import * Create a new Porter stemmer. >>> stemmer = PorterStemmer() Test the cons() (consonant) method. >>> stemmer.b = "ready" >>> stemmer.k = len("ready") - 1 >>> bool(stemmer.cons(0)) True >>> bool(stemmer.cons(1)) False >>> bool(stemmer.cons(4)) False >>> stemmer.b = "yield" >>> stemmer.k = len("yield") - 1 >>> bool(stemmer.cons(0)) True >>> stemmer.b = "abeyance" >>> stemmer.k = len("abeyance") - 1 >>> bool(stemmer.cons(3)) True Test the m() (number of vowel/consonant sequences from the start of a word to some offset) method. >>> stemmer.m() # 0 offset into the string 0 >>> stemmer.j = stemmer.k # Set the offset to be the final string char >>> stemmer.m() 3 Test the vowelinstem() method (checks for a vowel within the first j chars). >>> stemmer.b = "ready" >>> stemmer.k = len("ready") - 1 >>> stemmer.j = 0 >>> stemmer.vowelinstem() 0 >>> stemmer.j = stemmer.k >>> stemmer.vowelinstem() 1 Test the doublec() (identical double consonant) method. >>> stemmer.b = "riddle" >>> stemmer.k = len("riddle") - 1 >>> stemmer.doublec(0) # Can't use at the first char 0 >>> stemmer.doublec(4) # Chars at j and j-1 not identical 0 >>> stemmer.doublec(3) 1 Test the cvc() method. >>> stemmer.cvc(0) # Can't use at the first char 0 >>> stemmer.cvc(1) # Sequence not vowel, consonant 0 >>> stemmer.b = "away" >>> stemmer.cvc(1) 1 >>> stemmer.cvc(2) # Sequence not consonant, vowel, consonant 0 >>> stemmer.cvc(3) # Final consonant is a member of {'w', 'x', 'y'} 0 >>> stemmer.b = "trace" >>> stemmer.k = len("trace") - 1 >>> stemmer.cvc(3) 1 Test the ends() (end substring matching) method. >>> stemmer.ends("verylongstring") # Supplied string longer than buffer 0 >>> stemmer.ends("ice") # String doesn't match 0 >>> stemmer.ends("ace") 1 Test the setto() method (replaces the suffix of the buffered string). >>> stemmer.j = 3 >>> stemmer.setto("ing") >>> stemmer.b 'tracing' Test the stemmer on various pluralised words. >>> plurals = ['caresses', 'flies', 'dies', 'mules', 'denied', ... 'died', 'agreed', 'owned', 'humbled', 'sized', ... 'meeting', 'stating', 'siezing', 'itemization', ... 'sensational', 'traditional', 'reference', 'colonizer', ... 'plotted'] >>> singles = [] >>> for plural in plurals: ... singles.append(stemmer.stem(plural)) >>> singles # doctest: +NORMALIZE_WHITESPACE ['caress', 'fli', 'die', 'mule', 'deni', 'die', 'agre', 'own', 'humbl', 'size', 'meet', 'state', 'siez', 'item', 'sensat', 'tradit', 'refer', 'colon', 'plot'] Unit tests for Regexp stemmer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ >>> patterns = "ed$|ing$|able$|s$" >>> stemmer = RegexpStemmer(patterns, 4) >>> stemmer.stem("red") 'red' >>> stemmer.stem("hurried") 'hurri' >>> stemmer.stem("advisable") 'advis' >>> stemmer.stem("impossible") 'impossible' Unit tests for Snowball stemmer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ >>> from nltk.stem.snowball import SnowballStemmer See which languages are supported. >>> SnowballStemmer.languages # doctest: +NORMALIZE_WHITESPACE ('danish', 'dutch', 'finnish', 'french', 'german', 'hungarian', 'italian', 'norwegian', 'portuguese', 'romanian', 'russian', 'spanish', 'swedish') Create a new instance of a language specific subclass. >>> stemmer_german = SnowballStemmer("german") Stem a word. >>> stemmer_german.stem(u"Schränke") u'schrank' Decide not to stem stopwords. >>> stemmer_german2 = SnowballStemmer("german", ignore_stopwords=True) >>> stemmer_german.stem(u"keinen") u'kein' >>> stemmer_german2.stem(u"keinen") u'keinen' Russian words both consisting of Cyrillic and Roman letters can be stemmed. >>> stemmer_russian = SnowballStemmer("russian") >>> print stemmer_russian.stem(u"авенантненькаÑ") авенантненьк >>> print stemmer_russian.stem(u"avenantnen'kai^a") avenantnen'k nltk-2.0~b9/nltk/test/sourcedstring.doctest0000644000175000017500000015751411374105242021003 0ustar bhavanibhavani================ Sourced Strings ================ "Sourced strings" are strings that are annotated with information about the location in a document where they were originally found. Sourced strings are subclassed from Python strings. As a result, they can usually be used anywhere a normal Python string can be used. Creating Sourced Strings ======================== A sourced string for a document can be constructed by calling the `SourcedString` constructor with two arguments: a Python string (containing the contents of the document), and a document identifier (such as a file name): >>> from nltk.sourcedstring import * >>> newt_contents = """\ ... She turned me into a newt! ... I got better.""" >>> newt_doc = SourcedString(newt_contents, 'newt.txt') >>> print repr(newt_doc) 'She turned me into a newt!\nI got better.'@[0:40] >>> newt = newt_doc.split()[5] # Find the sixth word. >>> print repr(newt) 'newt!'@[21:26] The suffix ``@[0:40]`` at the end of ``newt_doc``'s string representation indicates that it is a sourced string beginning at offset 0, and ending at offset 40. Similarly, the suffix ``@[21:26]`` at the end of ``newt``'s string representation indicates that it spans from offset 21 to offset 26. .. note:: The `SourcedString` constructor automatically delegates to either `SimpleSourcedByteString` or `SimpleSourcedUnicodeString`, depending on whether its first argument has type ``str`` or ``unicode``. The subclasses of `SourcedString` are discussed in more detail in `Unicode and Sourced Strings`_. Sourced strings can also be created using the `SourcedStringStream` class, which wraps an existing stream object, and causes its read methods to return sourced strings. >>> from StringIO import StringIO >>> stream = SourcedStringStream(StringIO(newt_contents)) >>> for line in stream: ... print repr(line) 'She turned me into a newt!\n'@[0:27] 'I got better.'@[27:40] Finally, some of NLTK's corpus readers can be instructed to return sourced strings instead of Python strings: >>> from nltk.corpus import gutenberg >>> emma_words = gutenberg.words('austen-emma.txt', sourced=True) >>> remembrance = emma_words[114] >>> print repr(remembrance) 'remembrance'@[552:563] >>> emma_sents = gutenberg.sents('austen-emma.txt', sourced=True) >>> emma_sents[28] # doctest: +NORMALIZE_WHITESPACE ['The'@[4980:4983], 'Woodhouses'@[4984:4994], 'were'@[4995:4999], 'first'@[5000:5005], 'in'@[5006:5008], 'consequence'@[5009:5020], 'there'@[5021:5026], '.'@[5026]] String Sources ============== The location where a sourced string was found is recorded using the ``source`` attribute: >>> newt.source StringSource('newt.txt', begin=21, end=26) >>> remembrance.source StringSource('austen-emma.txt', begin=552, end=563) Sources are encoded using `StringSource` objects, which consist of a document identifier along with information about the offsets of the characters that make up the string. These offsets are typically either byte offsets or character offsets. (As we'll see below, byte offsets and character offsets are not equivalent when used to describe unicode strings.) String Sources define four attributes that describe the location where a string was found: ``docid``, ``begin``, ``end``, and ``offsets``. The ``docid`` attribute contains an identifier (such as a filename) that names the document where the string was found: >>> newt.source.docid 'newt.txt' The ``begin`` and ``end`` attributes should be interpreted in the same way as Python slice indices. In particular, the ``begin`` index specifies the offset of the first character in the string; and the ``end`` index specifies the offset just past the last character in the string: >>> newt.source.begin 21 >>> newt.source.end 26 >>> newt_contents[newt.source.begin:newt.source.end] 'newt!' The ``offsets`` attribute returns a tuple of offsets specifying the location of each character in the document: >>> newt.source.offsets (21, 22, 23, 24, 25, 26) In particular, for a `SourcedString` ``s``, character ``s[i]`` begins at offset ``s.source.offsets[i]`` and ends at offset ``s.source.offsets[i+1]``. Note that the ``offsets`` list contains one more offset than there are characters in the string: >>> len(newt), len(newt.source.offsets) (5, 6) That's because the `StringSource` specifies both the begin offset and the end offset for each character. The ``begin`` and ``end`` attributes are always equal to the first and last elements of the ``offsets`` attribute, respectively: >>> assert newt.source.begin == newt.source.offsets[0] >>> assert newt.source.end == newt.source.offsets[-1] The `pprint()` method (which stands for "pretty-print") is helpful for showing the relationship between offsets and characters. In the following example, compare the pretty-printed document with the list of offsets in newt's source: >>> print newt_doc.pprint(wrap='\n') [=======================newt.txt=======================] 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+ |S|h|e| |t|u|r|n|e|d| |m|e| |i|n|t|o| |a| |n|e|w|t|!|\n| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+ [=========newt.txt========] 2 2 2 3 3 3 3 3 3 3 3 3 3 4 7 8 9 0 1 2 3 4 5 6 7 8 9 0 +-+-+-+-+-+-+-+-+-+-+-+-+-+ |I| |g|o|t| |b|e|t|t|e|r|.| +-+-+-+-+-+-+-+-+-+-+-+-+-+ >>> newt.source.offsets (21, 22, 23, 24, 25, 26) At first, it may seem redundant to keep track of the offsets for every character in a string -- for many strings, the offset of ``s[i]`` is simply ``s.begin+i``. However, when byte offsets are used to describe unicode characters, we can no longer assume that the characters in a string have consecutive offsets. In the following example, we construct a `SourcedString` from a utf-8 encoded byte string (thus ensuring that we are using byte offsets); and then decode that string to unicode. When we print the `SourcedString`, we can see that several of its characters span two bytes: >>> students_and_time = SourcedString("""\ ... Le temps est un grand ma\xc3\xaetre, dit-on, le malheur est \ ... qu'il tue ses \xc3\xa9l\xc3\xa8ves""", 'Berlioz').decode('utf-8') >>> print students_and_time.pprint() [==============================Berlioz===============================] 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 6 7 8 9 0 1 2 3 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+------+-+-+-+-+-+-+-+ |L|e| |t|e|m|p|s| |e|s|t| |u|n| |g|r|a|n|d| |m|a|\u00ee|t|r|e|,| |d|i| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+------+-+-+-+-+-+-+-+ [===============================Berlioz===============================] 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |t|-|o|n|,| |l|e| |m|a|l|h|e|u|r| |e|s|t| |q|u|'|i|l| |t|u|e| |s|e|s| | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ [=======Berlioz=======] 6 7 7 7 7 7 7 8 0 1 3 4 5 6 +------+-+------+-+-+-+ |\u00e9|l|\u00e8|v|e|s| +------+-+------+-+-+-+ StringSource Subclasses ----------------------- In order to efficiently encode the sources of strings with consecutive characters while also accommodating strings without consecutive characters, the `StringSource` class defines two subclasses: - `ConsecutiveCharStringSource` is used to describe the source of strings whose characters have consecutive offsets. In particular, it is used for byte strings with byte offsets; and unicode strings with character offsets. It is encoded using a document identifier, a begin offset, and an end offset: >>> ConsecutiveCharStringSource('newt.txt', begin=12, end=18) StringSource('newt.txt', begin=12, end=18) - `ContiguousCharStringSource` is used to describe the source of strings whose characters are contiguous, but do not necessarily have consecutive offsets. In particular, it is used for unicode strings with byte offsets. It is encoded using a document identifier and a tuple of character offsets. >>> ContiguousCharStringSource('newt.txt', offsets=[12, 15, 16, 18]) StringSource('newt.txt', offsets=(12, 15, 16, 18)) The `StringSource` class itself is an abstract base class; but its constructor automatically delegates to the appropriate subclass, depending on how it was called: >>> StringSource('newt.txt', begin=12, end=18) StringSource('newt.txt', begin=12, end=18) >>> type(StringSource('newt.txt', begin=12, end=18)) >>> StringSource('newt.txt', offsets=[12, 15, 16, 18]) StringSource('newt.txt', offsets=(12, 15, 16, 18)) >>> type(StringSource('newt.txt', offsets=[12, 15, 16, 18])) SourcedString Source Attributes ------------------------------- For convenience, the ``SourcedString`` class defines the attributes ``begin``, ``end``, and ``docid``. Their value is identical to the corresponding attribute of the string's source: >>> assert newt.begin == newt.source.begin >>> assert newt.end == newt.source.end >>> assert newt.docid == newt.source.docid As we'll see `below `_, these three attributes are only defined for "simple sourced strings" -- i.e., strings that correspond to a single substring of a document. They are not defined for "compound sourced strings," which are constructed by concatenating strings from multiple sources. Substrings of Sourced Strings ============================= Operations that return substrings of a `SourcedString` (such as slicing, indexing, and `split()`) return them as `SourcedString`s: >>> newt_doc[4:10] 'turned'@[4:10] >>> newt_doc[5] 'u'@[5] >>> newt_doc.split() # doctest: +NORMALIZE_WHITESPACE ['She'@[0:3], 'turned'@[4:10], 'me'@[11:13], 'into'@[14:18], 'a'@[19], 'newt!'@[21:26], 'I'@[27], 'got'@[29:32], 'better.'@[33:40]] >>> newt_doc[:4].strip() 'She'@[0:3] Most regular expression operations will also return a `SourcedString` when given a `SourcedString` as input: >>> import re >>> re.findall(r'\w*e\w*', newt_doc) # doctest: +NORMALIZE_WHITESPACE ['She'@[0:3], 'turned'@[4:10], 'me'@[11:13], 'newt'@[21:25], 'better'@[33:39]] >>> re.search(r'\w+ed', newt_doc).group() 'turned'@[4:10] The exception to this rule is the regular expression substitution operations, ``re.sub`` and ``re.subn``. See `Limitations`_ for more information. Compound Sourced Strings ======================== When sourced strings are concatenated with other strings, the result is a compound sourced string: >>> better_newt = 'My orange ' + newt_doc[21:25] + ' is ' + newt_doc[33:39] >>> print better_newt.pprint() [newt.tx] [==newt.txt=] 2 2 2 2 2 3 3 3 3 3 3 3 1 2 3 4 5 3 4 5 6 7 8 9 +----------+-+-+-+-+----+-+-+-+-+-+-+ |My orange |n|e|w|t| is |b|e|t|t|e|r| +----------+-+-+-+-+----+-+-+-+-+-+-+ >>> print repr(better_newt) 'My orange newt is better'@[...,21:25,...,33:39] Compound sourced strings keep track of the sources of all the substrings they were composed from. The pieces that make up a compound sourced string can be retrieved using the ``substrings`` attribute: >>> better_newt.substrings ('My orange ', 'newt'@[21:25], ' is ', 'better'@[33:39]) The substrings of a compound sourced string are always either simple sourced strings or Python strings, never compound sourced strings. Slicing Compound Sourced Strings -------------------------------- The type of object that is returned by slicing a compound sourced string will depend on what portion of the compound sourced string is covered by the slice. If the slice falls within a single Python substring, then it will be returned as a Python string: >>> better_newt[3:9] # Returns a Python string 'orange' If the slice falls within a single simple sourced string, then it will be returned as a simple sourced string: >>> better_newt[10:13] # Returns a simple sourced string 'new'@[21:24] Otherwise, it will be returned as a compound sourced string: >>> better_newt[3:14] # Returns a compound sourced string 'orange newt'@[...,21:25] Note that a single-character sourced string may never be compound; therefore, indexing a Sourced String will always return either a Python character or a simple sourced string: >>> better_newt[8] 'e' >>> better_newt[10] 'n'@[21] If you are not sure what type of string will result from an operation, you can use ``isinstance()`` to check whether it's a Python string, a simple sourced string, or a compound sourced string: >>> def check_type(s): ... if isinstance(s, SimpleSourcedString): ... print 'simple sourced string' ... elif isinstance(s, CompoundSourcedString): ... print 'compound sourced string' ... else: ... print 'python string' >>> check_type(better_newt[1:2]) python string >>> check_type(better_newt[10:13]) simple sourced string >>> check_type(better_newt[3:14]) compound sourced string Alternatively, you can use ``hasattr()`` to check whether a substring has a source: >>> hasattr(better_newt[1:2], 'source') # Python string False >>> hasattr(better_newt[10:13], 'source') # Simple sourced string True >>> hasattr(better_newt[3:14], 'source') # Compound sourced string False Concatenating Compound Sourced Strings -------------------------------------- When two compound sourced strings ``c1`` and ``c2`` are concatenated together, the resulting compound sourced string ``c3`` does *not* contain ``c1`` and ``c2`` themselves as substrings. Instead, ``c3`` contains ``c1``'s substrings and ``c2``'s substrings. This "flattening" ensures that the substrings of a compound sourced string will always be either Python strings or simple sourced strings, and never compound strings. >>> c1 = better_newt >>> c2 = ' than your ' + better_newt[3:14] >>> c3 = c1+c2 >>> for substring in c3.substrings: ... print '%25r %s' % (substring, type(substring).__name__) 'My orange ' str 'newt'@[21:25] SimpleSourcedByteString ' is ' str 'better'@[33:39] SimpleSourcedByteString ' than your orange ' str 'newt'@[21:25] SimpleSourcedByteString Multi-Document Sourced Strings ------------------------------ It is possible to concatenate sourced strings that come from different documents: >>> doc2 = SourcedString("Hello World", 'hello.txt') >>> cello = ("Does "+newt[:-1].capitalize()+"on like my " + ... doc2[:5].replace('H','C')+"?") >>> print cello.pprint() [newt.tx] [hello.t] 2 2 2 2 2 1 2 3 4 5 1 2 3 4 5 +-----+-+-+-+-+------------+-+-+-+-+-+ |Does |N|e|w|t|on like my C|e|l|l|o|?| +-----+-+-+-+-+------------+-+-+-+-+-+ Transforming Sourced Strings ============================ The `SourcedString` methods that return a modified string will preseve source information whenever possible. Case Modification ----------------- Case modification methods return a sourced string with the same source as the original string: >>> sent = newt_doc.split('\n')[0] >>> sent.lower() 'she turned me into a newt!'@[0:26] >>> sent.title() 'She Turned Me Into A Newt!'@[0:26] >>> better_newt.title() 'My Orange Newt Is Better'@[...,21:25,...,33:39] In particular, the characters that are modified keep their original source information. This is in contrast with the `replace()` method (discussed below), where the replacement string has its own source information. Justification methods --------------------- The string justification methods preserve source information of the original string. The padding substring will usually be sourceless (unless you supply a sourced string as the fill character): >>> print newt.rjust(15).pprint() [=newt.txt] 2 2 2 2 2 2 1 2 3 4 5 6 +----------+-+-+-+-+-+ | |n|e|w|t|!| +----------+-+-+-+-+-+ >>> print newt.center(15, '.').pprint() [=newt.txt] 2 2 2 2 2 2 1 2 3 4 5 6 +-----+-+-+-+-+-+-----+ |.....|n|e|w|t|!|.....| +-----+-+-+-+-+-+-----+ Replacement Method ------------------ The ``replace`` method preserves string information for both the original string and the replacement string: >>> print sent.replace('newt', doc2[6:]).pprint() [=================newt.txt================][hello.txt][n] 1 1 1 1 1 1 1 1 1 1 2 2 1 12 2 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 16 7 8 9 0 15 6 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-++-+-+-+-+-++-+ |S|h|e| |t|u|r|n|e|d| |m|e| |i|n|t|o| |a| ||W|o|r|l|d||!| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-++-+-+-+-+-++-+ If the replacement string is a Python string, then the corresponding substring will be sourceless: >>> print newt.replace('!', 'on').pprint() [newt.tx] 2 2 2 2 2 1 2 3 4 5 +-+-+-+-+--+ |n|e|w|t|on| +-+-+-+-+--+ Other Modifications ------------------- Unfortunately, several other modification methods (such as ``str.join()`` and ``re.sub()``) do not always preserve source information. See `Limitations`_ for more details. Unicode and Sourced Strings =========================== The `SourcedString` class is an abstract base class. It defines two abtract subclasses, each of which defines two concrete subclasses:: SourcedString (abstract) | +-- SimpleSourcedString (abstract) | | | +-- SimpleSourcedByteString | | | +-- SimpleSourcedUnicodeString | +-- CompoundSourcedString (abstract) | +-- CompoundSourcedByteString | +-- CompoundSourcedUnicodeString The two ``-ByteString`` classes are subclassed from ``str``; and the two ``-UnicodeString`` classes are subclassed from ``unicode``. When the `SourcedString` constructor is called directly, it will delegate to the appropriate subclass, based on the type of the content string: >>> byte_str = 'He was a tall lumberjack.' >>> SourcedString(byte_str, 'lumberjack.txt') 'He was a tall lumberjack.'@[0:25] >>> type(SourcedString(byte_str, 'lumberjack.txt')) >>> unicode_str = u'He was a tall lumberjack.' >>> SourcedString(unicode_str, 'lumberjack.txt') u'He was a tall lumberjack.'@[0:25] >>> type(SourcedString(unicode_str, 'lumberjack.txt')) The ``CompoundSourced-*-String`` classes are not usually instantiated directly; instead, they are created by concatenating sourced strings with other strings. See `Compound Sourced Strings`_ for details. Equality ======== Two sourced strings are considered equal if their contents are equal, even if their sources differ: >>> newt_doc[3], newt_doc[10] (' '@[3], ' '@[10]) >>> newt_doc[3] == newt_doc[10] True Sourced strings may also be compared for equality with non-sourced strings: >>> newt == 'newt!' True >>> cello == "Does Newton like my Cello?" True The fact that string equality ignores sources is important in ensuring that sourced strings act like normal strings. In particular, it allows sourced strings to be used with code that was originally intended to process plain Python strings. E.g., this fact allows sourced strings to be parsed by standard parsing algorithms (which have no knowledge of sourced strings). If you wish to determine whether two simple sourced strings correspond to the same location in a document, simply compare their ``source`` attribute: >>> x = newt_doc[4:10] >>> y = newt_doc.split()[1] >>> z = x.upper() >>> (x, y, z) ('turned'@[4:10], 'turned'@[4:10], 'TURNED'@[4:10]) >>> x==y, x.source==y.source (True, True) >>> x==z, x.source==z.source (False, True) If you may be dealing with compound sourced strings, then you should use the ``sources`` attribute instead. This attribute is defined for both simple and compound sourced strings, and contains a sorted tuple of ``(index,source)`` pairs. Each such pair specifies that the source of the substring starting at ``index``, and extending ``len(source)`` characters, is ``source``: >>> newt.sources ((0, StringSource('newt.txt', begin=21, end=26)),) >>> cello.sources # doctest: +NORMALIZE_WHITESPACE ((5, StringSource('newt.txt', begin=21, end=25)), (20, StringSource('hello.txt', begin=0, end=0)), (21, StringSource('hello.txt', begin=1, end=5))) If you wish to compare two strings, and they might be simple sourced strings, compound sourced strings, or plain Python strings, then you can use ``getattr(s, 'sources', ())``, which will return ``s.sources`` for sourced strings, and ``()`` for plain Python strings: >>> print getattr(cello[:4], 'sources', ()) () >>> print getattr(cello[5:9], 'sources', ()) ((0, StringSource('newt.txt', begin=21, end=25)),) >>> print getattr(cello[17:], 'sources', ()) # doctest: +NORMALIZE_WHITESPACE ((3, StringSource('hello.txt', begin=0, end=0)), (4, StringSource('hello.txt', begin=1, end=5))) Sourced Strings as Dictionary Keys and Set Values ================================================== When sourced strings are used as dictionary keys, or placed in sets, we would sometimes like to ensure that strings with different sources are treated as different values. However, the fact that sourced string equality ignores sources makes this impossible. To get around this problem, you can use the sourced string's ``source`` (or ``sources`` for compound strings), or a tuple containing the sourced string and its ``source``, as a dictionary key or set value: >>> animals_contents = 'the dog and the cat' >>> animals = SourcedString(animals_contents, source='animals.txt') >>> # Create a list of words, including some case-normalized duplicates >>> words = animals.split() + re.findall('DOG|CAT', animals.upper()) >>> sorted(words) # doctest: +NORMALIZE_WHITESPACE ['CAT'@[16:19], 'DOG'@[4:7], 'and'@[8:11], 'cat'@[16:19], 'dog'@[4:7], 'the'@[0:3], 'the'@[12:15]] >>> # Show the set of unique words (using string equality). Note >>> # that the second occurence of 'the' was discarded. >>> sorted(set(words)) # doctest: +NORMALIZE_WHITESPACE ['CAT'@[16:19], 'DOG'@[4:7], 'and'@[8:11], 'cat'@[16:19], 'dog'@[4:7], 'the'@[0:3]] >>> # Show the set of locations where words occur. Note that >>> # the locations of 'cat' and 'dog' each appear only once. >>> sorted(set(word.source for word in words)) # doctest: +NORMALIZE_WHITESPACE [StringSource('animals.txt', begin=0, end=3), StringSource('animals.txt', begin=4, end=7), StringSource('animals.txt', begin=8, end=11), StringSource('animals.txt', begin=12, end=15), StringSource('animals.txt', begin=16, end=19)] >>> # Show the set of unique (string, location) pairs. Note >>> # that both occurences of 'the' appear; and that both copies >>> # of 'dog' and 'cat' appear. >>> sorted(set((word.source, word) for word in words)) # doctest: +NORMALIZE_WHITESPACE [(StringSource('animals.txt', begin=0, end=3), 'the'@[0:3]), (StringSource('animals.txt', begin=4, end=7), 'DOG'@[4:7]), (StringSource('animals.txt', begin=4, end=7), 'dog'@[4:7]), (StringSource('animals.txt', begin=8, end=11), 'and'@[8:11]), (StringSource('animals.txt', begin=12, end=15), 'the'@[12:15]), (StringSource('animals.txt', begin=16, end=19), 'CAT'@[16:19]), (StringSource('animals.txt', begin=16, end=19), 'cat'@[16:19])] Limitations =========== Some types of string manipulation can cause source information to be lost. In particular, functions and methods that accesses a sourced string using the low-level "buffer" interface will often bypass the sourced string's ability to preserve source information. Operations that are known to result in a loss of source information are listed below: - ``str.join()``, where the joining string is not sourced: >>> '+'.join(sent.split()) 'She+turned+me+into+a+newt!' - ``str.replace()``, where the original string is not sourced: >>> turned = newt_doc.split()[1] >>> 'I twisted around'.replace('twisted', turned) 'I turned around' - String formatting, where the format string is not sourced: >>> 'My %s is %s' % (newt_doc[21:25], newt_doc[33:39]) 'My newt is better' - Regular expression substitution, where the regular expression pattern string is not sourced: >>> re.sub('orange', 'green', better_newt) 'My green newt is better' >>> re.subn('orange', 'green', better_newt, 1) ('My green newt is better', 1) - String justification methods, where the string being justified is unsourced but the fill character is sourced: >>> 'coconut'.center(25, newt[-1]) '!!!!!!!!!coconut!!!!!!!!!' >>> 'coconut'.ljust(25, newt[-1]) 'coconut!!!!!!!!!!!!!!!!!!' >>> 'coconut'.rjust(25, newt[-1]) '!!!!!!!!!!!!!!!!!!coconut' .. ======================= Regression Tests =========================== Regression Tests ================ String Sources -------------- ConsecutiveCharStringSource ~~~~~~~~~~~~~~~~~~~~~~~~~~~ String representations: >>> source = ConsecutiveCharStringSource('coconut.txt', 5, 18) >>> repr(source) "StringSource('coconut.txt', begin=5, end=18)" >>> str(source) '@coconut.txt[5:18]' Attributes: >>> source = ConsecutiveCharStringSource('coconut.txt', 5, 18) >>> source.begin, source.end, source.docid (5, 18, 'coconut.txt') >>> source.docid 'coconut.txt' >>> source.offsets (5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18) Begin and end must be integers (or longs): >>> ConsecutiveCharStringSource('coconut.txt', 5, 10) StringSource('coconut.txt', begin=5, end=10) >>> ConsecutiveCharStringSource('coconut.txt', 5L, 10L) StringSource('coconut.txt', begin=5L, end=10L) >>> ConsecutiveCharStringSource('coconut.txt', 5.3, 10) Traceback (most recent call last): . . . TypeError: begin attribute expected an integer >>> ConsecutiveCharStringSource('coconut.txt', 5, 10.3) Traceback (most recent call last): . . . TypeError: end attribute expected an integer >>> ConsecutiveCharStringSource('coconut.txt', 'five', 10) Traceback (most recent call last): . . . TypeError: begin attribute expected an integer >>> ConsecutiveCharStringSource('coconut.txt', 5, 'ten') Traceback (most recent call last): . . . TypeError: end attribute expected an integer The end index must be greater than or equal to the begin offset: >>> ConsecutiveCharStringSource('coconut.txt', 5, 6) StringSource('coconut.txt', begin=5, end=6) >>> ConsecutiveCharStringSource('coconut.txt', 5, 5) StringSource('coconut.txt', begin=5, end=5) >>> ConsecutiveCharStringSource('coconut.txt', 5, 4) Traceback (most recent call last): . . . ValueError: begin must be less than or equal to end The begin and end offsets may be negative: >>> ConsecutiveCharStringSource('coconut.txt', -5, 5) StringSource('coconut.txt', begin=-5, end=5) >>> ConsecutiveCharStringSource('coconut.txt', -5, -2) StringSource('coconut.txt', begin=-5, end=-2) Length-1 source: >>> source = ConsecutiveCharStringSource('coconut.txt', 5, 6) >>> repr(source) "StringSource('coconut.txt', begin=5, end=6)" >>> str(source) '@coconut.txt[5]' >>> len(source) 1 >>> source.begin, source.end, source.docid (5, 6, 'coconut.txt') >>> source.offsets (5, 6) zero-length source: >>> source = ConsecutiveCharStringSource('coconut.txt', 5, 5) >>> repr(source) "StringSource('coconut.txt', begin=5, end=5)" >>> str(source) '@coconut.txt[5:5]' >>> len(source) 0 >>> source.begin, source.end, source.docid (5, 5, 'coconut.txt') >>> source.offsets (5,) Indexing: >>> source = ConsecutiveCharStringSource('coconut.txt', 15, 21) >>> for i in range(-len(source), len(source)): ... print ' source[%2d] = %r' % (i, source[i]) source[-6] = StringSource('coconut.txt', begin=15, end=16) source[-5] = StringSource('coconut.txt', begin=16, end=17) source[-4] = StringSource('coconut.txt', begin=17, end=18) source[-3] = StringSource('coconut.txt', begin=18, end=19) source[-2] = StringSource('coconut.txt', begin=19, end=20) source[-1] = StringSource('coconut.txt', begin=20, end=21) source[ 0] = StringSource('coconut.txt', begin=15, end=16) source[ 1] = StringSource('coconut.txt', begin=16, end=17) source[ 2] = StringSource('coconut.txt', begin=17, end=18) source[ 3] = StringSource('coconut.txt', begin=18, end=19) source[ 4] = StringSource('coconut.txt', begin=19, end=20) source[ 5] = StringSource('coconut.txt', begin=20, end=21) >>> source[len(source)] Traceback (most recent call last): . . . IndexError: StringSource index out of range >>> source[-len(source)-1] Traceback (most recent call last): . . . IndexError: StringSource index out of range Slicing: >>> def slice_test(source, *indices): ... """Print a table showing the result of slicing the given ... source, using each of the given indices as a start or end ... index for the slice.""" ... print ' |'+' '.join(str(j).center(5) for j in indices) ... print '-----+'+'------'*len(indices) ... for i in indices: ... print '%4s |' % i, ... for j in indices: ... if i is None and j is None: sliced_source = source[:] ... elif i is None: sliced_source = source[:j] ... elif j is None: sliced_source = source[i:] ... else: sliced_source = source[i:j] ... print '%2s:%-2s' % (sliced_source.begin, sliced_source.end), ... assert sliced_source.docid == 'coconut.txt' ... print ... >>> source = ConsecutiveCharStringSource('coconut.txt', 15, 28) >>> slice_test(source, None, 0, 1, len(source)-1, len(source), 100, ... -1, -len(source)+1, -len(source), -100) | None 0 1 12 13 100 -1 -12 -13 -100 -----+------------------------------------------------------------ None | 15:28 15:15 15:16 15:27 15:28 15:28 15:27 15:16 15:15 15:15 0 | 15:28 15:15 15:16 15:27 15:28 15:28 15:27 15:16 15:15 15:15 1 | 16:28 16:16 16:16 16:27 16:28 16:28 16:27 16:16 16:16 16:16 12 | 27:28 27:27 27:27 27:27 27:28 27:28 27:27 27:27 27:27 27:27 13 | 28:28 28:28 28:28 28:28 28:28 28:28 28:28 28:28 28:28 28:28 100 | 28:28 28:28 28:28 28:28 28:28 28:28 28:28 28:28 28:28 28:28 -1 | 27:28 27:27 27:27 27:27 27:28 27:28 27:27 27:27 27:27 27:27 -12 | 16:28 16:16 16:16 16:27 16:28 16:28 16:27 16:16 16:16 16:16 -13 | 15:28 15:15 15:16 15:27 15:28 15:28 15:27 15:16 15:15 15:15 -100 | 15:28 15:15 15:16 15:27 15:28 15:28 15:27 15:16 15:15 15:15 >>> source = ConsecutiveCharStringSource('coconut.txt', 50, 53) >>> slice_test(source, -4, -3, 0, -2, 1, -1, 2, 3, 4, 5) | -4 -3 0 -2 1 -1 2 3 4 5 -----+------------------------------------------------------------ -4 | 50:50 50:50 50:50 50:51 50:51 50:52 50:52 50:53 50:53 50:53 -3 | 50:50 50:50 50:50 50:51 50:51 50:52 50:52 50:53 50:53 50:53 0 | 50:50 50:50 50:50 50:51 50:51 50:52 50:52 50:53 50:53 50:53 -2 | 51:51 51:51 51:51 51:51 51:51 51:52 51:52 51:53 51:53 51:53 1 | 51:51 51:51 51:51 51:51 51:51 51:52 51:52 51:53 51:53 51:53 -1 | 52:52 52:52 52:52 52:52 52:52 52:52 52:52 52:53 52:53 52:53 2 | 52:52 52:52 52:52 52:52 52:52 52:52 52:52 52:53 52:53 52:53 3 | 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 4 | 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 5 | 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 ContiguousCharStringSource ~~~~~~~~~~~~~~~~~~~~~~~~~~ String representations: >>> offsets = [5, 7, 8, 9, 13, 18] >>> source = ContiguousCharStringSource('coconut.txt', offsets) >>> repr(source) "StringSource('coconut.txt', offsets=(5, 7, 8, 9, 13, 18))" >>> str(source) '@coconut.txt[5:18]' Attributes: >>> source = ContiguousCharStringSource('coconut.txt', offsets) >>> source.begin, source.end, source.docid (5, 18, 'coconut.txt') >>> source.docid 'coconut.txt' >>> source.offsets (5, 7, 8, 9, 13, 18) Offsets must be integers (or longs): >>> ContiguousCharStringSource.CONSTRUCTOR_CHECKS_OFFSETS = True >>> ContiguousCharStringSource('coconut.txt', [5, 6L, 7]) StringSource('coconut.txt', offsets=(5, 6L, 7)) >>> ContiguousCharStringSource('coconut.txt', [6.2]) Traceback (most recent call last): . . . TypeError: offsets must be integers >>> ContiguousCharStringSource('coconut.txt', ['five']) Traceback (most recent call last): . . . TypeError: offsets must be integers Offsets must be monotonic increasing: >>> ContiguousCharStringSource('coconut.txt', [5, 6, 7]) StringSource('coconut.txt', offsets=(5, 6, 7)) >>> ContiguousCharStringSource('coconut.txt', [5, 5]) StringSource('coconut.txt', offsets=(5, 5)) >>> ContiguousCharStringSource('coconut.txt', [7, 6, 5]) Traceback (most recent call last): . . . TypeError: offsets must be monotonic increasing Offsets may be negative: >>> ContiguousCharStringSource('coconut.txt', [-5, 5]) StringSource('coconut.txt', offsets=(-5, 5)) >>> ContiguousCharStringSource('coconut.txt', [-5, -2]) StringSource('coconut.txt', offsets=(-5, -2)) Length-1 source: >>> source = ContiguousCharStringSource('coconut.txt', [5,6]) >>> repr(source) "StringSource('coconut.txt', offsets=(5, 6))" >>> str(source) '@coconut.txt[5]' >>> len(source) 1 >>> source.begin, source.end, source.docid (5, 6, 'coconut.txt') >>> source.offsets (5, 6) zero-length source: >>> source = ContiguousCharStringSource('coconut.txt', [5]) >>> repr(source) "StringSource('coconut.txt', offsets=(5,))" >>> str(source) '@coconut.txt[5:5]' >>> len(source) 0 >>> source.begin, source.end, source.docid (5, 5, 'coconut.txt') >>> source.offsets (5,) Indexing: >>> source = ContiguousCharStringSource('coconut.txt', range(15, 22)) >>> for i in range(-len(source), len(source)): ... print ' source[%2d] = %r' % (i, source[i]) source[-6] = StringSource('coconut.txt', offsets=(15, 16)) source[-5] = StringSource('coconut.txt', offsets=(16, 17)) source[-4] = StringSource('coconut.txt', offsets=(17, 18)) source[-3] = StringSource('coconut.txt', offsets=(18, 19)) source[-2] = StringSource('coconut.txt', offsets=(19, 20)) source[-1] = StringSource('coconut.txt', offsets=(20, 21)) source[ 0] = StringSource('coconut.txt', offsets=(15, 16)) source[ 1] = StringSource('coconut.txt', offsets=(16, 17)) source[ 2] = StringSource('coconut.txt', offsets=(17, 18)) source[ 3] = StringSource('coconut.txt', offsets=(18, 19)) source[ 4] = StringSource('coconut.txt', offsets=(19, 20)) source[ 5] = StringSource('coconut.txt', offsets=(20, 21)) >>> source[len(source)] Traceback (most recent call last): . . . IndexError: StringSource index out of range >>> source[-len(source)-1] Traceback (most recent call last): . . . IndexError: StringSource index out of range Slicing: >>> def slice_test(source, *indices): ... """Print a table showing the result of slicing the given ... source, using each of the given indices as a start or end ... index for the slice.""" ... print ' |'+' '.join(str(j).center(5) for j in indices) ... print '-----+'+'------'*len(indices) ... for i in indices: ... print '%4s |' % i, ... for j in indices: ... if i is None and j is None: sliced_source = source[:] ... elif i is None: sliced_source = source[:j] ... elif j is None: sliced_source = source[i:] ... else: sliced_source = source[i:j] ... print '%2s:%-2s' % (sliced_source.begin, sliced_source.end), ... assert sliced_source.docid == 'coconut.txt' ... print ... >>> source = ContiguousCharStringSource('coconut.txt', range(15, 29)) >>> slice_test(source, None, 0, 1, len(source)-1, len(source), 100, ... -1, -len(source)+1, -len(source), -100) | None 0 1 12 13 100 -1 -12 -13 -100 -----+------------------------------------------------------------ None | 15:28 15:15 15:16 15:27 15:28 15:28 15:27 15:16 15:15 15:15 0 | 15:28 15:15 15:16 15:27 15:28 15:28 15:27 15:16 15:15 15:15 1 | 16:28 16:16 16:16 16:27 16:28 16:28 16:27 16:16 16:16 16:16 12 | 27:28 27:27 27:27 27:27 27:28 27:28 27:27 27:27 27:27 27:27 13 | 28:28 28:28 28:28 28:28 28:28 28:28 28:28 28:28 28:28 28:28 100 | 28:28 28:28 28:28 28:28 28:28 28:28 28:28 28:28 28:28 28:28 -1 | 27:28 27:27 27:27 27:27 27:28 27:28 27:27 27:27 27:27 27:27 -12 | 16:28 16:16 16:16 16:27 16:28 16:28 16:27 16:16 16:16 16:16 -13 | 15:28 15:15 15:16 15:27 15:28 15:28 15:27 15:16 15:15 15:15 -100 | 15:28 15:15 15:16 15:27 15:28 15:28 15:27 15:16 15:15 15:15 >>> source = ContiguousCharStringSource('coconut.txt', range(50, 54)) >>> slice_test(source, -4, -3, 0, -2, 1, -1, 2, 3, 4, 5) | -4 -3 0 -2 1 -1 2 3 4 5 -----+------------------------------------------------------------ -4 | 50:50 50:50 50:50 50:51 50:51 50:52 50:52 50:53 50:53 50:53 -3 | 50:50 50:50 50:50 50:51 50:51 50:52 50:52 50:53 50:53 50:53 0 | 50:50 50:50 50:50 50:51 50:51 50:52 50:52 50:53 50:53 50:53 -2 | 51:51 51:51 51:51 51:51 51:51 51:52 51:52 51:53 51:53 51:53 1 | 51:51 51:51 51:51 51:51 51:51 51:52 51:52 51:53 51:53 51:53 -1 | 52:52 52:52 52:52 52:52 52:52 52:52 52:52 52:53 52:53 52:53 2 | 52:52 52:52 52:52 52:52 52:52 52:52 52:52 52:53 52:53 52:53 3 | 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 4 | 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 5 | 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 53:53 Sourced Strings --------------- The following helper function that checks a sourced string's characters to make sure that the string they come from is what it should be. It looks in ``check.documents[docid]`` for the text of the document named ``docid``. >>> def check(sourced_string): ... for char in sourced_string: ... if isinstance(char, SourcedString): ... document = check.documents[char.docid] ... source_char = document[char.begin:char.end] ... assert (char == source_char or ... (isinstance(source_char, str) and ... isinstance(char, unicode) and ... char.decode('utf-8') == source_char)) >>> check.documents = {} Constructing string tokens: >>> from nltk.data import * >>> s = ("Good muffins cost $3.88\nin New York. Please buy me\n" ... "two of them.\n\nThanks.") >>> check.documents['muffins.txt'] = s >>> doc = SourcedString(s, source='muffins.txt') SourcedString indexing: >>> for i in 0, 1, 50, 72: ... check(doc[i]); check(doc[-i]) ... assert doc[i] == s[i] ... assert doc[-i] == s[-i] >>> doc[8], s[8] ('f'@[8], 'f') >>> doc[-1], s[-1], s[72] ('.'@[72], '.', '.') >>> doc[-5], s[-5], s[68] ('a'@[68], 'a', 'a') >>> doc[74] Traceback (most recent call last): . . . IndexError: string index out of range >>> doc[-74] Traceback (most recent call last): . . . IndexError: string index out of range >>> in_new_york = doc[-49:-38]; in_new_york 'in New York'@[24:35] >>> doc[27], in_new_york[3], s[27] ('N'@[27], 'N'@[27], 'N') SourcedString slicing: >>> len(in_new_york) 11 >>> def test_slice(sstring, string, print_indices, start, stop): ... check(sstring) ... assert (sstring == string) ... if (start in print_indices and stop in print_indices): ... s_repr = re.sub(r'^(.{30}).*(.{15})$', r'\1...\2', ... repr(sstring)) ... print 's[%4s:%4s] = %r' % (start, stop, s_repr) >>> def test_slices(sstring, string, test_indices, print_indices): ... test_slice(sstring[:], string[:], print_indices, '', '') ... for i in test_indices: ... test_slice(sstring[i:], string[i:], print_indices, i, '') ... test_slice(sstring[:i], string[:i], print_indices, '', i) ... for start in test_indices: ... for stop in test_indices: ... test_slice(sstring[start:stop], string[start:stop], ... print_indices, start, stop) >>> test_slices(in_new_york, 'in New York', ... range(-12, 13)+[None,100,-100, -20, 20], ... ('', 0, 1, -1, 5)) s[ : ] = "'in New York'@[24:35]" s[ -1: ] = "'k'@[34]" s[ : -1] = "'in New Yor'@[24:34]" s[ 0: ] = "'in New York'@[24:35]" s[ : 0] = "''@[24:24]" s[ 1: ] = "'n New York'@[25:35]" s[ : 1] = "'i'@[24]" s[ 5: ] = "'w York'@[29:35]" s[ : 5] = "'in Ne'@[24:29]" s[ -1: -1] = "''@[34:34]" s[ -1: 0] = "''@[34:34]" s[ -1: 1] = "''@[34:34]" s[ -1: 5] = "''@[34:34]" s[ 0: -1] = "'in New Yor'@[24:34]" s[ 0: 0] = "''@[24:24]" s[ 0: 1] = "'i'@[24]" s[ 0: 5] = "'in Ne'@[24:29]" s[ 1: -1] = "'n New Yor'@[25:34]" s[ 1: 0] = "''@[25:25]" s[ 1: 1] = "''@[25:25]" s[ 1: 5] = "'n Ne'@[25:29]" s[ 5: -1] = "'w Yor'@[29:34]" s[ 5: 0] = "''@[29:29]" s[ 5: 1] = "''@[29:29]" s[ 5: 5] = "''@[29:29]" >>> check(in_new_york[:]) >>> assert (in_new_york[:] == ... 'in New York'[:]) >>> for i in range(-12, 13)+[None,100,-100, -20, 20]: ... check(in_new_york[:i]) ... check(in_new_york[i:]) ... assert (in_new_york[i:] == 'in New York'[i:]) ... assert (in_new_york[:i] == 'in New York'[:i]) Misc other tests: >>> doc[5:12] 'muffins'@[5:12] >>> doc[:4] 'Good'@[0:4] >>> doc[-7:] 'Thanks.'@[66:73] >>> doc[-7:-1] 'Thanks'@[66:72] >>> doc[-46:-38] 'New York'@[27:35] >>> tok = doc[-49:-38] >>> tok[:] 'in New York'@[24:35] >>> tok[:2] 'in'@[24:26] >>> tok[3:] 'New York'@[27:35] >>> tok[3:4] 'N'@[27] When a token slice is taken, and the step is not 1, a plain unicode string is returned: >>> tok[::-1] 'kroY weN ni' >>> tok[1:-1:2] 'nNwYr' Regular expressions can be used to search SourcedStrings: >>> import re >>> intoks = re.findall('in', doc) >>> print intoks ['in'@[9:11], 'in'@[24:26]] Two tokens with the same string contents compare equal even if their source/begin/end differ: >>> intoks[0] == intoks[1] True Sourced strings can also be compared for equality with simple strings: >>> intoks[0] == 'in' True Case manipulation: >>> tok.capitalize() 'In new york'@[24:35] >>> tok.lower() 'in new york'@[24:35] >>> tok.upper() 'IN NEW YORK'@[24:35] >>> tok.swapcase() 'IN nEW yORK'@[24:35] >>> tok.title() 'In New York'@[24:35] Stripping: >>> wstok = SourcedString(u' Test ', 'source') >>> wstok.lstrip() u'Test '@[3:10] >>> wstok.rstrip() u' Test'@[0:7] >>> wstok.strip() u'Test'@[3:7] Splitting: >>> doc.split() # doctest: +NORMALIZE_WHITESPACE ['Good'@[0:4], 'muffins'@[5:12], 'cost'@[13:17], '$3.88'@[18:23], 'in'@[24:26], 'New'@[27:30], 'York.'@[31:36], 'Please'@[38:44], 'buy'@[45:48], 'me'@[49:51], 'two'@[52:55], 'of'@[56:58], 'them.'@[59:64], 'Thanks.'@[66:73]] >>> doc.split(None, 5) # doctest: +NORMALIZE_WHITESPACE ['Good'@[0:4], 'muffins'@[5:12], 'cost'@[13:17], '$3.88'@[18:23], 'in'@[24:26], 'New York. Please buy me\ntwo of them.\n\nThanks.'@[27:73]] >>> doc.split('\n') # doctest: +NORMALIZE_WHITESPACE ['Good muffins cost $3.88'@[0:23], 'in New York. Please buy me'@[24:51], 'two of them.'@[52:64], ''@[65:65], 'Thanks.'@[66:73]] >>> doc.split('\n', 1) # doctest: +NORMALIZE_WHITESPACE ['Good muffins cost $3.88'@[0:23], 'in New York. Please buy me\ntwo of them.\n\nThanks.'@[24:73]] >>> doc.rsplit() # doctest: +NORMALIZE_WHITESPACE ['Good'@[0:4], 'muffins'@[5:12], 'cost'@[13:17], '$3.88'@[18:23], 'in'@[24:26], 'New'@[27:30], 'York.'@[31:36], 'Please'@[38:44], 'buy'@[45:48], 'me'@[49:51], 'two'@[52:55], 'of'@[56:58], 'them.'@[59:64], 'Thanks.'@[66:73]] >>> doc.rsplit(None, 5) # doctest: +NORMALIZE_WHITESPACE ['Good muffins cost $3.88\nin New York. Please buy'@[0:48], 'me'@[49:51], 'two'@[52:55], 'of'@[56:58], 'them.'@[59:64], 'Thanks.'@[66:73]] >>> doc.rsplit('\n') # doctest: +NORMALIZE_WHITESPACE ['Good muffins cost $3.88'@[0:23], 'in New York. Please buy me'@[24:51], 'two of them.'@[52:64], ''@[65:65], 'Thanks.'@[66:73]] >>> doc.rsplit('\n', 3) # doctest: +NORMALIZE_WHITESPACE ['Good muffins cost $3.88\nin New York. Please buy me'@[0:51], 'two of them.'@[52:64], ''@[65:65], 'Thanks.'@[66:73]] Adding adjacent string tokens gives new string tokens: >>> doc[:4] + doc[4:12] 'Good muffins'@[0:12] Adding empty strings to string tokens gives string tokens: >>> tok + '' 'in New York'@[24:35] >>> '' + tok 'in New York'@[24:35] All other add operations give basic strings: >>> 'not '+tok 'not in New York'@[...,24:35] >>> doc[:4] + doc[12:17] 'Good cost'@[0:4,12:17] Regexps: >>> sent = newt_doc.split('\n')[1] >>> re.sub('better', 'worse', sent) 'I got worse.' >>> SourcedStringRegexp('better').sub('worse', sent) 'I got worse.'@[27:33,...,39:40] >>> SourcedStringRegexp.patch_re_module() >>> re.sub('better', 'worse', sent) 'I got worse.'@[27:33,...,39:40] >>> SourcedStringRegexp.unpatch_re_module() >>> re.sub('better', 'worse', sent) 'I got worse.' Str/Unicode Interactions ------------------------ >>> x = SourcedString('byte string \xcc', 'str') >>> y = SourcedString(u'unicode string \ubbbb', 'unicode') Any operation that combines a byte string with a unicode string will first decode the byte string using the default encoding. As a result, all of the following operations raise an exception (since the string ``x`` can't be decoded using the ASCII encoding): >>> x+y Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y+x Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> x.__radd__(y) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.__radd__(x) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> x.find(y) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.find(x) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> x.lstrip(y) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.lstrip(x) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> x.rstrip(y) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.rstrip(x) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> x.strip(y) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.strip(x) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> x.split(y) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.split(x) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> x.rsplit(y) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.rsplit(x) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> x.partition(y) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.partition(x) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> x.rpartition(y) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.rpartition(x) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> x.join(y) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.join(x) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 0: ordinal not in range(128) >>> x.center(100, y[-1]) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.center(100, x[-1]) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 0: ordinal not in range(128) >>> x.ljust(100, y[-1]) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.ljust(100, x[-1]) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 0: ordinal not in range(128) >>> x.rjust(100, y[-1]) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.rjust(100, x[-1]) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 0: ordinal not in range(128) >>> x.find(y) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.find(x) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> x.replace('x', y) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> x.replace(y, 'x') Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.replace('x', x) Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> y.replace(x, 'x') Traceback (most recent call last): . . . UnicodeDecodeError: 'ascii' codec can't decode byte 0xcc in position 12: ordinal not in range(128) >>> x = SourcedString('ascii byte string', 'str') >>> y = SourcedString(u'unicode string \ubbbb', 'unicode') But these will all work, because x is ASCII: >>> x+y u'ascii byte stringunicode string \ubbbb'@[0:17,0:16] >>> y+x u'unicode string \ubbbbascii byte string'@[0:16,0:17] >>> x.__radd__(y) u'unicode string \ubbbbascii byte string'@[0:16,0:17] >>> y.__radd__(x) u'ascii byte stringunicode string \ubbbb'@[0:17,0:16] >>> x.find(y) -1 >>> y.find(x) -1 >>> x.lstrip(y) 'ascii byte string'@[0:17] >>> y.lstrip(x) u'unicode string \ubbbb'@[0:16] >>> x.rstrip(y) 'ascii by'@[0:8] >>> y.rstrip(x) u'unicode string \ubbbb'@[0:16] >>> x.strip(y) 'ascii by'@[0:8] >>> y.strip(x) u'unicode string \ubbbb'@[0:16] >>> x.split(y) [u'ascii byte string'@[0:17]] >>> y.split(x) [u'unicode string \ubbbb'@[0:16]] >>> x.rsplit(y) [u'ascii byte string'@[0:17]] >>> y.rsplit(x) [u'unicode string \ubbbb'@[0:16]] >>> x.partition(y) ('ascii byte string'@[0:17], ''@[17:17], ''@[17:17]) >>> y.partition(x) (u'unicode string \ubbbb'@[0:16], u''@[16:16], u''@[16:16]) >>> x.rpartition(y) ('ascii byte string'@[0:17], ''@[17:17], ''@[17:17]) >>> y.rpartition(x) (u''@[0:0], u''@[0:0], u'unicode string \ubbbb'@[0:16]) >>> x.join(y) # doctest: +ELLIPSIS u'uascii byte stringnascii byte stringiascii byte stri...5:16] >>> y.join(x) # doctest: +ELLIPSIS u'aunicode string \ubbbbsunicode string \ubbbbcunicode...6:17] >>> x.center(100, y[-1]) # doctest: +ELLIPSIS u'\ubbbb\ubbbb\ubbbb\ubbbb\ubbbb\ubbbb\ubbbb\ubbbb\ubb...5:16] >>> y.center(100, x[-1]) # doctest: +ELLIPSIS u'ggggggggggggggggggggggggggggggggggggggggggunicode st...6:17] >>> x.ljust(100, y[-1]) # doctest: +ELLIPSIS u'ascii byte string\ubbbb\ubbbb\ubbbb\ubbbb\ubbbb\ubbb...5:16] >>> y.ljust(100, x[-1]) # doctest: +ELLIPSIS u'unicode string \ubbbbggggggggggggggggggggggggggggggg...6:17] >>> x.rjust(100, y[-1]) # doctest: +ELLIPSIS u'\ubbbb\ubbbb\ubbbb\ubbbb\ubbbb\ubbbb\ubbbb\ubbbb\ubb...0:17] >>> y.rjust(100, x[-1]) # doctest: +ELLIPSIS u'gggggggggggggggggggggggggggggggggggggggggggggggggggg...0:16] >>> x.find(y) -1 >>> y.find(x) -1 >>> x.replace('x', y) u'ascii byte string'@[0:17] >>> x.replace(y, 'x') u'ascii byte string'@[0:17] >>> y.replace('x', x) u'unicode string \ubbbb'@[0:16] >>> y.replace(x, 'x') u'unicode string \ubbbb'@[0:16] Translate >>> table = [chr(i) for i in range(256)] >>> table[ord('e')] = '3' >>> table[ord('!')] = '|' >>> newt.translate(''.join(table)) 'n3wt|'@[21:26] >>> newt.translate(''.join(table), 'n3t') '3w|'@[22:24,25:26] >>> newt.decode().translate({'e':'3', '!':'*'}) u'n3wt*'@[21:26] >>> newt.decode().translate({'e':'3', '!':'*', 'w': None}) u'n3t*'@[21:23,24:26] nltk-2.0~b9/nltk/test/simple.doctest0000644000175000017500000000441711331670064017374 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ================= EasyInstall Tests ================= This file contains some simple tests that will be run by EasyInstall in order to test the installation when NLTK-Data is absent. ------------ Tokenization ------------ >>> from nltk import wordpunct_tokenize >>> s = ("Good muffins cost $3.88\nin New York. Please buy me\n" ... "two of them.\n\nThanks.") >>> wordpunct_tokenize(s) # doctest: +NORMALIZE_WHITESPACE ['Good', 'muffins', 'cost', '$', '3', '.', '88', 'in', 'New', 'York', '.', 'Please', 'buy', 'me', 'two', 'of', 'them', '.', 'Thanks', '.'] ------- Metrics ------- >>> from nltk.metrics import precision, recall, f_measure >>> reference = 'DET NN VB DET JJ NN NN IN DET NN'.split() >>> test = 'DET VB VB DET NN NN NN IN DET NN'.split() >>> reference_set = set(reference) >>> test_set = set(test) >>> precision(reference_set, test_set) 1.0 >>> recall(reference_set, test_set) 0.80000000000000004 >>> f_measure(reference_set, test_set) 0.88888888888888884 ------------------ Feature Structures ------------------ >>> from nltk import FeatStruct >>> fs1 = FeatStruct(PER=3, NUM='pl', GND='fem') >>> fs2 = FeatStruct(POS='N', AGR=fs1) >>> print fs2 [ [ GND = 'fem' ] ] [ AGR = [ NUM = 'pl' ] ] [ [ PER = 3 ] ] [ ] [ POS = 'N' ] >>> print fs2['AGR'] [ GND = 'fem' ] [ NUM = 'pl' ] [ PER = 3 ] >>> print fs2['AGR']['PER'] 3 ------- Parsing ------- >>> from nltk import RecursiveDescentParser, parse_cfg >>> grammar = parse_cfg(""" ... S -> NP VP ... PP -> P NP ... NP -> 'the' N | N PP | 'the' N PP ... VP -> V NP | V PP | V NP PP ... N -> 'cat' | 'dog' | 'rug' ... V -> 'chased' ... P -> 'on' ... """) >>> rd = RecursiveDescentParser(grammar) >>> sent = 'the cat chased the dog on the rug'.split() >>> for t in rd.nbest_parse(sent): ... print t (S (NP the (N cat)) (VP (V chased) (NP the (N dog) (PP (P on) (NP the (N rug)))))) (S (NP the (N cat)) (VP (V chased) (NP the (N dog)) (PP (P on) (NP the (N rug))))) nltk-2.0~b9/nltk/test/semantics.doctest0000644000175000017500000006330211331670113020062 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ========= Semantics ========= >>> from nltk.sem import logic >>> logic._counter._value = 0 Overview ======== The ``nltk.sem`` module has two main components: - ``logic`` provides a parser for analyzing expressions of First Order Logic (FOL). - ``evaluate`` allows users to recursively determine truth in a model for formulas of FOL. A model consists of a domain of discourse and a valuation function, which assigns values to non-logical constants. We assume that entities in the domain are represented as strings such as ``'b1'``, ``'g1'``, etc. A ``Valuation`` is initialized with a list of (symbol, value) pairs, where values are entities, sets of entities or sets of tuples of entities. >>> import nltk >>> v = [('adam', 'b1'), ('betty', 'g1'), ('fido', 'd1'), ... ('girl', set(['g1', 'g2'])), ('boy', set(['b1', 'b2'])), ... ('dog', set(['d1'])), ... ('love', set([('b1', 'g1'), ('b2', 'g2'), ('g1', 'b1'), ('g2', 'b1')]))] >>> val = nltk.sem.Valuation(v) The domain of discourse can be inferred from the valuation, and model is then created with domain and valuation as parameters. >>> dom = val.domain >>> m = nltk.sem.Model(dom, val) Assignments ----------- A variable *Assignment* is a mapping from individual variables to entities in the domain. Individual variables are usually indicated with the letters ``'x'``, ``'y'``, ``'w'`` and ``'z'``, optionally followed by an integer (e.g., ``'x0'``, ``'y332'``). Assignments are created using the ``Assignment`` constructor, which also takes the domain as a parameter. >>> dom = set(['u1', 'u2', 'u3', 'u4']) >>> g3 = nltk.sem.Assignment(dom, [('x', 'u1'), ('y', 'u2')]) >>> g3 {'y': 'u2', 'x': 'u1'} There is also a ``print`` format for assignments which uses a notation closer to that in logic textbooks: >>> print g3 g[u2/y][u1/x] It is also possible to update an assignment using the ``add`` method: >>> dom = set(['u1', 'u2', 'u3', 'u4']) >>> g4 = nltk.sem.Assignment(dom) >>> g4.add('x', 'u1') {'x': 'u1'} With no arguments, ``purge()`` is equivalent to ``clear()`` on a dictionary: >>> g4.purge() >>> g4 {} Evaluation ---------- The top-level method of a ``Model`` instance is ``evaluate()``, which assigns a semantic value to expressions of the ``logic`` module, under an assignment ``g``: >>> dom = val.domain >>> g = nltk.sem.Assignment(dom) >>> m.evaluate('all x.(boy(x) -> - girl(x))', g) True ``evaluate()`` calls a recursive function ``satisfy()``, which in turn calls a function ``i()`` to interpret non-logical constants and individual variables. ``i()`` delegates the interpretation of these to the the model's ``Valuation`` and the variable assignment ``g`` respectively. Any atomic expression which cannot be assigned a value by ``i`` raises an ``Undefined`` exception; this is caught by ``evaluate``, which returns the string ``'Undefined'``. >>> m.evaluate('walk(adam)', g, trace=2) 'walk(adam)' is undefined under M, g 'Undefined' Batch Processing ---------------- The utility functions ``batch_interpret()`` and ``batch_evaluate()`` are intended to help with processing multiple sentences. Here's an example of the first of these: >>> sents = ['Mary walks'] >>> results = nltk.sem.batch_interpret(sents, 'grammars/sample_grammars/sem2.fcfg') >>> for result in results: ... for (synrep, semrep) in result: ... print synrep (S[SEM=] (NP[-LOC, NUM='sg', SEM=<\P.P(mary)>] (PropN[-LOC, NUM='sg', SEM=<\P.P(mary)>] Mary)) (VP[NUM='sg', SEM=<\x.walk(x)>] (IV[NUM='sg', SEM=<\x.walk(x)>, TNS='pres'] walks))) In order to provide backwards compatibility with 'legacy' grammars where the semantics value is specified with a lowercase ``sem`` feature, the relevant feature name can be passed to the function using the ``semkey`` parameter, as shown here: >>> sents = ['raining'] >>> g = nltk.parse_fcfg(""" ... % start S ... S[sem=] -> 'raining' ... """) >>> results = nltk.sem.batch_interpret(sents, g, semkey='sem') >>> for result in results: ... for (synrep, semrep) in result: ... print semrep raining The function ``batch_evaluate()`` works in a similar manner, but also needs to be passed a ``Model`` against which the semantic representations are evaluated. Unit Tests ========== Unit tests for relations and valuations --------------------------------------- >>> from nltk.sem import * Relations are sets of tuples, all of the same length. >>> s1 = set([('d1', 'd2'), ('d1', 'd1'), ('d2', 'd1')]) >>> is_rel(s1) True >>> s2 = set([('d1', 'd2'), ('d1', 'd2'), ('d1',)]) >>> is_rel(s2) Traceback (most recent call last): . . . ValueError: Set set([('d1', 'd2'), ('d1',)]) contains sequences of different lengths >>> s3 = set(['d1', 'd2']) >>> is_rel(s3) Traceback (most recent call last): . . . ValueError: Set set(['d2', 'd1']) contains sequences of different lengths >>> s4 = set2rel(s3) >>> is_rel(s4) True >>> is_rel(set()) True >>> null_binary_rel = set([(None, None)]) >>> is_rel(null_binary_rel) True Sets of entities are converted into sets of singleton tuples (containing strings). >>> set2rel(s3) set([('d1',), ('d2',)]) >>> set2rel(set([1,3,5,])) set(['1', '3', '5']) >>> set2rel(set()) set([]) >>> set2rel(set2rel(s3)) set([('d1',), ('d2',)]) Predication is evaluated by set membership. >>> ('d1', 'd2') in s1 True >>> ('d2', 'd2') in s1 False >>> ('d1',) in s1 False >>> 'd2' in s1 False >>> ('d1',) in s4 True >>> ('d1',) in set() False >>> 'd1' in null_binary_rel False >>> val = Valuation([('Fido', 'd1'), ('dog', set(['d1', 'd2'])), ('walk', set())]) >>> val['dog'] set([('d1',), ('d2',)]) >>> val.domain == set(['d1', 'd2']) True >>> print val.symbols ['Fido', 'dog', 'walk'] Parse a valuation from a string. >>> v = """ ... john => b1 ... mary => g1 ... suzie => g2 ... fido => d1 ... tess => d2 ... noosa => n ... girl => {g1, g2} ... boy => {b1, b2} ... dog => {d1, d2} ... bark => {d1, d2} ... walk => {b1, g2, d1} ... chase => {(b1, g1), (b2, g1), (g1, d1), (g2, d2)} ... see => {(b1, g1), (b2, d2), (g1, b1),(d2, b1), (g2, n)} ... in => {(b1, n), (b2, n), (d2, n)} ... with => {(b1, g1), (g1, b1), (d1, b1), (b1, d1)} ... """ >>> val = parse_valuation(v) >>> print val # doctest: +SKIP {'bark': set([('d1',), ('d2',)]), 'boy': set([('b1',), ('b2',)]), 'chase': set([('b1', 'g1'), ('g2', 'd2'), ('g1', 'd1'), ('b2', 'g1')]), 'dog': set([('d1',), ('d2',)]), 'fido': 'd1', 'girl': set([('g2',), ('g1',)]), 'in': set([('d2', 'n'), ('b1', 'n'), ('b2', 'n')]), 'john': 'b1', 'mary': 'g1', 'noosa': 'n', 'see': set([('b1', 'g1'), ('b2', 'd2'), ('d2', 'b1'), ('g2', 'n'), ('g1', 'b1')]), 'suzie': 'g2', 'tess': 'd2', 'walk': set([('d1',), ('b1',), ('g2',)]), 'with': set([('b1', 'g1'), ('d1', 'b1'), ('b1', 'd1'), ('g1', 'b1')])} Unit tests for function argument application in a Model ------------------------------------------------------- >>> v = [('adam', 'b1'), ('betty', 'g1'), ('fido', 'd1'),\ ... ('girl', set(['g1', 'g2'])), ('boy', set(['b1', 'b2'])), ('dog', set(['d1'])), ... ('love', set([('b1', 'g1'), ('b2', 'g2'), ('g1', 'b1'), ('g2', 'b1')])), ... ('kiss', null_binary_rel)] >>> val = Valuation(v) >>> dom = val.domain >>> m = Model(dom, val) >>> g = Assignment(dom) >>> print val['boy'] set([('b1',), ('b2',)]) >>> ('b1',) in val['boy'] True >>> ('g1',) in val['boy'] False >>> ('foo',) in val['boy'] False >>> ('b1', 'g1') in val['love'] True >>> ('b1', 'b1') in val['kiss'] False >>> val.domain set(['d1', 'g1', 'b1', 'b2', 'g2']) Model Tests =========== Extension of Lambda expressions >>> exprs = [ ... r'\x. \y. love(x, y)', ... r'\x. dog(x) (adam)', ... r'\x. (dog(x) | boy(x)) (adam)', ... r'\x. \y. love(x, y)(fido)', ... r'\x. \y. love(x, y)(adam)', ... r'\x. \y. love(x, y)(betty)', ... r'\x. \y. love(x, y)(betty)(adam)', ... r'\x. \y. love(x, y)(betty, adam)', ... r'\y. \x. love(x, y)(fido)(adam)', ... r'\y. \x. love(x, y)(betty, adam)', ... r'\x. exists y. love(x, y)', ... r'\z. adam', ... r'\z. love(x, y)' ... ] >>> v0 = [('adam', 'b1'), ('betty', 'g1'), ('fido', 'd1'),\ ... ('girl', set(['g1', 'g2'])), ('boy', set(['b1', 'b2'])), ... ('dog', set(['d1'])), ... ('love', set([('b1', 'g1'), ('b2', 'g2'), ('g1', 'b1'), ('g2', 'b1')]))] >>> val0 = Valuation(v0) >>> dom0 = val0.domain >>> m0 = Model(dom0, val0) >>> g0 = Assignment(dom0) >>> for e in exprs: ... print e ... print m0.evaluate(e, g0) \x. \y. love(x, y) {'g2': {'g2': False, 'b2': False, 'b1': True, 'g1': False, 'd1': False}, 'b2': {'g2': True, 'b2': False, 'b1': False, 'g1': False, 'd1': False}, 'b1': {'g2': False, 'b2': False, 'b1': False, 'g1': True, 'd1': False}, 'g1': {'g2': False, 'b2': False, 'b1': True, 'g1': False, 'd1': False}, 'd1': {'g2': False, 'b2': False, 'b1': False, 'g1': False, 'd1': False}} \x. dog(x) (adam) False \x. (dog(x) | boy(x)) (adam) True \x. \y. love(x, y)(fido) {'g2': False, 'b2': False, 'b1': False, 'g1': False, 'd1': False} \x. \y. love(x, y)(adam) {'g2': False, 'b2': False, 'b1': False, 'g1': True, 'd1': False} \x. \y. love(x, y)(betty) {'g2': False, 'b2': False, 'b1': True, 'g1': False, 'd1': False} \x. \y. love(x, y)(betty)(adam) True \x. \y. love(x, y)(betty, adam) True \y. \x. love(x, y)(fido)(adam) False \y. \x. love(x, y)(betty, adam) True \x. exists y. love(x, y) {'g2': True, 'b2': True, 'b1': True, 'g1': True, 'd1': False} \z. adam {'g2': 'b1', 'b2': 'b1', 'b1': 'b1', 'g1': 'b1', 'd1': 'b1'} \z. love(x, y) {'g2': False, 'b2': False, 'b1': False, 'g1': False, 'd1': False} Propositional Model Test ------------------------ >>> tests = [ ... ('P & Q', True), ... ('P & R', False), ... ('- P', False), ... ('- R', True), ... ('- - P', True), ... ('- (P & R)', True), ... ('P | R', True), ... ('R | P', True), ... ('R | R', False), ... ('- P | R', False), ... ('P | - P', True), ... ('P -> Q', True), ... ('P -> R', False), ... ('R -> P', True), ... ('P <-> P', True), ... ('R <-> R', True), ... ('P <-> R', False), ... ] >>> val1 = Valuation([('P', True), ('Q', True), ('R', False)]) >>> dom = set([]) >>> m = Model(dom, val1) >>> g = Assignment(dom) >>> for (sent, testvalue) in tests: ... semvalue = m.evaluate(sent, g) ... if semvalue == testvalue: ... print '*', * * * * * * * * * * * * * * * * * Test of i Function ------------------ >>> v = [('adam', 'b1'), ('betty', 'g1'), ('fido', 'd1'), ... ('girl', set(['g1', 'g2'])), ('boy', set(['b1', 'b2'])), ('dog', set(['d1'])), ... ('love', set([('b1', 'g1'), ('b2', 'g2'), ('g1', 'b1'), ('g2', 'b1')]))] >>> val = Valuation(v) >>> dom = val.domain >>> m = Model(dom, val) >>> g = Assignment(dom, [('x', 'b1'), ('y', 'g2')]) >>> exprs = ['adam', 'girl', 'love', 'walks', 'x', 'y', 'z'] >>> lp = LogicParser() >>> parsed_exprs = [lp.parse(e) for e in exprs] >>> for parsed in parsed_exprs: ... try: ... print "'%s' gets value %s" % (parsed, m.i(parsed, g)) ... except Undefined: ... print "'%s' is Undefined" % parsed 'adam' gets value b1 'girl' gets value set([('g2',), ('g1',)]) 'love' gets value set([('b1', 'g1'), ('b2', 'g2'), ('g1', 'b1'), ('g2', 'b1')]) 'walks' is Undefined 'x' gets value b1 'y' gets value g2 'z' is Undefined Test for formulas in Model -------------------------- >>> tests = [ ... ('love(adam, betty)', True), ... ('love(adam, sue)', 'Undefined'), ... ('dog(fido)', True), ... ('- dog(fido)', False), ... ('- - dog(fido)', True), ... ('- dog(sue)', 'Undefined'), ... ('dog(fido) & boy(adam)', True), ... ('- (dog(fido) & boy(adam))', False), ... ('- dog(fido) & boy(adam)', False), ... ('dog(fido) | boy(adam)', True), ... ('- (dog(fido) | boy(adam))', False), ... ('- dog(fido) | boy(adam)', True), ... ('- dog(fido) | - boy(adam)', False), ... ('dog(fido) -> boy(adam)', True), ... ('- (dog(fido) -> boy(adam))', False), ... ('- dog(fido) -> boy(adam)', True), ... ('exists x . love(adam, x)', True), ... ('all x . love(adam, x)', False), ... ('fido = fido', True), ... ('exists x . all y. love(x, y)', False), ... ('exists x . (x = fido)', True), ... ('all x . (dog(x) | - dog(x))', True), ... ('adam = mia', 'Undefined'), ... ('\\x. (boy(x) | girl(x))', {'g2': True, 'b2': True, 'b1': True, 'g1': True, 'd1': False}), ... ('\\x. exists y. (boy(x) & love(x, y))', {'g2': False, 'b2': True, 'b1': True, 'g1': False, 'd1': False}), ... ('exists z1. boy(z1)', True), ... ('exists x. (boy(x) & - (x = adam))', True), ... ('exists x. (boy(x) & all y. love(y, x))', False), ... ('all x. (boy(x) | girl(x))', False), ... ('all x. (girl(x) -> exists y. boy(y) & love(x, y))', False), ... ('exists x. (boy(x) & all y. (girl(y) -> love(y, x)))', True), ... ('exists x. (boy(x) & all y. (girl(y) -> love(x, y)))', False), ... ('all x. (dog(x) -> - girl(x))', True), ... ('exists x. exists y. (love(x, y) & love(x, y))', True), ... ] >>> for (sent, testvalue) in tests: ... semvalue = m.evaluate(sent, g) ... if semvalue == testvalue: ... print '*', ... else: ... print sent, semvalue * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Satisfier Tests --------------- >>> formulas = [ ... 'boy(x)', ... '(x = x)', ... '(boy(x) | girl(x))', ... '(boy(x) & girl(x))', ... 'love(adam, x)', ... 'love(x, adam)', ... '- (x = adam)', ... 'exists z22. love(x, z22)', ... 'exists y. love(y, x)', ... 'all y. (girl(y) -> love(x, y))', ... 'all y. (girl(y) -> love(y, x))', ... 'all y. (girl(y) -> (boy(x) & love(y, x)))', ... 'boy(x) & all y. (girl(y) -> love(x, y))', ... 'boy(x) & all y. (girl(y) -> love(y, x))', ... 'boy(x) & exists y. (girl(y) & love(y, x))', ... 'girl(x) -> dog(x)', ... 'all y. (dog(y) -> (x = y))', ... '- exists y. love(y, x)', ... 'exists y. (love(adam, y) & love(y, x))' ... ] >>> g.purge() >>> g.add('x', 'b1') {'x': 'b1'} >>> for f in formulas: # doctest: +NORMALIZE_WHITESPACE ... try: ... print "'%s' gets value: %s" % (f, m.evaluate(f, g)) ... except Undefined: ... print "'%s' is Undefined" % f 'boy(x)' gets value: True '(x = x)' gets value: True '(boy(x) | girl(x))' gets value: True '(boy(x) & girl(x))' gets value: False 'love(adam, x)' gets value: False 'love(x, adam)' gets value: False '- (x = adam)' gets value: False 'exists z22. love(x, z22)' gets value: True 'exists y. love(y, x)' gets value: True 'all y. (girl(y) -> love(x, y))' gets value: False 'all y. (girl(y) -> love(y, x))' gets value: True 'all y. (girl(y) -> (boy(x) & love(y, x)))' gets value: True 'boy(x) & all y. (girl(y) -> love(x, y))' gets value: False 'boy(x) & all y. (girl(y) -> love(y, x))' gets value: True 'boy(x) & exists y. (girl(y) & love(y, x))' gets value: True 'girl(x) -> dog(x)' gets value: True 'all y. (dog(y) -> (x = y))' gets value: False '- exists y. love(y, x)' gets value: False 'exists y. (love(adam, y) & love(y, x))' gets value: True >>> lp = LogicParser() >>> for fmla in formulas: # doctest: +NORMALIZE_WHITESPACE ... p = lp.parse(fmla) ... g.purge() ... print "Satisfiers of '%s':\n\t%s" % (p, m.satisfiers(p, 'x', g)) Satisfiers of 'boy(x)': set(['b1', 'b2']) Satisfiers of '(x = x)': set(['g2', 'b2', 'b1', 'g1', 'd1']) Satisfiers of '(boy(x) | girl(x))': set(['b2', 'b1', 'g1', 'g2']) Satisfiers of '(boy(x) & girl(x))': set([]) Satisfiers of 'love(adam,x)': set(['g1']) Satisfiers of 'love(x,adam)': set(['g2', 'g1']) Satisfiers of '-(x = adam)': set(['g2', 'b2', 'g1', 'd1']) Satisfiers of 'exists z22.love(x,z22)': set(['b2', 'b1', 'g1', 'g2']) Satisfiers of 'exists y.love(y,x)': set(['b1', 'g1', 'g2']) Satisfiers of 'all y.(girl(y) -> love(x,y))': set([]) Satisfiers of 'all y.(girl(y) -> love(y,x))': set(['b1']) Satisfiers of 'all y.(girl(y) -> (boy(x) & love(y,x)))': set(['b1']) Satisfiers of '(boy(x) & all y.(girl(y) -> love(x,y)))': set([]) Satisfiers of '(boy(x) & all y.(girl(y) -> love(y,x)))': set(['b1']) Satisfiers of '(boy(x) & exists y.(girl(y) & love(y,x)))': set(['b1']) Satisfiers of '(girl(x) -> dog(x))': set(['b1', 'b2', 'd1']) Satisfiers of 'all y.(dog(y) -> (x = y))': set(['d1']) Satisfiers of '-exists y.love(y,x)': set(['b2', 'd1']) Satisfiers of 'exists y.(love(adam,y) & love(y,x))': set(['b1']) Tests based on the Blackburn & Bos testsuite -------------------------------------------- >>> v1 = [('jules', 'd1'), ('vincent', 'd2'), ('pumpkin', 'd3'), ... ('honey_bunny', 'd4'), ('yolanda', 'd5'), ... ('customer', set(['d1', 'd2'])), ... ('robber', set(['d3', 'd4'])), ... ('love', set([('d3', 'd4')]))] >>> val1 = Valuation(v1) >>> dom1 = val1.domain >>> m1 = Model(dom1, val1) >>> g1 = Assignment(dom1) >>> v2 = [('jules', 'd1'), ('vincent', 'd2'), ('pumpkin', 'd3'), ... ('honey_bunny', 'd4'), ('yolanda', 'd4'), ... ('customer', set(['d1', 'd2', 'd5', 'd6'])), ... ('robber', set(['d3', 'd4'])), ... ('love', set([(None, None)]))] >>> val2 = Valuation(v2) >>> dom2 = set(['d1', 'd2', 'd3', 'd4', 'd5', 'd6']) >>> m2 = Model(dom2, val2) >>> g2 = Assignment(dom2) >>> g21 = Assignment(dom2) >>> g21.add('y', 'd3') {'y': 'd3'} >>> v3 = [('mia', 'd1'), ('jody', 'd2'), ('jules', 'd3'), ... ('vincent', 'd4'), ... ('woman', set(['d1', 'd2'])), ('man', set(['d3', 'd4'])), ... ('joke', set(['d5', 'd6'])), ('episode', set(['d7', 'd8'])), ... ('in', set([('d5', 'd7'), ('d5', 'd8')])), ... ('tell', set([('d1', 'd5'), ('d2', 'd6')]))] >>> val3 = Valuation(v3) >>> dom3 = set(['d1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8']) >>> m3 = Model(dom3, val3) >>> g3 = Assignment(dom3) >>> tests = [ ... ('exists x. robber(x)', m1, g1, True), ... ('exists x. exists y. love(y, x)', m1, g1, True), ... ('exists x0. exists x1. love(x1, x0)', m2, g2, False), ... ('all x. all y. love(y, x)', m2, g2, False), ... ('- (all x. all y. love(y, x))', m2, g2, True), ... ('all x. all y. - love(y, x)', m2, g2, True), ... ('yolanda = honey_bunny', m2, g2, True), ... ('mia = honey_bunny', m2, g2, 'Undefined'), ... ('- (yolanda = honey_bunny)', m2, g2, False), ... ('- (mia = honey_bunny)', m2, g2, 'Undefined'), ... ('all x. (robber(x) | customer(x))', m2, g2, True), ... ('- (all x. (robber(x) | customer(x)))', m2, g2, False), ... ('(robber(x) | customer(x))', m2, g2, 'Undefined'), ... ('(robber(y) | customer(y))', m2, g21, True), ... ('exists x. (man(x) & exists x. woman(x))', m3, g3, True), ... ('exists x. (man(x) & exists x. woman(x))', m3, g3, True), ... ('- exists x. woman(x)', m3, g3, False), ... ('exists x. (tasty(x) & burger(x))', m3, g3, 'Undefined'), ... ('- exists x. (tasty(x) & burger(x))', m3, g3, 'Undefined'), ... ('exists x. (man(x) & - exists y. woman(y))', m3, g3, False), ... ('exists x. (man(x) & - exists x. woman(x))', m3, g3, False), ... ('exists x. (woman(x) & - exists x. customer(x))', m2, g2, 'Undefined'), ... ] >>> for item in tests: ... sentence, model, g, testvalue = item ... semvalue = model.evaluate(sentence, g) ... if semvalue == testvalue: ... print '*', ... g.purge() * * * * * * * * * * * * * * * * * * * * * * Tests for mapping from syntax to semantics ------------------------------------------ Load a valuation from a file. >>> import nltk.data >>> val = nltk.data.load('grammars/sample_grammars/valuation1.val') >>> dom = val.domain >>> m = Model(dom, val) >>> g = Assignment(dom) >>> gramfile = 'grammars/sample_grammars/sem2.fcfg' >>> inputs = ['John sees a girl', 'every dog barks'] >>> parses = batch_parse(inputs, gramfile) >>> for sent, trees in zip(inputs, parses): ... print ... print "Sentence: %s" % sent ... for tree in trees: ... print "Parse:\n %s" %tree ... print "Semantics: %s" % root_semrep(tree) Sentence: John sees a girl Parse: (S[SEM=] (NP[-LOC, NUM='sg', SEM=<\P.P(john)>] (PropN[-LOC, NUM='sg', SEM=<\P.P(john)>] John)) (VP[NUM='sg', SEM=<\y.exists x.(girl(x) & see(y,x))>] (TV[NUM='sg', SEM=<\X y.X(\x.see(y,x))>, TNS='pres'] sees) (NP[NUM='sg', SEM=<\Q.exists x.(girl(x) & Q(x))>] (Det[NUM='sg', SEM=<\P Q.exists x.(P(x) & Q(x))>] a) (Nom[NUM='sg', SEM=<\x.girl(x)>] (N[NUM='sg', SEM=<\x.girl(x)>] girl))))) Semantics: exists x.(girl(x) & see(john,x)) Sentence: every dog barks Parse: (S[SEM= bark(x))>] (NP[NUM='sg', SEM=<\Q.all x.(dog(x) -> Q(x))>] (Det[NUM='sg', SEM=<\P Q.all x.(P(x) -> Q(x))>] every) (Nom[NUM='sg', SEM=<\x.dog(x)>] (N[NUM='sg', SEM=<\x.dog(x)>] dog))) (VP[NUM='sg', SEM=<\x.bark(x)>] (IV[NUM='sg', SEM=<\x.bark(x)>, TNS='pres'] barks))) Semantics: all x.(dog(x) -> bark(x)) >>> sent = "every dog barks" >>> result = nltk.sem.batch_interpret([sent], gramfile)[0] >>> for (syntree, semrep) in result: ... print syntree ... print ... print semrep (S[SEM= bark(x))>] (NP[NUM='sg', SEM=<\Q.all x.(dog(x) -> Q(x))>] (Det[NUM='sg', SEM=<\P Q.all x.(P(x) -> Q(x))>] every) (Nom[NUM='sg', SEM=<\x.dog(x)>] (N[NUM='sg', SEM=<\x.dog(x)>] dog))) (VP[NUM='sg', SEM=<\x.bark(x)>] (IV[NUM='sg', SEM=<\x.bark(x)>, TNS='pres'] barks))) all x.(dog(x) -> bark(x)) >>> result = nltk.sem.batch_evaluate([sent], gramfile, m, g)[0] >>> for (syntree, semrel, value) in result: ... print syntree ... print ... print semrep ... print ... print value (S[SEM= bark(x))>] (NP[NUM='sg', SEM=<\Q.all x.(dog(x) -> Q(x))>] (Det[NUM='sg', SEM=<\P Q.all x.(P(x) -> Q(x))>] every) (Nom[NUM='sg', SEM=<\x.dog(x)>] (N[NUM='sg', SEM=<\x.dog(x)>] dog))) (VP[NUM='sg', SEM=<\x.bark(x)>] (IV[NUM='sg', SEM=<\x.bark(x)>, TNS='pres'] barks))) all x.(dog(x) -> bark(x)) True >>> sents = ['Mary walks', 'John sees a dog'] >>> results = nltk.sem.batch_interpret(sents, 'grammars/sample_grammars/sem2.fcfg') >>> for result in results: ... for (synrep, semrep) in result: ... print synrep (S[SEM=] (NP[-LOC, NUM='sg', SEM=<\P.P(mary)>] (PropN[-LOC, NUM='sg', SEM=<\P.P(mary)>] Mary)) (VP[NUM='sg', SEM=<\x.walk(x)>] (IV[NUM='sg', SEM=<\x.walk(x)>, TNS='pres'] walks))) (S[SEM=] (NP[-LOC, NUM='sg', SEM=<\P.P(john)>] (PropN[-LOC, NUM='sg', SEM=<\P.P(john)>] John)) (VP[NUM='sg', SEM=<\y.exists x.(dog(x) & see(y,x))>] (TV[NUM='sg', SEM=<\X y.X(\x.see(y,x))>, TNS='pres'] sees) (NP[NUM='sg', SEM=<\Q.exists x.(dog(x) & Q(x))>] (Det[NUM='sg', SEM=<\P Q.exists x.(P(x) & Q(x))>] a) (Nom[NUM='sg', SEM=<\x.dog(x)>] (N[NUM='sg', SEM=<\x.dog(x)>] dog))))) Cooper Storage -------------- >>> from nltk.sem import cooper_storage as cs >>> sentence = 'every girl chases a dog' >>> trees = cs.parse_with_bindops(sentence, grammar='grammars/book_grammars/storage.fcfg') >>> semrep = trees[0].node['SEM'] >>> cs_semrep = cs.CooperStore(semrep) >>> print cs_semrep.core chase(z1,z2) >>> for bo in cs_semrep.store: ... print bo bo(\P.all x.(girl(x) -> P(x)),z1) bo(\P.exists x.(dog(x) & P(x)),z2) >>> cs_semrep.s_retrieve(trace=True) Permutation 1 (\P.all x.(girl(x) -> P(x)))(\z1.chase(z1,z2)) (\P.exists x.(dog(x) & P(x)))(\z2.all x.(girl(x) -> chase(x,z2))) Permutation 2 (\P.exists x.(dog(x) & P(x)))(\z2.chase(z1,z2)) (\P.all x.(girl(x) -> P(x)))(\z1.exists x.(dog(x) & chase(z1,x))) >>> for reading in cs_semrep.readings: ... print reading exists x.(dog(x) & all z3.(girl(z3) -> chase(z3,x))) all x.(girl(x) -> exists z4.(dog(z4) & chase(x,z4))) nltk-2.0~b9/nltk/test/resolution.doctest0000644000175000017500000001701711331670117020305 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ========================= Resolution Theorem Prover ========================= >>> from nltk.inference.resolution import * >>> from nltk.sem import logic >>> from nltk.sem.logic import * >>> logic._counter._value = 0 >>> lp = LogicParser() >>> P = lp.parse('P') >>> Q = lp.parse('Q') >>> R = lp.parse('R') >>> A = lp.parse('A') >>> B = lp.parse('B') >>> x = lp.parse('x') >>> y = lp.parse('y') >>> z = lp.parse('z') ------------------------------- Test most_general_unification() ------------------------------- >>> print most_general_unification(x, x) {} >>> print most_general_unification(A, A) {} >>> print most_general_unification(A, x) {x: A} >>> print most_general_unification(x, A) {x: A} >>> print most_general_unification(x, y) {x: y} >>> print most_general_unification(P(x), P(A)) {x: A} >>> print most_general_unification(P(x,B), P(A,y)) {y: B, x: A} >>> print most_general_unification(P(x,B), P(B,x)) {x: B} >>> print most_general_unification(P(x,y), P(A,x)) {y: x, x: A} >>> print most_general_unification(P(Q(x)), P(y)) {y: Q(x)} ------------ Test unify() ------------ >>> print Clause([]).unify(Clause([])) [] >>> print Clause([P(x)]).unify(Clause([-P(A)])) [{}] >>> print Clause([P(A), Q(x)]).unify(Clause([-P(x), R(x)])) [{R(A), Q(A)}] >>> print Clause([P(A), Q(x), R(x,y)]).unify(Clause([-P(x), Q(y)])) [{Q(y), Q(A), R(A,y)}] >>> print Clause([P(A), -Q(y)]).unify(Clause([-P(x), Q(B)])) [{}] >>> print Clause([P(x), Q(x)]).unify(Clause([-P(A), -Q(B)])) [{-Q(B), Q(A)}, {-P(A), P(B)}] >>> print Clause([P(x,x), Q(x), R(x)]).unify(Clause([-P(A,z), -Q(B)])) [{-Q(B), Q(A), R(A)}, {-P(A,z), R(B), P(B,B)}] >>> a = clausify(lp.parse('P(A)')) >>> b = clausify(lp.parse('A=B')) >>> print a[0].unify(b[0]) [{P(B)}] ------------------------- Test is_tautology() ------------------------- >>> print Clause([P(A), -P(A)]).is_tautology() True >>> print Clause([-P(A), P(A)]).is_tautology() True >>> print Clause([P(x), -P(A)]).is_tautology() False >>> print Clause([Q(B), -P(A), P(A)]).is_tautology() True >>> print Clause([-Q(A), P(R(A)), -P(R(A)), Q(x), -R(y)]).is_tautology() True >>> print Clause([P(x), -Q(A)]).is_tautology() False ------------------------- Test subsumes() ------------------------- >>> print Clause([P(A), Q(B)]).subsumes(Clause([P(A), Q(B)])) True >>> print Clause([-P(A)]).subsumes(Clause([P(A)])) False >>> print Clause([P(A), Q(B)]).subsumes(Clause([Q(B), P(A)])) True >>> print Clause([P(A), Q(B)]).subsumes(Clause([Q(B), R(A), P(A)])) True >>> print Clause([P(A), R(A), Q(B)]).subsumes(Clause([Q(B), P(A)])) False >>> print Clause([P(x)]).subsumes(Clause([P(A)])) True >>> print Clause([P(A)]).subsumes(Clause([P(x)])) True ------------ Test prove() ------------ >>> print ResolutionProverCommand(lp.parse('man(x)')).prove() False >>> print ResolutionProverCommand(lp.parse('(man(x) -> man(x))')).prove() True >>> print ResolutionProverCommand(lp.parse('(man(x) -> --man(x))')).prove() True >>> print ResolutionProverCommand(lp.parse('-(man(x) & -man(x))')).prove() True >>> print ResolutionProverCommand(lp.parse('(man(x) | -man(x))')).prove() True >>> print ResolutionProverCommand(lp.parse('(man(x) -> man(x))')).prove() True >>> print ResolutionProverCommand(lp.parse('-(man(x) & -man(x))')).prove() True >>> print ResolutionProverCommand(lp.parse('(man(x) | -man(x))')).prove() True >>> print ResolutionProverCommand(lp.parse('(man(x) -> man(x))')).prove() True >>> print ResolutionProverCommand(lp.parse('(man(x) <-> man(x))')).prove() True >>> print ResolutionProverCommand(lp.parse('-(man(x) <-> -man(x))')).prove() True >>> print ResolutionProverCommand(lp.parse('all x.man(x)')).prove() False >>> print ResolutionProverCommand(lp.parse('-all x.some y.F(x,y) & some x.all y.(-F(x,y))')).prove() False >>> print ResolutionProverCommand(lp.parse('some x.all y.sees(x,y)')).prove() False >>> p1 = lp.parse('all x.(man(x) -> mortal(x))') >>> p2 = lp.parse('man(Socrates)') >>> c = lp.parse('mortal(Socrates)') >>> ResolutionProverCommand(c, [p1,p2]).prove() True >>> p1 = lp.parse('all x.(man(x) -> walks(x))') >>> p2 = lp.parse('man(John)') >>> c = lp.parse('some y.walks(y)') >>> ResolutionProverCommand(c, [p1,p2]).prove() True >>> p = lp.parse('some e1.some e2.(believe(e1,john,e2) & walk(e2,mary))') >>> c = lp.parse('some e0.walk(e0,mary)') >>> ResolutionProverCommand(c, [p]).prove() True ------------ Test proof() ------------ >>> p1 = lp.parse('all x.(man(x) -> mortal(x))') >>> p2 = lp.parse('man(Socrates)') >>> c = lp.parse('mortal(Socrates)') >>> logic._counter._value = 0 >>> tp = ResolutionProverCommand(c, [p1,p2]) >>> tp.prove() True >>> print tp.proof() [1] {-mortal(Socrates)} A [2] {-man(z2), mortal(z2)} A [3] {man(Socrates)} A [4] {-man(Socrates)} (1, 2) [5] {mortal(Socrates)} (2, 3) [6] {} (1, 5) ------------------ Question Answering ------------------ One answer >>> p1 = lp.parse('father_of(art,john)') >>> p2 = lp.parse('father_of(bob,kim)') >>> p3 = lp.parse('all x.all y.(father_of(x,y) -> parent_of(x,y))') >>> c = lp.parse('all x.(parent_of(x,john) -> ANSWER(x))') >>> logic._counter._value = 0 >>> tp = ResolutionProverCommand(None, [p1,p2,p3,c]) >>> print tp.find_answers() set([]) >>> print tp.proof() [1] {father_of(art,john)} A [2] {father_of(bob,kim)} A [3] {-father_of(z4,z3), parent_of(z4,z3)} A [4] {-parent_of(z6,john), ANSWER(z6)} A [5] {parent_of(art,john)} (1, 3) [6] {parent_of(bob,kim)} (2, 3) [7] {ANSWER(z6), -father_of(z6,john)} (3, 4) [8] {ANSWER(art)} (1, 7) [9] {ANSWER(art)} (4, 5) Multiple answers >>> p1 = lp.parse('father_of(art,john)') >>> p2 = lp.parse('mother_of(ann,john)') >>> p3 = lp.parse('all x.all y.(father_of(x,y) -> parent_of(x,y))') >>> p4 = lp.parse('all x.all y.(mother_of(x,y) -> parent_of(x,y))') >>> c = lp.parse('all x.(parent_of(x,john) -> ANSWER(x))') >>> logic._counter._value = 0 >>> tp = ResolutionProverCommand(None, [p1,p2,p3,p4,c]) >>> print tp.find_answers() set([, ]) >>> print tp.proof() [ 1] {father_of(art,john)} A [ 2] {mother_of(ann,john)} A [ 3] {-father_of(z4,z3), parent_of(z4,z3)} A [ 4] {-mother_of(z8,z7), parent_of(z8,z7)} A [ 5] {-parent_of(z10,john), ANSWER(z10)} A [ 6] {parent_of(art,john)} (1, 3) [ 7] {parent_of(ann,john)} (2, 4) [ 8] {ANSWER(z10), -father_of(z10,john)} (3, 5) [ 9] {ANSWER(art)} (1, 8) [10] {ANSWER(z10), -mother_of(z10,john)} (4, 5) [11] {ANSWER(ann)} (2, 10) [12] {ANSWER(art)} (5, 6) [13] {ANSWER(ann)} (5, 7) nltk-2.0~b9/nltk/test/relextract.doctest0000644000175000017500000002231011331670122020243 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ====================== Information Extraction ====================== Information Extraction standardly consists of three subtasks: #. Named Entity Recognition #. Relation Extraction #. Template Filling Named Entities ~~~~~~~~~~~~~~ The IEER corpus is marked up for a variety of Named Entities. A `Named Entity`:dt: (more strictly, a Named Entity mention) is a name of an entity belonging to a specified class. For example, the Named Entity classes in IEER include PERSON, LOCATION, ORGANIZATION, DATE and so on. Within NLTK, Named Entities are represented as subtrees within a chunk structure: the class name is treated as node label, while the entity mention itself appears as the leaves of the subtree. This is illustrated below, where we have show an extract of the chunk representation of document NYT_19980315.064: >>> from nltk.corpus import ieer >>> docs = ieer.parsed_docs('NYT_19980315') >>> tree = docs[1].text >>> print tree # doctest: +ELLIPSIS (DOCUMENT ... ``It's a chance to think about first-level questions,'' said Ms. (PERSON Cohn) , a partner in the (ORGANIZATION McGlashan & Sarrail) firm in (LOCATION San Mateo) , (LOCATION Calif.) ...) Thus, the Named Entity mentions in this example are *Cohn*, *McGlashan & Sarrail*, *San Mateo* and *Calif.*. The CoNLL2002 Dutch and Spanish data is treated similarly, although in this case, the strings are also POS tagged. >>> from nltk.corpus import conll2002 >>> for doc in conll2002.chunked_sents('ned.train')[27]: ... print doc ('Het', 'Art') (ORG Hof/N van/Prep Cassatie/N) ('verbrak', 'V') ('het', 'Art') ('arrest', 'N') ('zodat', 'Conj') ('het', 'Pron') ('moest', 'V') ('worden', 'V') ('overgedaan', 'V') ('door', 'Prep') ('het', 'Art') ('hof', 'N') ('van', 'Prep') ('beroep', 'N') ('van', 'Prep') (LOC Antwerpen/N) ('.', 'Punc') Relation Extraction ~~~~~~~~~~~~~~~~~~~ Relation Extraction standardly consists of identifying specified relations between Named Entities. For example, assuming that we can recognize ORGANIZATIONs and LOCATIONs in text, we might want to also recognize pairs *(o, l)* of these kinds of entities such that *o* is located in *l*. The `sem.relextract` module provides some tools to help carry out a simple version of this task. The `mk_pairs()` function splits a chunk document into a list of two-member lists, each of which consists of a (possibly empty) string followed by a `Tree` (i.e., a Named Entity): >>> from nltk.sem import relextract >>> from string import join >>> pairs = relextract.mk_pairs(tree) >>> for s, tree in pairs[18:22]: ... print '("...%s", %s)' % (join(s[-5:]),tree) ("...about first-level questions,'' said Ms.", (PERSON Cohn)) ("..., a partner in the", (ORGANIZATION McGlashan & Sarrail)) ("...firm in", (LOCATION San Mateo)) ("...,", (LOCATION Calif.)) The function `mk_reldicts()` processes triples of these pairs, i.e., pairs of the form ``((string1, Tree1), (string2, Tree2), (string3, Tree3))`` and outputs a dictionary (a `reldict`) in which ``Tree1`` is the subject of the relation, ``string2`` is the filler and ``Tree3`` is the object of the relation. ``string1`` and ``string3`` are stored as left and right context respectively. >>> reldicts = relextract.mk_reldicts(pairs) >>> for k, v in reldicts[0].items(): ... print k, '=>', v # doctest: +ELLIPSIS lcon => transactions.'' Each week, they post filler => of messages to their own ``Cyberia'' ... objsym => white_house objclass => ORGANIZATION objtext => White House subjsym => hundreds subjclass => CARDINAL rcon => for access to its planned subjtext => hundreds The next example shows some of the values for two `reldict`\ s corresponding to the ``'NYT_19980315'`` text extract shown earlier. >>> for r in reldicts[18:20]: ... print '=' * 20 ... print r['subjtext'] ... print r['filler'] ... print r['objtext'] ==================== Cohn , a partner in the McGlashan & Sarrail ==================== McGlashan & Sarrail firm in San Mateo The function `relextract()` allows us to filter the `reldict`\ s according to the classes of the subject and object named entities. In addition, we can specify that the filler text has to match a given regular expression, as illustrated in the next example. Here, we are looking for pairs of entities in the IN relation, where IN has signature . >>> import re >>> IN = re.compile(r'.*\bin\b(?!\b.+ing\b)') >>> for fileid in ieer.fileids(): ... for doc in ieer.parsed_docs(fileid): ... for rel in relextract.extract_rels('ORG', 'LOC', doc, corpus='ieer', pattern = IN): ... print relextract.show_raw_rtuple(rel) # doctest: +ELLIPSIS [ORG: 'Christian Democrats'] ', the leading political forces in' [LOC: 'Italy'] [ORG: 'AP'] ') _ Lebanese guerrillas attacked Israeli forces in southern' [LOC: 'Lebanon'] [ORG: 'Security Council'] 'adopted Resolution 425. Huge yellow banners hung across intersections in' [LOC: 'Beirut'] [ORG: 'U.N.'] 'failures in' [LOC: 'Africa'] [ORG: 'U.N.'] 'peacekeeping operation in' [LOC: 'Somalia'] [ORG: 'U.N.'] 'partners on a more effective role in' [LOC: 'Africa'] [ORG: 'AP'] ') _ A bomb exploded in a mosque in central' [LOC: 'San`a'] [ORG: 'Krasnoye Sormovo'] 'shipyard in the Soviet city of' [LOC: 'Gorky'] [ORG: 'Kelab Golf Darul Ridzuan'] 'in' [LOC: 'Perak'] [ORG: 'U.N.'] 'peacekeeping operation in' [LOC: 'Somalia'] [ORG: 'WHYY'] 'in' [LOC: 'Philadelphia'] [ORG: 'McGlashan & Sarrail'] 'firm in' [LOC: 'San Mateo'] [ORG: 'Freedom Forum'] 'in' [LOC: 'Arlington'] [ORG: 'Brookings Institution'] ', the research group in' [LOC: 'Washington'] [ORG: 'Idealab'] ', a self-described business incubator based in' [LOC: 'Los Angeles'] [ORG: 'Open Text'] ', based in' [LOC: 'Waterloo'] ... The next example illustrates a case where the patter is a disjunction of roles that a PERSON can occupy in an ORGANIZATION. >>> roles = """ ... (.*( ... analyst| ... chair(wo)?man| ... commissioner| ... counsel| ... director| ... economist| ... editor| ... executive| ... foreman| ... governor| ... head| ... lawyer| ... leader| ... librarian).*)| ... manager| ... partner| ... president| ... producer| ... professor| ... researcher| ... spokes(wo)?man| ... writer| ... ,\sof\sthe?\s* # "X, of (the) Y" ... """ >>> ROLES = re.compile(roles, re.VERBOSE) >>> for fileid in ieer.fileids(): ... for doc in ieer.parsed_docs(fileid): ... for rel in relextract.extract_rels('PER', 'ORG', doc, corpus='ieer', pattern=ROLES): ... print relextract.show_raw_rtuple(rel) # doctest: +ELLIPSIS [PER: 'Kivutha Kibwana'] ', of the' [ORG: 'National Convention Assembly'] [PER: 'Boban Boskovic'] ', chief executive of the' [ORG: 'Plastika'] [PER: 'Annan'] ', the first sub-Saharan African to head the' [ORG: 'United Nations'] [PER: 'Kiriyenko'] 'became a foreman at the' [ORG: 'Krasnoye Sormovo'] [PER: 'Annan'] ', the first sub-Saharan African to head the' [ORG: 'United Nations'] [PER: 'Mike Godwin'] ', chief counsel for the' [ORG: 'Electronic Frontier Foundation'] ... In the case of the CoNLL2002 data, we can include POS tags in the query pattern. This example also illustrates how the output can be presented as something that looks more like a clause in a logical language. >>> de = """ ... .* ... ( ... de/SP| ... del/SP ... ) ... """ >>> DE = re.compile(de, re.VERBOSE) >>> rels = [rel for doc in conll2002.chunked_sents('esp.train') ... for rel in relextract.extract_rels('ORG', 'LOC', doc, corpus='conll2002', pattern = DE)] >>> for r in rels[:10]: ... print relextract.show_clause(r, relsym='DE') # doctest: +NORMALIZE_WHITESPACE DE('tribunal_supremo', 'victoria') DE('museo_de_arte', 'alcorc\xe3\xb3n') DE('museo_de_bellas_artes', 'a_coru\xe3\xb1a') DE('siria', 'l\xe3\xadbano') DE('uni\xe3\xb3n_europea', 'pek\xe3\xadn') DE('ej\xe3\xa9rcito', 'rogberi') DE('juzgado_de_instrucci\xe3\xb3n_n\xe3\xbamero_1', 'san_sebasti\xe3\xa1n') DE('psoe', 'villanueva_de_la_serena') DE('ej\xe3\xa9rcito', 'l\xe3\xadbano') DE('juzgado_de_lo_penal_n\xe3\xbamero_2', 'ceuta') >>> vnv = """ ... ( ... is/V| ... was/V| ... werd/V| ... wordt/V ... ) ... .* ... van/Prep ... """ >>> VAN = re.compile(vnv, re.VERBOSE) >>> for doc in conll2002.chunked_sents('ned.train'): ... for r in relextract.extract_rels('PER', 'ORG', doc, corpus='conll2002', pattern=VAN): ... print relextract.show_clause(r, relsym="VAN") VAN("cornet_d'elzius", 'buitenlandse_handel') VAN('johan_rottiers', 'kardinaal_van_roey_instituut') VAN('annie_lennox', 'eurythmics') nltk-2.0~b9/nltk/test/probability.doctest0000644000175000017500000001020611423114516020411 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT =========== Probability =========== >>> import nltk >>> from nltk.probability import * FreqDist -------- >>> text1 = ['no', 'good', 'fish', 'goes', 'anywhere', 'without', 'a', 'porpoise', '!'] >>> text2 = ['no', 'good', 'porpoise', 'likes', 'to', 'fish', 'fish', 'anywhere', '.'] >>> fd1 = nltk.FreqDist(text1) >>> fd1.items() [('!', 1), ('a', 1), ('anywhere', 1), ('fish', 1), ('goes', 1), ('good', 1), ('no', 1), ('porpoise', 1), ('without', 1)] >>> fd1 == nltk.FreqDist(text1) True Note that items are sorted in order of decreasing frequency; two items of the same frequency are sorted alphabetically by their key. >>> both = nltk.FreqDist(text1 + text2) >>> both.items() [('fish', 3), ('anywhere', 2), ('good', 2), ('no', 2), ('porpoise', 2), ('!', 1), ('.', 1), ('a', 1), ('goes', 1), ('likes', 1), ('to', 1), ('without', 1)] >>> both == fd1 + nltk.FreqDist(text2) True >>> fd1 == nltk.FreqDist(text1) # But fd1 is unchanged True >>> fd2 = nltk.FreqDist(text2) >>> fd1.update(fd2) >>> fd1 == both True >>> fd1 = nltk.FreqDist(text1) >>> fd1.update(text2) >>> fd1 == both True >>> fd1 = nltk.FreqDist(text1) >>> fd2 = nltk.FreqDist(fd1) >>> fd2 == fd1 True Testing some HMM estimators --------------------------- We extract a small part (500 sentences) of the Brown corpus >>> corpus = nltk.corpus.brown.tagged_sents(categories='adventure')[:500] >>> print len(corpus) 500 We create a HMM trainer - note that we need the tags and symbols from the whole corpus, not just the training corpus >>> tag_set = list(set([tag for sent in corpus for (word,tag) in sent])) >>> print len(tag_set) 92 >>> symbols = list(set([word for sent in corpus for (word,tag) in sent])) >>> print len(symbols) 1464 >>> trainer = nltk.HiddenMarkovModelTrainer(tag_set, symbols) We divide the corpus into 90% training and 10% testing >>> train_corpus = [] >>> test_corpus = [] >>> for i in range(len(corpus)): ... if i % 10: ... train_corpus += [corpus[i]] ... else: ... test_corpus += [corpus[i]] >>> print len(train_corpus) 450 >>> print len(test_corpus) 50 And now we can test the estimators >>> def train_and_test(est): ... hmm = trainer.train_supervised(train_corpus, estimator=est) ... print '%.2f%%' % (100 * nltk.tag.accuracy(hmm, test_corpus)) Maximum Likelihood Estimation - this resulted in an initialization error before r7209 >>> mle = lambda fd, bins: MLEProbDist(fd) >>> train_and_test(mle) 14.43% Laplace (= Lidstone with gamma==1) >>> train_and_test(LaplaceProbDist) 66.04% Expected Likelihood Estimation (= Lidstone with gamma==0.5) >>> train_and_test(ELEProbDist) 73.01% Lidstone Estimation, for gamma==0.1, 0.5 and 1 (the later two should be exactly equal to MLE and ELE above) >>> def lidstone(gamma): ... return lambda fd, bins: LidstoneProbDist(fd, gamma, bins) >>> train_and_test(lidstone(0.1)) 82.51% >>> train_and_test(lidstone(0.5)) 73.01% >>> train_and_test(lidstone(1.0)) 66.04% Witten Bell Estimation - This resulted in ZeroDivisionError before r7209 >>> train_and_test(WittenBellProbDist) 88.12% Good Turing Estimation - The accuracy is only 17%, see issues #26 and #133 >>> gt = lambda fd, bins: GoodTuringProbDist(fd) >>> train_and_test(gt) 0.34% >>> gt = lambda fd, bins: SimpleGoodTuringProbDist(fd) >>> train_and_test(gt) 14.43% Remains to be added: - Tests for HeldoutProbDist, CrossValidationProbDist and MutableProbDist Squashed bugs ------------- Issue 511: override pop and popitem to invalidate the cache >>> fd = nltk.FreqDist('a') >>> fd.keys() ['a'] >>> fd.pop('a') 1 >>> fd.keys() [] Issue 533: access cumulative frequencies with no arguments >>> fd = nltk.FreqDist('aab') >>> list(fd._cumulative_frequencies(['a'])) [2.0] >>> list(fd._cumulative_frequencies()) [2.0, 3.0] nltk-2.0~b9/nltk/test/portuguese_en.doctest0000644000175000017500000005534711331670304020774 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ================================== Examples for Portuguese Processing ================================== This HOWTO contains a variety of examples relating to the Portuguese language. It is intended to be read in conjunction with the NLTK book (``http://www.nltk.org/book``). For instructions on running the Python interpreter, please see the section *Getting Started with Python*, in Chapter 1. -------------------------------------------- Python Programming, with Portuguese Examples -------------------------------------------- Chapter 1 of the NLTK book contains many elementary programming examples, all with English texts. In this section, we'll see some corresponding examples using Portuguese. Please refer to the chapter for full discussion. *Vamos!* >>> from nltk.examples.pt import * *** Introductory Examples for the NLTK Book *** Loading ptext1, ... and psent1, ... Type the name of the text or sentence to view it. Type: 'texts()' or 'sents()' to list the materials. ptext1: Mem—rias P—stumas de Br‡s Cubas (1881) ptext2: Dom Casmurro (1899) ptext3: Gnesis ptext4: Folha de Sau Paulo (1994) >>> Any time we want to find out about these texts, we just have to enter their names at the Python prompt: >>> ptext2 Searching Text -------------- A concordance permits us to see words in context. >>> ptext1.concordance('olhos') Building index... Displaying 25 of 138 matches: De pŽ , ˆ cabeceira da cama , com os olhos estœpidos , a boca entreaberta , a t orelhas . Pela minha parte fechei os olhos e deixei - me ir ˆ ventura . J‡ agor x›es de cŽrebro enfermo . Como ia de olhos fechados , n‹o via o caminho ; lembr gelos eternos . Com efeito , abri os olhos e vi que o meu animal galopava numa me apareceu ent‹o , fitando - me uns olhos rutilantes como o sol . Tudo nessa f -mim mesmo . Ent‹o , encarei - a com olhos sœplices , e pedi mais alguns anos . For a given word, we can find words with a similar text distribution: >>> ptext1.similar('chegar') acabada acudir aludir avistar bramanismo casamento cheguei com contar contr‡rio corpo dali deixei desferirem dizer fazer filhos j‡ leitor lhe >>> ptext3.similar('chegar') achar alumiar arrombar destruir governar guardar ir lavrar passar que toda tomar ver vir We can search for the statistically significant collocations in a text: >>> ptext1.collocations() Building collocations list Quincas Borba; Lobo Neves; alguma coisa; Br‡s Cubas; meu pai; dia seguinte; n‹o sei; Meu pai; alguns instantes; outra vez; outra coisa; por exemplo; mim mesmo; coisa nenhuma; mesma coisa; n‹o era; dias depois; Passeio Pœblico; olhar para; das coisas We can search for words in context, with the help of *regular expressions*, e.g.: >>> ptext1.findall(" (<.*>)") estœpidos; e; fechados; rutilantes; sœplices; a; do; babavam; na; moles; se; da; umas; espraiavam; chamejantes; espetados; We can automatically generate random text based on a given text, e.g.: >>> ptext3.generate() No princ’pio , criou Deus os abenoou , dizendo : Onde { est‹o } e atŽ ˆ ave dos cŽus , { que } ser‡ . Disse mais Abr‹o : D‡ - me a mulher que tomaste ; porque daquele poo Eseque , { tinha .} E disse : N‹o poderemos descer ; mas , do campo ainda n‹o estava na casa do teu pescoo . E viveu Serugue , depois Sime‹o e Levi { s‹o } estes ? E o var‹o , porque habitava na terra de Node , da m‹o de Esaœ : Jeœs , Jal‹o e Cor‡ Texts as List of Words ---------------------- A few sentences have been defined for you. >>> psent1 ['o', 'amor', 'da', 'gl\xf3ria', 'era', 'a', 'coisa', 'mais', 'verdadeiramente', 'humana', 'que', 'h\xe1', 'no', 'homem', ',', 'e', ',', 'conseq\xfcentemente', ',', 'a', 'sua', 'mais', 'genu\xedna', 'fei\xe7\xe3o', '.'] >>> Notice that the sentence has been *tokenized*. Each token is represented as a string, represented using quotes, e.g. ``'coisa'``. Some strings contain special characters, e.g. ``\xf3``, the internal representation for —. The tokens are combined in the form of a *list*. How long is this list? >>> len(psent1) 25 >>> What is the vocabulary of this sentence? >>> sorted(set(psent1)) [',', '.', 'a', 'amor', 'coisa', 'conseq\xfcentemente', 'da', 'e' 'era', 'fei\xe7\xe3o', 'genu\xedna', 'gl\xf3ria', 'homem', 'humana', 'h\xe1', 'mais', 'no', 'o', 'que', 'sua', 'verdadeiramente'] >>> Let's iterate over each item in ``psent2``, and print information for each: >>> for w in psent1: ... print w.decode('latin-1'), len(w), w[-1]: ... N‹o 3 o consultes 9 s dicion‡rios 11 s . 1 . Observe how we make a human-readable version of a string, using ``decode()``. Also notice that we accessed the last character of a string ``w`` using ``w[-1]``. We just saw a ``for`` loop above. Another useful control structure is a *list comprehension*. >>> [w.upper() for w in psent2] ['N\xc3O', 'CONSULTES', 'DICION\xc1RIOS', '.'] >>> [w for w in psent1 if w.endswith('a')] ['da', 'gl\xf3ria', 'era', 'a', 'coisa', 'humana', 'a', 'sua', 'genu\xedna'] >>> [w for w in ptext4 if len(w) > 15] [u'norte-irlandeses', u'pan-nacionalismo', u'predominatemente', u'primeiro-ministro', u'primeiro-ministro', u'irlandesa-americana', u'responsabilidades', u'significativamente'] We can examine the relative frequency of words in a text, using ``FreqDist``: >>> fd1 = FreqDist(ptext1) >>> fd1 >>> fd1['olhos'] 137 >>> fd1.max() u',' >>> fd1.samples()[:100] [u',', u'.', u'a', u'que', u'de', u'e', u'-', u'o', u';', u'me', u'um', u'n\xe3o', u'\x97', u'se', u'do', u'da', u'uma', u'com', u'os', u'\xe9', u'era', u'as', u'eu', u'lhe', u'ao', u'em', u'para', u'mas', u'...', u'!', u'\xe0', u'na', u'mais', u'?', u'no', u'como', u'por', u'N\xe3o', u'dos', u'ou', u'ele', u':', u'Virg\xedlia', u'meu', u'disse', u'minha', u'das', u'O', u'/', u'A', u'CAP\xcdTULO', u'muito', u'depois', u'coisa', u'foi', u'sem', u'olhos', u'ela', u'nos', u'tinha', u'nem', u'E', u'outro', u'vida', u'nada', u'tempo', u'menos', u'outra', u'casa', u'homem', u'porque', u'quando', u'mim', u'mesmo', u'ser', u'pouco', u'estava', u'dia', u't\xe3o', u'tudo', u'Mas', u'at\xe9', u'D', u'ainda', u's\xf3', u'alguma', u'la', u'vez', u'anos', u'h\xe1', u'Era', u'pai', u'esse', u'lo', u'dizer', u'assim', u'ent\xe3o', u'dizia', u'aos', u'Borba'] --------------- Reading Corpora --------------- Accessing the Machado Text Corpus --------------------------------- NLTK includes the complete works of Machado de Assis. >>> from nltk.corpus import machado >>> machado.fileids() ['contos/macn001.txt', 'contos/macn002.txt', 'contos/macn003.txt', ...] Each file corresponds to one of the works of Machado de Assis. To see a complete list of works, you can look at the corpus README file: ``print machado.readme()``. Let's access the text of the *Posthumous Memories of Br‡s Cubas*. We can access the text as a list of characters, and access 200 characters starting from position 10,000. >>> raw_text = machado.raw('romance/marm05.txt') >>> raw_text[10000:10200] u', primou no\nEstado, e foi um dos amigos particulares do vice-rei Conde da Cunha.\n\nComo este apelido de Cubas lhe\ncheirasse excessivamente a tanoaria, alegava meu pai, bisneto de Dami\xe3o, que o\ndito ape' However, this is not a very useful way to work with a text. We generally think of a text as a sequence of words and punctuation, not characters: >>> text1 = machado.words('romance/marm05.txt') >>> text1 ['Romance', ',', 'Mem\xf3rias', 'P\xf3stumas', 'de', ...] >>> len(text1) 77098 >>> len(set(text1)) 10848 Here's a program that finds the most common ngrams that contain a particular target word. >>> from nltk import ingrams, FreqDist >>> target_word = 'olhos' >>> fd = FreqDist(ng ... for ng in ingrams(text1, 5) ... if target_word in ng) >>> for hit in fd.samples(): ... print ' '.join(hit) ... , com os olhos no com os olhos no ar com os olhos no ch‹o e todos com os olhos me estar com os olhos os olhos estœpidos , a os olhos na costura , os olhos no ar , , com os olhos espetados , com os olhos estœpidos , com os olhos fitos , com os olhos naquele , com os olhos para Accessing the MacMorpho Tagged Corpus ------------------------------------- NLTK includes the MAC-MORPHO Brazilian Portuguese POS-tagged news text, with over a million words of journalistic texts extracted from ten sections of the daily newspaper *Folha de Sao Paulo*, 1994. We can access this corpus as a sequence of words or tagged words as follows: >>> nltk.corpus.mac_morpho.words() ['Jersei', 'atinge', 'm\xe9dia', 'de', 'Cr$', '1,4', ...] >>> nltk.corpus.mac_morpho.sents() # doctest: +NORMALIZE_WHITESPACE [['Jersei', 'atinge', 'm\xe9dia', 'de', 'Cr$', '1,4', 'milh\xe3o', 'em', 'a', 'venda', 'de', 'a', 'Pinhal', 'em', 'S\xe3o', 'Paulo'], ['Programe', 'sua', 'viagem', 'a', 'a', 'Exposi\xe7\xe3o', 'Nacional', 'do', 'Zebu', ',', 'que', 'come\xe7a', 'dia', '25'], ...] >>> nltk.corpus.mac_morpho.tagged_words() [('Jersei', 'N'), ('atinge', 'V'), ('m\xe9dia', 'N'), ...] We can also access it in sentence chunks. >>> nltk.corpus.mac_morpho.tagged_sents() # doctest: +NORMALIZE_WHITESPACE [[('Jersei', 'N'), ('atinge', 'V'), ('m\xe9dia', 'N'), ('de', 'PREP'), ('Cr$', 'CUR'), ('1,4', 'NUM'), ('milh\xe3o', 'N'), ('em', 'PREP|+'), ('a', 'ART'), ('venda', 'N'), ('de', 'PREP|+'), ('a', 'ART'), ('Pinhal', 'NPROP'), ('em', 'PREP'), ('S\xe3o', 'NPROP'), ('Paulo', 'NPROP')], [('Programe', 'V'), ('sua', 'PROADJ'), ('viagem', 'N'), ('a', 'PREP|+'), ('a', 'ART'), ('Exposi\xe7\xe3o', 'NPROP'), ('Nacional', 'NPROP'), ('do', 'NPROP'), ('Zebu', 'NPROP'), (',', ','), ('que', 'PRO-KS-REL'), ('come\xe7a', 'V'), ('dia', 'N'), ('25', 'N|AP')], ...] This data can be used to train taggers (examples below for the Floresta treebank). Accessing the Floresta Portuguese Treebank ------------------------------------------ The NLTK data distribution includes the "Floresta Sinta(c)tica Corpus" version 7.4, available from ``http://www.linguateca.pt/Floresta/``. We can access this corpus as a sequence of words or tagged words as follows: >>> from nltk.corpus import floresta >>> floresta.words() ['Um', 'revivalismo', 'refrescante', 'O', '7_e_Meio', ...] >>> floresta.tagged_words() [('Um', '>N+art'), ('revivalismo', 'H+n'), ...] The tags consist of some syntactic information, followed by a plus sign, followed by a conventional part-of-speech tag. Let's strip off the material before the plus sign: >>> def simplify_tag(t): ... if "+" in t: ... return t[t.index("+")+1:] ... else: ... return t >>> twords = floresta.tagged_words() >>> twords = [(w.lower(), simplify_tag(t)) for (w,t) in twords] >>> twords[:10] # doctest: +NORMALIZE_WHITESPACE [('um', 'art'), ('revivalismo', 'n'), ('refrescante', 'adj'), ('o', 'art'), ('7_e_meio', 'prop'), ('\xe9', 'v-fin'), ('um', 'art'), ('ex-libris', 'n'), ('de', 'prp'), ('a', 'art')] Pretty printing the tagged words: >>> print ' '.join(word + '/' + tag for (word, tag) in twords[:10]) um/art revivalismo/n refrescante/adj o/art 7_e_meio/prop é/v-fin um/art ex-libris/n de/prp a/art Count the word tokens and types, and determine the most common word: >>> words = floresta.words() >>> len(words) 211852 >>> fd = nltk.FreqDist(words) >>> len(fd) 29421 >>> fd.max() 'de' List the 20 most frequent tags, in order of decreasing frequency: >>> tags = [simplify_tag(tag) for (word,tag) in floresta.tagged_words()] >>> fd = nltk.FreqDist(tags) >>> fd.keys()[:20] # doctest: +NORMALIZE_WHITESPACE ['n', 'prp', 'art', 'v-fin', ',', 'prop', 'adj', 'adv', '.', 'conj-c', 'v-inf', 'pron-det', 'v-pcp', 'num', 'pron-indp', 'pron-pers', '\xab', '\xbb', 'conj-s', '}'] We can also access the corpus grouped by sentence: >>> floresta.sents() # doctest: +NORMALIZE_WHITESPACE [['Um', 'revivalismo', 'refrescante'], ['O', '7_e_Meio', '\xe9', 'um', 'ex-libris', 'de', 'a', 'noite', 'algarvia', '.'], ...] >>> floresta.tagged_sents() # doctest: +NORMALIZE_WHITESPACE [[('Um', '>N+art'), ('revivalismo', 'H+n'), ('refrescante', 'N<+adj')], [('O', '>N+art'), ('7_e_Meio', 'H+prop'), ('\xe9', 'P+v-fin'), ('um', '>N+art'), ('ex-libris', 'H+n'), ('de', 'H+prp'), ('a', '>N+art'), ('noite', 'H+n'), ('algarvia', 'N<+adj'), ('.', '.')], ...] >>> floresta.parsed_sents() # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS [Tree('UTT+np', [Tree('>N+art', ['Um']), Tree('H+n', ['revivalismo']), Tree('N<+adj', ['refrescante'])]), Tree('STA+fcl', [Tree('SUBJ+np', [Tree('>N+art', ['O']), Tree('H+prop', ['7_e_Meio'])]), Tree('P+v-fin', ['\xe9']), Tree('SC+np', [Tree('>N+art', ['um']), Tree('H+n', ['ex-libris']), Tree('N<+pp', [Tree('H+prp', ['de']), Tree('P<+np', [Tree('>N+art', ['a']), Tree('H+n', ['noite']), Tree('N<+adj', ['algarvia'])])])]), Tree('.', ['.'])]), ...] To view a parse tree, use the ``draw()`` method, e.g.: >>> psents = floresta.parsed_sents() >>> psents[5].draw() # doctest: +SKIP Character Encodings ------------------- Python understands the common character encoding used for Portuguese, ISO 8859-1 (ISO Latin 1). >>> import os, nltk.test >>> testdir = os.path.split(nltk.test.__file__)[0] >>> text = open(os.path.join(testdir, 'floresta.txt')).read() >>> text[:60] 'O 7 e Meio \xe9 um ex-libris da noite algarvia.\n\xc9 uma das mais ' >>> print text[:60] O 7 e Meio é um ex-libris da noite algarvia. É uma das mais >>> text[:60].decode('latin-1') u'O 7 e Meio \xe9 um ex-libris da noite algarvia.\n\xc9 uma das mais ' >>> text[:60].decode('latin-1').encode('utf-16') '\xff\xfeO\x00 \x007\x00 \x00e\x00 \x00M\x00e\x00i\x00o\x00 \x00\xe9\x00 \x00u\x00m\x00 \x00e\x00x\x00-\x00l\x00i\x00b\x00r\x00i\x00s\x00 \x00d\x00a\x00 \x00n\x00o\x00i\x00t\x00e\x00 \x00a\x00l\x00g\x00a\x00r\x00v\x00i\x00a\x00.\x00\n\x00\xc9\x00 \x00u\x00m\x00a\x00 \x00d\x00a\x00s\x00 \x00m\x00a\x00i\x00s\x00 \x00' For more information about character encodings and Python, please see section 3.3 of the book. ---------------- Processing Tasks ---------------- Simple Concordancing -------------------- Here's a function that takes a word and a specified amount of context (measured in characters), and generates a concordance for that word. >>> def concordance(word, context=30): ... for sent in floresta.sents(): ... if word in sent: ... pos = sent.index(word) ... left = ' '.join(sent[:pos]) ... right = ' '.join(sent[pos+1:]) ... print '%*s %s %-*s' %\ ... (context, left[-context:], word, context, right[:context]) >>> concordance("dar") # doctest: +SKIP anduru , foi o suficiente para dar a volta a o resultado . 1. O P?BLICO veio dar a a imprensa di?ria portuguesa A fartura de pensamento pode dar maus resultados e n?s n?o quer Come?a a dar resultados a pol?tica de a Uni ial come?ar a incorporar- lo e dar forma a um ' site ' que tem se r com Constantino para ele lhe dar tamb?m os pap?is assinados . va a brincar , pois n?o lhe ia dar procura??o nenhuma enquanto n? ?rica como o ant?doto capaz de dar sentido a o seu enorme poder . . . . >>> concordance("vender") # doctest: +SKIP er recebido uma encomenda para vender 4000 blindados a o Iraque . m?rico_Amorim caso conseguisse vender o lote de ac??es de o empres?r mpre ter jovens simp?ticos a ? vender ? chega ! } Disse que o governo vai vender ? desde autom?vel at? particip ndiciou ontem duas pessoas por vender carro com ?gio . A inten??o de Fleury ? vender as a??es para equilibrar as fi Part-of-Speech Tagging ---------------------- Let's begin by getting the tagged sentence data, and simplifying the tags as described earlier. >>> from nltk.corpus import floresta >>> tsents = floresta.tagged_sents() >>> tsents = [[(w.lower(),simplify_tag(t)) for (w,t) in sent] for sent in tsents if sent] >>> train = tsents[100:] >>> test = tsents[:100] We already know that ``n`` is the most common tag, so we can set up a default tagger that tags every word as a noun, and see how well it does: >>> tagger0 = nltk.DefaultTagger('n') >>> nltk.tag.accuracy(tagger0, test) 0.17697228144989338 Evidently, about one in every six words is a noun. Let's improve on this by training a unigram tagger: >>> tagger1 = nltk.UnigramTagger(train, backoff=tagger0) >>> nltk.tag.accuracy(tagger1, test) 0.87029140014214645 Next a bigram tagger: >>> tagger2 = nltk.BigramTagger(train, backoff=tagger1) >>> nltk.tag.accuracy(tagger2, test) 0.89019189765458417 Sentence Segmentation --------------------- Punkt is a language-neutral sentence segmentation tool. We >>> sent_tokenizer=nltk.data.load('tokenizers/punkt/portuguese.pickle') >>> raw_text = machado.raw('romance/marm05.txt') >>> sentences = sent_tokenizer.tokenize(raw_text) >>> for sent in sentences[1000:1005]: ... print sent: >>> for sent in sentences[1000:1005]: ... print "<<", sent, ">>" ... << Em verdade, parecia ainda mais mulher do que era; seria criana nos seus folgares de moa; mas assim quieta, impass’vel, tinha a compostura da mulher casada. >> << Talvez essa circunst‰ncia lhe diminu’a um pouco da graa virginal. >> << Depressa nos familiarizamos; a m‹e fazia-lhe grandes elogios, eu escutava-os de boa sombra, e ela sorria com os olhos fœlgidos, como se l‡ dentro do cŽrebro lhe estivesse a voar uma borboletinha de asas de ouro e olhos de diamante... >> << Digo l‡ dentro, porque c‡ fora o que esvoaou foi uma borboleta preta, que subitamente penetrou na varanda, e comeou a bater as asas em derredor de D. EusŽbia. >> << D. EusŽbia deu um grito, levantou-se, praguejou umas palavras soltas: - T'esconjuro!... >> The sentence tokenizer can be trained and evaluated on other text. The source text (from the Floresta Portuguese Treebank) contains one sentence per line. We read the text, split it into its lines, and then join these lines together using spaces. Now the information about sentence breaks has been discarded. We split this material into training and testing data: >>> import os, nltk.test >>> testdir = os.path.split(nltk.test.__file__)[0] >>> text = open(os.path.join(testdir, 'floresta.txt')).read() >>> lines = text.split('\n') >>> train = ' '.join(lines[10:]) >>> test = ' '.join(lines[:10]) Now we train the sentence segmenter (or sentence tokenizer) and use it on our test sentences: >>> stok = nltk.PunktSentenceTokenizer(train) >>> print stok.tokenize(test) # doctest: +NORMALIZE_WHITESPACE ['O 7 e Meio \xe9 um ex-libris da noite algarvia.', '\xc9 uma das mais antigas discotecas do Algarve, situada em Albufeira, que continua a manter os tra\xe7os decorativos e as clientelas de sempre.', '\xc9 um pouco a vers\xe3o de uma esp\xe9cie de \xaboutro lado\xbb da noite, a meio caminho entre os devaneios de uma fauna perif\xe9rica, seja de Lisboa, Londres, Dublin ou Faro e Portim\xe3o, e a postura circunspecta dos fi\xe9is da casa, que dela esperam a m\xfasica \xabgeracionista\xbb dos 60 ou dos 70.', 'N\xe3o deixa de ser, nos tempos que correm, um certo \xabvery typical\xbb algarvio, cabe\xe7a de cartaz para os que querem fugir a algumas movimenta\xe7\xf5es nocturnas j\xe1 a caminho da ritualiza\xe7\xe3o de massas, do g\xe9nero \xabvamos todos ao Calypso e encontramo-nos na Locomia\xbb.', 'E assim, aos 2,5 milh\xf5es que o Minist\xe9rio do Planeamento e Administra\xe7\xe3o do Territ\xf3rio j\xe1 gasta no pagamento do pessoal afecto a estes organismos, v\xeam juntar-se os montantes das obras propriamente ditas, que os munic\xedpios, j\xe1 com projectos na m\xe3o, v\xeam reivindicar junto do Executivo, como salienta aquele membro do Governo.', 'E o dinheiro \xabn\xe3o falta s\xf3 \xe0s c\xe2maras\xbb, lembra o secret\xe1rio de Estado, que considera que a solu\xe7\xe3o para as autarquias \xe9 \xabespecializarem-se em fundos comunit\xe1rios\xbb.', 'Mas como, se muitas n\xe3o disp\xf5em, nos seus quadros, dos t\xe9cnicos necess\xe1rios?', '\xabEncomendem-nos a projectistas de fora\xbb porque, se as obras vierem a ser financiadas, eles at\xe9 saem de gra\xe7a, j\xe1 que, nesse caso, \xabos fundos comunit\xe1rios pagam os projectos, o mesmo n\xe3o acontecendo quando eles s\xe3o feitos pelos GAT\xbb, dado serem organismos do Estado.', 'Essa poder\xe1 vir a ser uma hip\xf3tese, at\xe9 porque, no terreno, a capacidade dos GAT est\xe1 cada vez mais enfraquecida.', 'Alguns at\xe9 j\xe1 desapareceram, como o de Castro Verde, e outros t\xeam vindo a perder quadros.'] NLTK's data collection includes a trained model for Portuguese sentence segmentation, which can be loaded as follows. It is faster to load a trained model than to retrain it. >>> stok = nltk.data.load('tokenizers/punkt/portuguese.pickle') Stemming -------- NLTK includes the RSLP Portuguese stemmer. Here we use it to stem some Portuguese text: >>> stemmer = nltk.stem.RSLPStemmer() >>> stemmer.stem("copiar") u'copi' >>> stemmer.stem("paisagem") u'pais' Stopwords --------- NLTK includes Portuguese stopwords: >>> stopwords = nltk.corpus.stopwords.words('portuguese') >>> stopwords[:10] ['a', 'ao', 'aos', 'aquela', 'aquelas', 'aquele', 'aqueles', 'aquilo', 'as', 'at\xe9'] Now we can use these to filter text. Let's find the most frequent words (other than stopwords) and print them in descending order of frequency: >>> fd = nltk.FreqDist(w.lower() for w in floresta.words() if w not in stopwords) >>> for word in fd.keys()[:20]: ... print word, fd[word] # doctest: +NORMALIZE_WHITESPACE , 13444 . 7725 « 2369 » 2310 é 1305 o 1086 } 1047 { 1044 a 897 ; 633 em 516 ser 466 sobre 349 os 313 anos 301 ontem 292 ainda 279 segundo 256 ter 249 dois 231 nltk-2.0~b9/nltk/test/parse.doctest0000644000175000017500000011114511354233125017210 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ========= Parsing ========= Unit tests for the Context Free Grammar class --------------------------------------------- >>> from nltk import Nonterminal, nonterminals, Production, parse_cfg, ContextFreeGrammar >>> nt1 = Nonterminal('NP') >>> nt2 = Nonterminal('VP') >>> nt1.symbol() 'NP' >>> nt1 == Nonterminal('NP') True >>> nt1 == nt2 False >>> S, NP, VP, PP = nonterminals('S, NP, VP, PP') >>> N, V, P, DT = nonterminals('N, V, P, DT') >>> prod1 = Production(S, [NP, VP]) >>> prod2 = Production(NP, [DT, NP]) >>> prod1.lhs() S >>> prod1.rhs() (NP, VP) >>> prod1 == Production(S, [NP, VP]) True >>> prod1 == prod2 False >>> grammar = parse_cfg(""" ... S -> NP VP ... PP -> P NP ... NP -> 'the' N | N PP | 'the' N PP ... VP -> V NP | V PP | V NP PP ... N -> 'cat' ... N -> 'dog' ... N -> 'rug' ... V -> 'chased' ... V -> 'sat' ... P -> 'in' ... P -> 'on' ... """) Unit tests for the rd (Recursive Descent Parser) class ------------------------------------------------------ Create and run a recursive descent parser over both a syntactically ambiguous and unambiguous sentence. >>> from nltk.parse import RecursiveDescentParser >>> rd = RecursiveDescentParser(grammar) >>> sentence1 = 'the cat chased the dog'.split() >>> sentence2 = 'the cat chased the dog on the rug'.split() >>> for t in rd.nbest_parse(sentence1): ... print t (S (NP the (N cat)) (VP (V chased) (NP the (N dog)))) >>> for t in rd.nbest_parse(sentence2): ... print t (S (NP the (N cat)) (VP (V chased) (NP the (N dog) (PP (P on) (NP the (N rug)))))) (S (NP the (N cat)) (VP (V chased) (NP the (N dog)) (PP (P on) (NP the (N rug))))) (dolist (expr doctest-font-lock-keywords) (add-to-list 'font-lock-keywords expr)) font-lock-keywords (add-to-list 'font-lock-keywords (car doctest-font-lock-keywords)) Unit tests for the sr (Shift Reduce Parser) class ------------------------------------------------- Create and run a shift reduce parser over both a syntactically ambiguous and unambiguous sentence. Note that unlike the recursive descent parser, one and only one parse is ever returned. >>> from nltk.parse import ShiftReduceParser >>> sr = ShiftReduceParser(grammar) >>> sentence1 = 'the cat chased the dog'.split() >>> sentence2 = 'the cat chased the dog on the rug'.split() >>> for t in sr.nbest_parse(sentence1): ... print t (S (NP the (N cat)) (VP (V chased) (NP the (N dog)))) The shift reduce parser uses heuristics to decide what to do when there are multiple possible shift or reduce operations available - for the supplied grammar clearly the wrong operation is selected. >>> for t in sr.nbest_parse(sentence2): ... print t Unit tests for the Chart Parser class ------------------------------------- We use the demo() function for testing. We must turn off showing of times. >>> import nltk First we test tracing with a short sentence >>> nltk.parse.chart.demo(2, should_print_times=False, trace=1, ... sent='I saw a dog', numparses=1) * Sentence: I saw a dog ['I', 'saw', 'a', 'dog'] * Strategy: Bottom-up |. I . saw . a . dog .| |[---------] . . .| [0:1] 'I' |. [---------] . .| [1:2] 'saw' |. . [---------] .| [2:3] 'a' |. . . [---------]| [3:4] 'dog' |> . . . .| [0:0] NP -> * 'I' |[---------] . . .| [0:1] NP -> 'I' * |> . . . .| [0:0] S -> * NP VP |> . . . .| [0:0] NP -> * NP PP |[---------> . . .| [0:1] S -> NP * VP |[---------> . . .| [0:1] NP -> NP * PP |. > . . .| [1:1] Verb -> * 'saw' |. [---------] . .| [1:2] Verb -> 'saw' * |. > . . .| [1:1] VP -> * Verb NP |. > . . .| [1:1] VP -> * Verb |. [---------> . .| [1:2] VP -> Verb * NP |. [---------] . .| [1:2] VP -> Verb * |. > . . .| [1:1] VP -> * VP PP |[-------------------] . .| [0:2] S -> NP VP * |. [---------> . .| [1:2] VP -> VP * PP |. . > . .| [2:2] Det -> * 'a' |. . [---------] .| [2:3] Det -> 'a' * |. . > . .| [2:2] NP -> * Det Noun |. . [---------> .| [2:3] NP -> Det * Noun |. . . > .| [3:3] Noun -> * 'dog' |. . . [---------]| [3:4] Noun -> 'dog' * |. . [-------------------]| [2:4] NP -> Det Noun * |. . > . .| [2:2] S -> * NP VP |. . > . .| [2:2] NP -> * NP PP |. [-----------------------------]| [1:4] VP -> Verb NP * |. . [------------------->| [2:4] S -> NP * VP |. . [------------------->| [2:4] NP -> NP * PP |[=======================================]| [0:4] S -> NP VP * |. [----------------------------->| [1:4] VP -> VP * PP Nr edges in chart: 33 (S (NP I) (VP (Verb saw) (NP (Det a) (Noun dog)))) Then we test the different parsing Strategies. Note that the number of edges differ between the strategies. Top-down >>> nltk.parse.chart.demo(1, should_print_times=False, trace=0, ... sent='I saw John with a dog', numparses=2) * Sentence: I saw John with a dog ['I', 'saw', 'John', 'with', 'a', 'dog'] * Strategy: Top-down Nr edges in chart: 48 (S (NP I) (VP (VP (Verb saw) (NP John)) (PP with (NP (Det a) (Noun dog))))) (S (NP I) (VP (Verb saw) (NP (NP John) (PP with (NP (Det a) (Noun dog)))))) Bottom-up >>> nltk.parse.chart.demo(2, should_print_times=False, trace=0, ... sent='I saw John with a dog', numparses=2) * Sentence: I saw John with a dog ['I', 'saw', 'John', 'with', 'a', 'dog'] * Strategy: Bottom-up Nr edges in chart: 53 (S (NP I) (VP (VP (Verb saw) (NP John)) (PP with (NP (Det a) (Noun dog))))) (S (NP I) (VP (Verb saw) (NP (NP John) (PP with (NP (Det a) (Noun dog)))))) Bottom-up Left-Corner >>> nltk.parse.chart.demo(3, should_print_times=False, trace=0, ... sent='I saw John with a dog', numparses=2) * Sentence: I saw John with a dog ['I', 'saw', 'John', 'with', 'a', 'dog'] * Strategy: Bottom-up left-corner Nr edges in chart: 36 (S (NP I) (VP (VP (Verb saw) (NP John)) (PP with (NP (Det a) (Noun dog))))) (S (NP I) (VP (Verb saw) (NP (NP John) (PP with (NP (Det a) (Noun dog)))))) Left-Corner with Bottom-Up Filter >>> nltk.parse.chart.demo(4, should_print_times=False, trace=0, ... sent='I saw John with a dog', numparses=2) * Sentence: I saw John with a dog ['I', 'saw', 'John', 'with', 'a', 'dog'] * Strategy: Filtered left-corner Nr edges in chart: 28 (S (NP I) (VP (VP (Verb saw) (NP John)) (PP with (NP (Det a) (Noun dog))))) (S (NP I) (VP (Verb saw) (NP (NP John) (PP with (NP (Det a) (Noun dog)))))) The stepping chart parser >>> nltk.parse.chart.demo(5, should_print_times=False, trace=1, ... sent='I saw John with a dog', numparses=2) * Sentence: I saw John with a dog ['I', 'saw', 'John', 'with', 'a', 'dog'] * Strategy: Stepping (top-down vs bottom-up) *** SWITCH TO TOP DOWN |[------] . . . . .| [0:1] 'I' |. [------] . . . .| [1:2] 'saw' |. . [------] . . .| [2:3] 'John' |. . . [------] . .| [3:4] 'with' |. . . . [------] .| [4:5] 'a' |. . . . . [------]| [5:6] 'dog' |> . . . . . .| [0:0] S -> * NP VP |> . . . . . .| [0:0] NP -> * NP PP |> . . . . . .| [0:0] NP -> * Det Noun |> . . . . . .| [0:0] NP -> * 'I' |[------] . . . . .| [0:1] NP -> 'I' * |[------> . . . . .| [0:1] S -> NP * VP |[------> . . . . .| [0:1] NP -> NP * PP |. > . . . . .| [1:1] VP -> * VP PP |. > . . . . .| [1:1] VP -> * Verb NP |. > . . . . .| [1:1] VP -> * Verb |. > . . . . .| [1:1] Verb -> * 'saw' |. [------] . . . .| [1:2] Verb -> 'saw' * |. [------> . . . .| [1:2] VP -> Verb * NP |. [------] . . . .| [1:2] VP -> Verb * |[-------------] . . . .| [0:2] S -> NP VP * |. [------> . . . .| [1:2] VP -> VP * PP *** SWITCH TO BOTTOM UP |. . > . . . .| [2:2] NP -> * 'John' |. . . > . . .| [3:3] PP -> * 'with' NP |. . . > . . .| [3:3] Prep -> * 'with' |. . . . > . .| [4:4] Det -> * 'a' |. . . . . > .| [5:5] Noun -> * 'dog' |. . [------] . . .| [2:3] NP -> 'John' * |. . . [------> . .| [3:4] PP -> 'with' * NP |. . . [------] . .| [3:4] Prep -> 'with' * |. . . . [------] .| [4:5] Det -> 'a' * |. . . . . [------]| [5:6] Noun -> 'dog' * |. [-------------] . . .| [1:3] VP -> Verb NP * |[--------------------] . . .| [0:3] S -> NP VP * |. [-------------> . . .| [1:3] VP -> VP * PP |. . > . . . .| [2:2] S -> * NP VP |. . > . . . .| [2:2] NP -> * NP PP |. . . . > . .| [4:4] NP -> * Det Noun |. . [------> . . .| [2:3] S -> NP * VP |. . [------> . . .| [2:3] NP -> NP * PP |. . . . [------> .| [4:5] NP -> Det * Noun |. . . . [-------------]| [4:6] NP -> Det Noun * |. . . [--------------------]| [3:6] PP -> 'with' NP * |. [----------------------------------]| [1:6] VP -> VP PP * *** SWITCH TO TOP DOWN |. . > . . . .| [2:2] NP -> * Det Noun |. . . . > . .| [4:4] NP -> * NP PP |. . . > . . .| [3:3] VP -> * VP PP |. . . > . . .| [3:3] VP -> * Verb NP |. . . > . . .| [3:3] VP -> * Verb |[=========================================]| [0:6] S -> NP VP * |. [---------------------------------->| [1:6] VP -> VP * PP |. . [---------------------------]| [2:6] NP -> NP PP * |. . . . [------------->| [4:6] NP -> NP * PP |. [----------------------------------]| [1:6] VP -> Verb NP * |. . [--------------------------->| [2:6] S -> NP * VP |. . [--------------------------->| [2:6] NP -> NP * PP |[=========================================]| [0:6] S -> NP VP * |. [---------------------------------->| [1:6] VP -> VP * PP |. . . . . . >| [6:6] VP -> * VP PP |. . . . . . >| [6:6] VP -> * Verb NP |. . . . . . >| [6:6] VP -> * Verb *** SWITCH TO BOTTOM UP |. . . . > . .| [4:4] S -> * NP VP |. . . . [------------->| [4:6] S -> NP * VP *** SWITCH TO TOP DOWN *** SWITCH TO BOTTOM UP *** SWITCH TO TOP DOWN *** SWITCH TO BOTTOM UP *** SWITCH TO TOP DOWN *** SWITCH TO BOTTOM UP Nr edges in chart: 61 (S (NP I) (VP (VP (Verb saw) (NP John)) (PP with (NP (Det a) (Noun dog))))) (S (NP I) (VP (Verb saw) (NP (NP John) (PP with (NP (Det a) (Noun dog)))))) Unit tests for the Incremental Chart Parser class ------------------------------------------------- The incremental chart parsers are defined in earleychart.py. We use the demo() function for testing. We must turn off showing of times. >>> import nltk Earley Chart Parser >>> nltk.parse.earleychart.demo(should_print_times=False, trace=1, ... sent='I saw John with a dog', numparses=2) * Sentence: I saw John with a dog ['I', 'saw', 'John', 'with', 'a', 'dog'] |. I . saw . John . with . a . dog .| |[------] . . . . .| [0:1] 'I' |. [------] . . . .| [1:2] 'saw' |. . [------] . . .| [2:3] 'John' |. . . [------] . .| [3:4] 'with' |. . . . [------] .| [4:5] 'a' |. . . . . [------]| [5:6] 'dog' |> . . . . . .| [0:0] S -> * NP VP |> . . . . . .| [0:0] NP -> * NP PP |> . . . . . .| [0:0] NP -> * Det Noun |> . . . . . .| [0:0] NP -> * 'I' |[------] . . . . .| [0:1] NP -> 'I' * |[------> . . . . .| [0:1] S -> NP * VP |[------> . . . . .| [0:1] NP -> NP * PP |. > . . . . .| [1:1] VP -> * VP PP |. > . . . . .| [1:1] VP -> * Verb NP |. > . . . . .| [1:1] VP -> * Verb |. > . . . . .| [1:1] Verb -> * 'saw' |. [------] . . . .| [1:2] Verb -> 'saw' * |. [------> . . . .| [1:2] VP -> Verb * NP |. [------] . . . .| [1:2] VP -> Verb * |[-------------] . . . .| [0:2] S -> NP VP * |. [------> . . . .| [1:2] VP -> VP * PP |. . > . . . .| [2:2] NP -> * NP PP |. . > . . . .| [2:2] NP -> * Det Noun |. . > . . . .| [2:2] NP -> * 'John' |. . [------] . . .| [2:3] NP -> 'John' * |. [-------------] . . .| [1:3] VP -> Verb NP * |. . [------> . . .| [2:3] NP -> NP * PP |. . . > . . .| [3:3] PP -> * 'with' NP |[--------------------] . . .| [0:3] S -> NP VP * |. [-------------> . . .| [1:3] VP -> VP * PP |. . . [------> . .| [3:4] PP -> 'with' * NP |. . . . > . .| [4:4] NP -> * NP PP |. . . . > . .| [4:4] NP -> * Det Noun |. . . . > . .| [4:4] Det -> * 'a' |. . . . [------] .| [4:5] Det -> 'a' * |. . . . [------> .| [4:5] NP -> Det * Noun |. . . . . > .| [5:5] Noun -> * 'dog' |. . . . . [------]| [5:6] Noun -> 'dog' * |. . . . [-------------]| [4:6] NP -> Det Noun * |. . . [--------------------]| [3:6] PP -> 'with' NP * |. . . . [------------->| [4:6] NP -> NP * PP |. . [---------------------------]| [2:6] NP -> NP PP * |. [----------------------------------]| [1:6] VP -> VP PP * |[=========================================]| [0:6] S -> NP VP * |. [---------------------------------->| [1:6] VP -> VP * PP |. [----------------------------------]| [1:6] VP -> Verb NP * |. . [--------------------------->| [2:6] NP -> NP * PP |[=========================================]| [0:6] S -> NP VP * |. [---------------------------------->| [1:6] VP -> VP * PP (S (NP I) (VP (VP (Verb saw) (NP John)) (PP with (NP (Det a) (Noun dog))))) (S (NP I) (VP (Verb saw) (NP (NP John) (PP with (NP (Det a) (Noun dog)))))) Unit tests for LARGE context-free grammars ------------------------------------------ Reading the ATIS grammar. >>> grammar = nltk.data.load('grammars/large_grammars/atis.cfg') >>> grammar Reading the test sentences. >>> sentences = nltk.data.load('grammars/large_grammars/atis_sentences.txt', format='raw') >>> sentences = nltk.parse.util.extract_test_sentences(sentences) >>> len(sentences) 98 >>> testsentence = sentences[22] >>> testsentence[0] ['show', 'me', 'northwest', 'flights', 'to', 'detroit', '.'] >>> testsentence[1] 17 >>> sentence = testsentence[0] Now we test all different parsing strategies. Note that the number of edges differ between the strategies. Bottom-up parsing. >>> parser = nltk.parse.BottomUpChartParser(grammar) >>> chart = parser.chart_parse(sentence) >>> print chart.num_edges() 7661 >>> print len(chart.parses(grammar.start())) 17 Bottom-up Left-corner parsing. >>> parser = nltk.parse.BottomUpLeftCornerChartParser(grammar) >>> chart = parser.chart_parse(sentence) >>> print chart.num_edges() 4986 >>> print len(chart.parses(grammar.start())) 17 Left-corner parsing with bottom-up filter. >>> parser = nltk.parse.LeftCornerChartParser(grammar) >>> chart = parser.chart_parse(sentence) >>> print chart.num_edges() 1342 >>> print len(chart.parses(grammar.start())) 17 Top-down parsing. >>> parser = nltk.parse.TopDownChartParser(grammar) >>> chart = parser.chart_parse(sentence) >>> print chart.num_edges() 28352 >>> print len(chart.parses(grammar.start())) 17 Incremental Bottom-up parsing. >>> parser = nltk.parse.IncrementalBottomUpChartParser(grammar) >>> chart = parser.chart_parse(sentence) >>> print chart.num_edges() 7661 >>> print len(chart.parses(grammar.start())) 17 Incremental Bottom-up Left-corner parsing. >>> parser = nltk.parse.IncrementalBottomUpLeftCornerChartParser(grammar) >>> chart = parser.chart_parse(sentence) >>> print chart.num_edges() 4986 >>> print len(chart.parses(grammar.start())) 17 Incremental Left-corner parsing with bottom-up filter. >>> parser = nltk.parse.IncrementalLeftCornerChartParser(grammar) >>> chart = parser.chart_parse(sentence) >>> print chart.num_edges() 1342 >>> print len(chart.parses(grammar.start())) 17 Incremental Top-down parsing. >>> parser = nltk.parse.IncrementalTopDownChartParser(grammar) >>> chart = parser.chart_parse(sentence) >>> print chart.num_edges() 28352 >>> print len(chart.parses(grammar.start())) 17 Earley parsing. This is similar to the incremental top-down algorithm. >>> parser = nltk.parse.EarleyChartParser(grammar) >>> chart = parser.chart_parse(sentence) >>> print chart.num_edges() 28352 >>> print len(chart.parses(grammar.start())) 17 Unit tests for the Probabilistic CFG class ------------------------------------------ >>> from nltk.corpus import treebank >>> from itertools import islice >>> from nltk import parse_pcfg, induce_pcfg, toy_pcfg1, toy_pcfg2 Create a set of probabilistic CFG productions. >>> grammar = parse_pcfg(""" ... A -> B B [.3] | C B C [.7] ... B -> B D [.5] | C [.5] ... C -> 'a' [.1] | 'b' [0.9] ... D -> 'b' [1.0] ... """) >>> prod = grammar.productions()[0] >>> prod A -> B B [0.3] >>> prod.lhs() A >>> prod.rhs() (B, B) >>> prod.prob() 0.29999999999999999 >>> grammar.start() A >>> grammar.productions() [A -> B B [0.3], A -> C B C [0.7], B -> B D [0.5], B -> C [0.5], C -> 'a' [0.1], C -> 'b' [0.9], D -> 'b' [1.0]] Induce some productions using parsed Treebank data. >>> productions = [] >>> for fileid in treebank.fileids()[:2]: ... for t in treebank.parsed_sents(fileid): ... productions += t.productions() >>> grammar = induce_pcfg(S, productions) >>> grammar >>> grammar.productions()[:5] [PP -> IN NP [1.0], NNP -> 'Nov.' [0.0714285714286], NNP -> 'Agnew' [0.0714285714286], JJ -> 'industrial' [0.142857142857], NP -> CD NNS [0.133333333333]] Unit tests for the Probabilistic Chart Parse classes ---------------------------------------------------- >>> tokens = "Jack saw Bob with my cookie".split() >>> grammar = toy_pcfg2 >>> print grammar Grammar with 23 productions (start state = S) S -> NP VP [1.0] VP -> V NP [0.59] VP -> V [0.4] VP -> VP PP [0.01] NP -> Det N [0.41] NP -> Name [0.28] NP -> NP PP [0.31] PP -> P NP [1.0] V -> 'saw' [0.21] V -> 'ate' [0.51] V -> 'ran' [0.28] N -> 'boy' [0.11] N -> 'cookie' [0.12] N -> 'table' [0.13] N -> 'telescope' [0.14] N -> 'hill' [0.5] Name -> 'Jack' [0.52] Name -> 'Bob' [0.48] P -> 'with' [0.61] P -> 'under' [0.39] Det -> 'the' [0.41] Det -> 'a' [0.31] Det -> 'my' [0.28] Create several parsers using different queuing strategies and show the resulting parses. >>> from nltk.parse import pchart >>> parser = pchart.InsideChartParser(grammar) >>> for t in parser.nbest_parse(tokens): ... print t (S (NP (Name Jack)) (VP (V saw) (NP (NP (Name Bob)) (PP (P with) (NP (Det my) (N cookie)))))) (p=6.31606532355e-06) (S (NP (Name Jack)) (VP (VP (V saw) (NP (Name Bob))) (PP (P with) (NP (Det my) (N cookie))))) (p=2.03744042695e-07) >>> parser = pchart.RandomChartParser(grammar) >>> for t in parser.nbest_parse(tokens): ... print t (S (NP (Name Jack)) (VP (V saw) (NP (NP (Name Bob)) (PP (P with) (NP (Det my) (N cookie)))))) (p=6.31606532355e-06) (S (NP (Name Jack)) (VP (VP (V saw) (NP (Name Bob))) (PP (P with) (NP (Det my) (N cookie))))) (p=2.03744042695e-07) >>> parser = pchart.UnsortedChartParser(grammar) >>> for t in parser.nbest_parse(tokens): ... print t (S (NP (Name Jack)) (VP (V saw) (NP (NP (Name Bob)) (PP (P with) (NP (Det my) (N cookie)))))) (p=6.31606532355e-06) (S (NP (Name Jack)) (VP (VP (V saw) (NP (Name Bob))) (PP (P with) (NP (Det my) (N cookie))))) (p=2.03744042695e-07) >>> parser = pchart.LongestChartParser(grammar) >>> for t in parser.nbest_parse(tokens): ... print t (S (NP (Name Jack)) (VP (V saw) (NP (NP (Name Bob)) (PP (P with) (NP (Det my) (N cookie)))))) (p=6.31606532355e-06) (S (NP (Name Jack)) (VP (VP (V saw) (NP (Name Bob))) (PP (P with) (NP (Det my) (N cookie))))) (p=2.03744042695e-07) >>> parser = pchart.InsideChartParser(grammar, beam_size = len(tokens)+1) >>> for t in parser.nbest_parse(tokens): ... print t Unit tests for the Viterbi Parse classes ---------------------------------------- >>> from nltk.parse import ViterbiParser >>> tokens = "Jack saw Bob with my cookie".split() >>> grammar = toy_pcfg2 Parse the tokenized sentence. >>> parser = ViterbiParser(grammar) >>> for t in parser.nbest_parse(tokens): ... print t (S (NP (Name Jack)) (VP (V saw) (NP (NP (Name Bob)) (PP (P with) (NP (Det my) (N cookie)))))) (p=6.31606532355e-06) Unit tests for the FeatStructNonterminal class ---------------------------------------------- >>> from nltk.parse import FeatStructNonterminal >>> FeatStructNonterminal( ... pos='n', agr=FeatStructNonterminal(number='pl', gender='f')) [agr=[gender='f', number='pl'], pos='n'] >>> FeatStructNonterminal('VP[+fin]/NP[+pl]') VP[+fin]/NP[+pl] Unit tests for the Feature Chart Parser classes ----------------------------------------------- Let's use the demo() function as a unit test. Top-down. >>> nltk.parse.featurechart.demo(should_print_times=False, ... should_print_grammar=False, trace=1, ... parser=nltk.FeatureTopDownChartParser, ... sent='I saw John with a dog') * FeatureTopDownChartParser Sentence: I saw John with a dog |.I.s.J.w.a.d.| |[-] . . . . .| [0:1] 'I' |. [-] . . . .| [1:2] 'saw' |. . [-] . . .| [2:3] 'John' |. . . [-] . .| [3:4] 'with' |. . . . [-] .| [4:5] 'a' |. . . . . [-]| [5:6] 'dog' |> . . . . . .| [0:0] [INIT][] -> * S[] {} |> . . . . . .| [0:0] S[] -> * NP[] VP[] {} |> . . . . . .| [0:0] NP[] -> * NP[] PP[] {} |> . . . . . .| [0:0] NP[] -> * Det[pl=?x] Noun[pl=?x] {} |> . . . . . .| [0:0] NP[] -> * 'John' {} |> . . . . . .| [0:0] NP[] -> * 'I' {} |[-] . . . . .| [0:1] NP[] -> 'I' * |[-> . . . . .| [0:1] S[] -> NP[] * VP[] {} |[-> . . . . .| [0:1] NP[] -> NP[] * PP[] {} |. > . . . . .| [1:1] PP[] -> * Prep[] NP[] {} |. > . . . . .| [1:1] Prep[] -> * 'with' {} |. > . . . . .| [1:1] Prep[] -> * 'under' {} |. > . . . . .| [1:1] VP[] -> * VP[] PP[] {} |. > . . . . .| [1:1] VP[] -> * Verb[] NP[] {} |. > . . . . .| [1:1] VP[] -> * Verb[] {} |. > . . . . .| [1:1] Verb[] -> * 'ate' {} |. > . . . . .| [1:1] Verb[] -> * 'saw' {} |. [-] . . . .| [1:2] Verb[] -> 'saw' * |. [-> . . . .| [1:2] VP[] -> Verb[] * NP[] {} |. [-] . . . .| [1:2] VP[] -> Verb[] * |[---] . . . .| [0:2] S[] -> NP[] VP[] * |. [-> . . . .| [1:2] VP[] -> VP[] * PP[] {} |. . > . . . .| [2:2] PP[] -> * Prep[] NP[] {} |. . > . . . .| [2:2] Prep[] -> * 'with' {} |. . > . . . .| [2:2] Prep[] -> * 'under' {} |[---] . . . .| [0:2] [INIT][] -> S[] * |. . > . . . .| [2:2] NP[] -> * NP[] PP[] {} |. . > . . . .| [2:2] NP[] -> * Det[pl=?x] Noun[pl=?x] {} |. . > . . . .| [2:2] NP[] -> * 'John' {} |. . > . . . .| [2:2] NP[] -> * 'I' {} |. . [-] . . .| [2:3] NP[] -> 'John' * |. [---] . . .| [1:3] VP[] -> Verb[] NP[] * |. . [-> . . .| [2:3] NP[] -> NP[] * PP[] {} |. . . > . . .| [3:3] PP[] -> * Prep[] NP[] {} |. . . > . . .| [3:3] Prep[] -> * 'with' {} |. . . > . . .| [3:3] Prep[] -> * 'under' {} |. . . [-] . .| [3:4] Prep[] -> 'with' * |. . . [-> . .| [3:4] PP[] -> Prep[] * NP[] {} |. . . . > . .| [4:4] NP[] -> * NP[] PP[] {} |. . . . > . .| [4:4] NP[] -> * Det[pl=?x] Noun[pl=?x] {} |. . . . > . .| [4:4] NP[] -> * 'John' {} |. . . . > . .| [4:4] NP[] -> * 'I' {} |. . . . > . .| [4:4] Det[] -> * 'the' {} |. . . . > . .| [4:4] Det[] -> * 'my' {} |. . . . > . .| [4:4] Det[-pl] -> * 'a' {} |. . . . [-] .| [4:5] Det[-pl] -> 'a' * |. . . . [-> .| [4:5] NP[] -> Det[pl=?x] * Noun[pl=?x] {?x: False} |. . . . . > .| [5:5] Noun[-pl] -> * 'dog' {} |. . . . . > .| [5:5] Noun[-pl] -> * 'cookie' {} |. . . . . [-]| [5:6] Noun[-pl] -> 'dog' * |. . . . [---]| [4:6] NP[] -> Det[-pl] Noun[-pl] * |. . . [-----]| [3:6] PP[] -> Prep[] NP[] * |. . . . [--->| [4:6] NP[] -> NP[] * PP[] {} |. . . . . . >| [6:6] PP[] -> * Prep[] NP[] {} |. . . . . . >| [6:6] Prep[] -> * 'with' {} |. . . . . . >| [6:6] Prep[] -> * 'under' {} |. . [-------]| [2:6] NP[] -> NP[] PP[] * |. [---------]| [1:6] VP[] -> Verb[] NP[] * |. . [------->| [2:6] NP[] -> NP[] * PP[] {} |[===========]| [0:6] S[] -> NP[] VP[] * |. [--------->| [1:6] VP[] -> VP[] * PP[] {} |[===========]| [0:6] [INIT][] -> S[] * |[-----] . . .| [0:3] S[] -> NP[] VP[] * |. [---> . . .| [1:3] VP[] -> VP[] * PP[] {} |. [---------]| [1:6] VP[] -> VP[] PP[] * |[===========]| [0:6] S[] -> NP[] VP[] * |. [--------->| [1:6] VP[] -> VP[] * PP[] {} |[-----] . . .| [0:3] [INIT][] -> S[] * |. . > . . . .| [2:2] Det[] -> * 'the' {} |. . > . . . .| [2:2] Det[] -> * 'my' {} |. . > . . . .| [2:2] Det[-pl] -> * 'a' {} |> . . . . . .| [0:0] Det[] -> * 'the' {} |> . . . . . .| [0:0] Det[] -> * 'my' {} |> . . . . . .| [0:0] Det[-pl] -> * 'a' {} (S[] (NP[] I) (VP[] (Verb[] saw) (NP[] (NP[] John) (PP[] (Prep[] with) (NP[] (Det[-pl] a) (Noun[-pl] dog)))))) (S[] (NP[] I) (VP[] (VP[] (Verb[] saw) (NP[] John)) (PP[] (Prep[] with) (NP[] (Det[-pl] a) (Noun[-pl] dog))))) Bottom-Up Left-Corner. >>> nltk.parse.featurechart.demo(should_print_times=False, ... should_print_grammar=False, trace=1, ... parser=nltk.FeatureBottomUpLeftCornerChartParser, ... sent='I saw John with a dog') * FeatureBottomUpLeftCornerChartParser Sentence: I saw John with a dog |.I.s.J.w.a.d.| |[-] . . . . .| [0:1] 'I' |. [-] . . . .| [1:2] 'saw' |. . [-] . . .| [2:3] 'John' |. . . [-] . .| [3:4] 'with' |. . . . [-] .| [4:5] 'a' |. . . . . [-]| [5:6] 'dog' |[-] . . . . .| [0:1] NP[] -> 'I' * |[-> . . . . .| [0:1] S[] -> NP[] * VP[] {} |[-> . . . . .| [0:1] NP[] -> NP[] * PP[] {} |. [-] . . . .| [1:2] Verb[] -> 'saw' * |. [-> . . . .| [1:2] VP[] -> Verb[] * NP[] {} |. [-] . . . .| [1:2] VP[] -> Verb[] * |. [-> . . . .| [1:2] VP[] -> VP[] * PP[] {} |[---] . . . .| [0:2] S[] -> NP[] VP[] * |. . [-] . . .| [2:3] NP[] -> 'John' * |. . [-> . . .| [2:3] S[] -> NP[] * VP[] {} |. . [-> . . .| [2:3] NP[] -> NP[] * PP[] {} |. [---] . . .| [1:3] VP[] -> Verb[] NP[] * |. [---> . . .| [1:3] VP[] -> VP[] * PP[] {} |[-----] . . .| [0:3] S[] -> NP[] VP[] * |. . . [-] . .| [3:4] Prep[] -> 'with' * |. . . [-> . .| [3:4] PP[] -> Prep[] * NP[] {} |. . . . [-] .| [4:5] Det[-pl] -> 'a' * |. . . . [-> .| [4:5] NP[] -> Det[pl=?x] * Noun[pl=?x] {?x: False} |. . . . . [-]| [5:6] Noun[-pl] -> 'dog' * |. . . . [---]| [4:6] NP[] -> Det[-pl] Noun[-pl] * |. . . . [--->| [4:6] S[] -> NP[] * VP[] {} |. . . . [--->| [4:6] NP[] -> NP[] * PP[] {} |. . . [-----]| [3:6] PP[] -> Prep[] NP[] * |. . [-------]| [2:6] NP[] -> NP[] PP[] * |. [---------]| [1:6] VP[] -> VP[] PP[] * |. [--------->| [1:6] VP[] -> VP[] * PP[] {} |[===========]| [0:6] S[] -> NP[] VP[] * |. . [------->| [2:6] S[] -> NP[] * VP[] {} |. . [------->| [2:6] NP[] -> NP[] * PP[] {} |. [---------]| [1:6] VP[] -> Verb[] NP[] * |. [--------->| [1:6] VP[] -> VP[] * PP[] {} |[===========]| [0:6] S[] -> NP[] VP[] * (S[] (NP[] I) (VP[] (Verb[] saw) (NP[] (NP[] John) (PP[] (Prep[] with) (NP[] (Det[-pl] a) (Noun[-pl] dog)))))) (S[] (NP[] I) (VP[] (VP[] (Verb[] saw) (NP[] John)) (PP[] (Prep[] with) (NP[] (Det[-pl] a) (Noun[-pl] dog))))) Earley. >>> nltk.parse.featurechart.demo(should_print_times=False, ... should_print_grammar=False, trace=1, ... parser=nltk.FeatureEarleyChartParser, ... sent='I saw John with a dog') * FeatureEarleyChartParser Sentence: I saw John with a dog |.I.s.J.w.a.d.| |[-] . . . . .| [0:1] 'I' |. [-] . . . .| [1:2] 'saw' |. . [-] . . .| [2:3] 'John' |. . . [-] . .| [3:4] 'with' |. . . . [-] .| [4:5] 'a' |. . . . . [-]| [5:6] 'dog' |> . . . . . .| [0:0] [INIT][] -> * S[] {} |> . . . . . .| [0:0] S[] -> * NP[] VP[] {} |> . . . . . .| [0:0] NP[] -> * NP[] PP[] {} |> . . . . . .| [0:0] NP[] -> * Det[pl=?x] Noun[pl=?x] {} |> . . . . . .| [0:0] NP[] -> * 'John' {} |> . . . . . .| [0:0] NP[] -> * 'I' {} |> . . . . . .| [0:0] Det[] -> * 'the' {} |> . . . . . .| [0:0] Det[] -> * 'my' {} |> . . . . . .| [0:0] Det[-pl] -> * 'a' {} |[-] . . . . .| [0:1] NP[] -> 'I' * |[-> . . . . .| [0:1] S[] -> NP[] * VP[] {} |[-> . . . . .| [0:1] NP[] -> NP[] * PP[] {} |. > . . . . .| [1:1] PP[] -> * Prep[] NP[] {} |. > . . . . .| [1:1] Prep[] -> * 'with' {} |. > . . . . .| [1:1] Prep[] -> * 'under' {} |. > . . . . .| [1:1] VP[] -> * VP[] PP[] {} |. > . . . . .| [1:1] VP[] -> * Verb[] NP[] {} |. > . . . . .| [1:1] VP[] -> * Verb[] {} |. > . . . . .| [1:1] Verb[] -> * 'ate' {} |. > . . . . .| [1:1] Verb[] -> * 'saw' {} |. [-] . . . .| [1:2] Verb[] -> 'saw' * |. [-> . . . .| [1:2] VP[] -> Verb[] * NP[] {} |. [-] . . . .| [1:2] VP[] -> Verb[] * |[---] . . . .| [0:2] S[] -> NP[] VP[] * |. [-> . . . .| [1:2] VP[] -> VP[] * PP[] {} |. . > . . . .| [2:2] PP[] -> * Prep[] NP[] {} |. . > . . . .| [2:2] Prep[] -> * 'with' {} |. . > . . . .| [2:2] Prep[] -> * 'under' {} |[---] . . . .| [0:2] [INIT][] -> S[] * |. . > . . . .| [2:2] NP[] -> * NP[] PP[] {} |. . > . . . .| [2:2] NP[] -> * Det[pl=?x] Noun[pl=?x] {} |. . > . . . .| [2:2] NP[] -> * 'John' {} |. . > . . . .| [2:2] NP[] -> * 'I' {} |. . > . . . .| [2:2] Det[] -> * 'the' {} |. . > . . . .| [2:2] Det[] -> * 'my' {} |. . > . . . .| [2:2] Det[-pl] -> * 'a' {} |. . [-] . . .| [2:3] NP[] -> 'John' * |. [---] . . .| [1:3] VP[] -> Verb[] NP[] * |. . [-> . . .| [2:3] NP[] -> NP[] * PP[] {} |. . . > . . .| [3:3] PP[] -> * Prep[] NP[] {} |. . . > . . .| [3:3] Prep[] -> * 'with' {} |. . . > . . .| [3:3] Prep[] -> * 'under' {} |[-----] . . .| [0:3] S[] -> NP[] VP[] * |. [---> . . .| [1:3] VP[] -> VP[] * PP[] {} |[-----] . . .| [0:3] [INIT][] -> S[] * |. . . [-] . .| [3:4] Prep[] -> 'with' * |. . . [-> . .| [3:4] PP[] -> Prep[] * NP[] {} |. . . . > . .| [4:4] NP[] -> * NP[] PP[] {} |. . . . > . .| [4:4] NP[] -> * Det[pl=?x] Noun[pl=?x] {} |. . . . > . .| [4:4] NP[] -> * 'John' {} |. . . . > . .| [4:4] NP[] -> * 'I' {} |. . . . > . .| [4:4] Det[] -> * 'the' {} |. . . . > . .| [4:4] Det[] -> * 'my' {} |. . . . > . .| [4:4] Det[-pl] -> * 'a' {} |. . . . [-] .| [4:5] Det[-pl] -> 'a' * |. . . . [-> .| [4:5] NP[] -> Det[pl=?x] * Noun[pl=?x] {?x: False} |. . . . . > .| [5:5] Noun[-pl] -> * 'dog' {} |. . . . . > .| [5:5] Noun[-pl] -> * 'cookie' {} |. . . . . [-]| [5:6] Noun[-pl] -> 'dog' * |. . . . [---]| [4:6] NP[] -> Det[-pl] Noun[-pl] * |. . . [-----]| [3:6] PP[] -> Prep[] NP[] * |. . . . [--->| [4:6] NP[] -> NP[] * PP[] {} |. . . . . . >| [6:6] PP[] -> * Prep[] NP[] {} |. . . . . . >| [6:6] Prep[] -> * 'with' {} |. . . . . . >| [6:6] Prep[] -> * 'under' {} |. . [-------]| [2:6] NP[] -> NP[] PP[] * |. [---------]| [1:6] VP[] -> VP[] PP[] * |[===========]| [0:6] S[] -> NP[] VP[] * |. [--------->| [1:6] VP[] -> VP[] * PP[] {} |[===========]| [0:6] [INIT][] -> S[] * |. [---------]| [1:6] VP[] -> Verb[] NP[] * |. . [------->| [2:6] NP[] -> NP[] * PP[] {} |[===========]| [0:6] S[] -> NP[] VP[] * |. [--------->| [1:6] VP[] -> VP[] * PP[] {} (S[] (NP[] I) (VP[] (Verb[] saw) (NP[] (NP[] John) (PP[] (Prep[] with) (NP[] (Det[-pl] a) (Noun[-pl] dog)))))) (S[] (NP[] I) (VP[] (VP[] (Verb[] saw) (NP[] John)) (PP[] (Prep[] with) (NP[] (Det[-pl] a) (Noun[-pl] dog))))) Tests for loading grammar files ------------------------------- >>> from nltk.parse import FeatureEarleyChartParser >>> from nltk import data >>> feat_cfg = data.load('grammars/book_grammars/feat0.fcfg') >>> fcp = FeatureEarleyChartParser(feat_cfg) nltk-2.0~b9/nltk/test/nonmonotonic.doctest0000644000175000017500000002272211331670151020617 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ====================== Nonmonotonic Reasoning ====================== >>> from nltk import * >>> from nltk.inference.nonmonotonic import * >>> from nltk.sem import logic >>> logic._counter._value = 0 >>> lp = logic.LogicParser() ------------------------ Closed Domain Assumption ------------------------ The only entities in the domain are those found in the assumptions or goal. If the domain only contains "A" and "B", then the expression "exists x.P(x)" can be replaced with "P(A) | P(B)" and an expression "all x.P(x)" can be replaced with "P(A) & P(B)". >>> p1 = lp.parse(r'all x.(man(x) -> mortal(x))') >>> p2 = lp.parse(r'man(Socrates)') >>> c = lp.parse(r'mortal(Socrates)') >>> prover = Prover9Command(c, [p1,p2]) >>> prover.prove() True >>> cdp = ClosedDomainProver(prover) >>> for a in cdp.assumptions(): print a (man(Socrates) -> mortal(Socrates)) man(Socrates) >>> cdp.prove() True >>> p1 = lp.parse(r'exists x.walk(x)') >>> p2 = lp.parse(r'man(Socrates)') >>> c = lp.parse(r'walk(Socrates)') >>> prover = Prover9Command(c, [p1,p2]) >>> prover.prove() False >>> cdp = ClosedDomainProver(prover) >>> for a in cdp.assumptions(): print a walk(Socrates) man(Socrates) >>> cdp.prove() True >>> p1 = lp.parse(r'exists x.walk(x)') >>> p2 = lp.parse(r'man(Socrates)') >>> p3 = lp.parse(r'-walk(Bill)') >>> c = lp.parse(r'walk(Socrates)') >>> prover = Prover9Command(c, [p1,p2,p3]) >>> prover.prove() False >>> cdp = ClosedDomainProver(prover) >>> for a in cdp.assumptions(): print a (walk(Socrates) | walk(Bill)) man(Socrates) -walk(Bill) >>> cdp.prove() True >>> p1 = lp.parse(r'walk(Socrates)') >>> p2 = lp.parse(r'walk(Bill)') >>> c = lp.parse(r'all x.walk(x)') >>> prover = Prover9Command(c, [p1,p2]) >>> prover.prove() False >>> cdp = ClosedDomainProver(prover) >>> for a in cdp.assumptions(): print a walk(Socrates) walk(Bill) >>> print cdp.goal() (walk(Socrates) & walk(Bill)) >>> cdp.prove() True >>> p1 = lp.parse(r'girl(mary)') >>> p2 = lp.parse(r'dog(rover)') >>> p3 = lp.parse(r'all x.(girl(x) -> -dog(x))') >>> p4 = lp.parse(r'all x.(dog(x) -> -girl(x))') >>> p5 = lp.parse(r'chase(mary, rover)') >>> c = lp.parse(r'exists y.(dog(y) & all x.(girl(x) -> chase(x,y)))') >>> prover = Prover9Command(c, [p1,p2,p3,p4,p5]) >>> print prover.prove() False >>> cdp = ClosedDomainProver(prover) >>> for a in cdp.assumptions(): print a girl(mary) dog(rover) ((girl(rover) -> -dog(rover)) & (girl(mary) -> -dog(mary))) ((dog(rover) -> -girl(rover)) & (dog(mary) -> -girl(mary))) chase(mary,rover) >>> print cdp.goal() ((dog(rover) & ((girl(rover) -> chase(rover,rover)) & (girl(mary) -> chase(mary,rover)))) | (dog(mary) & ((girl(rover) -> chase(rover,mary)) & (girl(mary) -> chase(mary,mary))))) >>> print cdp.prove() True ----------------------- Unique Names Assumption ----------------------- No two entities in the domain represent the same entity unless it can be explicitly proven that they do. Therefore, if the domain contains "A" and "B", then add the assumption "-(A = B)" if it is not the case that " \|- (A = B)". >>> p1 = lp.parse(r'man(Socrates)') >>> p2 = lp.parse(r'man(Bill)') >>> c = lp.parse(r'exists x.exists y.-(x = y)') >>> prover = Prover9Command(c, [p1,p2]) >>> prover.prove() False >>> unp = UniqueNamesProver(prover) >>> for a in unp.assumptions(): print a man(Socrates) man(Bill) -(Socrates = Bill) >>> unp.prove() True >>> p1 = lp.parse(r'all x.(walk(x) -> (x = Socrates))') >>> p2 = lp.parse(r'Bill = William') >>> p3 = lp.parse(r'Bill = Billy') >>> c = lp.parse(r'-walk(William)') >>> prover = Prover9Command(c, [p1,p2,p3]) >>> prover.prove() False >>> unp = UniqueNamesProver(prover) >>> for a in unp.assumptions(): print a all x.(walk(x) -> (x = Socrates)) (Bill = William) (Bill = Billy) -(William = Socrates) -(Billy = Socrates) -(Socrates = Bill) >>> unp.prove() True ----------------------- Closed World Assumption ----------------------- The only entities that have certain properties are those that is it stated have the properties. We accomplish this assumption by "completing" predicates. If the assumptions contain "P(A)", then "all x.(P(x) -> (x=A))" is the completion of "P". If the assumptions contain "all x.(ostrich(x) -> bird(x))", then "all x.(bird(x) -> ostrich(x))" is the completion of "bird". If the assumptions don't contain anything that are "P", then "all x.-P(x)" is the completion of "P". >>> p1 = lp.parse(r'walk(Socrates)') >>> p2 = lp.parse(r'-(Socrates = Bill)') >>> c = lp.parse(r'-walk(Bill)') >>> prover = Prover9Command(c, [p1,p2]) >>> prover.prove() False >>> cwp = ClosedWorldProver(prover) >>> for a in cwp.assumptions(): print a walk(Socrates) -(Socrates = Bill) all z1.(walk(z1) -> (z1 = Socrates)) >>> cwp.prove() True >>> p1 = lp.parse(r'see(Socrates, John)') >>> p2 = lp.parse(r'see(John, Mary)') >>> p3 = lp.parse(r'-(Socrates = John)') >>> p4 = lp.parse(r'-(John = Mary)') >>> c = lp.parse(r'-see(Socrates, Mary)') >>> prover = Prover9Command(c, [p1,p2,p3,p4]) >>> prover.prove() False >>> cwp = ClosedWorldProver(prover) >>> for a in cwp.assumptions(): print a see(Socrates,John) see(John,Mary) -(Socrates = John) -(John = Mary) all z3 z4.(see(z3,z4) -> (((z3 = Socrates) & (z4 = John)) | ((z3 = John) & (z4 = Mary)))) >>> cwp.prove() True >>> p1 = lp.parse(r'all x.(ostrich(x) -> bird(x))') >>> p2 = lp.parse(r'bird(Tweety)') >>> p3 = lp.parse(r'-ostrich(Sam)') >>> p4 = lp.parse(r'Sam != Tweety') >>> c = lp.parse(r'-bird(Sam)') >>> prover = Prover9Command(c, [p1,p2,p3,p4]) >>> prover.prove() False >>> cwp = ClosedWorldProver(prover) >>> for a in cwp.assumptions(): print a all x.(ostrich(x) -> bird(x)) bird(Tweety) -ostrich(Sam) -(Sam = Tweety) all z7.-ostrich(z7) all z8.(bird(z8) -> ((z8 = Tweety) | ostrich(z8))) >>> print cwp.prove() True ----------------------- Multi-Decorator Example ----------------------- Decorators can be nested to utilize multiple assumptions. >>> p1 = lp.parse(r'see(Socrates, John)') >>> p2 = lp.parse(r'see(John, Mary)') >>> c = lp.parse(r'-see(Socrates, Mary)') >>> prover = Prover9Command(c, [p1,p2]) >>> print prover.prove() False >>> cmd = ClosedDomainProver(UniqueNamesProver(ClosedWorldProver(prover))) >>> print cmd.prove() True ----------------- Default Reasoning ----------------- >>> logic._counter._value = 0 >>> premises = [] define the taxonomy >>> premises.append(lp.parse(r'all x.(elephant(x) -> animal(x))')) >>> premises.append(lp.parse(r'all x.(bird(x) -> animal(x))')) >>> premises.append(lp.parse(r'all x.(dove(x) -> bird(x))')) >>> premises.append(lp.parse(r'all x.(ostrich(x) -> bird(x))')) >>> premises.append(lp.parse(r'all x.(flying_ostrich(x) -> ostrich(x))')) default the properties using abnormalities >>> premises.append(lp.parse(r'all x.((animal(x) & -Ab1(x)) -> -fly(x))')) #normal animals don't fly >>> premises.append(lp.parse(r'all x.((bird(x) & -Ab2(x)) -> fly(x))')) #normal birds fly >>> premises.append(lp.parse(r'all x.((ostrich(x) & -Ab3(x)) -> -fly(x))')) #normal ostriches don't fly specify abnormal entities >>> premises.append(lp.parse(r'all x.(bird(x) -> Ab1(x))')) #flight >>> premises.append(lp.parse(r'all x.(ostrich(x) -> Ab2(x))')) #non-flying bird >>> premises.append(lp.parse(r'all x.(flying_ostrich(x) -> Ab3(x))')) #flying ostrich define entities >>> premises.append(lp.parse(r'elephant(E)')) >>> premises.append(lp.parse(r'dove(D)')) >>> premises.append(lp.parse(r'ostrich(O)')) print the augmented assumptions list >>> prover = Prover9Command(None, premises) >>> command = UniqueNamesProver(ClosedWorldProver(prover)) >>> for a in command.assumptions(): print a all x.(elephant(x) -> animal(x)) all x.(bird(x) -> animal(x)) all x.(dove(x) -> bird(x)) all x.(ostrich(x) -> bird(x)) all x.(flying_ostrich(x) -> ostrich(x)) all x.((animal(x) & -Ab1(x)) -> -fly(x)) all x.((bird(x) & -Ab2(x)) -> fly(x)) all x.((ostrich(x) & -Ab3(x)) -> -fly(x)) all x.(bird(x) -> Ab1(x)) all x.(ostrich(x) -> Ab2(x)) all x.(flying_ostrich(x) -> Ab3(x)) elephant(E) dove(D) ostrich(O) all z1.(animal(z1) -> (elephant(z1) | bird(z1))) all z2.(Ab1(z2) -> bird(z2)) all z3.(bird(z3) -> (dove(z3) | ostrich(z3))) all z4.(dove(z4) -> (z4 = D)) all z5.(Ab2(z5) -> ostrich(z5)) all z6.(Ab3(z6) -> flying_ostrich(z6)) all z7.(ostrich(z7) -> ((z7 = O) | flying_ostrich(z7))) all z8.-flying_ostrich(z8) all z9.(elephant(z9) -> (z9 = E)) -(E = D) -(E = O) -(D = O) >>> UniqueNamesProver(ClosedWorldProver(Prover9Command(lp.parse('-fly(E)'), premises))).prove() True >>> UniqueNamesProver(ClosedWorldProver(Prover9Command(lp.parse('fly(D)'), premises))).prove() True >>> UniqueNamesProver(ClosedWorldProver(Prover9Command(lp.parse('-fly(O)'), premises))).prove() True nltk-2.0~b9/nltk/test/misc.doctest0000644000175000017500000000615211331670153017033 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT -------------------------------------------------------------------------------- Unit tests for the miscellaneous sort functions. -------------------------------------------------------------------------------- >>> from copy import deepcopy >>> from nltk.misc.sort import * A (very) small list of unsorted integers. >>> test_data = [12, 67, 7, 28, 92, 56, 53, 720, 91, 57, 20, 20] Test each sorting method - each method returns the number of operations required to sort the data, and sorts in-place (desctructively - hence the need for multiple copies). >>> sorted_data = deepcopy(test_data) >>> selection(sorted_data) 66 >>> sorted_data [7, 12, 20, 20, 28, 53, 56, 57, 67, 91, 92, 720] >>> sorted_data = deepcopy(test_data) >>> bubble(sorted_data) 30 >>> sorted_data [7, 12, 20, 20, 28, 53, 56, 57, 67, 91, 92, 720] >>> sorted_data = deepcopy(test_data) >>> merge(sorted_data) 30 >>> sorted_data [7, 12, 20, 20, 28, 53, 56, 57, 67, 91, 92, 720] >>> sorted_data = deepcopy(test_data) >>> quick(sorted_data) 13 >>> sorted_data [7, 12, 20, 20, 28, 53, 56, 57, 67, 91, 92, 720] -------------------------------------------------------------------------------- Unit tests for Wordfinder class -------------------------------------------------------------------------------- >>> import random >>> random.seed(12345) >>> from nltk.misc import wordfinder >>> wordfinder.word_finder() Word Finder J V L A I R O T A T I S I V O D E R E T H U U B E A R O E P O C S O R E T N E P A D A U Z E E S R A P P A L L M E N T R C X A D Q S Z T P E O R S N G P J A D E I G Y K K T I A A R G F I D T E L C N S R E C N B H T R L T N N B W N T A O A I A Y I L O E I A M E I A A Y U R P L L D G L T V S T S F E A D I P H D O O H N I R L S E C I N I L R N N M E C G R U E A A A Y G I C E N L L E O I G Q R T A E L M R C E T I S T A E T L L E U A E N R L O U O T A S E E C S O O N H Y P A T G Y E M H O M M D R E S F P U L T H C F N V L A C A I M A M A N L B R U T E D O M I O R I L N E E E E E U A R S C R Y L I P H T R K E S N N M S I L A S R E V I N U T X T A A O U T K S E T A R R E S I B J A E D L E L J I F O O R P E L K N I R W K H A I D E Q O P R I C K T I M B E R P Z K D O O H G N I H T U R V E Y D R O P 1: INTERCHANGER 2: TEARLESSNESS 3: UNIVERSALISM 4: DESENSITIZER 5: INTERMENTION 6: TRICHOCYSTIC 7: EXTRAMURALLY 8: VEGETOALKALI 9: PALMELLACEAE 10: AESTHETICISM 11: PETROGRAPHER 12: VISITATORIAL 13: OLEOMARGARIC 14: WRINKLEPROOF 15: PRICKTIMBER 16: PRESIDIALLY 17: SCITAMINEAE 18: ENTEROSCOPE 19: APPALLMENT 20: TURVEYDROP 21: THINGHOOD 22: BISERRATE 23: GREENLAND 24: BRUTEDOM 25: POLONIAN 26: ACOLHUAN 27: LAPORTEA 28: TENDING 29: TEREDO 30: MESOLE 31: UNLIMP 32: OSTARA 33: PILY 34: DUNT 35: ONYX 36: KATH 37: JUNE nltk-2.0~b9/nltk/test/metrics.doctest0000644000175000017500000001440111331670157017546 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ======= Metrics ======= The `nltk.metrics` package provides a variety of *evaluation measures* which can be used for a wide variety of NLP tasks. >>> from nltk.metrics import * ------------------ Standard IR Scores ------------------ We can use standard scores from information retrieval to test the performance of taggers, chunkers, etc. >>> reference = 'DET NN VB DET JJ NN NN IN DET NN'.split() >>> test = 'DET VB VB DET NN NN NN IN DET NN'.split() >>> accuracy(reference, test) 0.80000000000000004 The following measures apply to sets: >>> reference_set = set(reference) >>> test_set = set(test) >>> precision(reference_set, test_set) 1.0 >>> recall(reference_set, test_set) 0.80000000000000004 >>> f_measure(reference_set, test_set) 0.88888888888888884 Measuring the likelihood of the data, given probability distributions: >>> from nltk import FreqDist, MLEProbDist >>> pdist1 = MLEProbDist(FreqDist("aldjfalskfjaldsf")) >>> pdist2 = MLEProbDist(FreqDist("aldjfalssjjlldss")) >>> log_likelihood(['a', 'd'], [pdist1, pdist2]) -2.707518749639422 ---------------- Distance Metrics ---------------- String edit distance (Levenshtein): >>> edit_distance("rain", "shine") 3 Other distance measures: >>> s1 = set([1,2,3,4]) >>> s2 = set([3,4,5]) >>> binary_distance(s1, s2) 1.0 >>> jaccard_distance(s1, s2) 0.59999999999999998 >>> masi_distance(s1, s2) 0.5 ---------------------- Miscellaneous Measures ---------------------- Rank Correlation works with two dictionaries mapping keys to ranks. The dictionaries should have the same set of keys. >>> spearman_correlation({'e':1, 't':2, 'a':3}, {'e':1, 'a':2, 't':3}) 0.5 Windowdiff uses a sliding window in comparing two segmentations of the same input (e.g. tokenizations, chunkings). Segmentations are represented using strings of zeros and ones. >>> s1 = "00000010000000001000000" >>> s2 = "00000001000000010000000" >>> s3 = "00010000000000000001000" >>> windowdiff(s1, s1, 3) 0 >>> windowdiff(s1, s2, 3) 4 >>> windowdiff(s2, s3, 3) 16 ---------------- Confusion Matrix ---------------- >>> reference = 'This is the reference data. Testing 123. aoaeoeoe' >>> test = 'Thos iz_the rifirenci data. Testeng 123. aoaeoeoe' >>> print ConfusionMatrix(reference, test) | . 1 2 3 T _ a c d e f g h i n o r s t z | --+-------------------------------------------+ |<8>. . . . . 1 . . . . . . . . . . . . . . | . | .<2>. . . . . . . . . . . . . . . . . . . | 1 | . .<1>. . . . . . . . . . . . . . . . . . | 2 | . . .<1>. . . . . . . . . . . . . . . . . | 3 | . . . .<1>. . . . . . . . . . . . . . . . | T | . . . . .<2>. . . . . . . . . . . . . . . | _ | . . . . . .<.>. . . . . . . . . . . . . . | a | . . . . . . .<4>. . . . . . . . . . . . . | c | . . . . . . . .<1>. . . . . . . . . . . . | d | . . . . . . . . .<1>. . . . . . . . . . . | e | . . . . . . . . . .<6>. . . 3 . . . . . . | f | . . . . . . . . . . .<1>. . . . . . . . . | g | . . . . . . . . . . . .<1>. . . . . . . . | h | . . . . . . . . . . . . .<2>. . . . . . . | i | . . . . . . . . . . 1 . . .<1>. 1 . . . . | n | . . . . . . . . . . . . . . .<2>. . . . . | o | . . . . . . . . . . . . . . . .<3>. . . . | r | . . . . . . . . . . . . . . . . .<2>. . . | s | . . . . . . . . . . . . . . . . . .<2>. 1 | t | . . . . . . . . . . . . . . . . . . .<3>. | z | . . . . . . . . . . . . . . . . . . . .<.>| --+-------------------------------------------+ (row = reference; col = test) >>> cm = ConfusionMatrix(reference, test) >>> print cm.pp(sort_by_count=True) | e a i o s t . T h n r 1 2 3 c d f g _ z | --+-------------------------------------------+ |<8>. . . . . . . . . . . . . . . . . . 1 . | e | .<6>. 3 . . . . . . . . . . . . . . . . . | a | . .<4>. . . . . . . . . . . . . . . . . . | i | . 1 .<1>1 . . . . . . . . . . . . . . . . | o | . . . .<3>. . . . . . . . . . . . . . . . | s | . . . . .<2>. . . . . . . . . . . . . . 1 | t | . . . . . .<3>. . . . . . . . . . . . . . | . | . . . . . . .<2>. . . . . . . . . . . . . | T | . . . . . . . .<2>. . . . . . . . . . . . | h | . . . . . . . . .<2>. . . . . . . . . . . | n | . . . . . . . . . .<2>. . . . . . . . . . | r | . . . . . . . . . . .<2>. . . . . . . . . | 1 | . . . . . . . . . . . .<1>. . . . . . . . | 2 | . . . . . . . . . . . . .<1>. . . . . . . | 3 | . . . . . . . . . . . . . .<1>. . . . . . | c | . . . . . . . . . . . . . . .<1>. . . . . | d | . . . . . . . . . . . . . . . .<1>. . . . | f | . . . . . . . . . . . . . . . . .<1>. . . | g | . . . . . . . . . . . . . . . . . .<1>. . | _ | . . . . . . . . . . . . . . . . . . .<.>. | z | . . . . . . . . . . . . . . . . . . . .<.>| --+-------------------------------------------+ (row = reference; col = test) >>> print cm.pp(sort_by_count=True, truncate=10) | e a i o s t . T h | --+---------------------+ |<8>. . . . . . . . . | e | .<6>. 3 . . . . . . | a | . .<4>. . . . . . . | i | . 1 .<1>1 . . . . . | o | . . . .<3>. . . . . | s | . . . . .<2>. . . . | t | . . . . . .<3>. . . | . | . . . . . . .<2>. . | T | . . . . . . . .<2>. | h | . . . . . . . . .<2>| --+---------------------+ (row = reference; col = test) >>> print cm.pp(sort_by_count=True, truncate=10, values_in_chart=False) | 1 | | 1 2 3 4 5 6 7 8 9 0 | ---+---------------------+ 1 |<8>. . . . . . . . . | 2 | .<6>. 3 . . . . . . | 3 | . .<4>. . . . . . . | 4 | . 1 .<1>1 . . . . . | 5 | . . . .<3>. . . . . | 6 | . . . . .<2>. . . . | 7 | . . . . . .<3>. . . | 8 | . . . . . . .<2>. . | 9 | . . . . . . . .<2>. | 10 | . . . . . . . . .<2>| ---+---------------------+ (row = reference; col = test) Value key: 1: 2: e 3: a 4: i 5: o 6: s 7: t 8: . 9: T 10: h nltk-2.0~b9/nltk/test/logic.doctest0000644000175000017500000007471411377057401017213 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ======================= Logic & Lambda Calculus ======================= The `nltk.logic` package allows expressions of First-Order Logic (FOL) to be parsed into ``Expression`` objects. In addition to FOL, the parser handles lambda-abstraction with variables of higher order. -------- Overview -------- >>> from nltk.sem.logic import * The default inventory of logical constants is the following: >>> boolean_ops() # doctest: +NORMALIZE_WHITESPACE negation - conjunction & disjunction | implication -> equivalence <-> >>> equality_preds() # doctest: +NORMALIZE_WHITESPACE equality = inequality != >>> binding_ops() # doctest: +NORMALIZE_WHITESPACE existential exists universal all lambda \ ---------------- Regression Tests ---------------- Untyped Logic +++++++++++++ Test for equality under alpha-conversion ======================================== >>> lp = LogicParser() >>> e1 = lp.parse('exists x.P(x)') >>> print e1 exists x.P(x) >>> e2 = e1.alpha_convert(Variable('z')) >>> print e2 exists z.P(z) >>> e1 == e2 True >>> l = lp.parse(r'\X.\X.X(X)(1)').simplify() >>> id = lp.parse(r'\X.X(X)') >>> l == id True Test numerals ============= >>> zero = lp.parse(r'\F x.x') >>> one = lp.parse(r'\F x.F(x)') >>> two = lp.parse(r'\F x.F(F(x))') >>> three = lp.parse(r'\F x.F(F(F(x)))') >>> four = lp.parse(r'\F x.F(F(F(F(x))))') >>> succ = lp.parse(r'\N F x.F(N(F,x))') >>> plus = lp.parse(r'\M N F x.M(F,N(F,x))') >>> mult = lp.parse(r'\M N F.M(N(F))') >>> pred = lp.parse(r'\N F x.(N(\G H.H(G(F)))(\u.x)(\u.u))') >>> v1 = ApplicationExpression(succ, zero).simplify() >>> v1 == one True >>> v2 = ApplicationExpression(succ, v1).simplify() >>> v2 == two True >>> v3 = ApplicationExpression(ApplicationExpression(plus, v1), v2).simplify() >>> v3 == three True >>> v4 = ApplicationExpression(ApplicationExpression(mult, v2), v2).simplify() >>> v4 == four True >>> v5 = ApplicationExpression(pred, ApplicationExpression(pred, v4)).simplify() >>> v5 == two True Overloaded operators also exist, for convenience. >>> print succ(zero).simplify() == one True >>> print plus(one,two).simplify() == three True >>> print mult(two,two).simplify() == four True >>> print pred(pred(four)).simplify() == two True >>> john = lp.parse(r'john') >>> man = lp.parse(r'\x.man(x)') >>> walk = lp.parse(r'\x.walk(x)') >>> man(john).simplify() >>> print -walk(john).simplify() -walk(john) >>> print (man(john) & walk(john)).simplify() (man(john) & walk(john)) >>> print (man(john) | walk(john)).simplify() (man(john) | walk(john)) >>> print (man(john) > walk(john)).simplify() (man(john) -> walk(john)) >>> print (man(john) < walk(john)).simplify() (man(john) <-> walk(john)) Python's built-in lambda operator can also be used with Expressions >>> john = VariableExpression(Variable('john')) >>> run_var = VariableExpression(Variable('run')) >>> run = lambda x: run_var(x) >>> run(john) ``betaConversionTestSuite.pl`` ------------------------------ Tests based on Blackburn & Bos' book, *Representation and Inference for Natural Language*. >>> x1 = lp.parse(r'\P.P(mia)(\x.walk(x))').simplify() >>> x2 = lp.parse(r'walk(mia)').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'exists x.(man(x) & ((\P.exists x.(woman(x) & P(x)))(\y.love(x,y))))').simplify() >>> x2 = lp.parse(r'exists x.(man(x) & exists y.(woman(y) & love(x,y)))').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'\a.sleep(a)(mia)').simplify() >>> x2 = lp.parse(r'sleep(mia)').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'\a.\b.like(b,a)(mia)').simplify() >>> x2 = lp.parse(r'\b.like(b,mia)').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'\a.(\b.like(b,a)(vincent))').simplify() >>> x2 = lp.parse(r'\a.like(vincent,a)').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'\a.((\b.like(b,a)(vincent)) & sleep(a))').simplify() >>> x2 = lp.parse(r'\a.(like(vincent,a) & sleep(a))').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'(\a.\b.like(b,a)(mia)(vincent))').simplify() >>> x2 = lp.parse(r'like(vincent,mia)').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'P((\a.sleep(a)(vincent)))').simplify() >>> x2 = lp.parse(r'P(sleep(vincent))').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'\A.A((\b.sleep(b)(vincent)))').simplify() >>> x2 = lp.parse(r'\A.A(sleep(vincent))').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'\A.A(sleep(vincent))').simplify() >>> x2 = lp.parse(r'\A.A(sleep(vincent))').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'(\A.A(vincent)(\b.sleep(b)))').simplify() >>> x2 = lp.parse(r'sleep(vincent)').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'\A.believe(mia,A(vincent))(\b.sleep(b))').simplify() >>> x2 = lp.parse(r'believe(mia,sleep(vincent))').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'(\A.(A(vincent) & A(mia)))(\b.sleep(b))').simplify() >>> x2 = lp.parse(r'(sleep(vincent) & sleep(mia))').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'\A.\B.(\C.C(A(vincent))(\d.probably(d)) & (\C.C(B(mia))(\d.improbably(d))))(\f.walk(f))(\f.talk(f))').simplify() >>> x2 = lp.parse(r'(probably(walk(vincent)) & improbably(talk(mia)))').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'(\a.\b.(\C.C(a,b)(\d.\f.love(d,f))))(jules)(mia)').simplify() >>> x2 = lp.parse(r'love(jules,mia)').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'(\A.\B.exists c.(A(c) & B(c)))(\d.boxer(d),\d.sleep(d))').simplify() >>> x2 = lp.parse(r'exists c.(boxer(c) & sleep(c))').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'\A.Z(A)(\c.\a.like(a,c))').simplify() >>> x2 = lp.parse(r'Z(\c.\a.like(a,c))').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'\A.\b.A(b)(\c.\b.like(b,c))').simplify() >>> x2 = lp.parse(r'\b.(\c.\b.like(b,c)(b))').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'(\a.\b.(\C.C(a,b)(\b.\a.loves(b,a))))(jules)(mia)').simplify() >>> x2 = lp.parse(r'loves(jules,mia)').simplify() >>> x1 == x2 True >>> x1 = lp.parse(r'(\A.\b.(exists b.A(b) & A(b)))(\c.boxer(c))(vincent)').simplify() >>> x2 = lp.parse(r'((exists b.boxer(b)) & boxer(vincent))').simplify() >>> x1 == x2 True Test Parser =========== >>> print lp.parse(r'john') john >>> print lp.parse(r'x') x >>> print lp.parse(r'-man(x)') -man(x) >>> print lp.parse(r'--man(x)') --man(x) >>> print lp.parse(r'(man(x))') man(x) >>> print lp.parse(r'((man(x)))') man(x) >>> print lp.parse(r'man(x) <-> tall(x)') (man(x) <-> tall(x)) >>> print lp.parse(r'(man(x) <-> tall(x))') (man(x) <-> tall(x)) >>> print lp.parse(r'(man(x) & tall(x) & walks(x))') ((man(x) & tall(x)) & walks(x)) >>> print lp.parse(r'((man(x) & tall(x)) & walks(x))') ((man(x) & tall(x)) & walks(x)) >>> print lp.parse(r'man(x) & (tall(x) & walks(x))') (man(x) & (tall(x) & walks(x))) >>> print lp.parse(r'(man(x) & (tall(x) & walks(x)))') (man(x) & (tall(x) & walks(x))) >>> print lp.parse(r'P(x) -> Q(x) <-> R(x) | S(x) & T(x)') ((P(x) -> Q(x)) <-> (R(x) | (S(x) & T(x)))) >>> print lp.parse(r'exists x.man(x)') exists x.man(x) >>> print lp.parse(r'exists x.(man(x) & tall(x))') exists x.(man(x) & tall(x)) >>> print lp.parse(r'exists x.(man(x) & tall(x) & walks(x))') exists x.((man(x) & tall(x)) & walks(x)) >>> print lp.parse(r'-P(x) & Q(x)') (-P(x) & Q(x)) >>> lp.parse(r'-P(x) & Q(x)') == lp.parse(r'(-P(x)) & Q(x)') True >>> print lp.parse(r'\x.man(x)') \x.man(x) >>> print lp.parse(r'\x.man(x)(john)') \x.man(x)(john) >>> print lp.parse(r'\x.man(x)(john) & tall(x)') (\x.man(x)(john) & tall(x)) >>> print lp.parse(r'\x.\y.sees(x,y)') \x y.sees(x,y) >>> print lp.parse(r'\x y.sees(x,y)') \x y.sees(x,y) >>> print lp.parse(r'\x.\y.sees(x,y)(a)') (\x y.sees(x,y))(a) >>> print lp.parse(r'\x y.sees(x,y)(a)') (\x y.sees(x,y))(a) >>> print lp.parse(r'\x.\y.sees(x,y)(a)(b)') ((\x y.sees(x,y))(a))(b) >>> print lp.parse(r'\x y.sees(x,y)(a)(b)') ((\x y.sees(x,y))(a))(b) >>> print lp.parse(r'\x.\y.sees(x,y)(a,b)') ((\x y.sees(x,y))(a))(b) >>> print lp.parse(r'\x y.sees(x,y)(a,b)') ((\x y.sees(x,y))(a))(b) >>> print lp.parse(r'((\x.\y.sees(x,y))(a))(b)') ((\x y.sees(x,y))(a))(b) >>> print lp.parse(r'P(x)(y)(z)') P(x,y,z) >>> print lp.parse(r'P(Q)') P(Q) >>> print lp.parse(r'P(Q(x))') P(Q(x)) >>> print lp.parse(r'(\x.exists y.walks(x,y))(x)') (\x.exists y.walks(x,y))(x) >>> print lp.parse(r'exists x.(x = john)') exists x.(x = john) >>> print lp.parse(r'((\P.\Q.exists x.(P(x) & Q(x)))(\x.dog(x)))(\x.bark(x))') ((\P Q.exists x.(P(x) & Q(x)))(\x.dog(x)))(\x.bark(x)) >>> a = lp.parse(r'exists c.exists b.A(b,c) & A(b,c)') >>> b = lp.parse(r'(exists c.(exists b.A(b,c))) & A(b,c)') >>> print a == b True >>> a = lp.parse(r'exists c.(exists b.A(b,c) & A(b,c))') >>> b = lp.parse(r'exists c.((exists b.A(b,c)) & A(b,c))') >>> print a == b True >>> print lp.parse(r'exists x.x = y') exists x.(x = y) >>> print lp.parse(r'A != B') -(A = B) >>> print lp.parse('P(x) & x=y & P(y)') ((P(x) & (x = y)) & P(y)) >>> try: print lp.parse(r'\walk.walk(x)') ... except ParseException,e: print e 'walk' is an illegal variable name. Constants may not be abstracted. \walk.walk(x) ^ >>> try: print lp.parse(r'all walk.walk(john)') ... except ParseException,e: print e 'walk' is an illegal variable name. Constants may not be quantified. all walk.walk(john) ^ >>> try: print lp.parse(r'x(john)') ... except ParseException,e: print e 'x' is an illegal predicate name. Individual variables may not be used as predicates. x(john) ^ >>> lpq = LogicParser() >>> lpq.quote_chars = [("'", "'", "\\", False)] >>> print lpq.parse(r"(man(x) & 'tall\'s,' (x) & walks (x) )") ((man(x) & tall's,(x)) & walks(x)) >>> lpq.quote_chars = [("'", "'", "\\", True)] >>> print lpq.parse(r"'tall\'s,'") 'tall\'s,' >>> print lpq.parse(r"'spaced name(x)'") 'spaced name(x)' >>> print lpq.parse(r"-'tall\'s,'(x)") -'tall\'s,'(x) >>> print lpq.parse(r"(man(x) & 'tall\'s,' (x) & walks (x) )") ((man(x) & 'tall\'s,'(x)) & walks(x)) Simplify ======== >>> print lp.parse(r'\x.man(x)(john)').simplify() man(john) >>> print lp.parse(r'\x.((man(x)))(john)').simplify() man(john) >>> print lp.parse(r'\x.\y.sees(x,y)(john, mary)').simplify() sees(john,mary) >>> print lp.parse(r'\x y.sees(x,y)(john, mary)').simplify() sees(john,mary) >>> print lp.parse(r'\x.\y.sees(x,y)(john)(mary)').simplify() sees(john,mary) >>> print lp.parse(r'\x y.sees(x,y)(john)(mary)').simplify() sees(john,mary) >>> print lp.parse(r'\x.\y.sees(x,y)(john)').simplify() \y.sees(john,y) >>> print lp.parse(r'\x y.sees(x,y)(john)').simplify() \y.sees(john,y) >>> print lp.parse(r'(\x.\y.sees(x,y)(john))(mary)').simplify() sees(john,mary) >>> print lp.parse(r'(\x y.sees(x,y)(john))(mary)').simplify() sees(john,mary) >>> print lp.parse(r'exists x.(man(x) & (\x.exists y.walks(x,y))(x))').simplify() exists x.(man(x) & exists y.walks(x,y)) >>> e1 = lp.parse(r'exists x.(man(x) & (\x.exists y.walks(x,y))(y))').simplify() >>> e2 = lp.parse(r'exists x.(man(x) & exists z1.walks(y,z1))') >>> e1 == e2 True >>> print lp.parse(r'(\P Q.exists x.(P(x) & Q(x)))(\x.dog(x))').simplify() \Q.exists x.(dog(x) & Q(x)) >>> print lp.parse(r'((\P.\Q.exists x.(P(x) & Q(x)))(\x.dog(x)))(\x.bark(x))').simplify() exists x.(dog(x) & bark(x)) >>> print lp.parse(r'\P.(P(x)(y))(\a b.Q(a,b))').simplify() Q(x,y) Replace ======= >>> a = lp.parse(r'a') >>> x = lp.parse(r'x') >>> y = lp.parse(r'y') >>> z = lp.parse(r'z') >>> print lp.parse(r'man(x)').replace(x.variable, a, False) man(a) >>> print lp.parse(r'(man(x) & tall(x))').replace(x.variable, a, False) (man(a) & tall(a)) >>> print lp.parse(r'exists x.man(x)').replace(x.variable, a, False) exists x.man(x) >>> print lp.parse(r'exists x.man(x)').replace(x.variable, a, True) exists a.man(a) >>> print lp.parse(r'exists x.give(x,y,z)').replace(y.variable, a, False) exists x.give(x,a,z) >>> print lp.parse(r'exists x.give(x,y,z)').replace(y.variable, a, True) exists x.give(x,a,z) >>> e1 = lp.parse(r'exists x.give(x,y,z)').replace(y.variable, x, False) >>> e2 = lp.parse(r'exists z1.give(z1,x,z)') >>> e1 == e2 True >>> e1 = lp.parse(r'exists x.give(x,y,z)').replace(y.variable, x, True) >>> e2 = lp.parse(r'exists z1.give(z1,x,z)') >>> e1 == e2 True >>> print lp.parse(r'\x y z.give(x,y,z)').replace(y.variable, a, False) \x y z.give(x,y,z) >>> print lp.parse(r'\x y z.give(x,y,z)').replace(y.variable, a, True) \x a z.give(x,a,z) >>> print lp.parse(r'\x.\y.give(x,y,z)').replace(z.variable, a, False) \x y.give(x,y,a) >>> print lp.parse(r'\x.\y.give(x,y,z)').replace(z.variable, a, True) \x y.give(x,y,a) >>> e1 = lp.parse(r'\x.\y.give(x,y,z)').replace(z.variable, x, False) >>> e2 = lp.parse(r'\z1.\y.give(z1,y,x)') >>> e1 == e2 True >>> e1 = lp.parse(r'\x.\y.give(x,y,z)').replace(z.variable, x, True) >>> e2 = lp.parse(r'\z1.\y.give(z1,y,x)') >>> e1 == e2 True >>> print lp.parse(r'\x.give(x,y,z)').replace(z.variable, y, False) \x.give(x,y,y) >>> print lp.parse(r'\x.give(x,y,z)').replace(z.variable, y, True) \x.give(x,y,y) >>> from nltk.sem import logic >>> logic._counter._value = 0 >>> e1 = lp.parse('e1') >>> e2 = lp.parse('e2') >>> print lp.parse('exists e1 e2.(walk(e1) & talk(e2))').replace(e1.variable, e2, True) exists e2 e01.(walk(e2) & talk(e01)) Variables / Free ================ >>> print lp.parse(r'walk(john)').variables() set([Variable('john'), Variable('walk')]) >>> print lp.parse(r'walk(x)').variables() set([Variable('x'), Variable('walk')]) >>> print lp.parse(r'see(john,mary)').variables() set([Variable('see'), Variable('john'), Variable('mary')]) >>> print lp.parse(r'exists x.walk(x)').variables() set([Variable('walk')]) >>> print lp.parse(r'\x.see(john,x)').variables() set([Variable('see'), Variable('john')]) >>> print lp.parse(r'\x.see(john,x)(mary)').variables() set([Variable('see'), Variable('john'), Variable('mary')]) >>> print lp.parse(r'walk(john)').free() set([]) >>> print lp.parse(r'walk(x)').free() set([Variable('x')]) >>> print lp.parse(r'see(john,mary)').free() set([]) >>> print lp.parse(r'exists x.walk(x)').free() set([]) >>> print lp.parse(r'\x.see(john,x)').free() set([]) >>> print lp.parse(r'\x.see(john,x)(mary)').free() set([]) >>> print lp.parse(r'walk(john)').free(False) set([Variable('john')]) >>> print lp.parse(r'walk(x)').free(False) set([Variable('x')]) >>> print lp.parse(r'see(john,mary)').free(False) set([Variable('john'), Variable('mary')]) >>> print lp.parse(r'exists x.walk(x)').free(False) set([]) >>> print lp.parse(r'\x.see(john,x)').free(False) set([Variable('john')]) >>> print lp.parse(r'\x.see(john,x)(mary)').free(False) set([Variable('john'), Variable('mary')]) `normalize` >>> print lp.parse(r'\e083.(walk(e083, z472) & talk(e092, z938))').normalize() \e01.(walk(e01,z3) & talk(e02,z4)) Typed Logic +++++++++++ >>> from nltk.sem.logic import * >>> tlp = LogicParser(True) .type >>> print tlp.parse(r'man(x)').type ? >>> print tlp.parse(r'walk(angus)').type ? >>> print tlp.parse(r'-man(x)').type t >>> print tlp.parse(r'(man(x) <-> tall(x))').type t >>> print tlp.parse(r'exists x.(man(x) & tall(x))').type t >>> print tlp.parse(r'\x.man(x)').type >>> print tlp.parse(r'john').type e >>> print tlp.parse(r'\x y.sees(x,y)').type > >>> print tlp.parse(r'\x.man(x)(john)').type ? >>> print tlp.parse(r'\x.\y.sees(x,y)(john)').type >>> print tlp.parse(r'\x.\y.sees(x,y)(john)(mary)').type ? >>> print tlp.parse(r'\P.\Q.exists x.(P(x) & Q(x))').type <,<,t>> >>> print tlp.parse(r'\x.y').type >>> print tlp.parse(r'\P.P(x)').type <,?> >>> parsed = tlp.parse('see(john,mary)') >>> print parsed.type ? >>> print parsed.function see(john) >>> print parsed.function.type >>> print parsed.function.function see >>> print parsed.function.function.type > >>> parsed = tlp.parse('P(x,y)') >>> print parsed P(x,y) >>> print parsed.type ? >>> print parsed.function P(x) >>> print parsed.function.type >>> print parsed.function.function P >>> print parsed.function.function.type > >>> print tlp.parse(r'P').type ? >>> print tlp.parse(r'P', {'P': 't'}).type t >>> a = tlp.parse(r'P(x)') >>> print a.type ? >>> print a.function.type >>> print a.argument.type e >>> a = tlp.parse(r'-P(x)') >>> print a.type t >>> print a.term.type t >>> print a.term.function.type >>> print a.term.argument.type e >>> a = tlp.parse(r'P & Q') >>> print a.type t >>> print a.first.type t >>> print a.second.type t >>> a = tlp.parse(r'(P(x) & Q(x))') >>> print a.type t >>> print a.first.type t >>> print a.first.function.type >>> print a.first.argument.type e >>> print a.second.type t >>> print a.second.function.type >>> print a.second.argument.type e >>> a = tlp.parse(r'\x.P(x)') >>> print a.type >>> print a.term.function.type >>> print a.term.argument.type e >>> a = tlp.parse(r'\P.P(x)') >>> print a.type <,?> >>> print a.term.function.type >>> print a.term.argument.type e >>> a = tlp.parse(r'(\x.P(x)(john)) & Q(x)') >>> print a.type t >>> print a.first.type t >>> print a.first.function.type >>> print a.first.function.term.function.type >>> print a.first.function.term.argument.type e >>> print a.first.argument.type e >>> a = tlp.parse(r'\x y.P(x,y)(john)(mary) & Q(x)') >>> print a.type t >>> print a.first.type t >>> print a.first.function.type >>> print a.first.function.function.type > >>> a = tlp.parse(r'--P') >>> print a.type t >>> print a.term.type t >>> print a.term.term.type t >>> tlp.parse(r'\x y.P(x,y)').type > >>> tlp.parse(r'\x y.P(x,y)', {'P': '>'}).type > >>> a = tlp.parse(r'\P y.P(john,y)(\x y.see(x,y))') >>> a.type >>> a.function.type <>,> >>> a.function.term.term.function.function.type > >>> a.argument.type > >>> a = tlp.parse(r'exists c f.(father(c) = f)') >>> a.type t >>> a.term.term.type t >>> a.term.term.first.type e >>> a.term.term.first.function.type >>> a.term.term.second.type e typecheck() >>> a = tlp.parse('P(x)') >>> b = tlp.parse('Q(x)') >>> a.type ? >>> c = a & b >>> c.first.type ? >>> c.typecheck() # doctest: +ELLIPSIS {...} >>> c.first.type t >>> a = tlp.parse('P(x)') >>> b = tlp.parse('P(x) & Q(x)') >>> a.type ? >>> typecheck([a,b]) # doctest: +ELLIPSIS {...} >>> a.type t >>> e = tlp.parse(r'man(x)') >>> print e.typecheck() {'x': e, 'man': } >>> sig = {'man': ''} >>> e = tlp.parse(r'man(x)', sig) >>> print e.function.type >>> print e.typecheck() {'x': e, 'man': } >>> print e.function.type >>> print e.typecheck(sig) {'x': e, 'man': } findtype() >>> print tlp.parse(r'man(x)').findtype(Variable('man')) >>> print tlp.parse(r'see(x,y)').findtype(Variable('see')) > >>> print tlp.parse(r'P(Q(R(x)))').findtype(Variable('Q')) ? parse_type() >>> print parse_type('e') e >>> print parse_type('') >>> print parse_type('<,>') <,> >>> print parse_type('<,?>') <,?> alternative type format >>> print parse_type('e').str() IND >>> print parse_type('').str() (IND -> ANY) >>> print parse_type('<,t>').str() ((IND -> BOOL) -> BOOL) Type.__eq__() >>> from nltk.sem.logic import * >>> e = ENTITY_TYPE >>> t = TRUTH_TYPE >>> a = ANY_TYPE >>> et = ComplexType(e,t) >>> eet = ComplexType(e,ComplexType(e,t)) >>> at = ComplexType(a,t) >>> ea = ComplexType(e,a) >>> aa = ComplexType(a,a) >>> e == e True >>> t == t True >>> e == t False >>> a == t False >>> t == a False >>> a == a True >>> et == et True >>> a == et False >>> et == a False >>> a == ComplexType(a,aa) True >>> ComplexType(a,aa) == a True matches() >>> e.matches(t) False >>> a.matches(t) True >>> t.matches(a) True >>> a.matches(et) True >>> et.matches(a) True >>> ea.matches(eet) True >>> eet.matches(ea) True >>> aa.matches(et) True >>> aa.matches(t) True Type error during parsing ========================= >>> try: print tlp.parse(r'exists x y.(P(x) & P(x,y))') ... except InconsistentTypeHierarchyException, e: print e The variable 'P' was found in multiple places with different types. >>> try: tlp.parse(r'\x y.see(x,y)(\x.man(x))') ... except TypeException, e: print e The function '\x y.see(x,y)' is of type '>' and cannot be applied to '\x.man(x)' of type ''. Its argument must match type 'e'. >>> try: tlp.parse(r'\P x y.-P(x,y)(\x.-man(x))') ... except TypeException, e: print e The function '\P x y.-P(x,y)' is of type '<>,>>' and cannot be applied to '\x.-man(x)' of type ''. Its argument must match type '>'. >>> a = tlp.parse(r'-talk(x)') >>> signature = a.typecheck() >>> try: print tlp.parse(r'-talk(x,y)', signature) ... except InconsistentTypeHierarchyException, e: print e The variable 'talk' was found in multiple places with different types. >>> a = tlp.parse(r'-P(x)') >>> b = tlp.parse(r'-P(x,y)') >>> a.typecheck() # doctest: +ELLIPSIS {...} >>> b.typecheck() # doctest: +ELLIPSIS {...} >>> try: typecheck([a,b]) ... except InconsistentTypeHierarchyException, e: print e The variable 'P' was found in multiple places with different types. >>> a = tlp.parse(r'P(x)') >>> b = tlp.parse(r'P(x,y)') >>> signature = {'P': ''} >>> a.typecheck(signature) # doctest: +ELLIPSIS {...} >>> try: typecheck([a,b], signature) ... except InconsistentTypeHierarchyException, e: print e The variable 'P' was found in multiple places with different types. Parse errors ============ >>> try: lp.parse(r'') ... except ParseException, e: print e End of input found. Expression expected. ^ >>> try: lp.parse(r'(') ... except ParseException, e: print e End of input found. Expression expected. ( ^ >>> try: lp.parse(r')') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. ) ^ >>> try: lp.parse(r'()') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. () ^ >>> try: lp.parse(r'(P(x) & Q(x)') ... except ParseException, e: print e End of input found. Expected token ')'. (P(x) & Q(x) ^ >>> try: lp.parse(r'(P(x) &') ... except ParseException, e: print e End of input found. Expression expected. (P(x) & ^ >>> try: lp.parse(r'(P(x) | )') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. (P(x) | ) ^ >>> try: lp.parse(r'P(x) ->') ... except ParseException, e: print e End of input found. Expression expected. P(x) -> ^ >>> try: lp.parse(r'P(x') ... except ParseException, e: print e End of input found. Expected token ')'. P(x ^ >>> try: lp.parse(r'P(x,') ... except ParseException, e: print e End of input found. Expression expected. P(x, ^ >>> try: lp.parse(r'P(x,)') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. P(x,) ^ >>> try: lp.parse(r'exists') ... except ParseException, e: print e End of input found. Variable and Expression expected following quantifier 'exists'. exists ^ >>> try: lp.parse(r'exists x') ... except ParseException, e: print e End of input found. Expression expected. exists x ^ >>> try: lp.parse(r'exists x.') ... except ParseException, e: print e End of input found. Expression expected. exists x. ^ >>> try: lp.parse(r'\ ') ... except ParseException, e: print e End of input found. Variable and Expression expected following lambda operator. \ ^ >>> try: lp.parse(r'\ x') ... except ParseException, e: print e End of input found. Expression expected. \ x ^ >>> try: lp.parse(r'\ x y') ... except ParseException, e: print e End of input found. Expression expected. \ x y ^ >>> try: lp.parse(r'\ x.') ... except ParseException, e: print e End of input found. Expression expected. \ x. ^ >>> try: lp.parse(r'P(x)Q(x)') ... except ParseException, e: print e Unexpected token: 'Q'. P(x)Q(x) ^ >>> try: lp.parse(r'(P(x)Q(x)') ... except ParseException, e: print e Unexpected token: 'Q'. Expected token ')'. (P(x)Q(x) ^ >>> try: lp.parse(r'exists x y') ... except ParseException, e: print e End of input found. Expression expected. exists x y ^ >>> try: lp.parse(r'exists x y.') ... except ParseException, e: print e End of input found. Expression expected. exists x y. ^ >>> try: lp.parse(r'exists x -> y') ... except ParseException, e: print e Unexpected token: '->'. Expression expected. exists x -> y ^ >>> try: lp.parse(r'A -> ((P(x) & Q(x)) -> Z') ... except ParseException, e: print e End of input found. Expected token ')'. A -> ((P(x) & Q(x)) -> Z ^ >>> try: lp.parse(r'A -> ((P(x) &) -> Z') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. A -> ((P(x) &) -> Z ^ >>> try: lp.parse(r'A -> ((P(x) | )) -> Z') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. A -> ((P(x) | )) -> Z ^ >>> try: lp.parse(r'A -> (P(x) ->) -> Z') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. A -> (P(x) ->) -> Z ^ >>> try: lp.parse(r'A -> (P(x) -> Z') ... except ParseException, e: print e End of input found. Expected token ')'. A -> (P(x) -> Z ^ >>> try: lp.parse(r'A -> (P(x,) -> Z') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. A -> (P(x,) -> Z ^ >>> try: lp.parse(r'A -> (P(x,)) -> Z') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. A -> (P(x,)) -> Z ^ >>> try: lp.parse(r'A -> (exists) -> Z') ... except ParseException, e: print e ')' is an illegal variable name. Constants may not be quantified. A -> (exists) -> Z ^ >>> try: lp.parse(r'A -> (exists x) -> Z') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. A -> (exists x) -> Z ^ >>> try: lp.parse(r'A -> (exists x.) -> Z') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. A -> (exists x.) -> Z ^ >>> try: lp.parse(r'A -> (\ ) -> Z') ... except ParseException, e: print e ')' is an illegal variable name. Constants may not be abstracted. A -> (\ ) -> Z ^ >>> try: lp.parse(r'A -> (\ x) -> Z') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. A -> (\ x) -> Z ^ >>> try: lp.parse(r'A -> (\ x y) -> Z') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. A -> (\ x y) -> Z ^ >>> try: lp.parse(r'A -> (\ x.) -> Z') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. A -> (\ x.) -> Z ^ >>> try: lp.parse(r'A -> (P(x)Q(x)) -> Z') ... except ParseException, e: print e Unexpected token: 'Q'. Expected token ')'. A -> (P(x)Q(x)) -> Z ^ >>> try: lp.parse(r'A -> ((P(x)Q(x)) -> Z') ... except ParseException, e: print e Unexpected token: 'Q'. Expected token ')'. A -> ((P(x)Q(x)) -> Z ^ >>> try: lp.parse(r'A -> (all x y) -> Z') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. A -> (all x y) -> Z ^ >>> try: lp.parse(r'A -> (exists x y.) -> Z') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. A -> (exists x y.) -> Z ^ >>> try: lp.parse(r'A -> (exists x -> y) -> Z') ... except ParseException, e: print e Unexpected token: '->'. Expression expected. A -> (exists x -> y) -> Z ^ nltk-2.0~b9/nltk/test/internals.doctest0000644000175000017500000000764011331670166020106 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ========================================== Unit tests for the nltk.utilities module ========================================== overridden() ~~~~~~~~~~~~ >>> from nltk.internals import overridden The typical use case is in defining methods for an interface or abstract base class, in such a way that subclasses don't have to implement all of the methods: >>> class EaterI(object): ... '''Subclass must define eat() or batch_eat().''' ... def eat(self, food): ... if overridden(self.batch_eat): ... return self.batch_eat([food])[0] ... else: ... raise NotImplementedError() ... def batch_eat(self, foods): ... return [self.eat(food) for food in foods] As long as a subclass implements one method, it will be used to perform the other method: >>> class GoodEater1(EaterI): ... def eat(self, food): ... return 'yum' >>> GoodEater1().eat('steak') 'yum' >>> GoodEater1().batch_eat(['steak', 'peas']) ['yum', 'yum'] >>> class GoodEater2(EaterI): ... def batch_eat(self, foods): ... return ['yum' for food in foods] >>> GoodEater2().eat('steak') 'yum' >>> GoodEater2().batch_eat(['steak', 'peas']) ['yum', 'yum'] But if a subclass doesn't implement either one, then they'll get an error when they try to call them. (nb this is better than infinite recursion): >>> class BadEater1(EaterI): ... pass >>> BadEater1().eat('steak') Traceback (most recent call last): . . . NotImplementedError >>> BadEater1().batch_eat(['steak', 'peas']) Traceback (most recent call last): . . . NotImplementedError Trying to use the abstract base class itself will also result in an error: >>> class EaterI(EaterI): ... pass >>> EaterI().eat('steak') Traceback (most recent call last): . . . NotImplementedError >>> EaterI().batch_eat(['steak', 'peas']) Traceback (most recent call last): . . . NotImplementedError It's ok to use intermediate abstract classes: >>> class AbstractEater(EaterI): ... pass >>> class GoodEater3(AbstractEater): ... def eat(self, food): ... return 'yum' ... >>> GoodEater3().eat('steak') 'yum' >>> GoodEater3().batch_eat(['steak', 'peas']) ['yum', 'yum'] >>> class GoodEater4(AbstractEater): ... def batch_eat(self, foods): ... return ['yum' for food in foods] >>> GoodEater4().eat('steak') 'yum' >>> GoodEater4().batch_eat(['steak', 'peas']) ['yum', 'yum'] >>> class BadEater2(AbstractEater): ... pass >>> BadEater2().eat('steak') Traceback (most recent call last): . . . NotImplementedError >>> BadEater2().batch_eat(['steak', 'peas']) Traceback (most recent call last): . . . NotImplementedError Here's some extra tests: >>> class A(object): ... def f(x): pass >>> class B(A): ... def f(x): pass >>> class C(A): pass >>> class D(B): pass >>> overridden(A().f) False >>> overridden(B().f) True >>> overridden(C().f) False >>> overridden(D().f) True overridden() can be called on unbound methods as well: >>> overridden(A.f) False >>> overridden(B.f) True >>> overridden(C.f) False >>> overridden(D.f) True It works for classic classes, too: >>> class A: ... def f(x): pass >>> class B(A): ... def f(x): pass >>> class C(A): pass >>> class D(B): pass >>> overridden(A().f) False >>> overridden(B().f) True >>> overridden(C().f) False >>> overridden(D().f) True >>> overridden(A.f) False >>> overridden(B.f) True >>> overridden(C.f) False >>> overridden(D.f) True nltk-2.0~b9/nltk/test/inference.doctest0000644000175000017500000004530611377057401020047 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ==================================== Logical Inference and Model Building ==================================== >>> from nltk import * >>> from nltk.sem.drt import DrtParser >>> from nltk.sem import logic >>> logic._counter._value = 0 ------------ Introduction ------------ Within the area of automated reasoning, first order theorem proving and model building (or model generation) have both received much attention, and have given rise to highly sophisticated techniques. We focus therefore on providing an NLTK interface to third party tools for these tasks. In particular, the module ``nltk.inference`` can be used to access both theorem provers and model builders. --------------------------------- NLTK Interface to Theorem Provers --------------------------------- The main class used to interface with a theorem prover is the ``Prover`` class, found in ``nltk.api``. The ``prove()`` method takes three optional arguments: a goal, a list of assumptions, and a ``verbose`` boolean to indicate whether the proof should be printed to the console. The proof goal and any assumptions need to be instances of the ``Expression`` class specified by ``nltk.sem.logic``. There are currently three theorem provers included with NLTK: ``Prover9``, ``TableauProver``, and ``ResolutionProver``. The first is an off-the-shelf prover, while the other two are written in Python and included in the ``nltk.inference`` package. >>> lp = LogicParser() >>> p1 = lp.parse('man(socrates)') >>> p2 = lp.parse('all x.(man(x) -> mortal(x))') >>> c = lp.parse('mortal(socrates)') >>> Prover9().prove(c, [p1,p2]) True >>> TableauProver().prove(c, [p1,p2]) True >>> ResolutionProver().prove(c, [p1,p2], verbose=True) [1] {-mortal(socrates)} A [2] {man(socrates)} A [3] {-man(z2), mortal(z2)} A [4] {-man(socrates)} (1, 3) [5] {mortal(socrates)} (2, 3) [6] {} (1, 5) True --------------------- The ``ProverCommand`` --------------------- A ``ProverCommand`` is a stateful holder for a theorem prover. The command stores a theorem prover instance (of type ``Prover``), a goal, a list of assumptions, the result of the proof, and a string version of the entire proof. Corresponding to the three included ``Prover`` implementations, there are three ``ProverCommand`` implementations: ``Prover9Command``, ``TableauProverCommand``, and ``ResolutionProverCommand``. The ``ProverCommand``'s constructor takes its goal and assumptions. The ``prove()`` command executes the ``Prover`` and ``proof()`` returns a String form of the proof If the ``prove()`` method has not been called, then the prover command will be unable to display a proof. >>> prover = ResolutionProverCommand(c, [p1,p2]) >>> print prover.proof() # doctest: +ELLIPSIS Traceback (most recent call last): File "...", line 1212, in __run compileflags, 1) in test.globs File "", line 1, in File "...", line ..., in proof raise LookupError("You have to call prove() first to get a proof!") LookupError: You have to call prove() first to get a proof! >>> prover.prove() True >>> print prover.proof() [1] {-mortal(socrates)} A [2] {man(socrates)} A [3] {-man(z4), mortal(z4)} A [4] {-man(socrates)} (1, 3) [5] {mortal(socrates)} (2, 3) [6] {} (1, 5) The prover command stores the result of proving so that if ``prove()`` is called again, then the command can return the result without executing the prover again. This allows the user to access the result of the proof without wasting time re-computing what it already knows. >>> prover.prove() True >>> prover.prove() True The assumptions and goal may be accessed using the ``assumptions()`` and ``goal()`` methods, respectively. >>> prover.assumptions() [, mortal(x))>] >>> prover.goal() The assumptions list may be modified using the ``add_assumptions()`` and ``retract_assumptions()`` methods. Both methods take a list of ``Expression`` objects. Since adding or removing assumptions may change the result of the proof, the stored result is cleared when either of these methods are called. That means that ``proof()`` will be unavailable until ``prove()`` is called and a call to ``prove()`` will execute the theorem prover. >>> prover.retract_assumptions([lp.parse('man(socrates)')]) >>> print prover.proof() # doctest: +ELLIPSIS Traceback (most recent call last): File "...", line 1212, in __run compileflags, 1) in test.globs File "", line 1, in File "...", line ..., in proof raise LookupError("You have to call prove() first to get a proof!") LookupError: You have to call prove() first to get a proof! >>> prover.prove() False >>> print prover.proof() [1] {-mortal(socrates)} A [2] {-man(z6), mortal(z6)} A [3] {-man(socrates)} (1, 2) >>> prover.add_assumptions([lp.parse('man(socrates)')]) >>> prover.prove() True ------- Prover9 ------- Prover9 Installation ~~~~~~~~~~~~~~~~~~~~ You can download Prover9 from http://www.cs.unm.edu/~mccune/prover9/. Extract the source code into a suitable directory and follow the instructions in the Prover9 ``README.make`` file to compile the executables. Install these into an appropriate location; the ``prover9_search`` variable is currently configured to look in the following locations: >>> p = Prover9() >>> p.binary_locations() # doctest: +NORMALIZE_WHITESPACE ['/usr/local/bin/prover9', '/usr/local/bin/prover9/bin', '/usr/local/bin', '/usr/bin', '/usr/local/prover9', '/usr/local/share/prover9'] Alternatively, the environment variable ``PROVER9HOME`` may be configured with the binary's location. The path to the correct directory can be set manually in the following manner: >>> config_prover9(path='/usr/local/bin') # doctest: +SKIP [Found prover9: /usr/local/bin/prover9] If the executables cannot be found, ``Prover9`` will issue a warning message: >>> p.prove() # doctest: +SKIP Traceback (most recent call last): ... LookupError: =========================================================================== NLTK was unable to find the prover9 executable! Use config_prover9() or set the PROVER9HOME environment variable. >> config_prover9('/path/to/prover9') For more information, on prover9, see: =========================================================================== Using Prover9 ~~~~~~~~~~~~~ The general case in theorem proving is to determine whether ``S |- g`` holds, where ``S`` is a possibly empty set of assumptions, and ``g`` is a proof goal. As mentioned earlier, NLTK input to ``Prover9`` must be ``Expression``\ s of ``nltk.sem.logic``. A ``Prover9`` instance is initialized with a proof goal and, possibly, some assumptions. The ``prove()`` method attempts to find a proof of the goal, given the list of assumptions (in this case, none). >>> goal = lp.parse('(man(x) <-> --man(x))') >>> prover = Prover9Command(goal) >>> prover.prove() True Given a ``ProverCommand`` instance ``prover``, the method ``prover.proof()`` will return a String of the extensive proof information provided by Prover9, shown in abbreviated form here:: ============================== Prover9 =============================== Prover9 (32) version ... Process ... was started by ... on ... ... The command was ".../prover9 -f ...". ============================== end of head =========================== ============================== INPUT ================================= % Reading from file /var/... formulas(goals). (all x (man(x) -> man(x))). end_of_list. ... ============================== end of search ========================= THEOREM PROVED Exiting with 1 proof. Process 6317 exit (max_proofs) Mon Jan 21 15:23:28 2008 As mentioned earlier, we may want to list some assumptions for the proof, as shown here. >>> g = lp.parse('mortal(socrates)') >>> a1 = lp.parse('all x.(man(x) -> mortal(x))') >>> prover = Prover9Command(g, assumptions=[a1]) >>> prover.print_assumptions() all x.(man(x) -> mortal(x)) However, the assumptions are not sufficient to derive the goal: >>> print prover.prove() False So let's add another assumption: >>> a2 = lp.parse('man(socrates)') >>> prover.add_assumptions([a2]) >>> prover.print_assumptions() all x.(man(x) -> mortal(x)) man(socrates) >>> print prover.prove() True We can also show the assumptions in ``Prover9`` format. >>> prover.print_assumptions(output_format='Prover9') all x (man(x) -> mortal(x)) man(socrates) >>> prover.print_assumptions(output_format='Spass') Traceback (most recent call last): . . . NameError: Unrecognized value for 'output_format': Spass Assumptions can be retracted from the list of assumptions. >>> prover.retract_assumptions([a1]) >>> prover.print_assumptions() man(socrates) >>> prover.retract_assumptions([a1]) Statements can be loaded from a file and parsed. We can then add these statements as new assumptions. >>> g = lp.parse('all x.(boxer(x) -> -boxerdog(x))') >>> prover = Prover9Command(g) >>> prover.prove() False >>> import nltk.data >>> new = nltk.data.load('grammars/sample_grammars/background0.fol') >>> for a in new: ... print a all x.(boxerdog(x) -> dog(x)) all x.(boxer(x) -> person(x)) all x.-(dog(x) & person(x)) exists x.boxer(x) exists x.boxerdog(x) >>> prover.add_assumptions(new) >>> print prover.prove() True >>> print prover.proof() # doctest: +ELLIPSIS ============================== prooftrans ============================ Prover9 (...) version ... Process ... was started by ... on ... ... The command was ".../prover9". ============================== end of head =========================== ============================== end of input ========================== ============================== PROOF ================================= % -------- Comments from original proof -------- % Proof 1 at ... seconds. % Length of proof is 13. % Level of proof is 4. % Maximum clause weight is 0. % Given clauses 0. 1 (all x (boxerdog(x) -> dog(x))). [assumption]. 2 (all x (boxer(x) -> person(x))). [assumption]. 3 (all x -(dog(x) & person(x))). [assumption]. 6 (all x (boxer(x) -> -boxerdog(x))). [goal]. 8 -boxerdog(x) | dog(x). [clausify(1)]. 9 boxerdog(c3). [deny(6)]. 11 -boxer(x) | person(x). [clausify(2)]. 12 boxer(c3). [deny(6)]. 14 -dog(x) | -person(x). [clausify(3)]. 15 dog(c3). [resolve(9,a,8,a)]. 18 person(c3). [resolve(12,a,11,a)]. 19 -person(c3). [resolve(15,a,14,a)]. 20 $F. [resolve(19,a,18,a)]. ============================== end of proof ========================== ---------------------- The tp_equals() method ---------------------- One application of the theorem prover functionality is to check if two Expressions have the same meaning. The ``tp_equals()`` method calls a theorem prover to determine whether two Expressions are logically equivalent. >>> a = lp.parse(r'exists x.(man(x) & walks(x))') >>> b = lp.parse(r'exists x.(walks(x) & man(x))') >>> print a.tp_equals(b) True The same method can be used on Discourse Representation Structures (DRSs). In this case, each DRS is converted to a first order logic form, and then passed to the theorem prover. >>> dp = DrtParser() >>> a = dp.parse(r'([x],[man(x), walks(x)])') >>> b = dp.parse(r'([x],[walks(x), man(x)])') >>> print a.tp_equals(b) True -------------------------------- NLTK Interface to Model Builders -------------------------------- The top-level to model builders is parallel to that for theorem-provers. The ``ModelBuilder`` interface is located in ``nltk.inference.api``. It is currently only implemented by ``Mace``, which interfaces with the Mace4 model builder. Typically we use a model builder to show that some set of formulas has a model, and is therefore consistent. One way of doing this is by treating our candidate set of sentences as assumptions, and leaving the goal unspecified. Thus, the following interaction shows how both ``{a, c1}`` and ``{a, c2}`` are consistent sets, since Mace succeeds in a building a model for each of them, while ``{c1, c2}`` is inconsistent. >>> a3 = lp.parse('exists x.(man(x) and walks(x))') >>> c1 = lp.parse('mortal(socrates)') >>> c2 = lp.parse('-mortal(socrates)') >>> mace = Mace() >>> print mace.build_model(None, [a3, c1]) True >>> print mace.build_model(None, [a3, c2]) True We can also use the model builder as an adjunct to theorem prover. Let's suppose we are trying to prove ``S |- g``, i.e. that ``g`` is logically entailed by assumptions ``S = {s1, s2, ..., sn}``. We can this same input to Mace4, and the model builder will try to find a counterexample, that is, to show that ``g`` does *not* follow from ``S``. So, given this input, Mace4 will try to find a model for the set ``S' = {s1, s2, ..., sn, (not g)}``. If ``g`` fails to follow from ``S``, then Mace4 may well return with a counterexample faster than Prover9 concludes that it cannot find the required proof. Conversely, if ``g`` *is* provable from ``S``, Mace4 may take a long time unsuccessfully trying to find a counter model, and will eventually give up. In the following example, we see that the model builder does succeed in building a model of the assumptions together with the negation of the goal. That is, it succeeds in finding a model where there is a woman that every man loves; Adam is a man; Eve is a woman; but Adam does not love Eve. >>> a4 = lp.parse('exists y. (woman(y) & all x. (man(x) -> love(x,y)))') >>> a5 = lp.parse('man(adam)') >>> a6 = lp.parse('woman(eve)') >>> g = lp.parse('love(adam,eve)') >>> print mace.build_model(g, [a4, a5, a6]) True The Model Builder will fail to find a model if the assumptions do entail the goal. Mace will continue to look for models of ever-increasing sizes until the end_size number is reached. By default, end_size is 500, but it can be set manually for quicker response time. >>> a7 = lp.parse('all x.(man(x) -> mortal(x))') >>> a8 = lp.parse('man(socrates)') >>> g2 = lp.parse('mortal(socrates)') >>> print Mace(end_size=50).build_model(g2, [a7, a8]) False There is also a ``ModelBuilderCommand`` class that, like ``ProverCommand``, stores a ``ModelBuilder``, a goal, assumptions, a result, and a model. The only implementation in NLTK is ``MaceCommand``. ----- Mace4 ----- Mace4 Installation ~~~~~~~~~~~~~~~~~~ Mace4 is packaged with Prover9, and can be downloaded from the same source, namely http://www.cs.unm.edu/~mccune/prover9/. It is installed in the same manner as Prover9. Using Mace4 ~~~~~~~~~~~ Check whether Mace4 can find a model. >>> a = lp.parse('(see(mary,john) & -(mary = john))') >>> mb = MaceCommand(assumptions=[a]) >>> mb.build_model() True Show the model in 'tabular' format. >>> print mb.model(format='tabular') % number = 1 % seconds = 0 % Interpretation of size 2 john : 0 mary : 1 see : | 0 1 ---+---- 0 | 0 0 1 | 1 0 Show the model in 'tabular' format. >>> print mb.model(format='cooked') % number = 1 % seconds = 0 % Interpretation of size 2 john = 0. mary = 1. - see(0,0). - see(0,1). see(1,0). - see(1,1). The property ``valuation`` accesses the stored ``Valuation``. >>> print mb.valuation {'john': 'a', 'mary': 'b', 'see': set([('b', 'a')])} We can return to our earlier example and inspect the model: >>> mb = MaceCommand(g, assumptions=[a4, a5, a6]) >>> m = mb.build_model() >>> print mb.model(format='cooked') % number = 1 % seconds = 0 % Interpretation of size 2 adam = 0. eve = 0. c1 = 1. man(0). - man(1). woman(0). woman(1). - love(0,0). love(0,1). - love(1,0). - love(1,1). Here, we can see that ``adam`` and ``eve`` have been assigned the same individual, namely ``0`` as value; ``0`` is both a man and a woman; a second individual ``1`` is also a woman; and ``0`` loves ``1``. Thus, this is an interpretation in which there is a woman that every man loves but Adam doesn't love Eve. Mace can also be used with propositional logic. >>> p = lp.parse('P') >>> q = lp.parse('Q') >>> mb = MaceCommand(q, [p, p>-q]) >>> mb.build_model() True >>> mb.valuation {'Q': False, 'P': True} -------------------------------------------------------- Running a Theorem Prover and a Model Builder in Parallel -------------------------------------------------------- The ParallelProverBuilder gives the ability to run a Theorem Prover and a Model Builder in parallel, using the result of whichever finishes first. This is beneficial because if the Theorem Prover finds a proof, then we can be certain that the Model Builder will not find a model. Conversely, if the Model Build finds a model, we know that there is no proof. >>> p = lp.parse('P') >>> q = lp.parse('Q') >>> prover = Prover9() >>> builder = Mace(end_size=50) >>> ppb = ParallelProverBuilder(prover, builder) >>> ppb.prove(q, [p, p>q]) True >>> ppb.build_model(q, [p, p>q]) False >>> ppb = ParallelProverBuilder(prover, builder) >>> ppb.prove(-q, [p, p>q]) False >>> ppb.build_model(-q, [p, p>q]) True The ParallelProverBuilderCommand offers the same functionality in a stateful command format. >>> ppbc = ParallelProverBuilderCommand(prover, builder, q, [p, p>q]) >>> ppbc.prove() True >>> ppbc.build_model() False >>> ppbc = ParallelProverBuilderCommand(prover, builder, -q, [p, p>q]) >>> ppbc.prove() False >>> ppbc.build_model() True nltk-2.0~b9/nltk/test/grammartestsuites.doctest0000644000175000017500000000620411331670175021665 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ========================== Test Suites for Grammars ========================== Sentences in the test suite are divided into two classes: - grammatical (*accept*) and - ungrammatical (*reject*). If a sentence should parse accordng to the grammar, the value of ``trees`` will be a non-empty list. If a sentence should be rejected according to the grammar, then the value of ``trees`` will be ``None``. >>> from nltk.parse import TestGrammar >>> germantest1 = {} >>> germantest1['doc'] = "Tests for person agreement" >>> germantest1['accept'] = [ ... 'ich komme', ... 'ich sehe mich', ... 'du kommst', ... 'du siehst mich', ... 'sie kommt', ... 'sie sieht mich', ... 'ihr kommt', ... 'wir kommen', ... 'sie kommen', ... 'du magst mich', ... 'er mag mich', ... 'du folgst mir', ... 'sie hilft mir', ... ] >>> germantest1['reject'] = [ ... 'ich kommt', ... 'ich kommst', ... 'ich siehst mich', ... 'du komme', ... 'du sehe mich', ... 'du kommt', ... 'er komme', ... 'er siehst mich', ... 'wir komme', ... 'wir kommst', ... 'die Katzen kommst', ... 'sie komme', ... 'sie kommst', ... 'du mag mich', ... 'er magst mich', ... 'du folgt mir', ... 'sie hilfst mir', ... ] >>> germantest2 = {} >>> germantest2['doc'] = "Tests for number agreement" >>> germantest2['accept'] = [ ... 'der Hund kommt', ... 'die Hunde kommen', ... 'ich komme', ... 'wir kommen', ... 'ich sehe die Katzen', ... 'ich folge den Katzen', ... 'ich sehe die Katzen', ... 'ich folge den Katzen', ... 'wir sehen die Katzen', ... 'wir folgen den Katzen' ... ] >>> germantest2['reject'] = [ ... 'ich kommen', ... 'wir komme', ... 'der Hunde kommt', ... 'der Hunde kommen', ... 'die Katzen kommt', ... 'ich sehe der Hunde', ... 'ich folge den Hund', ... 'ich sehen der Hunde', ... 'ich folgen den Hund', ... 'wir sehe die Katzen', ... 'wir folge den Katzen' ... ] >>> germantest3 = {} >>> germantest3['doc'] = "Tests for case government and subcategorization" >>> germantest3['accept'] = [ ... 'der Hund sieht mich', ... 'der Hund kommt', ... 'ich sehe den Hund', ... 'ich helfe dem Hund', ... ] >>> germantest3['reject'] = [ ... 'ich sehe', ... 'ich helfe', ... 'ich komme den Hund', ... 'ich sehe den Hund die Katzen', ... 'du hilfst mich', ... 'du siehst mir', ... 'du siehst ich', ... 'der Hunde kommt mich', ... 'die Hunde sehe die Hunde', ... 'der Hund sehe die Hunde', ... 'ich hilft den Hund', ... 'ich hilft der Hund', ... 'ich sehe dem Hund', ... ] >>> germantestsuites = [germantest1, germantest2, germantest3] >>> tester = TestGrammar('grammars/book_grammars/german.fcfg', germantestsuites) >>> tester.run() Tests for person agreement: All tests passed! Tests for number agreement: All tests passed! Tests for case government and subcategorization: All tests passed! nltk-2.0~b9/nltk/test/grammar.doctest0000644000175000017500000000254311332742411017524 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT =============== Grammar Parsing =============== Grammars can be parsed from strings: >>> from nltk import parse_cfg >>> grammar = parse_cfg(""" ... S -> NP VP ... PP -> P NP ... NP -> Det N | NP PP ... VP -> V NP | VP PP ... Det -> 'a' | 'the' ... N -> 'dog' | 'cat' ... V -> 'chased' | 'sat' ... P -> 'on' | 'in' ... """) >>> grammar >>> grammar.start() S >>> grammar.productions() # doctest: +NORMALIZE_WHITESPACE [S -> NP VP, PP -> P NP, NP -> Det N, NP -> NP PP, VP -> V NP, VP -> VP PP, Det -> 'a', Det -> 'the', N -> 'dog', N -> 'cat', V -> 'chased', V -> 'sat', P -> 'on', P -> 'in'] Probabilistic CFGs: >>> from nltk import parse_pcfg >>> toy_pcfg1 = parse_pcfg(""" ... S -> NP VP [1.0] ... NP -> Det N [0.5] | NP PP [0.25] | 'John' [0.1] | 'I' [0.15] ... Det -> 'the' [0.8] | 'my' [0.2] ... N -> 'man' [0.5] | 'telescope' [0.5] ... VP -> VP PP [0.1] | V NP [0.7] | V [0.2] ... V -> 'ate' [0.35] | 'saw' [0.65] ... PP -> P NP [1.0] ... P -> 'with' [0.61] | 'under' [0.39] ... """) Chomsky Normal Form grammar (Test for bug 474) >>> g = parse_cfg("VP^ -> VBP NP^") >>> g.productions()[0].lhs() VP^ nltk-2.0~b9/nltk/test/gluesemantics.doctest0000644000175000017500000003254511331670201020742 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ============================================================================== Glue Semantics ============================================================================== .. include:: ../../doc/definitions.rst ====================== Linear logic ====================== >>> from nltk.sem.linearlogic import * >>> from nltk.sem.glue import * >>> llp = LinearLogicParser() >>> from nltk.sem import logic Parser >>> print llp.parse(r'f') f >>> print llp.parse(r'(g -o f)') (g -o f) >>> print llp.parse(r'(g -o (h -o f))') (g -o (h -o f)) >>> print llp.parse(r'((g -o G) -o G)') ((g -o G) -o G) >>> print llp.parse(r'(g -o f)(g)') (g -o f)(g) >>> print llp.parse(r'((g -o G) -o G)((g -o f))') ((g -o G) -o G)((g -o f)) Simplify >>> print llp.parse(r'f').simplify() f >>> print llp.parse(r'(g -o f)').simplify() (g -o f) >>> print llp.parse(r'((g -o G) -o G)').simplify() ((g -o G) -o G) >>> print llp.parse(r'(g -o f)(g)').simplify() f >>> try: llp.parse(r'(g -o f)(f)').simplify() ... except LinearLogicApplicationException, e: print e ... Cannot apply (g -o f) to f. Cannot unify g with f given {} >>> print llp.parse(r'(G -o f)(g)').simplify() f >>> print llp.parse(r'((g -o G) -o G)((g -o f))').simplify() f Test BindingDict >>> h = ConstantExpression('h') >>> g = ConstantExpression('g') >>> f = ConstantExpression('f') >>> H = VariableExpression('H') >>> G = VariableExpression('G') >>> F = VariableExpression('F') >>> d1 = BindingDict([(H,h)]) >>> d2 = BindingDict([(F,f),(G,F)]) >>> d12 = d1 + d2 >>> all12 = ['%s: %s' % (v, d12[v]) for v in d12.d] >>> all12.sort() >>> print all12 ['F: f', 'G: f', 'H: h'] >>> d4 = BindingDict([(F,f)]) >>> try: d4[F] = g ... except VariableBindingException, e: print e Variable F already bound to another value Test Unify >>> try: f.unify(g, BindingDict()) ... except UnificationException, e: print e ... Cannot unify f with g given {} >>> print f.unify(G, BindingDict()) {G: f} >>> try: f.unify(G, BindingDict([(G,h)])) ... except UnificationException, e: print e ... Cannot unify f with G given {G: h} >>> print f.unify(G, BindingDict([(G,f)])) {G: f} >>> print f.unify(G, BindingDict([(H,f)])) {H: f, G: f} >>> print G.unify(f, BindingDict()) {G: f} >>> try: G.unify(f, BindingDict([(G,h)])) ... except UnificationException, e: print e ... Cannot unify G with f given {G: h} >>> print G.unify(f, BindingDict([(G,f)])) {G: f} >>> print G.unify(f, BindingDict([(H,f)])) {H: f, G: f} >>> print G.unify(F, BindingDict()) {G: F} >>> try: G.unify(F, BindingDict([(G,H)])) ... except UnificationException, e: print e ... Cannot unify G with F given {G: H} >>> print G.unify(F, BindingDict([(G,F)])) {G: F} >>> print G.unify(F, BindingDict([(H,F)])) {H: F, G: F} Test Compile >>> print llp.parse('g').compile_pos(Counter(), GlueFormula) (, []) >>> print llp.parse('(g -o f)').compile_pos(Counter(), GlueFormula) (, []) >>> print llp.parse('(g -o (h -o f))').compile_pos(Counter(), GlueFormula) (, []) ====================== Glue ====================== Demo of "John walks" -------------------- >>> john = GlueFormula("John", "g") >>> print john John : g >>> walks = GlueFormula(r"\x.walks(x)", "(g -o f)") >>> print walks \x.walks(x) : (g -o f) >>> print walks.applyto(john) \x.walks(x)(John) : (g -o f)(g) >>> print walks.applyto(john).simplify() walks(John) : f Demo of "A dog walks" --------------------- >>> a = GlueFormula("\P Q.some x.(P(x) and Q(x))", "((gv -o gr) -o ((g -o G) -o G))") >>> print a \P Q.exists x.(P(x) & Q(x)) : ((gv -o gr) -o ((g -o G) -o G)) >>> man = GlueFormula(r"\x.man(x)", "(gv -o gr)") >>> print man \x.man(x) : (gv -o gr) >>> walks = GlueFormula(r"\x.walks(x)", "(g -o f)") >>> print walks \x.walks(x) : (g -o f) >>> a_man = a.applyto(man) >>> print a_man.simplify() \Q.exists x.(man(x) & Q(x)) : ((g -o G) -o G) >>> a_man_walks = a_man.applyto(walks) >>> print a_man_walks.simplify() exists x.(man(x) & walks(x)) : f Demo of 'every girl chases a dog' --------------------------------- Individual words: >>> every = GlueFormula("\P Q.all x.(P(x) -> Q(x))", "((gv -o gr) -o ((g -o G) -o G))") >>> print every \P Q.all x.(P(x) -> Q(x)) : ((gv -o gr) -o ((g -o G) -o G)) >>> girl = GlueFormula(r"\x.girl(x)", "(gv -o gr)") >>> print girl \x.girl(x) : (gv -o gr) >>> chases = GlueFormula(r"\x y.chases(x,y)", "(g -o (h -o f))") >>> print chases \x y.chases(x,y) : (g -o (h -o f)) >>> a = GlueFormula("\P Q.some x.(P(x) and Q(x))", "((hv -o hr) -o ((h -o H) -o H))") >>> print a \P Q.exists x.(P(x) & Q(x)) : ((hv -o hr) -o ((h -o H) -o H)) >>> dog = GlueFormula(r"\x.dog(x)", "(hv -o hr)") >>> print dog \x.dog(x) : (hv -o hr) Noun Quantification can only be done one way: >>> every_girl = every.applyto(girl) >>> print every_girl.simplify() \Q.all x.(girl(x) -> Q(x)) : ((g -o G) -o G) >>> a_dog = a.applyto(dog) >>> print a_dog.simplify() \Q.exists x.(dog(x) & Q(x)) : ((h -o H) -o H) The first reading is achieved by combining 'chases' with 'a dog' first. Since 'a girl' requires something of the form '(h -o H)' we must get rid of the 'g' in the glue of 'see'. We will do this with the '-o elimination' rule. So, x1 will be our subject placeholder. >>> xPrime = GlueFormula("x1", "g") >>> print xPrime x1 : g >>> xPrime_chases = chases.applyto(xPrime) >>> print xPrime_chases.simplify() \y.chases(x1,y) : (h -o f) >>> xPrime_chases_a_dog = a_dog.applyto(xPrime_chases) >>> print xPrime_chases_a_dog.simplify() exists x.(dog(x) & chases(x1,x)) : f Now we can retract our subject placeholder using lambda-abstraction and combine with the true subject. >>> chases_a_dog = xPrime_chases_a_dog.lambda_abstract(xPrime) >>> print chases_a_dog.simplify() \x1.exists x.(dog(x) & chases(x1,x)) : (g -o f) >>> every_girl_chases_a_dog = every_girl.applyto(chases_a_dog) >>> r1 = every_girl_chases_a_dog.simplify() >>> r2 = GlueFormula(r'all x.(girl(x) -> exists z1.(dog(z1) & chases(x,z1)))', 'f') >>> r1 == r2 True The second reading is achieved by combining 'every girl' with 'chases' first. >>> xPrime = GlueFormula("x1", "g") >>> print xPrime x1 : g >>> xPrime_chases = chases.applyto(xPrime) >>> print xPrime_chases.simplify() \y.chases(x1,y) : (h -o f) >>> yPrime = GlueFormula("x2", "h") >>> print yPrime x2 : h >>> xPrime_chases_yPrime = xPrime_chases.applyto(yPrime) >>> print xPrime_chases_yPrime.simplify() chases(x1,x2) : f >>> chases_yPrime = xPrime_chases_yPrime.lambda_abstract(xPrime) >>> print chases_yPrime.simplify() \x1.chases(x1,x2) : (g -o f) >>> every_girl_chases_yPrime = every_girl.applyto(chases_yPrime) >>> print every_girl_chases_yPrime.simplify() all x.(girl(x) -> chases(x,x2)) : f >>> every_girl_chases = every_girl_chases_yPrime.lambda_abstract(yPrime) >>> print every_girl_chases.simplify() \x2.all x.(girl(x) -> chases(x,x2)) : (h -o f) >>> every_girl_chases_a_dog = a_dog.applyto(every_girl_chases) >>> r1 = every_girl_chases_a_dog.simplify() >>> r2 = GlueFormula(r'exists x.(dog(x) & all z2.(girl(z2) -> chases(z2,x)))', 'f') >>> r1 == r2 True Compilation ----------- >>> for cp in GlueFormula('m', '(b -o a)').compile(Counter()): print cp m : (b -o a) : {1} >>> for cp in GlueFormula('m', '((c -o b) -o a)').compile(Counter()): print cp v1 : c : {1} m : (b[1] -o a) : {2} >>> for cp in GlueFormula('m', '((d -o (c -o b)) -o a)').compile(Counter()): print cp v1 : c : {1} v2 : d : {2} m : (b[1, 2] -o a) : {3} >>> for cp in GlueFormula('m', '((d -o e) -o ((c -o b) -o a))').compile(Counter()): print cp v1 : d : {1} v2 : c : {2} m : (e[1] -o (b[2] -o a)) : {3} >>> for cp in GlueFormula('m', '(((d -o c) -o b) -o a)').compile(Counter()): print cp v1 : (d -o c) : {1} m : (b[1] -o a) : {2} >>> for cp in GlueFormula('m', '((((e -o d) -o c) -o b) -o a)').compile(Counter()): print cp v1 : e : {1} v2 : (d[1] -o c) : {2} m : (b[2] -o a) : {3} Demo of 'a man walks' using Compilation --------------------------------------- Premises >>> a = GlueFormula('\\P Q.some x.(P(x) and Q(x))', '((gv -o gr) -o ((g -o G) -o G))') >>> print a \P Q.exists x.(P(x) & Q(x)) : ((gv -o gr) -o ((g -o G) -o G)) >>> man = GlueFormula('\\x.man(x)', '(gv -o gr)') >>> print man \x.man(x) : (gv -o gr) >>> walks = GlueFormula('\\x.walks(x)', '(g -o f)') >>> print walks \x.walks(x) : (g -o f) Compiled Premises: >>> counter = Counter() >>> ahc = a.compile(counter) >>> g1 = ahc[0] >>> print g1 v1 : gv : {1} >>> g2 = ahc[1] >>> print g2 v2 : g : {2} >>> g3 = ahc[2] >>> print g3 \P Q.exists x.(P(x) & Q(x)) : (gr[1] -o (G[2] -o G)) : {3} >>> g4 = man.compile(counter)[0] >>> print g4 \x.man(x) : (gv -o gr) : {4} >>> g5 = walks.compile(counter)[0] >>> print g5 \x.walks(x) : (g -o f) : {5} Derivation: >>> g14 = g4.applyto(g1) >>> print g14.simplify() man(v1) : gr : {1, 4} >>> g134 = g3.applyto(g14) >>> print g134.simplify() \Q.exists x.(man(x) & Q(x)) : (G[2] -o G) : {1, 3, 4} >>> g25 = g5.applyto(g2) >>> print g25.simplify() walks(v2) : f : {2, 5} >>> g12345 = g134.applyto(g25) >>> print g12345.simplify() exists x.(man(x) & walks(x)) : f : {1, 2, 3, 4, 5} --------------------------------- Dependency Graph to Glue Formulas --------------------------------- >>> depgraph = DependencyGraph("""1 John _ NNP NNP _ 2 SUBJ _ _ ... 2 sees _ VB VB _ 0 ROOT _ _ ... 3 a _ ex_quant ex_quant _ 4 SPEC _ _ ... 4 dog _ NN NN _ 2 OBJ _ _ ... """) >>> gfl = GlueDict('glue.semtype').to_glueformula_list(depgraph) >>> for gf in gfl: print gf \x y.sees(x,y) : (f -o (i -o g)) \P Q.exists x.(P(x) & Q(x)) : ((fv -o fr) -o ((f -o F2) -o F2)) \x.John(x) : (fv -o fr) \x.dog(x) : (iv -o ir) \P Q.exists x.(P(x) & Q(x)) : ((iv -o ir) -o ((i -o I5) -o I5)) >>> glue = Glue() >>> for r in glue.get_readings(glue.gfl_to_compiled(gfl)): ... print r.simplify().normalize() exists x.(dog(x) & exists z1.(John(z1) & sees(z1,x))) exists x.(John(x) & exists z1.(dog(z1) & sees(x,z1))) ----------------------------------- Dependency Graph to LFG f-structure ----------------------------------- >>> from nltk.sem.lfg import FStructure >>> fstruct = FStructure.read_depgraph(depgraph) >>> print fstruct f:[pred 'sees' obj h:[pred 'dog' spec 'a'] subj g:[pred 'John']] --------------------------------- LFG f-structure to Glue --------------------------------- >>> for gf in fstruct.to_glueformula_list(GlueDict('glue.semtype')): ... print gf \x y.sees(x,y) : (i -o (g -o f)) \x.dog(x) : (gv -o gr) \P Q.exists x.(P(x) & Q(x)) : ((gv -o gr) -o ((g -o G3) -o G3)) \P Q.exists x.(P(x) & Q(x)) : ((iv -o ir) -o ((i -o I4) -o I4)) \x.John(x) : (iv -o ir) -------------------------------- Initialize the Dependency Parser -------------------------------- >>> tagger = RegexpTagger( ... [('^(John|Mary)$', 'NNP'), ... ('^(sees|chases)$', 'VB'), ... ('^(a)$', 'ex_quant'), ... ('^(every)$', 'univ_quant'), ... ('^(girl|dog)$', 'NN') ... ]) >>> depparser = MaltParser(tagger=tagger) -------------------- Automated Derivation -------------------- >>> glue = Glue(depparser=depparser) >>> for reading in glue.parse_to_meaning('every girl chases a dog'): ... print reading.simplify().normalize() all x.(girl(x) -> exists z1.(dog(z1) & chases(x,z1))) exists x.(dog(x) & all z1.(girl(z1) -> chases(z1,x))) >>> drtglue = DrtGlue(depparser=depparser) >>> for reading in drtglue.parse_to_meaning('every girl chases a dog'): ... print reading.simplify().normalize() ([],[(([x],[girl(x)]) -> ([z1],[dog(z1), chases(x,z1)]))]) ([z1],[dog(z1), (([x],[girl(x)]) -> ([],[chases(x,z1)]))]) -------------- With inference -------------- Checking for equality of two DRSs is very useful when generating readings of a sentence. For example, the ``glue`` module generates two readings for the sentence *John sees Mary*: >>> from nltk.sem.glue import DrtGlue >>> readings = drtglue.parse_to_meaning('John sees Mary') >>> for drs in readings: ... print drs.simplify().normalize() ([x,z1],[John(x), Mary(z1), sees(x,z1)]) ([x,z1],[Mary(x), John(z1), sees(z1,x)]) However, it is easy to tell that these two readings are logically the same, and therefore one of them is superfluous. We can use the theorem prover to determine this equivalence, and then delete one of them. A particular theorem prover may be specified, or the argument may be left off to use the default. >>> readings[0].tp_equals(readings[1]) True nltk-2.0~b9/nltk/test/featstruct.doctest0000644000175000017500000011154211366157570020277 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ================================== Feature Structures & Unification ================================== >>> from nltk.featstruct import FeatStruct >>> from nltk.sem.logic import Variable, VariableExpression, LogicParser .. note:: For now, featstruct uses the older lambdalogic semantics module. Eventually, it should be updated to use the new first order predicate logic module. Overview ~~~~~~~~ A feature structure is a mapping from feature identifiers to feature values, where feature values can be simple values (like strings or ints), nested feature structures, or variables: >>> fs1 = FeatStruct(number='singular', person=3) >>> print fs1 [ number = 'singular' ] [ person = 3 ] Feature structure may be nested: >>> fs2 = FeatStruct(type='NP', agr=fs1) >>> print fs2 [ agr = [ number = 'singular' ] ] [ [ person = 3 ] ] [ ] [ type = 'NP' ] Variables are used to indicate that two features should be assigned the same value. For example, the following feature structure requires that the feature fs3['agr']['number'] be bound to the same value as the feature fs3['subj']['number']. >>> fs3 = FeatStruct(agr=FeatStruct(number=Variable('?n')), ... subj=FeatStruct(number=Variable('?n'))) >>> print fs3 [ agr = [ number = ?n ] ] [ ] [ subj = [ number = ?n ] ] Feature structures are typically used to represent partial information about objects. A feature name that is not mapped to a value stands for a feature whose value is unknown (*not* a feature without a value). Two feature structures that represent (potentially overlapping) information about the same object can be combined by *unification*. >>> print fs2.unify(fs3) [ agr = [ number = 'singular' ] ] [ [ person = 3 ] ] [ ] [ subj = [ number = 'singular' ] ] [ ] [ type = 'NP' ] When two inconsistent feature structures are unified, the unification fails and returns ``None``. >>> fs4 = FeatStruct(agr=FeatStruct(person=1)) >>> print fs4.unify(fs2) None >>> print fs2.unify(fs4) None .. >>> del fs1, fs2, fs3, fs4 # clean-up Feature Structure Types ----------------------- There are actually two types of feature structure: - *feature dictionaries*, implemented by `FeatDict`, act like Python dictionaries. Feature identifiers may be strings or instances of the `Feature` class. - *feature lists*, implemented by `FeatList`, act like Python lists. Feature identifiers are integers. When you construct a feature structure using the `FeatStruct` constructor, it will automatically decide which type is appropriate: >>> type(FeatStruct(number='singular')) >>> type(FeatStruct([1,2,3])) Usually, we will just use feature dictionaries; but sometimes feature lists can be useful too. Two feature lists will unify with each other only if they have equal lengths, and all of their feature values match. If you wish to write a feature list that contains 'unknown' values, you must use variables: >>> fs1 = FeatStruct([1,2,Variable('?y')]) >>> fs2 = FeatStruct([1,Variable('?x'),3]) >>> fs1.unify(fs2) [1, 2, 3] .. >>> del fs1, fs2 # clean-up Parsing Feature Structure Strings --------------------------------- Feature structures can be constructed directly from strings. Often, this is more convenient than constructing them directly. NLTK can parse most feature strings to produce the corresponding feature structures. (But you must restrict your base feature values to strings, ints, logic expressions (`nltk.sem.logic.Expression`), and a few other types discussed below). Feature dictionaries are written like Python dictionaries, except that keys are not put in quotes; and square brackets (``[]``) are used instead of braces (``{}``): >>> FeatStruct('[tense="past", agr=[number="sing", person=3]]') [agr=[number='sing', person=3], tense='past'] If a feature value is a single alphanumeric word, then it does not need to be quoted -- it will be automatically treated as a string: >>> FeatStruct('[tense=past, agr=[number=sing, person=3]]') [agr=[number='sing', person=3], tense='past'] Feature lists are written like python lists: >>> FeatStruct('[1, 2, 3]') [1, 2, 3] The expression ``[]`` is treated as an empty feature dictionary, not an empty feature list: >>> type(FeatStruct('[]')) Feature Paths ------------- Features can be specified using *feature paths*, or tuples of feature identifiers that specify path through the nested feature structures to a value. >>> fs1 = FeatStruct('[x=1, y=[1,2,[z=3]]]') >>> fs1['y'] [1, 2, [z=3]] >>> fs1['y', 2] [z=3] >>> fs1['y', 2, 'z'] 3 .. >>> del fs1 # clean-up Reentrance ---------- Feature structures may contain reentrant feature values. A *reentrant feature value* is a single feature structure that can be accessed via multiple feature paths. >>> fs1 = FeatStruct(x='val') >>> fs2 = FeatStruct(a=fs1, b=fs1) >>> print fs2 [ a = (1) [ x = 'val' ] ] [ ] [ b -> (1) ] >>> fs2 [a=(1)[x='val'], b->(1)] As you can see, reentrane is displayed by marking a feature structure with a unique identifier, in this case ``(1)``, the first time it is encountered; and then using the special form ``var -> id`` whenever it is encountered again. You can use the same notation to directly create reentrant feature structures from strings. >>> FeatStruct('[a=(1)[], b->(1), c=[d->(1)]]') [a=(1)[], b->(1), c=[d->(1)]] Reentrant feature structures may contain cycles: >>> fs3 = FeatStruct('(1)[a->(1)]') >>> fs3['a', 'a', 'a', 'a'] (1)[a->(1)] >>> fs3['a', 'a', 'a', 'a'] is fs3 True Unification preserves the reentrance relations imposed by both of the unified feature structures. In the feature structure resulting from unification, any modifications to a reentrant feature value will be visible using any of its feature paths. >>> fs3.unify(FeatStruct('[a=[b=12], c=33]')) (1)[a->(1), b=12, c=33] .. >>> del fs1, fs2, fs3 # clean-up Feature Structure Equality -------------------------- Two feature structures are considered equal if they assign the same values to all features, *and* they contain the same reentrances. >>> fs1 = FeatStruct('[a=(1)[x=1], b->(1)]') >>> fs2 = FeatStruct('[a=(1)[x=1], b->(1)]') >>> fs3 = FeatStruct('[a=[x=1], b=[x=1]]') >>> print fs1 == fs1, fs1 is fs1 True True >>> print fs1 == fs2, fs1 is fs2 True False >>> print fs1 == fs3, fs1 is fs3 False False Note that this differs from how Python dictionaries and lists define equality -- in particular, Python dictionaries and lists ignore reentrance relations. To test two feature structures for equality while ignoring reentrance relations, use the `equal_values()` method: >>> print fs1.equal_values(fs1) True >>> print fs1.equal_values(fs2) True >>> print fs1.equal_values(fs3) True .. >>> del fs1, fs2, fs3 # clean-up Feature Value Sets & Feature Value Tuples ----------------------------------------- `nltk.featstruct` defines two new data types that are intended to be used as feature values: `FeatureValueTuple` and `FeatureValueSet`. Both of these types are considered base values -- i.e., unification does *not* apply to them. However, variable binding *does* apply to any values that they contain. Feature value tuples are written with parentheses: >>> fs1 = FeatStruct('[x=(?x, ?y)]') >>> fs1 [x=(?x, ?y)] >>> fs1.substitute_bindings({Variable('?x'): 1, Variable('?y'): 2}) [x=(1, 2)] Feature sets are written with braces: >>> fs1 = FeatStruct('[x={?x, ?y}]') >>> fs1 [x={?x, ?y}] >>> fs1.substitute_bindings({Variable('?x'): 1, Variable('?y'): 2}) [x={1, 2}] In addition to the basic feature value tuple & set classes, nltk defines feature value unions (for sets) and feature value concatenations (for tuples). These are written using '+', and can be used to combine sets & tuples: >>> fs1 = FeatStruct('[x=((1, 2)+?z), z=?z]') >>> fs1 [x=((1, 2)+?z), z=?z] >>> fs1.unify(FeatStruct('[z=(3, 4, 5)]')) [x=(1, 2, 3, 4, 5), z=(3, 4, 5)] Thus, feature value tuples and sets can be used to build up tuples and sets of values over the corse of unification. For example, when parsing sentences using a semantic feature grammar, feature sets or feature tuples can be used to build a list of semantic predicates as the sentence is parsed. As was mentioned above, unification does not apply to feature value tuples and sets. One reason for this that it's impossible to define a single correct answer for unification when concatenation is used. Consider the following example: >>> fs1 = FeatStruct('[x=(1, 2, 3, 4)]') >>> fs2 = FeatStruct('[x=(?a+?b), a=?a, b=?b]') If unification applied to feature tuples, then the unification algorithm would have to arbitrarily choose how to divide the tuple (1,2,3,4) into two parts. Instead, the unification algorithm refuses to make this decision, and simply unifies based on value. Because (1,2,3,4) is not equal to (?a+?b), fs1 and fs2 will not unify: >>> print fs1.unify(fs2) None If you need a list-like structure that unification does apply to, use `FeatList`. .. >>> del fs1, fs2 # clean-up Light-weight Feature Structures ------------------------------- Many of the functions defined by `nltk.featstruct` can be applied directly to simple Python dictionaries and lists, rather than to full-fledged `FeatDict` and `FeatList` objects. In other words, Python ``dicts`` and ``lists`` can be used as "light-weight" feature structures. >>> from nltk.featstruct import unify >>> unify(dict(x=1, y=dict()), dict(a='a', y=dict(b='b'))) {'y': {'b': 'b'}, 'x': 1, 'a': 'a'} However, you should keep in mind the following caveats: - Python dictionaries & lists ignore reentrance when checking for equality between values. But two FeatStructs with different reentrances are considered nonequal, even if all their base values are equal. - FeatStructs can be easily frozen, allowing them to be used as keys in hash tables. Python dictionaries and lists can not. - FeatStructs display reentrance in their string representations; Python dictionaries and lists do not. - FeatStructs may *not* be mixed with Python dictionaries and lists (e.g., when performing unification). - FeatStructs provide a number of useful methods, such as `walk()` and `cyclic()`, which are not available for Python dicts & lists. In general, if your feature structures will contain any reentrances, or if you plan to use them as dictionary keys, it is strongly recommended that you use full-fledged `FeatStruct` objects. Custom Feature Values --------------------- The abstract base class `CustomFeatureValue` can be used to define new base value types that have custom unification methods. For example, the following feature value type encodes a range, and defines unification as taking the intersection on the ranges: >>> from nltk.featstruct import CustomFeatureValue, UnificationFailure >>> class Range(CustomFeatureValue): ... def __init__(self, low, high): ... assert low <= high ... self.low = low ... self.high = high ... def unify(self, other): ... if not isinstance(other, Range): ... return UnificationFailure ... low = max(self.low, other.low) ... high = min(self.high, other.high) ... if low <= high: return Range(low, high) ... else: return UnificationFailure ... def __repr__(self): ... return '(%s>> fs1 = FeatStruct(x=Range(5,8), y=FeatStruct(z=Range(7,22))) >>> print fs1.unify(FeatStruct(x=Range(6, 22))) [ x = (6>> print fs1.unify(FeatStruct(x=Range(9, 12))) None >>> print fs1.unify(FeatStruct(x=12)) None >>> print fs1.unify(FeatStruct('[x=?x, y=[z=?x]]')) [ x = (7>> fs1 = FeatStruct(a=1, b=2, c=3) >>> fs2 = FeatStruct(x=fs1, y='x') Feature structures support all dictionary methods (excluding the class method `dict.fromkeys()`). Non-mutating methods: >>> sorted(fs2.keys()) # keys() ['x', 'y'] >>> sorted(fs2.values()) # values() [[a=1, b=2, c=3], 'x'] >>> sorted(fs2.items()) # items() [('x', [a=1, b=2, c=3]), ('y', 'x')] >>> sorted(fs2.iterkeys()) # iterkeys() ['x', 'y'] >>> sorted(fs2.itervalues()) # itervalues() [[a=1, b=2, c=3], 'x'] >>> sorted(fs2.iteritems()) # iteritems() [('x', [a=1, b=2, c=3]), ('y', 'x')] >>> sorted(fs2) # __iter__() ['x', 'y'] >>> 'a' in fs2, 'x' in fs2 # __contains__() (False, True) >>> fs2.has_key('a'), fs2.has_key('x') # has_key() (False, True) >>> fs2['x'], fs2['y'] # __getitem__() ([a=1, b=2, c=3], 'x') >>> fs2['a'] # __getitem__() Traceback (most recent call last): . . . KeyError: 'a' >>> fs2.get('x'), fs2.get('y'), fs2.get('a') # get() ([a=1, b=2, c=3], 'x', None) >>> fs2.get('x', 'hello'), fs2.get('a', 'hello') # get() ([a=1, b=2, c=3], 'hello') >>> len(fs1), len(fs2) # __len__ (3, 2) >>> fs2.copy() # copy() [x=[a=1, b=2, c=3], y='x'] >>> fs2.copy() is fs2 # copy() False Note: by default, `FeatStruct.copy()` does a deep copy. Use `FeatStruct.copy(deep=False)` for a shallow copy. .. >>> del fs1, fs2 # clean-up. Dictionary access methods (mutating) ------------------------------------ >>> fs1 = FeatStruct(a=1, b=2, c=3) >>> fs2 = FeatStruct(x=fs1, y='x') Setting features (`__setitem__()`) >>> fs1['c'] = 5 >>> fs1 [a=1, b=2, c=5] >>> fs1['x'] = 12 >>> fs1 [a=1, b=2, c=5, x=12] >>> fs2['x', 'a'] = 2 >>> fs2 [x=[a=2, b=2, c=5, x=12], y='x'] >>> fs1 [a=2, b=2, c=5, x=12] Deleting features (`__delitem__()`) >>> del fs1['x'] >>> fs1 [a=2, b=2, c=5] >>> del fs2['x', 'a'] >>> fs1 [b=2, c=5] `setdefault()`: >>> fs1.setdefault('b', 99) 2 >>> fs1 [b=2, c=5] >>> fs1.setdefault('x', 99) 99 >>> fs1 [b=2, c=5, x=99] `update()`: >>> fs2.update({'a':'A', 'b':'B'}, c='C') >>> fs2 [a='A', b='B', c='C', x=[b=2, c=5, x=99], y='x'] `pop()`: >>> fs2.pop('a') 'A' >>> fs2 [b='B', c='C', x=[b=2, c=5, x=99], y='x'] >>> fs2.pop('a') Traceback (most recent call last): . . . KeyError: 'a' >>> fs2.pop('a', 'foo') 'foo' >>> fs2 [b='B', c='C', x=[b=2, c=5, x=99], y='x'] `clear()`: >>> fs1.clear() >>> fs1 [] >>> fs2 [b='B', c='C', x=[], y='x'] `popitem()`: >>> sorted([fs2.popitem() for i in range(len(fs2))]) [('b', 'B'), ('c', 'C'), ('x', []), ('y', 'x')] >>> fs2 [] Once a feature structure has been frozen, it may not be mutated. >>> fs1 = FeatStruct('[x=1, y=2, z=[a=3]]') >>> fs1.freeze() >>> fs1.frozen() True >>> fs1['z'].frozen() True >>> fs1['x'] = 5 Traceback (most recent call last): . . . ValueError: Frozen FeatStructs may not be modified. >>> del fs1['x'] Traceback (most recent call last): . . . ValueError: Frozen FeatStructs may not be modified. >>> fs1.clear() Traceback (most recent call last): . . . ValueError: Frozen FeatStructs may not be modified. >>> fs1.pop('x') Traceback (most recent call last): . . . ValueError: Frozen FeatStructs may not be modified. >>> fs1.popitem() Traceback (most recent call last): . . . ValueError: Frozen FeatStructs may not be modified. >>> fs1.setdefault('x') Traceback (most recent call last): . . . ValueError: Frozen FeatStructs may not be modified. >>> fs1.update(z=22) Traceback (most recent call last): . . . ValueError: Frozen FeatStructs may not be modified. .. >>> del fs1, fs2 # clean-up. Feature Paths ------------- Make sure that __getitem__ with feature paths works as intended: >>> fs1 = FeatStruct(a=1, b=2, ... c=FeatStruct( ... d=FeatStruct(e=12), ... f=FeatStruct(g=55, h='hello'))) >>> fs1[()] [a=1, b=2, c=[d=[e=12], f=[g=55, h='hello']]] >>> fs1['a'], fs1[('a',)] (1, 1) >>> fs1['c','d','e'] 12 >>> fs1['c','f','g'] 55 Feature paths that select unknown features raise KeyError: >>> fs1['c', 'f', 'e'] Traceback (most recent call last): . . . KeyError: ('c', 'f', 'e') >>> fs1['q', 'p'] Traceback (most recent call last): . . . KeyError: ('q', 'p') Feature paths that try to go 'through' a feature that's not a feature structure raise KeyError: >>> fs1['a', 'b'] Traceback (most recent call last): . . . KeyError: ('a', 'b') Feature paths can go through reentrant structures: >>> fs2 = FeatStruct('(1)[a=[b=[c->(1), d=5], e=11]]') >>> fs2['a', 'b', 'c', 'a', 'e'] 11 >>> fs2['a', 'b', 'c', 'a', 'b', 'd'] 5 >>> fs2[tuple('abcabcabcabcabcabcabcabcabcabca')] (1)[b=[c=[a->(1)], d=5], e=11] Indexing requires strings, `Feature`\s, or tuples; other types raise a TypeError: >>> fs2[12] Traceback (most recent call last): . . . TypeError: Expected feature name or path. Got 12. >>> fs2[list('abc')] Traceback (most recent call last): . . . TypeError: Expected feature name or path. Got ['a', 'b', 'c']. Feature paths can also be used with `get()`, `has_key()`, and `__contains__()`. >>> fpath1 = tuple('abcabc') >>> fpath2 = tuple('abcabz') >>> fs2.get(fpath1), fs2.get(fpath2) ((1)[a=[b=[c->(1), d=5], e=11]], None) >>> fpath1 in fs2, fpath2 in fs2 (True, False) >>> fs2.has_key(fpath1), fs2.has_key(fpath2) (True, False) .. >>> del fs1, fs2 # clean-up Parsing ------- Empty feature struct: >>> FeatStruct('[]') [] Test features with integer values: >>> FeatStruct('[a=12, b=-33, c=0]') [a=12, b=-33, c=0] Test features with string values. Either single or double quotes may be used. Strings are evaluated just like python strings -- in particular, you can use escape sequences and 'u' and 'r' prefixes, and triple-quoted strings. >>> FeatStruct('[a="", b="hello", c="\'", d=\'\', e=\'"\']') [a='', b='hello', c="'", d='', e='"'] >>> FeatStruct(r'[a="\\", b="\"", c="\x6f\\y", d="12"]') [a='\\', b='"', c='o\\y', d='12'] >>> FeatStruct(r'[a=u"\u1234", b=r"a\b\c"]') [a=u'\u1234', b='a\\b\\c'] >>> FeatStruct('[x="""a"""]') [x='a'] Test parsing of reentrant feature structures. >>> FeatStruct('[a=(1)[], b->(1)]') [a=(1)[], b->(1)] >>> FeatStruct('[a=(1)[x=1, y=2], b->(1)]') [a=(1)[x=1, y=2], b->(1)] Test parsing of cyclic feature structures. >>> FeatStruct('[a=(1)[b->(1)]]') [a=(1)[b->(1)]] >>> FeatStruct('(1)[a=[b=[c->(1)]]]') (1)[a=[b=[c->(1)]]] Strings of the form "+name" and "-name" may be used to specify boolean values. >>> FeatStruct('[-bar, +baz, +foo]') [-bar, +baz, +foo] None, True, and False are recognized as values: >>> FeatStruct('[bar=True, baz=False, foo=None]') [+bar, -baz, foo=None] Special features: >>> FeatStruct('NP/VP') NP[]/VP[] >>> FeatStruct('?x/?x') ?x[]/?x[] >>> print FeatStruct('VP[+fin, agr=?x, tense=past]/NP[+pl, agr=?x]') [ *type* = 'VP' ] [ ] [ [ *type* = 'NP' ] ] [ *slash* = [ agr = ?x ] ] [ [ pl = True ] ] [ ] [ agr = ?x ] [ fin = True ] [ tense = 'past' ] Here the slash feature gets coerced: >>> FeatStruct('[*slash*=a, x=b, *type*="NP"]') NP[x='b']/a[] >>> FeatStruct('NP[sem=]/NP') NP[sem=]/NP[] >>> FeatStruct('S[sem=]') S[sem=] >>> print FeatStruct('NP[sem=]/NP') [ *type* = 'NP' ] [ ] [ *slash* = [ *type* = 'NP' ] ] [ ] [ sem = ] Playing with ranges: >>> from nltk.featstruct import RangeFeature, FeatStructParser >>> width = RangeFeature('width') >>> parser = FeatStructParser([width]) >>> fs1 = parser.parse('[*width*=-5:12]') >>> fs2 = parser.parse('[*width*=2:123]') >>> fs3 = parser.parse('[*width*=-7:-2]') >>> fs1.unify(fs2) [*width*=(2, 12)] >>> fs1.unify(fs3) [*width*=(-5, -2)] >>> print fs2.unify(fs3) # no overlap in width. None The slash feature has a default value of 'False': >>> print FeatStruct('NP[]/VP').unify(FeatStruct('NP[]'), trace=1) Unification trace: / NP[]/VP[] |\ NP[] | | Unify feature: *type* | / 'NP' | |\ 'NP' | | | +-->'NP' | | Unify feature: *slash* | / VP[] | |\ False | | X X <-- FAIL None The demo structures from category.py. They all parse, but they don't do quite the right thing, -- ?x vs x. >>> FeatStruct(pos='n', agr=FeatStruct(number='pl', gender='f')) [agr=[gender='f', number='pl'], pos='n'] >>> FeatStruct(r'NP[sem=]/NP') NP[sem=]/NP[] >>> FeatStruct(r'S[sem=]') S[sem=] >>> FeatStruct('?x/?x') ?x[]/?x[] >>> FeatStruct('VP[+fin, agr=?x, tense=past]/NP[+pl, agr=?x]') VP[agr=?x, +fin, tense='past']/NP[agr=?x, +pl] >>> FeatStruct('S[sem = ]') S[sem=] >>> FeatStruct('S') S[] The parser also includes support for reading sets and tuples. >>> FeatStruct('[x={1,2,2,2}, y={/}]') [x={1, 2}, y={/}] >>> FeatStruct('[x=(1,2,2,2), y=()]') [x=(1, 2, 2, 2), y=()] >>> print FeatStruct('[x=(1,[z=(1,2,?x)],?z,{/})]') [ x = (1, [ z = (1, 2, ?x) ], ?z, {/}) ] Note that we can't put a featstruct inside a tuple, because doing so would hash it, and it's not frozen yet: >>> print FeatStruct('[x={[]}]') Traceback (most recent call last): . . . TypeError: FeatStructs must be frozen before they can be hashed. There's a special syntax for taking the union of sets: "{...+...}". The elements should only be variables or sets. >>> FeatStruct('[x={?a+?b+{1,2,3}}]') [x={?a+?b+{1, 2, 3}}] There's a special syntax for taking the concatenation of tuples: "(...+...)". The elements should only be variables or tuples. >>> FeatStruct('[x=(?a+?b+(1,2,3))]') [x=(?a+?b+(1, 2, 3))] Parsing gives helpful messages if your string contains an error. >>> FeatStruct('[a=, b=5]]') Traceback (most recent call last): . . . ValueError: Error parsing feature structure [a=, b=5]] ^ Expected value >>> FeatStruct('[a=12 22, b=33]') Traceback (most recent call last): . . . ValueError: Error parsing feature structure [a=12 22, b=33] ^ Expected comma >>> FeatStruct('[a=5] [b=6]') Traceback (most recent call last): . . . ValueError: Error parsing feature structure [a=5] [b=6] ^ Expected end of string >>> FeatStruct(' *++*') Traceback (most recent call last): . . . ValueError: Error parsing feature structure *++* ^ Expected open bracket or identifier >>> FeatStruct('[x->(1)]') Traceback (most recent call last): . . . ValueError: Error parsing feature structure [x->(1)] ^ Expected bound identifier >>> FeatStruct('[x->y]') Traceback (most recent call last): . . . ValueError: Error parsing feature structure [x->y] ^ Expected identifier >>> FeatStruct('') Traceback (most recent call last): . . . ValueError: Error parsing feature structure ^ Expected open bracket or identifier Unification ----------- Very simple unifications give the expected results: >>> FeatStruct().unify(FeatStruct()) [] >>> FeatStruct(number='singular').unify(FeatStruct()) [number='singular'] >>> FeatStruct().unify(FeatStruct(number='singular')) [number='singular'] >>> FeatStruct(number='singular').unify(FeatStruct(person=3)) [number='singular', person=3] Merging nested structures: >>> fs1 = FeatStruct('[A=[B=b]]') >>> fs2 = FeatStruct('[A=[C=c]]') >>> fs1.unify(fs2) [A=[B='b', C='c']] >>> fs2.unify(fs1) [A=[B='b', C='c']] A basic case of reentrant unification >>> fs4 = FeatStruct('[A=(1)[B=b], E=[F->(1)]]') >>> fs5 = FeatStruct("[A=[C='c'], E=[F=[D='d']]]") >>> fs4.unify(fs5) [A=(1)[B='b', C='c', D='d'], E=[F->(1)]] >>> fs5.unify(fs4) [A=(1)[B='b', C='c', D='d'], E=[F->(1)]] More than 2 paths to a value >>> fs1 = FeatStruct("[a=[],b=[],c=[],d=[]]") >>> fs2 = FeatStruct('[a=(1)[], b->(1), c->(1), d->(1)]') >>> fs1.unify(fs2) [a=(1)[], b->(1), c->(1), d->(1)] fs1[a] gets unified with itself >>> fs1 = FeatStruct('[x=(1)[], y->(1)]') >>> fs2 = FeatStruct('[x=(1)[], y->(1)]') >>> fs1.unify(fs2) [x=(1)[], y->(1)] Bound variables should get forwarded appropriately >>> fs1 = FeatStruct('[A=(1)[X=x], B->(1), C=?cvar, D=?dvar]') >>> fs2 = FeatStruct('[A=(1)[Y=y], B=(2)[Z=z], C->(1), D->(2)]') >>> fs1.unify(fs2) [A=(1)[X='x', Y='y', Z='z'], B->(1), C->(1), D->(1)] >>> fs2.unify(fs1) [A=(1)[X='x', Y='y', Z='z'], B->(1), C->(1), D->(1)] Cyclic structure created by unification. >>> fs1 = FeatStruct('[F=(1)[], G->(1)]') >>> fs2 = FeatStruct('[F=[H=(2)[]], G->(2)]') >>> fs3 = fs1.unify(fs2) >>> fs3 [F=(1)[H->(1)], G->(1)] >>> fs3['F'] is fs3['G'] True >>> fs3['F'] is fs3['G']['H'] True >>> fs3['F'] is fs3['G']['H']['H'] True >>> fs3['F'] is fs3['F']['H']['H']['H']['H']['H']['H']['H']['H'] True Cyclic structure created w/ variables. >>> fs1 = FeatStruct('[F=[H=?x]]') >>> fs2 = FeatStruct('[F=?x]') >>> fs3 = fs1.unify(fs2, rename_vars=False) >>> fs3 [F=(1)[H->(1)]] >>> fs3['F'] is fs3['F']['H'] True >>> fs3['F'] is fs3['F']['H']['H'] True >>> fs3['F'] is fs3['F']['H']['H']['H']['H']['H']['H']['H']['H'] True Unifying w/ a cyclic feature structure. >>> fs4 = FeatStruct('[F=[H=[H=[H=(1)[]]]], K->(1)]') >>> fs3.unify(fs4) [F=(1)[H->(1)], K->(1)] >>> fs4.unify(fs3) [F=(1)[H->(1)], K->(1)] Variable bindings should preserve reentrance. >>> bindings = {} >>> fs1 = FeatStruct("[a=?x]") >>> fs2 = fs1.unify(FeatStruct("[a=[]]"), bindings) >>> fs2['a'] is bindings[Variable('?x')] True >>> fs2.unify(FeatStruct("[b=?x]"), bindings) [a=(1)[], b->(1)] Aliased variable tests >>> fs1 = FeatStruct("[a=?x, b=?x]") >>> fs2 = FeatStruct("[b=?y, c=?y]") >>> bindings = {} >>> fs3 = fs1.unify(fs2, bindings) >>> fs3 [a=?x, b=?x, c=?x] >>> bindings {Variable('?y'): Variable('?x')} >>> fs3.unify(FeatStruct("[a=1]")) [a=1, b=1, c=1] If we keep track of the bindings, then we can use the same variable over multiple calls to unify. >>> bindings = {} >>> fs1 = FeatStruct('[a=?x]') >>> fs2 = fs1.unify(FeatStruct('[a=[]]'), bindings) >>> fs2.unify(FeatStruct('[b=?x]'), bindings) [a=(1)[], b->(1)] >>> bindings {Variable('?x'): []} .. >>> del fs1, fs2, fs3, fs4, fs5 # clean-up Unification Bindings -------------------- >>> bindings = {} >>> fs1 = FeatStruct('[a=?x]') >>> fs2 = FeatStruct('[a=12]') >>> fs3 = FeatStruct('[b=?x]') >>> fs1.unify(fs2, bindings) [a=12] >>> bindings {Variable('?x'): 12} >>> fs3.substitute_bindings(bindings) [b=12] >>> fs3 # substitute_bindings didn't mutate fs3. [b=?x] >>> fs2.unify(fs3, bindings) [a=12, b=12] >>> bindings = {} >>> fs1 = FeatStruct('[a=?x, b=1]') >>> fs2 = FeatStruct('[a=5, b=?x]') >>> fs1.unify(fs2, bindings) [a=5, b=1] >>> sorted(bindings.items()) [(Variable('?x'), 5), (Variable('?x2'), 1)] .. >>> del fs1, fs2, fs3 # clean-up Expressions ----------- >>> e=LogicParser().parse('\\P y.P(z,y)') >>> fs1 = FeatStruct(x=e, y=Variable('z')) >>> fs2 = FeatStruct(y=VariableExpression(Variable('John'))) >>> fs1.unify(fs2) [x=<\P y.P(John,y)>, y=] Remove Variables ---------------- >>> FeatStruct('[a=?x, b=12, c=[d=?y]]').remove_variables() [b=12, c=[]] >>> FeatStruct('(1)[a=[b=?x,c->(1)]]').remove_variables() (1)[a=[c->(1)]] Equality & Hashing ------------------ The `equal_values` method checks whether two feature structures assign the same value to every feature. If the optional argument ``check_reentrances`` is supplied, then it also returns false if there is any difference in the reentrances. >>> a = FeatStruct('(1)[x->(1)]') >>> b = FeatStruct('(1)[x->(1)]') >>> c = FeatStruct('(1)[x=[x->(1)]]') >>> d = FeatStruct('[x=(1)[x->(1)]]') >>> e = FeatStruct('(1)[x=[x->(1), y=1], y=1]') >>> def compare(x,y): ... assert x.equal_values(y, True) == y.equal_values(x, True) ... assert x.equal_values(y, False) == y.equal_values(x, False) ... if x.equal_values(y, True): ... assert x.equal_values(y, False) ... print 'equal values, same reentrance' ... elif x.equal_values(y, False): ... print 'equal values, different reentrance' ... else: ... print 'different values' >>> compare(a, a) equal values, same reentrance >>> compare(a, b) equal values, same reentrance >>> compare(a, c) equal values, different reentrance >>> compare(a, d) equal values, different reentrance >>> compare(c, d) equal values, different reentrance >>> compare(a, e) different values >>> compare(c, e) different values >>> compare(d, e) different values >>> compare(e, e) equal values, same reentrance Feature structures may not be hashed until they are frozen: >>> hash(a) Traceback (most recent call last): . . . TypeError: FeatStructs must be frozen before they can be hashed. >>> a.freeze() >>> v = hash(a) Feature structures define hash consistently. The following example looks at the hash value for each (fs1,fs2) pair; if their hash values are not equal, then they must not be equal. If their hash values are equal, then display a message, and indicate whether their values are indeed equal. Note that c and d currently have the same hash value, even though they are not equal. That is not a bug, strictly speaking, but it wouldn't be a bad thing if it changed. >>> for fstruct in (a, b, c, d, e): ... fstruct.freeze() >>> for fs1_name in 'abcde': ... for fs2_name in 'abcde': ... fs1 = locals()[fs1_name] ... fs2 = locals()[fs2_name] ... if hash(fs1) != hash(fs2): ... assert fs1 != fs2 ... else: ... print ('%s and %s have the same hash value,' % ... (fs1_name, fs2_name)), ... if fs1 == fs2: print 'and are equal' ... else: print 'and are not equal' a and a have the same hash value, and are equal a and b have the same hash value, and are equal b and a have the same hash value, and are equal b and b have the same hash value, and are equal c and c have the same hash value, and are equal c and d have the same hash value, and are not equal d and c have the same hash value, and are not equal d and d have the same hash value, and are equal e and e have the same hash value, and are equal .. >>> del a, b, c, d, e, v # clean-up Tracing ------- >>> fs1 = FeatStruct('[a=[b=(1)[], c=?x], d->(1), e=[f=?x]]') >>> fs2 = FeatStruct('[a=(1)[c="C"], e=[g->(1)]]') >>> fs1.unify(fs2, trace=True) Unification trace: / [a=[b=(1)[], c=?x], d->(1), e=[f=?x]] |\ [a=(1)[c='C'], e=[g->(1)]] | | Unify feature: a | / [b=[], c=?x] | |\ [c='C'] | | | | Unify feature: a.c | | / ?x | | |\ 'C' | | | | | +-->Variable('?x') | | | +-->[b=[], c=?x] | Bindings: {?x: 'C'} | | Unify feature: e | / [f=?x] | |\ [g=[c='C']] | | | +-->[f=?x, g=[b=[], c=?x]] | Bindings: {?x: 'C'} | +-->[a=(1)[b=(2)[], c='C'], d->(2), e=[f='C', g->(1)]] Bindings: {?x: 'C'} [a=(1)[b=(2)[], c='C'], d->(2), e=[f='C', g->(1)]] >>> >>> fs1 = FeatStruct('[a=?x, b=?z, c=?z]') >>> fs2 = FeatStruct('[a=?y, b=?y, c=?q]') >>> #fs1.unify(fs2, trace=True) >>> .. >>> del fs1, fs2 # clean-up Unification on Dicts & Lists ---------------------------- It's possible to do unification on dictionaries: >>> # Note: pformat prints dicts sorted, but only guaranteed if width=1 >>> from pprint import pformat >>> from nltk.featstruct import unify >>> print pformat(unify(dict(x=1, y=dict(z=2)), dict(x=1, q=5)), width=1) {'q': 5, 'x': 1, 'y': {'z': 2}} It's possible to do unification on lists as well: >>> unify([1, 2, 3], [1, Variable('x'), 3]) [1, 2, 3] Mixing dicts and lists is fine: >>> print pformat(unify([dict(x=1, y=dict(z=2)),3], [dict(x=1, q=5),3]), ... width=1) [{'q': 5, 'x': 1, 'y': {'z': 2}}, 3] Mixing dicts and FeatStructs is discouraged: >>> unify(dict(x=1), FeatStruct(x=1)) Traceback (most recent call last): . . . ValueError: Mixing FeatStruct objects with Python dicts and lists is not supported. But you can do it if you really want, by explicitly stating that both dictionaries and FeatStructs should be treated as feature structures: >>> unify(dict(x=1), FeatStruct(x=1), fs_class=(dict, FeatStruct)) {'x': 1} Finding Conflicts ----------------- >>> from nltk.featstruct import conflicts >>> fs1 = FeatStruct('[a=[b=(1)[c=2], d->(1), e=[f->(1)]]]') >>> fs2 = FeatStruct('[a=[b=[c=[x=5]], d=[c=2], e=[f=[c=3]]]]') >>> for path in conflicts(fs1, fs2): ... print '%-8s: %r vs %r' % ('.'.join(path), fs1[path], fs2[path]) a.b.c : 2 vs [x=5] a.e.f.c : 2 vs 3 .. >>> del fs1, fs2 # clean-up Retracting Bindings ------------------- >>> from nltk.featstruct import retract_bindings >>> bindings = {} >>> fs1 = FeatStruct('[a=?x, b=[c=?y]]') >>> fs2 = FeatStruct('[a=(1)[c=[d=1]], b->(1)]') >>> fs3 = fs1.unify(fs2, bindings) >>> print fs3 [ a = (1) [ c = [ d = 1 ] ] ] [ ] [ b -> (1) ] >>> print bindings {Variable('?y'): [d=1], Variable('?x'): [c=[d=1]]} >>> retract_bindings(fs3, bindings) [a=?x, b=?x] >>> print bindings {Variable('?y'): [d=1], Variable('?x'): [c=?y]} Squashed Bugs ~~~~~~~~~~~~~ In svn rev 5167, unifying two feature structures that used the same variable would cause those variables to become aliased in the output. >>> fs1 = FeatStruct('[a=?x]') >>> fs2 = FeatStruct('[b=?x]') >>> fs1.unify(fs2) [a=?x, b=?x2] There was a bug in svn revision 5172 that caused `rename_variables` to rename variables to names that are already used. >>> FeatStruct('[a=?x, b=?x2]').rename_variables( ... vars=[Variable('?x')]) [a=?x3, b=?x2] >>> fs1 = FeatStruct('[a=?x]') >>> fs2 = FeatStruct('[a=?x, b=?x2]') >>> fs1.unify(fs2) [a=?x, b=?x2] There was a bug in svn rev 5167 that caused us to get the following example wrong. Basically the problem was that we only followed 'forward' pointers for other, not self, when unifying two feature structures. (nb: this test assumes that features are unified in alphabetical order -- if they are not, it might pass even if the bug is present.) >>> fs1 = FeatStruct('[a=[x=1], b=?x, c=?x]') >>> fs2 = FeatStruct('[a=(1)[], b->(1), c=[x=2]]') >>> print fs1.unify(fs2) None .. >>> del fs1, fs2 # clean-up nltk-2.0~b9/nltk/test/featgram.doctest0000644000175000017500000007250711331670207017675 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ========================= Feature Grammar Parsing ========================= .. include:: ../../doc/definitions.rst Grammars can be parsed from strings. >>> import nltk >>> from nltk import grammar, parse >>> g = """ ... % start DP ... DP[AGR=?a] -> D[AGR=?a] N[AGR=?a] ... D[AGR=[NUM='sg', PERS=3]] -> 'this' | 'that' ... D[AGR=[NUM='pl', PERS=3]] -> 'these' | 'those' ... D[AGR=[NUM='pl', PERS=1]] -> 'we' ... D[AGR=[PERS=2]] -> 'you' ... N[AGR=[NUM='sg', GND='m']] -> 'boy' ... N[AGR=[NUM='pl', GND='m']] -> 'boys' ... N[AGR=[NUM='sg', GND='f']] -> 'girl' ... N[AGR=[NUM='pl', GND='f']] -> 'girls' ... N[AGR=[NUM='sg']] -> 'student' ... N[AGR=[NUM='pl']] -> 'students' ... """ >>> grammar = grammar.parse_fcfg(g) >>> tokens = 'these girls'.split() >>> parser = parse.FeatureEarleyChartParser(grammar) >>> trees = parser.nbest_parse(tokens) >>> for tree in trees: print tree (DP[AGR=[GND='f', NUM='pl', PERS=3]] (D[AGR=[NUM='pl', PERS=3]] these) (N[AGR=[GND='f', NUM='pl']] girls)) In general, when we are trying to develop even a very small grammar, it is convenient to put the rules in a file where they can be edited, tested and revised. Let's assume that we have saved feat0cfg_ as a file named ``'feat0.fcfg'`` and placed it in the NLTK ``data`` directory. We can inspect it as follows: .. _feat0cfg: http://nltk.svn.sourceforge.net/svnroot/nltk/trunk/nltk/data/grammars/feat0.fcfg >>> nltk.data.show_cfg('grammars/book_grammars/feat0.fcfg') % start S # ################### # Grammar Productions # ################### # S expansion productions S -> NP[NUM=?n] VP[NUM=?n] # NP expansion productions NP[NUM=?n] -> N[NUM=?n] NP[NUM=?n] -> PropN[NUM=?n] NP[NUM=?n] -> Det[NUM=?n] N[NUM=?n] NP[NUM=pl] -> N[NUM=pl] # VP expansion productions VP[TENSE=?t, NUM=?n] -> IV[TENSE=?t, NUM=?n] VP[TENSE=?t, NUM=?n] -> TV[TENSE=?t, NUM=?n] NP # ################### # Lexical Productions # ################### Det[NUM=sg] -> 'this' | 'every' Det[NUM=pl] -> 'these' | 'all' Det -> 'the' | 'some' | 'several' PropN[NUM=sg]-> 'Kim' | 'Jody' N[NUM=sg] -> 'dog' | 'girl' | 'car' | 'child' N[NUM=pl] -> 'dogs' | 'girls' | 'cars' | 'children' IV[TENSE=pres, NUM=sg] -> 'disappears' | 'walks' TV[TENSE=pres, NUM=sg] -> 'sees' | 'likes' IV[TENSE=pres, NUM=pl] -> 'disappear' | 'walk' TV[TENSE=pres, NUM=pl] -> 'see' | 'like' IV[TENSE=past] -> 'disappeared' | 'walked' TV[TENSE=past] -> 'saw' | 'liked' Assuming we have saved feat0cfg_ as a file named ``'feat0.fcfg'``, the function ``parse.load_parser`` allows us to read the grammar into NLTK, ready for use in parsing. >>> cp = parse.load_parser('grammars/book_grammars/feat0.fcfg', trace=1) >>> sent = 'Kim likes children' >>> tokens = sent.split() >>> tokens ['Kim', 'likes', 'children'] >>> trees = cp.nbest_parse(tokens) |.Kim .like.chil.| |[----] . .| [0:1] 'Kim' |. [----] .| [1:2] 'likes' |. . [----]| [2:3] 'children' |[----] . .| [0:1] PropN[NUM='sg'] -> 'Kim' * |[----] . .| [0:1] NP[NUM='sg'] -> PropN[NUM='sg'] * |[----> . .| [0:1] S[] -> NP[NUM=?n] * VP[NUM=?n] {?n: 'sg'} |. [----] .| [1:2] TV[NUM='sg', TENSE='pres'] -> 'likes' * |. [----> .| [1:2] VP[NUM=?n, TENSE=?t] -> TV[NUM=?n, TENSE=?t] * NP[] {?n: 'sg', ?t: 'pres'} |. . [----]| [2:3] N[NUM='pl'] -> 'children' * |. . [----]| [2:3] NP[NUM='pl'] -> N[NUM='pl'] * |. . [---->| [2:3] S[] -> NP[NUM=?n] * VP[NUM=?n] {?n: 'pl'} |. [---------]| [1:3] VP[NUM='sg', TENSE='pres'] -> TV[NUM='sg', TENSE='pres'] NP[] * |[==============]| [0:3] S[] -> NP[NUM='sg'] VP[NUM='sg'] * >>> for tree in trees: print tree (S[] (NP[NUM='sg'] (PropN[NUM='sg'] Kim)) (VP[NUM='sg', TENSE='pres'] (TV[NUM='sg', TENSE='pres'] likes) (NP[NUM='pl'] (N[NUM='pl'] children)))) The parser works directly with the underspecified productions given by the grammar. That is, the Predictor rule does not attempt to compile out all admissible feature combinations before trying to expand the non-terminals on the left hand side of a production. However, when the Scanner matches an input word against a lexical production that has been predicted, the new edge will typically contain fully specified features; e.g., the edge [PropN[`num`:feat: = `sg`:fval:] |rarr| 'Kim', (0, 1)]. Recall from Chapter 8 that the Fundamental (or Completer) Rule in standard CFGs is used to combine an incomplete edge that's expecting a nonterminal *B* with a following, complete edge whose left hand side matches *B*. In our current setting, rather than checking for a complete match, we test whether the expected category *B* will `unify`:dt: with the left hand side *B'* of a following complete edge. We will explain in more detail in Section 9.2 how unification works; for the moment, it is enough to know that as a result of unification, any variable values of features in *B* will be instantiated by constant values in the corresponding feature structure in *B'*, and these instantiated values will be used in the new edge added by the Completer. This instantiation can be seen, for example, in the edge [NP [`num`:feat:\ =\ `sg`:fval:] |rarr| PropN[`num`:feat:\ =\ `sg`:fval:] |dot|, (0, 1)] in Example 9.2, where the feature `num`:feat: has been assigned the value `sg`:fval:. Feature structures in NLTK are ... Atomic feature values can be strings or integers. >>> fs1 = nltk.FeatStruct(TENSE='past', NUM='sg') >>> print fs1 [ NUM = 'sg' ] [ TENSE = 'past' ] We can think of a feature structure as being like a Python dictionary, and access its values by indexing in the usual way. >>> fs1 = nltk.FeatStruct(PER=3, NUM='pl', GND='fem') >>> print fs1['GND'] fem We can also define feature structures which have complex values, as discussed earlier. >>> fs2 = nltk.FeatStruct(POS='N', AGR=fs1) >>> print fs2 [ [ GND = 'fem' ] ] [ AGR = [ NUM = 'pl' ] ] [ [ PER = 3 ] ] [ ] [ POS = 'N' ] >>> print fs2['AGR'] [ GND = 'fem' ] [ NUM = 'pl' ] [ PER = 3 ] >>> print fs2['AGR']['PER'] 3 Feature structures can also be constructed using the ``parse()`` method of the ``nltk.FeatStruct`` class. Note that in this case, atomic feature values do not need to be enclosed in quotes. >>> f1 = nltk.FeatStruct("[NUMBER = sg]") >>> f2 = nltk.FeatStruct("[PERSON = 3]") >>> print nltk.unify(f1, f2) [ NUMBER = 'sg' ] [ PERSON = 3 ] >>> f1 = nltk.FeatStruct("[A = [B = b, D = d]]") >>> f2 = nltk.FeatStruct("[A = [C = c, D = d]]") >>> print nltk.unify(f1, f2) [ [ B = 'b' ] ] [ A = [ C = 'c' ] ] [ [ D = 'd' ] ] Feature Structures as Graphs ---------------------------- Feature structures are not inherently tied to linguistic objects; they are general purpose structures for representing knowledge. For example, we could encode information about a person in a feature structure: >>> person01 = nltk.FeatStruct("[NAME=Lee, TELNO='01 27 86 42 96',AGE=33]") >>> print person01 [ AGE = 33 ] [ NAME = 'Lee' ] [ TELNO = '01 27 86 42 96' ] There are a number of notations for representing reentrancy in matrix-style representations of feature structures. In NLTK, we adopt the following convention: the first occurrence of a shared feature structure is prefixed with an integer in parentheses, such as ``(1)``, and any subsequent reference to that structure uses the notation ``->(1)``, as shown below. >>> fs = nltk.FeatStruct("""[NAME=Lee, ADDRESS=(1)[NUMBER=74, STREET='rue Pascal'], ... SPOUSE=[NAME=Kim, ADDRESS->(1)]]""") >>> print fs [ ADDRESS = (1) [ NUMBER = 74 ] ] [ [ STREET = 'rue Pascal' ] ] [ ] [ NAME = 'Lee' ] [ ] [ SPOUSE = [ ADDRESS -> (1) ] ] [ [ NAME = 'Kim' ] ] There can be any number of tags within a single feature structure. >>> fs3 = nltk.FeatStruct("[A=(1)[B=b], C=(2)[], D->(1), E->(2)]") >>> print fs3 [ A = (1) [ B = 'b' ] ] [ ] [ C = (2) [] ] [ ] [ D -> (1) ] [ E -> (2) ] >>> fs1 = nltk.FeatStruct(NUMBER=74, STREET='rue Pascal') >>> fs2 = nltk.FeatStruct(CITY='Paris') >>> print nltk.unify(fs1, fs2) [ CITY = 'Paris' ] [ NUMBER = 74 ] [ STREET = 'rue Pascal' ] Unification is symmetric: >>> nltk.unify(fs1, fs2) == nltk.unify(fs2, fs1) True Unification is commutative: >>> fs3 = nltk.FeatStruct(TELNO='01 27 86 42 96') >>> nltk.unify(nltk.unify(fs1, fs2), fs3) == nltk.unify(fs1, nltk.unify(fs2, fs3)) True Unification between `FS`:math:\ :subscript:`0` and `FS`:math:\ :subscript:`1` will fail if the two feature structures share a path |pi|, but the value of |pi| in `FS`:math:\ :subscript:`0` is a distinct atom from the value of |pi| in `FS`:math:\ :subscript:`1`. In NLTK, this is implemented by setting the result of unification to be ``None``. >>> fs0 = nltk.FeatStruct(A='a') >>> fs1 = nltk.FeatStruct(A='b') >>> print nltk.unify(fs0, fs1) None Now, if we look at how unification interacts with structure-sharing, things become really interesting. >>> fs0 = nltk.FeatStruct("""[NAME=Lee, ... ADDRESS=[NUMBER=74, ... STREET='rue Pascal'], ... SPOUSE= [NAME=Kim, ... ADDRESS=[NUMBER=74, ... STREET='rue Pascal']]]""") >>> print fs0 [ ADDRESS = [ NUMBER = 74 ] ] [ [ STREET = 'rue Pascal' ] ] [ ] [ NAME = 'Lee' ] [ ] [ [ ADDRESS = [ NUMBER = 74 ] ] ] [ SPOUSE = [ [ STREET = 'rue Pascal' ] ] ] [ [ ] ] [ [ NAME = 'Kim' ] ] >>> fs1 = nltk.FeatStruct("[SPOUSE=[ADDRESS=[CITY=Paris]]]") >>> print nltk.unify(fs0, fs1) [ ADDRESS = [ NUMBER = 74 ] ] [ [ STREET = 'rue Pascal' ] ] [ ] [ NAME = 'Lee' ] [ ] [ [ [ CITY = 'Paris' ] ] ] [ [ ADDRESS = [ NUMBER = 74 ] ] ] [ SPOUSE = [ [ STREET = 'rue Pascal' ] ] ] [ [ ] ] [ [ NAME = 'Kim' ] ] >>> fs2 = nltk.FeatStruct("""[NAME=Lee, ADDRESS=(1)[NUMBER=74, STREET='rue Pascal'], ... SPOUSE=[NAME=Kim, ADDRESS->(1)]]""") >>> print fs2 [ ADDRESS = (1) [ NUMBER = 74 ] ] [ [ STREET = 'rue Pascal' ] ] [ ] [ NAME = 'Lee' ] [ ] [ SPOUSE = [ ADDRESS -> (1) ] ] [ [ NAME = 'Kim' ] ] >>> print nltk.unify(fs2, fs1) [ [ CITY = 'Paris' ] ] [ ADDRESS = (1) [ NUMBER = 74 ] ] [ [ STREET = 'rue Pascal' ] ] [ ] [ NAME = 'Lee' ] [ ] [ SPOUSE = [ ADDRESS -> (1) ] ] [ [ NAME = 'Kim' ] ] >>> fs1 = nltk.FeatStruct("[ADDRESS1=[NUMBER=74, STREET='rue Pascal']]") >>> fs2 = nltk.FeatStruct("[ADDRESS1=?x, ADDRESS2=?x]") >>> print fs2 [ ADDRESS1 = ?x ] [ ADDRESS2 = ?x ] >>> print nltk.unify(fs1, fs2) [ ADDRESS1 = (1) [ NUMBER = 74 ] ] [ [ STREET = 'rue Pascal' ] ] [ ] [ ADDRESS2 -> (1) ] >>> sent = 'who do you claim that you like' >>> tokens = sent.split() >>> cp = parse.load_parser('grammars/book_grammars/feat1.fcfg', trace=1) >>> trees = cp.nbest_parse(tokens) |.w.d.y.c.t.y.l.| |[-] . . . . . .| [0:1] 'who' |. [-] . . . . .| [1:2] 'do' |. . [-] . . . .| [2:3] 'you' |. . . [-] . . .| [3:4] 'claim' |. . . . [-] . .| [4:5] 'that' |. . . . . [-] .| [5:6] 'you' |. . . . . . [-]| [6:7] 'like' |# . . . . . . .| [0:0] NP[]/NP[] -> * |. # . . . . . .| [1:1] NP[]/NP[] -> * |. . # . . . . .| [2:2] NP[]/NP[] -> * |. . . # . . . .| [3:3] NP[]/NP[] -> * |. . . . # . . .| [4:4] NP[]/NP[] -> * |. . . . . # . .| [5:5] NP[]/NP[] -> * |. . . . . . # .| [6:6] NP[]/NP[] -> * |. . . . . . . #| [7:7] NP[]/NP[] -> * |[-] . . . . . .| [0:1] NP[+WH] -> 'who' * |[-> . . . . . .| [0:1] S[-INV] -> NP[] * VP[] {} |[-> . . . . . .| [0:1] S[-INV]/?x[] -> NP[] * VP[]/?x[] {} |[-> . . . . . .| [0:1] S[-INV] -> NP[] * S[]/NP[] {} |. [-] . . . . .| [1:2] V[+AUX] -> 'do' * |. [-> . . . . .| [1:2] S[+INV] -> V[+AUX] * NP[] VP[] {} |. [-> . . . . .| [1:2] S[+INV]/?x[] -> V[+AUX] * NP[] VP[]/?x[] {} |. [-> . . . . .| [1:2] VP[] -> V[+AUX] * VP[] {} |. [-> . . . . .| [1:2] VP[]/?x[] -> V[+AUX] * VP[]/?x[] {} |. . [-] . . . .| [2:3] NP[-WH] -> 'you' * |. . [-> . . . .| [2:3] S[-INV] -> NP[] * VP[] {} |. . [-> . . . .| [2:3] S[-INV]/?x[] -> NP[] * VP[]/?x[] {} |. . [-> . . . .| [2:3] S[-INV] -> NP[] * S[]/NP[] {} |. [---> . . . .| [1:3] S[+INV] -> V[+AUX] NP[] * VP[] {} |. [---> . . . .| [1:3] S[+INV]/?x[] -> V[+AUX] NP[] * VP[]/?x[] {} |. . . [-] . . .| [3:4] V[-AUX, SUBCAT='clause'] -> 'claim' * |. . . [-> . . .| [3:4] VP[] -> V[-AUX, SUBCAT='clause'] * SBar[] {} |. . . [-> . . .| [3:4] VP[]/?x[] -> V[-AUX, SUBCAT='clause'] * SBar[]/?x[] {} |. . . . [-] . .| [4:5] Comp[] -> 'that' * |. . . . [-> . .| [4:5] SBar[] -> Comp[] * S[-INV] {} |. . . . [-> . .| [4:5] SBar[]/?x[] -> Comp[] * S[-INV]/?x[] {} |. . . . . [-] .| [5:6] NP[-WH] -> 'you' * |. . . . . [-> .| [5:6] S[-INV] -> NP[] * VP[] {} |. . . . . [-> .| [5:6] S[-INV]/?x[] -> NP[] * VP[]/?x[] {} |. . . . . [-> .| [5:6] S[-INV] -> NP[] * S[]/NP[] {} |. . . . . . [-]| [6:7] V[-AUX, SUBCAT='trans'] -> 'like' * |. . . . . . [->| [6:7] VP[] -> V[-AUX, SUBCAT='trans'] * NP[] {} |. . . . . . [->| [6:7] VP[]/?x[] -> V[-AUX, SUBCAT='trans'] * NP[]/?x[] {} |. . . . . . [-]| [6:7] VP[]/NP[] -> V[-AUX, SUBCAT='trans'] NP[]/NP[] * |. . . . . [---]| [5:7] S[-INV]/NP[] -> NP[] VP[]/NP[] * |. . . . [-----]| [4:7] SBar[]/NP[] -> Comp[] S[-INV]/NP[] * |. . . [-------]| [3:7] VP[]/NP[] -> V[-AUX, SUBCAT='clause'] SBar[]/NP[] * |. . [---------]| [2:7] S[-INV]/NP[] -> NP[] VP[]/NP[] * |. [-----------]| [1:7] S[+INV]/NP[] -> V[+AUX] NP[] VP[]/NP[] * |[=============]| [0:7] S[-INV] -> NP[] S[]/NP[] * >>> for tree in trees: print tree (S[-INV] (NP[+WH] who) (S[+INV]/NP[] (V[+AUX] do) (NP[-WH] you) (VP[]/NP[] (V[-AUX, SUBCAT='clause'] claim) (SBar[]/NP[] (Comp[] that) (S[-INV]/NP[] (NP[-WH] you) (VP[]/NP[] (V[-AUX, SUBCAT='trans'] like) (NP[]/NP[] ))))))) A different parser should give the same parse trees, but perhaps in a different order: >>> cp2 = parse.load_parser('grammars/book_grammars/feat1.fcfg', trace=1, ... parser=parse.FeatureEarleyChartParser) >>> trees2 = cp2.nbest_parse(tokens) |.w.d.y.c.t.y.l.| |[-] . . . . . .| [0:1] 'who' |. [-] . . . . .| [1:2] 'do' |. . [-] . . . .| [2:3] 'you' |. . . [-] . . .| [3:4] 'claim' |. . . . [-] . .| [4:5] 'that' |. . . . . [-] .| [5:6] 'you' |. . . . . . [-]| [6:7] 'like' |> . . . . . . .| [0:0] [INIT][] -> * S[] {} |> . . . . . . .| [0:0] S[-INV] -> * NP[] VP[] {} |> . . . . . . .| [0:0] S[-INV] -> * NP[] S[]/NP[] {} |> . . . . . . .| [0:0] S[-INV] -> * Adv[+NEG] S[+INV] {} |> . . . . . . .| [0:0] S[+INV] -> * V[+AUX] NP[] VP[] {} |> . . . . . . .| [0:0] V[+AUX] -> * 'do' {} |> . . . . . . .| [0:0] V[+AUX] -> * 'can' {} |> . . . . . . .| [0:0] Adv[+NEG] -> * 'rarely' {} |> . . . . . . .| [0:0] Adv[+NEG] -> * 'never' {} |> . . . . . . .| [0:0] NP[-WH] -> * 'you' {} |> . . . . . . .| [0:0] NP[-WH] -> * 'cats' {} |> . . . . . . .| [0:0] NP[+WH] -> * 'who' {} |[-] . . . . . .| [0:1] NP[+WH] -> 'who' * |[-> . . . . . .| [0:1] S[-INV] -> NP[] * VP[] {} |[-> . . . . . .| [0:1] S[-INV] -> NP[] * S[]/NP[] {} |. > . . . . . .| [1:1] S[-INV]/?x[] -> * NP[] VP[]/?x[] {} |. > . . . . . .| [1:1] S[+INV]/?x[] -> * V[+AUX] NP[] VP[]/?x[] {} |. > . . . . . .| [1:1] V[+AUX] -> * 'do' {} |. > . . . . . .| [1:1] V[+AUX] -> * 'can' {} |. > . . . . . .| [1:1] NP[-WH] -> * 'you' {} |. > . . . . . .| [1:1] NP[-WH] -> * 'cats' {} |. > . . . . . .| [1:1] NP[+WH] -> * 'who' {} |. > . . . . . .| [1:1] VP[] -> * V[-AUX, SUBCAT='intrans'] {} |. > . . . . . .| [1:1] VP[] -> * V[-AUX, SUBCAT='trans'] NP[] {} |. > . . . . . .| [1:1] VP[] -> * V[-AUX, SUBCAT='clause'] SBar[] {} |. > . . . . . .| [1:1] VP[] -> * V[+AUX] VP[] {} |. > . . . . . .| [1:1] V[-AUX, SUBCAT='clause'] -> * 'say' {} |. > . . . . . .| [1:1] V[-AUX, SUBCAT='clause'] -> * 'claim' {} |. > . . . . . .| [1:1] V[-AUX, SUBCAT='trans'] -> * 'see' {} |. > . . . . . .| [1:1] V[-AUX, SUBCAT='trans'] -> * 'like' {} |. > . . . . . .| [1:1] V[-AUX, SUBCAT='intrans'] -> * 'walk' {} |. > . . . . . .| [1:1] V[-AUX, SUBCAT='intrans'] -> * 'sing' {} |. [-] . . . . .| [1:2] V[+AUX] -> 'do' * |. [-> . . . . .| [1:2] S[+INV]/?x[] -> V[+AUX] * NP[] VP[]/?x[] {} |. [-> . . . . .| [1:2] VP[] -> V[+AUX] * VP[] {} |. . > . . . . .| [2:2] VP[] -> * V[-AUX, SUBCAT='intrans'] {} |. . > . . . . .| [2:2] VP[] -> * V[-AUX, SUBCAT='trans'] NP[] {} |. . > . . . . .| [2:2] VP[] -> * V[-AUX, SUBCAT='clause'] SBar[] {} |. . > . . . . .| [2:2] VP[] -> * V[+AUX] VP[] {} |. . > . . . . .| [2:2] V[+AUX] -> * 'do' {} |. . > . . . . .| [2:2] V[+AUX] -> * 'can' {} |. . > . . . . .| [2:2] V[-AUX, SUBCAT='clause'] -> * 'say' {} |. . > . . . . .| [2:2] V[-AUX, SUBCAT='clause'] -> * 'claim' {} |. . > . . . . .| [2:2] V[-AUX, SUBCAT='trans'] -> * 'see' {} |. . > . . . . .| [2:2] V[-AUX, SUBCAT='trans'] -> * 'like' {} |. . > . . . . .| [2:2] V[-AUX, SUBCAT='intrans'] -> * 'walk' {} |. . > . . . . .| [2:2] V[-AUX, SUBCAT='intrans'] -> * 'sing' {} |. . > . . . . .| [2:2] NP[-WH] -> * 'you' {} |. . > . . . . .| [2:2] NP[-WH] -> * 'cats' {} |. . > . . . . .| [2:2] NP[+WH] -> * 'who' {} |. . [-] . . . .| [2:3] NP[-WH] -> 'you' * |. [---> . . . .| [1:3] S[+INV]/?x[] -> V[+AUX] NP[] * VP[]/?x[] {} |. . . > . . . .| [3:3] VP[]/?x[] -> * V[-AUX, SUBCAT='trans'] NP[]/?x[] {} |. . . > . . . .| [3:3] VP[]/?x[] -> * V[-AUX, SUBCAT='clause'] SBar[]/?x[] {} |. . . > . . . .| [3:3] VP[]/?x[] -> * V[+AUX] VP[]/?x[] {} |. . . > . . . .| [3:3] V[+AUX] -> * 'do' {} |. . . > . . . .| [3:3] V[+AUX] -> * 'can' {} |. . . > . . . .| [3:3] V[-AUX, SUBCAT='clause'] -> * 'say' {} |. . . > . . . .| [3:3] V[-AUX, SUBCAT='clause'] -> * 'claim' {} |. . . > . . . .| [3:3] V[-AUX, SUBCAT='trans'] -> * 'see' {} |. . . > . . . .| [3:3] V[-AUX, SUBCAT='trans'] -> * 'like' {} |. . . [-] . . .| [3:4] V[-AUX, SUBCAT='clause'] -> 'claim' * |. . . [-> . . .| [3:4] VP[]/?x[] -> V[-AUX, SUBCAT='clause'] * SBar[]/?x[] {} |. . . . > . . .| [4:4] SBar[]/?x[] -> * Comp[] S[-INV]/?x[] {} |. . . . > . . .| [4:4] Comp[] -> * 'that' {} |. . . . [-] . .| [4:5] Comp[] -> 'that' * |. . . . [-> . .| [4:5] SBar[]/?x[] -> Comp[] * S[-INV]/?x[] {} |. . . . . > . .| [5:5] S[-INV]/?x[] -> * NP[] VP[]/?x[] {} |. . . . . > . .| [5:5] NP[-WH] -> * 'you' {} |. . . . . > . .| [5:5] NP[-WH] -> * 'cats' {} |. . . . . > . .| [5:5] NP[+WH] -> * 'who' {} |. . . . . [-] .| [5:6] NP[-WH] -> 'you' * |. . . . . [-> .| [5:6] S[-INV]/?x[] -> NP[] * VP[]/?x[] {} |. . . . . . > .| [6:6] VP[]/?x[] -> * V[-AUX, SUBCAT='trans'] NP[]/?x[] {} |. . . . . . > .| [6:6] VP[]/?x[] -> * V[-AUX, SUBCAT='clause'] SBar[]/?x[] {} |. . . . . . > .| [6:6] VP[]/?x[] -> * V[+AUX] VP[]/?x[] {} |. . . . . . > .| [6:6] V[+AUX] -> * 'do' {} |. . . . . . > .| [6:6] V[+AUX] -> * 'can' {} |. . . . . . > .| [6:6] V[-AUX, SUBCAT='clause'] -> * 'say' {} |. . . . . . > .| [6:6] V[-AUX, SUBCAT='clause'] -> * 'claim' {} |. . . . . . > .| [6:6] V[-AUX, SUBCAT='trans'] -> * 'see' {} |. . . . . . > .| [6:6] V[-AUX, SUBCAT='trans'] -> * 'like' {} |. . . . . . [-]| [6:7] V[-AUX, SUBCAT='trans'] -> 'like' * |. . . . . . [->| [6:7] VP[]/?x[] -> V[-AUX, SUBCAT='trans'] * NP[]/?x[] {} |. . . . . . . #| [7:7] NP[]/NP[] -> * |. . . . . . [-]| [6:7] VP[]/NP[] -> V[-AUX, SUBCAT='trans'] NP[]/NP[] * |. . . . . [---]| [5:7] S[-INV]/NP[] -> NP[] VP[]/NP[] * |. . . . [-----]| [4:7] SBar[]/NP[] -> Comp[] S[-INV]/NP[] * |. . . [-------]| [3:7] VP[]/NP[] -> V[-AUX, SUBCAT='clause'] SBar[]/NP[] * |. [-----------]| [1:7] S[+INV]/NP[] -> V[+AUX] NP[] VP[]/NP[] * |[=============]| [0:7] S[-INV] -> NP[] S[]/NP[] * |[=============]| [0:7] [INIT][] -> S[] * >>> sorted(trees) == sorted(trees2) True Let's load a German grammar: >>> cp = parse.load_parser('grammars/book_grammars/german.fcfg', trace=0) >>> sent = 'die Katze sieht den Hund' >>> tokens = sent.split() >>> trees = cp.nbest_parse(tokens) >>> for tree in trees: print tree (S[] (NP[AGR=[GND='fem', NUM='sg', PER=3], CASE='nom'] (Det[AGR=[GND='fem', NUM='sg', PER=3], CASE='nom'] die) (N[AGR=[GND='fem', NUM='sg', PER=3]] Katze)) (VP[AGR=[NUM='sg', PER=3]] (TV[AGR=[NUM='sg', PER=3], OBJCASE='acc'] sieht) (NP[AGR=[GND='masc', NUM='sg', PER=3], CASE='acc'] (Det[AGR=[GND='masc', NUM='sg', PER=3], CASE='acc'] den) (N[AGR=[GND='masc', NUM='sg', PER=3]] Hund)))) Grammar with Binding Operators ------------------------------ The `bindop.fcfg`_ grammar is a semantic grammar that uses lambda calculus. Each element has a core semantics, which is a single lambda calculus expression; and a set of binding operators, which bind variables. .. _bindop.fcfg: http://nltk.svn.sourceforge.net/svnroot/nltk/trunk/nltk/data/grammars/bindop.fcfg In order to make the binding operators work right, they need to instantiate their bound variable every time they are added to the chart. To do this, we use a special subclass of `Chart`, called `InstantiateVarsChart`. >>> from nltk.parse.featurechart import InstantiateVarsChart >>> cp = parse.load_parser('grammars/sample_grammars/bindop.fcfg', trace=1, ... chart_class=InstantiateVarsChart) >>> print cp.grammar() Grammar with 15 productions (start state = S[]) S[SEM=[BO={?b1+?b2}, CORE=]] -> NP[SEM=[BO=?b1, CORE=?subj]] VP[SEM=[BO=?b2, CORE=?vp]] VP[SEM=[BO={?b1+?b2}, CORE=]] -> TV[SEM=[BO=?b1, CORE=?v]] NP[SEM=[BO=?b2, CORE=?obj]] VP[SEM=?s] -> IV[SEM=?s] NP[SEM=[BO={?b1+?b2+{bo(?det(?n),@x)}}, CORE=<@x>]] -> Det[SEM=[BO=?b1, CORE=?det]] N[SEM=[BO=?b2, CORE=?n]] Det[SEM=[BO={/}, CORE=<\Q P.exists x.(Q(x) & P(x))>]] -> 'a' N[SEM=[BO={/}, CORE=]] -> 'dog' N[SEM=[BO={/}, CORE=]] -> 'cat' N[SEM=[BO={/}, CORE=]] -> 'mouse' IV[SEM=[BO={/}, CORE=<\x.bark(x)>]] -> 'barks' IV[SEM=[BO={/}, CORE=<\x.bark(x)>]] -> 'eats' IV[SEM=[BO={/}, CORE=<\x.bark(x)>]] -> 'walks' TV[SEM=[BO={/}, CORE=<\x y.feed(y,x)>]] -> 'feeds' TV[SEM=[BO={/}, CORE=<\x y.feed(y,x)>]] -> 'walks' NP[SEM=[BO={bo(\P.P(John),@x)}, CORE=<@x>]] -> 'john' NP[SEM=[BO={bo(\P.P(John),@x)}, CORE=<@x>]] -> 'alex' A simple intransitive sentence: >>> from nltk.sem import logic >>> logic._counter._value = 100 >>> trees = cp.nbest_parse('john barks'.split()) |. john.barks.| |[-----] .| [0:1] 'john' |. [-----]| [1:2] 'barks' |[-----] .| [0:1] NP[SEM=[BO={bo(\P.P(John),z101)}, CORE=]] -> 'john' * |[-----> .| [0:1] S[SEM=[BO={?b1+?b2}, CORE=]] -> NP[SEM=[BO=?b1, CORE=?subj]] * VP[SEM=[BO=?b2, CORE=?vp]] {?b1: {bo(\P.P(John),z101)}, ?subj: } |. [-----]| [1:2] IV[SEM=[BO={/}, CORE=<\x.bark(x)>]] -> 'barks' * |. [-----]| [1:2] VP[SEM=[BO={/}, CORE=<\x.bark(x)>]] -> IV[SEM=[BO={/}, CORE=<\x.bark(x)>]] * |[===========]| [0:2] S[SEM=[BO={bo(\P.P(John),z101)}, CORE=]] -> NP[SEM=[BO={bo(\P.P(John),z101)}, CORE=]] VP[SEM=[BO={/}, CORE=<\x.bark(x)>]] * >>> for tree in trees: print tree (S[SEM=[BO={bo(\P.P(John),z101)}, CORE=]] (NP[SEM=[BO={bo(\P.P(John),z101)}, CORE=]] john) (VP[SEM=[BO={/}, CORE=<\x.bark(x)>]] (IV[SEM=[BO={/}, CORE=<\x.bark(x)>]] barks))) A transitive sentence: >>> trees = cp.nbest_parse('john feeds a dog'.split()) |.joh.fee. a .dog.| |[---] . . .| [0:1] 'john' |. [---] . .| [1:2] 'feeds' |. . [---] .| [2:3] 'a' |. . . [---]| [3:4] 'dog' |[---] . . .| [0:1] NP[SEM=[BO={bo(\P.P(John),z102)}, CORE=]] -> 'john' * |[---> . . .| [0:1] S[SEM=[BO={?b1+?b2}, CORE=]] -> NP[SEM=[BO=?b1, CORE=?subj]] * VP[SEM=[BO=?b2, CORE=?vp]] {?b1: {bo(\P.P(John),z102)}, ?subj: } |. [---] . .| [1:2] TV[SEM=[BO={/}, CORE=<\x y.feed(y,x)>]] -> 'feeds' * |. [---> . .| [1:2] VP[SEM=[BO={?b1+?b2}, CORE=]] -> TV[SEM=[BO=?b1, CORE=?v]] * NP[SEM=[BO=?b2, CORE=?obj]] {?b1: {/}, ?v: } |. . [---] .| [2:3] Det[SEM=[BO={/}, CORE=<\Q P.exists x.(Q(x) & P(x))>]] -> 'a' * |. . [---> .| [2:3] NP[SEM=[BO={?b1+?b2+{bo(?det(?n),@x)}}, CORE=<@x>]] -> Det[SEM=[BO=?b1, CORE=?det]] * N[SEM=[BO=?b2, CORE=?n]] {?b1: {/}, ?det: } |. . . [---]| [3:4] N[SEM=[BO={/}, CORE=]] -> 'dog' * |. . [-------]| [2:4] NP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z103)}, CORE=]] -> Det[SEM=[BO={/}, CORE=<\Q P.exists x.(Q(x) & P(x))>]] N[SEM=[BO={/}, CORE=]] * |. . [------->| [2:4] S[SEM=[BO={?b1+?b2}, CORE=]] -> NP[SEM=[BO=?b1, CORE=?subj]] * VP[SEM=[BO=?b2, CORE=?vp]] {?b1: {bo(\P.exists x.(dog(x) & P(x)),z103)}, ?subj: } |. [-----------]| [1:4] VP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z103)}, CORE=<\y.feed(y,z103)>]] -> TV[SEM=[BO={/}, CORE=<\x y.feed(y,x)>]] NP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z103)}, CORE=]] * |[===============]| [0:4] S[SEM=[BO={bo(\P.P(John),z102), bo(\P.exists x.(dog(x) & P(x)),z103)}, CORE=]] -> NP[SEM=[BO={bo(\P.P(John),z102)}, CORE=]] VP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z103)}, CORE=<\y.feed(y,z103)>]] * >>> for tree in trees: print tree (S[SEM=[BO={bo(\P.P(John),z102), bo(\P.exists x.(dog(x) & P(x)),z103)}, CORE=]] (NP[SEM=[BO={bo(\P.P(John),z102)}, CORE=]] john) (VP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z103)}, CORE=<\y.feed(y,z103)>]] (TV[SEM=[BO={/}, CORE=<\x y.feed(y,x)>]] feeds) (NP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z103)}, CORE=]] (Det[SEM=[BO={/}, CORE=<\Q P.exists x.(Q(x) & P(x))>]] a) (N[SEM=[BO={/}, CORE=]] dog)))) Turn down the verbosity: >>> cp = parse.load_earley('grammars/sample_grammars/bindop.fcfg', trace=0, ... chart_class=InstantiateVarsChart) Reuse the same lexical item twice: >>> trees = cp.nbest_parse('john feeds john'.split()) >>> for tree in trees: print tree (S[SEM=[BO={bo(\P.P(John),z104), bo(\P.P(John),z105)}, CORE=]] (NP[SEM=[BO={bo(\P.P(John),z104)}, CORE=]] john) (VP[SEM=[BO={bo(\P.P(John),z105)}, CORE=<\y.feed(y,z105)>]] (TV[SEM=[BO={/}, CORE=<\x y.feed(y,x)>]] feeds) (NP[SEM=[BO={bo(\P.P(John),z105)}, CORE=]] john))) >>> trees = cp.nbest_parse('a dog feeds a dog'.split()) >>> for tree in trees: print tree (S[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z106), bo(\P.exists x.(dog(x) & P(x)),z107)}, CORE=]] (NP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z106)}, CORE=]] (Det[SEM=[BO={/}, CORE=<\Q P.exists x.(Q(x) & P(x))>]] a) (N[SEM=[BO={/}, CORE=]] dog)) (VP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z107)}, CORE=<\y.feed(y,z107)>]] (TV[SEM=[BO={/}, CORE=<\x y.feed(y,x)>]] feeds) (NP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z107)}, CORE=]] (Det[SEM=[BO={/}, CORE=<\Q P.exists x.(Q(x) & P(x))>]] a) (N[SEM=[BO={/}, CORE=]] dog)))) nltk-2.0~b9/nltk/test/drt.doctest0000644000175000017500000003250411377057401016676 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ================================ Discourse Representation Theory ================================ >>> from nltk.sem import logic >>> from nltk.inference import TableauProver Overview ======== A DRS can be created with the ``DRS()`` constructor. This takes two arguments: a list of discourse referents and list of conditions. . >>> from nltk.sem.drt import * >>> dp = DrtParser() >>> man_x = dp.parse('man(x)') >>> walk_x = dp.parse('walk(x)') >>> x = dp.parse('x') >>> print DRS([x], [man_x, walk_x]) ([x],[man(x), walk(x)]) The ``parse()`` method can also be applied directly to DRS expressions, which allows them to be specified more easily. >>> drs1 = dp.parse('([x],[man(x),walk(x)])') >>> print drs1 ([x],[man(x), walk(x)]) DRSs can be *merged* using the ``+`` operator. >>> drs2 = dp.parse('([y],[woman(y),stop(y)])') >>> drs3 = drs1 + drs2 >>> print drs3 (([x],[man(x), walk(x)]) + ([y],[woman(y), stop(y)])) >>> print drs3.simplify() ([x,y],[man(x), walk(x), woman(y), stop(y)]) We can embed DRSs as components of an ``implies`` condition. >>> s = '([], [(%s -> %s)])' % (drs1, drs2) >>> print dp.parse(s) ([],[(([x],[man(x), walk(x)]) -> ([y],[woman(y), stop(y)]))]) The ``fol()`` method converts DRSs into FOL formulae. >>> print dp.parse(r'([x],[man(x), walks(x)])').fol() exists x.(man(x) & walks(x)) >>> print dp.parse(r'([],[(([x],[man(x)]) -> ([],[walks(x)]))])').fol() all x.(man(x) -> walks(x)) In order to visualize a DRS, the ``draw()`` method can be use. >>> drs3.draw() # doctest: +SKIP Parse to semantics ------------------ .. >>> logic._counter._value = 0 DRSs can be used for building compositional semantics in a feature based grammar. To specify that we want to use DRSs, the appropriate logic parser needs be passed as a parameter to ``load_earley()`` >>> from nltk.parse import load_earley >>> parser = load_earley('grammars/book_grammars/drt.fcfg', trace=0, logic_parser=DrtParser()) >>> trees = parser.nbest_parse('a dog barks'.split()) >>> print trees[0].node['SEM'].simplify() ([x],[dog(x), bark(x)]) Alternatively, a ``FeatStructParser`` can be passed with the ``logic_parser`` set on it >>> from nltk.featstruct import FeatStructParser >>> from nltk.grammar import FeatStructNonterminal >>> parser = load_earley('grammars/book_grammars/drt.fcfg', trace=0, fstruct_parser=FeatStructParser(fdict_class=FeatStructNonterminal, logic_parser=DrtParser())) >>> trees = parser.nbest_parse('every girl chases a dog'.split()) >>> print trees[0].node['SEM'].simplify() ([],[(([x],[girl(x)]) -> ([z2],[dog(z2), chase(x,z2)]))]) Unit Tests ========== Parser ------ >>> print dp.parse(r'([x,y],[sees(x,y)])') ([x,y],[sees(x,y)]) >>> print dp.parse(r'([x],[man(x), walks(x)])') ([x],[man(x), walks(x)]) >>> print dp.parse(r'\x.([],[man(x), walks(x)])') \x.([],[man(x), walks(x)]) >>> print dp.parse(r'\x.\y.([],[sees(x,y)])') \x y.([],[sees(x,y)]) >>> print dp.parse(r'([x,y],[(x = y)])') ([x,y],[(x = y)]) >>> print dp.parse(r'([x,y],[(x != y)])') ([x,y],[-(x = y)]) >>> print dp.parse(r'\x.([],[walks(x)])(john)') (\x.([],[walks(x)]))(john) >>> print dp.parse(r'\R.\x.([],[big(x,R)])(\y.([],[mouse(y)]))') (\R x.([],[big(x,R)]))(\y.([],[mouse(y)])) >>> print dp.parse(r'(([x],[walks(x)]) + ([y],[runs(y)]))') (([x],[walks(x)]) + ([y],[runs(y)])) >>> print dp.parse(r'(([x,y],[walks(x), jumps(y)]) + (([z],[twos(z)]) + ([w],[runs(w)])))') (([x,y],[walks(x), jumps(y)]) + (([z],[twos(z)]) + ([w],[runs(w)]))) >>> print dp.parse(r'((([],[walks(x)]) + ([],[twos(x)])) + ([],[runs(x)]))') ((([],[walks(x)]) + ([],[twos(x)])) + ([],[runs(x)])) >>> print dp.parse(r'((([],[walks(x)]) + ([],[runs(x)])) + (([],[threes(x)]) + ([],[fours(x)])))') ((([],[walks(x)]) + ([],[runs(x)])) + (([],[threes(x)]) + ([],[fours(x)]))) >>> print dp.parse(r'(([],[walks(x)]) -> ([],[runs(x)]))') (([],[walks(x)]) -> ([],[runs(x)])) >>> print dp.parse(r'([x],[PRO(x), sees(John,x)])') ([x],[PRO(x), sees(John,x)]) >>> print dp.parse(r'([x],[man(x), -([],[walks(x)])])') ([x],[man(x), -([],[walks(x)])]) >>> print dp.parse(r'([],[(([x],[man(x)]) -> ([],[walks(x)]))])') ([],[(([x],[man(x)]) -> ([],[walks(x)]))]) >>> print dp.parse(r'DRS([x],[walk(x)])') ([x],[walk(x)]) >>> print dp.parse(r'DRS([x][walk(x)])') ([x],[walk(x)]) >>> print dp.parse(r'([x][walk(x)])') ([x],[walk(x)]) ``simplify()`` -------------- >>> print dp.parse(r'\x.([],[man(x), walks(x)])(john)').simplify() ([],[man(john), walks(john)]) >>> print dp.parse(r'\x.\y.([z],[dog(z),sees(x,y)])(john)(mary)').simplify() ([z],[dog(z), sees(john,mary)]) >>> print dp.parse(r'\R x.([],[big(x,R)])(\y.([],[mouse(y)]))').simplify() \x.([],[big(x,\y.([],[mouse(y)]))]) >>> print dp.parse(r'(([x],[walks(x)]) + ([y],[runs(y)]))').simplify() ([x,y],[walks(x), runs(y)]) >>> print dp.parse(r'(([x,y],[walks(x), jumps(y)]) + (([z],[twos(z)]) + ([w],[runs(w)])))').simplify() ([x,y,z,w],[walks(x), jumps(y), twos(z), runs(w)]) >>> print dp.parse(r'((([],[walks(x)]) + ([],[runs(x)]) + ([],[threes(x)]) + ([],[fours(x)])))').simplify() ([],[walks(x), runs(x), threes(x), fours(x)]) >>> dp.parse(r'([x],[man(x)])+([x],[walks(x)])').simplify() == \ ... dp.parse(r'([x,z1],[man(x), walks(z1)])') True >>> dp.parse(r'([y],[boy(y), (([x],[dog(x)]) -> ([],[chase(x,y)]))])+([x],[run(x)])').simplify() == \ ... dp.parse(r'([y,z1],[boy(y), (([x],[dog(x)]) -> ([],[chase(x,y)])), run(z1)])') True >>> dp.parse(r'\Q.(([x],[john(x),walks(x)]) + Q)(([x],[PRO(x),leaves(x)]))').simplify() == \ ... dp.parse(r'([x,z1],[john(x), walks(x), PRO(z1), leaves(z1)])') True >>> logic._counter._value = 0 >>> print dp.parse('([],[(([x],[dog(x)]) -> ([y,e],[boy(y), chase(e), subj(e,x), obj(e,y)]))])+([x,e],[PRO(x), run(e), subj(e,x)])').simplify() ([z1,e02],[(([x],[dog(x)]) -> ([y,e],[boy(y), chase(e), subj(e,x), obj(e,y)])), PRO(z1), run(e02), subj(e02,z1)]) ``fol()`` ----------- >>> print dp.parse(r'([x,y],[sees(x,y)])').fol() exists x y.sees(x,y) >>> print dp.parse(r'([x],[man(x), walks(x)])').fol() exists x.(man(x) & walks(x)) >>> print dp.parse(r'\x.([],[man(x), walks(x)])').fol() \x.(man(x) & walks(x)) >>> print dp.parse(r'\x y.([],[sees(x,y)])').fol() \x y.sees(x,y) >>> print dp.parse(r'\x.([],[walks(x)])(john)').fol() \x.walks(x)(john) >>> print dp.parse(r'\R x.([],[big(x,R)])(\y.([],[mouse(y)]))').fol() (\R x.big(x,R))(\y.mouse(y)) >>> print dp.parse(r'(([x],[walks(x)]) + ([y],[runs(y)]))').fol() (exists x.walks(x) & exists y.runs(y)) >>> print dp.parse(r'(([],[walks(x)]) -> ([],[runs(x)]))').fol() (walks(x) -> runs(x)) >>> print dp.parse(r'([x],[PRO(x), sees(John,x)])').fol() exists x.(PRO(x) & sees(John,x)) >>> print dp.parse(r'([x],[man(x), -([],[walks(x)])])').fol() exists x.(man(x) & -walks(x)) >>> print dp.parse(r'([],[(([x],[man(x)]) -> ([],[walks(x)]))])').fol() all x.(man(x) -> walks(x)) >>> print dp.parse(r'([x],[man(x) | walks(x)])').fol() exists x.(man(x) | walks(x)) >>> print dp.parse(r'([x],[man(x) <-> walks(x)])').fol() exists x.(man(x) <-> walks(x)) >>> print dp.parse(r'P(x) + ([x],[walks(x)])').fol() (P(x) & exists x.walks(x)) ``resolve_anaphora()`` ---------------------- >>> from nltk.sem.drt import AnaphoraResolutionException >>> print resolve_anaphora(dp.parse(r'([x,y,z],[dog(x), cat(y), walks(z), PRO(z)])')) ([x,y,z],[dog(x), cat(y), walks(z), (z = [x,y])]) >>> print resolve_anaphora(dp.parse(r'([],[(([x],[dog(x)]) -> ([y],[walks(y), PRO(y)]))])')) ([],[(([x],[dog(x)]) -> ([y],[walks(y), (y = x)]))]) >>> print resolve_anaphora(dp.parse(r'(([x,y],[]) + ([],[PRO(x)]))')).simplify() ([x,y],[(x = y)]) >>> try: print resolve_anaphora(dp.parse(r'([x],[walks(x), PRO(x)])')) ... except AnaphoraResolutionException, e: print e Variable 'x' does not resolve to anything. >>> print resolve_anaphora(dp.parse('([z6,z7,e1],[boy(z6), PRO(z7), run(e1), subj(e1,z7)])')) ([z6,z7,e1],[boy(z6), (z7 = z6), run(e1), subj(e1,z7)]) ``tp_equals()``: ---------------- >>> a = dp.parse(r'([x],[man(x), walks(x)])') >>> b = dp.parse(r'([x],[walks(x), man(x)])') >>> print a.tp_equals(b, TableauProver()) True ``replace()``: -------------- >>> a = dp.parse(r'a') >>> w = dp.parse(r'w') >>> x = dp.parse(r'x') >>> y = dp.parse(r'y') >>> z = dp.parse(r'z') replace bound ------------- >>> print dp.parse(r'([x],[give(x,y,z)])').replace(x.variable, a, False) ([x],[give(x,y,z)]) >>> print dp.parse(r'([x],[give(x,y,z)])').replace(x.variable, a, True) ([a],[give(a,y,z)]) replace unbound --------------- >>> print dp.parse(r'([x],[give(x,y,z)])').replace(y.variable, a, False) ([x],[give(x,a,z)]) >>> print dp.parse(r'([x],[give(x,y,z)])').replace(y.variable, a, True) ([x],[give(x,a,z)]) replace unbound with bound -------------------------- >>> dp.parse(r'([x],[give(x,y,z)])').replace(y.variable, x, False) == \ ... dp.parse('([z1],[give(z1,x,z)])') True >>> dp.parse(r'([x],[give(x,y,z)])').replace(y.variable, x, True) == \ ... dp.parse('([z1],[give(z1,x,z)])') True replace unbound with unbound ---------------------------- >>> print dp.parse(r'([x],[give(x,y,z)])').replace(y.variable, z, False) ([x],[give(x,z,z)]) >>> print dp.parse(r'([x],[give(x,y,z)])').replace(y.variable, z, True) ([x],[give(x,z,z)]) replace unbound --------------- >>> print dp.parse(r'([x],[P(x,y,z)])+([y],[Q(x,y,z)])').replace(z.variable, a, False) (([x],[P(x,y,a)]) + ([y],[Q(x,y,a)])) >>> print dp.parse(r'([x],[P(x,y,z)])+([y],[Q(x,y,z)])').replace(z.variable, a, True) (([x],[P(x,y,a)]) + ([y],[Q(x,y,a)])) replace bound ------------- >>> print dp.parse(r'([x],[P(x,y,z)])+([y],[Q(x,y,z)])').replace(x.variable, a, False) (([x],[P(x,y,z)]) + ([y],[Q(x,y,z)])) >>> print dp.parse(r'([x],[P(x,y,z)])+([y],[Q(x,y,z)])').replace(x.variable, a, True) (([a],[P(a,y,z)]) + ([y],[Q(a,y,z)])) replace unbound with unbound ---------------------------- >>> print dp.parse(r'([x],[P(x,y,z)])+([y],[Q(x,y,z)])').replace(z.variable, a, False) (([x],[P(x,y,a)]) + ([y],[Q(x,y,a)])) >>> print dp.parse(r'([x],[P(x,y,z)])+([y],[Q(x,y,z)])').replace(z.variable, a, True) (([x],[P(x,y,a)]) + ([y],[Q(x,y,a)])) replace unbound with bound on same side --------------------------------------- >>> dp.parse(r'([x],[P(x,y,z)])+([y],[Q(x,y,w)])').replace(z.variable, x, False) == \ ... dp.parse(r'(([z1],[P(z1,y,x)]) + ([y],[Q(z1,y,w)]))') True >>> dp.parse(r'([x],[P(x,y,z)])+([y],[Q(x,y,w)])').replace(z.variable, x, True) == \ ... dp.parse(r'(([z1],[P(z1,y,x)]) + ([y],[Q(z1,y,w)]))') True replace unbound with bound on other side ---------------------------------------- >>> dp.parse(r'([x],[P(x,y,z)])+([y],[Q(x,y,w)])').replace(w.variable, x, False) == \ ... dp.parse(r'(([z1],[P(z1,y,z)]) + ([y],[Q(z1,y,x)]))') True >>> dp.parse(r'([x],[P(x,y,z)])+([y],[Q(x,y,w)])').replace(w.variable, x, True) == \ ... dp.parse(r'(([z1],[P(z1,y,z)]) + ([y],[Q(z1,y,x)]))') True replace unbound with double bound --------------------------------- >>> dp.parse(r'([x],[P(x,y,z)])+([x],[Q(x,y,w)])').replace(z.variable, x, False) == \ ... dp.parse(r'(([z1],[P(z1,y,x)]) + ([z1],[Q(z1,y,w)]))') True >>> dp.parse(r'([x],[P(x,y,z)])+([x],[Q(x,y,w)])').replace(z.variable, x, True) == \ ... dp.parse(r'(([z1],[P(z1,y,x)]) + ([z1],[Q(z1,y,w)]))') True Parse errors ============ >>> try: dp.parse(r'') ... except ParseException, e: print e End of input found. Expression expected. ^ >>> try: dp.parse(r'(') ... except ParseException, e: print e End of input found. Expression expected. ( ^ >>> try: dp.parse(r'()') ... except ParseException, e: print e Unexpected token: ')'. Expression expected. () ^ >>> try: dp.parse(r'([') ... except ParseException, e: print e End of input found. Expected token ']'. ([ ^ >>> try: dp.parse(r'([,') ... except ParseException, e: print e ',' is an illegal variable name. Constants may not be quantified. ([, ^ >>> try: dp.parse(r'([x,') ... except ParseException, e: print e End of input found. Variable expected. ([x, ^ >>> try: dp.parse(r'([]') ... except ParseException, e: print e End of input found. Expected token '['. ([] ^ >>> try: dp.parse(r'([][') ... except ParseException, e: print e End of input found. Expected token ']'. ([][ ^ >>> try: dp.parse(r'([][,') ... except ParseException, e: print e Unexpected token: ','. Expression expected. ([][, ^ >>> try: dp.parse(r'([][]') ... except ParseException, e: print e End of input found. Expected token ')'. ([][] ^ >>> try: dp.parse(r'([x][man(x)]) |') ... except ParseException, e: print e End of input found. Expression expected. ([x][man(x)]) | ^ nltk-2.0~b9/nltk/test/discourse.doctest0000644000175000017500000004153611331670217020106 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ================== Discourse Checking ================== >>> from nltk import * >>> from nltk.sem import logic >>> logic._counter._value = 0 Introduction ============ The NLTK discourse module makes it possible to test consistency and redundancy of simple discourses, using theorem-proving and model-building from `nltk.inference`. The ``DiscourseTester`` constructor takes a list of sentences as a parameter. >>> dt = DiscourseTester(['a boxer walks', 'every boxer chases a girl']) The ``DiscourseTester`` parses each sentence into a list of logical forms. Once we have created ``DiscourseTester`` object, we can inspect various properties of the discourse. First off, we might want to double-check what sentences are currently stored as the discourse. >>> dt.sentences() s0: a boxer walks s1: every boxer chases a girl As you will see, each sentence receives an identifier `s`\ :subscript:`i`. We might also want to check what grammar the ``DiscourseTester`` is using (by default, ``book_grammars/discourse.fcfg``): >>> dt.grammar() # doctest: +ELLIPSIS % start S # Grammar Rules S[SEM = ] -> NP[NUM=?n,SEM=?subj] VP[NUM=?n,SEM=?vp] NP[NUM=?n,SEM= ] -> Det[NUM=?n,SEM=?det] Nom[NUM=?n,SEM=?nom] NP[LOC=?l,NUM=?n,SEM=?np] -> PropN[LOC=?l,NUM=?n,SEM=?np] ... A different grammar can be invoked by using the optional ``gramfile`` parameter when a ``DiscourseTester`` object is created. Readings and Threads ==================== Depending on the grammar used, we may find some sentences have more than one logical form. To check this, use the ``readings()`` method. Given a sentence identifier of the form `s`\ :subscript:`i`, each reading of that sentence is given an identifier `s`\ :sub:`i`-`r`\ :sub:`j`. >>> dt.readings() s0 readings: s0-r0: exists x.(boxer(x) & walk(x)) s0-r1: exists x.(boxerdog(x) & walk(x)) s1 readings: s1-r0: all x.(boxer(x) -> exists z1.(girl(z1) & chase(x,z1))) s1-r1: all x.(boxerdog(x) -> exists z2.(girl(z2) & chase(x,z2))) In this case, the only source of ambiguity lies in the word *boxer*, which receives two translations: ``boxer`` and ``boxerdog``. The intention is that one of these corresponds to the ``person`` sense and one to the ``dog`` sense. In principle, we would also expect to see a quantifier scope ambiguity in ``s1``. However, the simple grammar we are using, namely `sem4.fcfg `_, doesn't support quantifier scope ambiguity. We can also investigate the readings of a specific sentence: >>> dt.readings('a boxer walks') The sentence 'a boxer walks' has these readings: exists x.(boxer(x) & walk(x)) exists x.(boxerdog(x) & walk(x)) Given that each sentence is two-ways ambiguous, we potentially have four different discourse 'threads', taking all combinations of readings. To see these, specify the ``threaded=True`` parameter on the ``readings()`` method. Again, each thread is assigned an identifier of the form `d`\ :sub:`i`. Following the identifier is a list of the readings that constitute that thread. >>> dt.readings(threaded=True) # doctest: +NORMALIZE_WHITESPACE d0: ['s0-r0', 's1-r0'] d1: ['s0-r0', 's1-r1'] d2: ['s0-r1', 's1-r0'] d3: ['s0-r1', 's1-r1'] Of course, this simple-minded approach doesn't scale: a discourse with, say, three sentences, each of which has 3 readings, will generate 27 different threads. It is an interesting exercise to consider how to manage discourse ambiguity more efficiently. Checking Consistency ==================== Now, we can check whether some or all of the discourse threads are consistent, using the ``models()`` method. With no parameter, this method will try to find a model for every discourse thread in the current discourse. However, we can also specify just one thread, say ``d1``. >>> dt.models('d1') -------------------------------------------------------------------------------- Model for Discourse Thread d1 -------------------------------------------------------------------------------- % number = 1 % seconds = 0 % Interpretation of size 2 c1 = 0. f1(0) = 0. f1(1) = 0. boxer(0). - boxer(1). - boxerdog(0). - boxerdog(1). - girl(0). - girl(1). walk(0). - walk(1). - chase(0,0). - chase(0,1). - chase(1,0). - chase(1,1). Consistent discourse: d1 ['s0-r0', 's1-r1']: s0-r0: exists x.(boxer(x) & walk(x)) s1-r1: all x.(boxerdog(x) -> exists z8.(girl(z8) & chase(x,z8))) There are various formats for rendering **Mace4** models --- here, we have used the 'cooked' format (which is intended to be human-readable). There are a number of points to note. #. The entities in the domain are all treated as non-negative integers. In this case, there are only two entities, ``0`` and ``1``. #. The ``-`` symbol indicates negation. So ``0`` is the only ``boxerdog`` and the only thing that ``walk``\ s. Nothing is a ``boxer``, or a ``girl`` or in the ``chase`` relation. Thus the universal sentence is vacuously true. #. ``c1`` is an introduced constant that denotes ``0``. #. ``f1`` is a Skolem function, but it plays no significant role in this model. We might want to now add another sentence to the discourse, and there is method ``add_sentence()`` for doing just this. >>> dt.add_sentence('John is a boxer') >>> dt.sentences() s0: a boxer walks s1: every boxer chases a girl s2: John is a boxer We can now test all the properties as before; here, we just show a couple of them. >>> dt.readings() s0 readings: s0-r0: exists x.(boxer(x) & walk(x)) s0-r1: exists x.(boxerdog(x) & walk(x)) s1 readings: s1-r0: all x.(boxer(x) -> exists z9.(girl(z9) & chase(x,z9))) s1-r1: all x.(boxerdog(x) -> exists z10.(girl(z10) & chase(x,z10))) s2 readings: s2-r0: boxer(John) s2-r1: boxerdog(John) >>> dt.readings(threaded=True) # doctest: +NORMALIZE_WHITESPACE d0: ['s0-r0', 's1-r0', 's2-r0'] d1: ['s0-r0', 's1-r0', 's2-r1'] d2: ['s0-r0', 's1-r1', 's2-r0'] d3: ['s0-r0', 's1-r1', 's2-r1'] d4: ['s0-r1', 's1-r0', 's2-r0'] d5: ['s0-r1', 's1-r0', 's2-r1'] d6: ['s0-r1', 's1-r1', 's2-r0'] d7: ['s0-r1', 's1-r1', 's2-r1'] If you are interested in a particular thread, the ``expand_threads()`` method will remind you of what readings it consists of: >>> thread = dt.expand_threads('d1') >>> for rid, reading in thread: ... print rid, str(reading) s0-r0 exists x.(boxer(x) & walk(x)) s1-r0 all x.(boxer(x) -> exists z11.(girl(z11) & chase(x,z11))) s2-r1 boxerdog(John) Suppose we have already defined a discourse, as follows: >>> dt = DiscourseTester(['A student dances', 'Every student is a person']) Now, when we add a new sentence, is it consistent with what we already have? The `` consistchk=True`` parameter of ``add_sentence()`` allows us to check: >>> dt.add_sentence('No person dances', consistchk=True) Inconsistent discourse: d0 ['s0-r0', 's1-r0', 's2-r0']: s0-r0: exists x.(student(x) & dance(x)) s1-r0: all x.(student(x) -> person(x)) s2-r0: -exists x.(person(x) & dance(x)) >>> dt.readings() s0 readings: s0-r0: exists x.(student(x) & dance(x)) s1 readings: s1-r0: all x.(student(x) -> person(x)) s2 readings: s2-r0: -exists x.(person(x) & dance(x)) So let's retract the inconsistent sentence: >>> dt.retract_sentence('No person dances', verbose=True) # doctest: +NORMALIZE_WHITESPACE Current sentences are s0: A student dances s1: Every student is a person We can now verify that result is consistent. >>> dt.models() -------------------------------------------------------------------------------- Model for Discourse Thread d0 -------------------------------------------------------------------------------- % number = 1 % seconds = 0 % Interpretation of size 2 c1 = 0. dance(0). - dance(1). person(0). - person(1). student(0). - student(1). Consistent discourse: d0 ['s0-r0', 's1-r0']: s0-r0: exists x.(student(x) & dance(x)) s1-r0: all x.(student(x) -> person(x)) Checking Informativity ====================== Let's assume that we are still trying to extend the discourse *A student dances.* *Every student is a person.* We add a new sentence, but this time, we check whether it is informative with respect to what has gone before. >>> dt.add_sentence('A person dances', informchk=True) Sentence 'A person dances' under reading 'exists x.(person(x) & dance(x))': Not informative relative to thread 'd0' In fact, we are just checking whether the new sentence is entailed by the preceding discourse. >>> dt.models() -------------------------------------------------------------------------------- Model for Discourse Thread d0 -------------------------------------------------------------------------------- % number = 1 % seconds = 0 % Interpretation of size 2 c1 = 0. c2 = 0. dance(0). - dance(1). person(0). - person(1). student(0). - student(1). Consistent discourse: d0 ['s0-r0', 's1-r0', 's2-r0']: s0-r0: exists x.(student(x) & dance(x)) s1-r0: all x.(student(x) -> person(x)) s2-r0: exists x.(person(x) & dance(x)) Adding Background Knowledge =========================== Let's build a new discourse, and look at the readings of the component sentences: >>> dt = DiscourseTester(['Vincent is a boxer', 'Fido is a boxer', 'Vincent is married', 'Fido barks']) >>> dt.readings() s0 readings: s0-r0: boxer(Vincent) s0-r1: boxerdog(Vincent) s1 readings: s1-r0: boxer(Fido) s1-r1: boxerdog(Fido) s2 readings: s2-r0: married(Vincent) s3 readings: s3-r0: bark(Fido) This gives us a lot of threads: >>> dt.readings(threaded=True) # doctest: +NORMALIZE_WHITESPACE d0: ['s0-r0', 's1-r0', 's2-r0', 's3-r0'] d1: ['s0-r0', 's1-r1', 's2-r0', 's3-r0'] d2: ['s0-r1', 's1-r0', 's2-r0', 's3-r0'] d3: ['s0-r1', 's1-r1', 's2-r0', 's3-r0'] We can eliminate some of the readings, and hence some of the threads, by adding background information. >>> import nltk.data >>> bg = nltk.data.load('grammars/book_grammars/background.fol') >>> dt.add_background(bg) >>> dt.background() all x.(boxerdog(x) -> dog(x)) all x.(boxer(x) -> person(x)) all x.-(dog(x) & person(x)) all x.(married(x) <-> exists y.marry(x,y)) all x.(bark(x) -> dog(x)) all x y.(marry(x,y) -> (person(x) & person(y))) -(Vincent = Mia) -(Vincent = Fido) -(Mia = Fido) The background information allows us to reject three of the threads as inconsistent. To see what remains, use the ``filter=True`` parameter on ``readings()``. >>> dt.readings(filter=True) # doctest: +NORMALIZE_WHITESPACE d1: ['s0-r0', 's1-r1', 's2-r0', 's3-r0'] The ``models()`` method gives us more information about the surviving thread. >>> dt.models() -------------------------------------------------------------------------------- Model for Discourse Thread d0 -------------------------------------------------------------------------------- No model found! -------------------------------------------------------------------------------- Model for Discourse Thread d1 -------------------------------------------------------------------------------- % number = 1 % seconds = 0 % Interpretation of size 3 Fido = 0. Mia = 1. Vincent = 2. f1(0) = 0. f1(1) = 0. f1(2) = 2. bark(0). - bark(1). - bark(2). - boxer(0). - boxer(1). boxer(2). boxerdog(0). - boxerdog(1). - boxerdog(2). dog(0). - dog(1). - dog(2). - married(0). - married(1). married(2). - person(0). - person(1). person(2). - marry(0,0). - marry(0,1). - marry(0,2). - marry(1,0). - marry(1,1). - marry(1,2). - marry(2,0). - marry(2,1). marry(2,2). -------------------------------------------------------------------------------- Model for Discourse Thread d2 -------------------------------------------------------------------------------- No model found! -------------------------------------------------------------------------------- Model for Discourse Thread d3 -------------------------------------------------------------------------------- No model found! Inconsistent discourse: d0 ['s0-r0', 's1-r0', 's2-r0', 's3-r0']: s0-r0: boxer(Vincent) s1-r0: boxer(Fido) s2-r0: married(Vincent) s3-r0: bark(Fido) Consistent discourse: d1 ['s0-r0', 's1-r1', 's2-r0', 's3-r0']: s0-r0: boxer(Vincent) s1-r1: boxerdog(Fido) s2-r0: married(Vincent) s3-r0: bark(Fido) Inconsistent discourse: d2 ['s0-r1', 's1-r0', 's2-r0', 's3-r0']: s0-r1: boxerdog(Vincent) s1-r0: boxer(Fido) s2-r0: married(Vincent) s3-r0: bark(Fido) Inconsistent discourse: d3 ['s0-r1', 's1-r1', 's2-r0', 's3-r0']: s0-r1: boxerdog(Vincent) s1-r1: boxerdog(Fido) s2-r0: married(Vincent) s3-r0: bark(Fido) .. This will not be visible in the html output: create a tempdir to play in. >>> import tempfile, os >>> tempdir = tempfile.mkdtemp() >>> old_dir = os.path.abspath('.') >>> os.chdir(tempdir) In order to play around with your own version of background knowledge, you might want to start off with a local copy of ``background.fol``: >>> nltk.data.retrieve('grammars/book_grammars/background.fol') Retrieving 'grammars/book_grammars/background.fol', saving to 'background.fol' After you have modified the file, the ``parse_logic()`` function will parse the strings in the file into expressions of ``nltk.logic``. >>> from nltk.inference.discourse import parse_fol >>> mybg = parse_fol(open('background.fol').read()) The result can be loaded as an argument of ``add_background()`` in the manner shown earlier. .. This will not be visible in the html output: clean up the tempdir. >>> os.chdir(old_dir) >>> for f in os.listdir(tempdir): ... os.remove(os.path.join(tempdir, f)) >>> os.rmdir(tempdir) >>> nltk.data.clear_cache() Regression Testing from book ============================ >>> logic._counter._value = 0 >>> from nltk.tag import RegexpTagger >>> tagger = RegexpTagger( ... [('^(chases|runs)$', 'VB'), ... ('^(a)$', 'ex_quant'), ... ('^(every)$', 'univ_quant'), ... ('^(dog|boy)$', 'NN'), ... ('^(He)$', 'PRP') ... ]) >>> rc = DrtGlueReadingCommand(depparser=MaltParser(tagger=tagger)) >>> dt = DiscourseTester(['Every dog chases a boy', 'He runs'], rc) >>> dt.readings() s0 readings: s0-r0: ([],[(([x],[dog(x)]) -> ([z1],[boy(z1), chases(x,z1)]))]) s0-r1: ([z2],[boy(z2), (([x],[dog(x)]) -> ([],[chases(x,z2)]))]) s1 readings: s1-r0: ([x],[PRO(x), runs(x)]) >>> dt.readings(show_thread_readings=True) d0: ['s0-r0', 's1-r0'] : INVALID: AnaphoraResolutionException d1: ['s0-r1', 's1-r0'] : ([z6,z10],[boy(z6), (([x],[dog(x)]) -> ([],[chases(x,z6)])), (z10 = z6), runs(z10)]) >>> dt.readings(filter=True, show_thread_readings=True) d1: ['s0-r1', 's1-r0'] : ([z12,z15],[boy(z12), (([x],[dog(x)]) -> ([],[chases(x,z12)])), (z15 = z12), runs(z15)]) >>> from nltk.parse import load_earley >>> from nltk.sem.drt import DrtParser >>> parser = load_earley('grammars/book_grammars/drt.fcfg', trace=0, logic_parser=DrtParser()) >>> trees = parser.nbest_parse('Angus owns a dog'.split()) >>> print trees[0].node['SEM'].simplify() ([x,z17],[Angus(x), dog(z17), own(x,z17)]) nltk-2.0~b9/nltk/test/dependency.doctest0000644000175000017500000000700311331670223020210 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT =================== Dependency Grammars =================== >>> from nltk.grammar import * >>> from nltk.parse import * CoNLL Data ---------- >>> treebank_data = """Pierre NNP 2 NMOD ... Vinken NNP 8 SUB ... , , 2 P ... 61 CD 5 NMOD ... years NNS 6 AMOD ... old JJ 2 NMOD ... , , 2 P ... will MD 0 ROOT ... join VB 8 VC ... the DT 11 NMOD ... board NN 9 OBJ ... as IN 9 VMOD ... a DT 15 NMOD ... nonexecutive JJ 15 NMOD ... director NN 12 PMOD ... Nov. NNP 9 VMOD ... 29 CD 16 NMOD ... . . 9 VMOD ... """ >>> dg = DependencyGraph(treebank_data) >>> print dg.tree().pprint() (will (Vinken Pierre , (old (years 61)) ,) (join (board the) (as (director a nonexecutive)) (Nov. 29) .)) Using the dependency-parsed version of the Penn Treebank corpus sample. >>> from nltk.corpus import dependency_treebank >>> t = dependency_treebank.parsed_sents()[0] >>> print t.to_conll(3) # doctest: +NORMALIZE_WHITESPACE Pierre NNP 2 Vinken NNP 8 , , 2 61 CD 5 years NNS 6 old JJ 2 , , 2 will MD 0 join VB 8 the DT 11 board NN 9 as IN 9 a DT 15 nonexecutive JJ 15 director NN 12 Nov. NNP 9 29 CD 16 . . 8 Projective Dependency Parsing ----------------------------- >>> grammar = parse_dependency_grammar(""" ... 'fell' -> 'price' | 'stock' ... 'price' -> 'of' 'the' ... 'of' -> 'stock' ... 'stock' -> 'the' ... """) >>> print grammar Dependency grammar with 5 productions 'fell' -> 'price' 'fell' -> 'stock' 'price' -> 'of' 'the' 'of' -> 'stock' 'stock' -> 'the' >>> dp = ProjectiveDependencyParser(grammar) >>> for t in dp.parse(['the', 'price', 'of', 'the', 'stock', 'fell']): ... print t (fell (price the of the) stock) (fell (price the of) (stock the)) (fell (price the (of (stock the)))) Non-Projective Dependency Parsing --------------------------------- >>> grammar = parse_dependency_grammar(""" ... 'taught' -> 'play' | 'man' ... 'man' -> 'the' ... 'play' -> 'golf' | 'dog' | 'to' ... 'dog' -> 'his' ... """) >>> print grammar Dependency grammar with 7 productions 'taught' -> 'play' 'taught' -> 'man' 'man' -> 'the' 'play' -> 'golf' 'play' -> 'dog' 'play' -> 'to' 'dog' -> 'his' >>> dp = NonprojectiveDependencyParser(grammar) >>> for g in dp.parse(['the', 'man', 'taught', 'his', 'dog', 'to', 'play', 'golf']): ... print g [{'address': 0, 'deps': 3, 'rel': 'TOP', 'tag': 'TOP', 'word': None}, {'address': 1, 'deps': [], 'word': 'the'}, {'address': 2, 'deps': [1], 'word': 'man'}, {'address': 3, 'deps': [2, 7], 'word': 'taught'}, {'address': 4, 'deps': [], 'word': 'his'}, {'address': 5, 'deps': [4], 'word': 'dog'}, {'address': 6, 'deps': [], 'word': 'to'}, {'address': 7, 'deps': [5, 6, 8], 'word': 'play'}, {'address': 8, 'deps': [], 'word': 'golf'}] nltk-2.0~b9/nltk/test/data.doctest0000644000175000017500000002710011331670226017006 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ========================================= Loading Resources From the Data Package ========================================= >>> import nltk.data Overview ~~~~~~~~ The `nltk.data` module contains functions that can be used to load NLTK resource files, such as corpora, grammars, and saved processing objects. Loading Data Files ~~~~~~~~~~~~~~~~~~ Resources are loaded using the function `nltk.data.load()`, which takes as its first argument a URL specifying what file should be loaded. The ``nltk:`` protocol loads files from the NLTK data distribution: >>> tokenizer = nltk.data.load('nltk:tokenizers/punkt/english.pickle') >>> tokenizer.tokenize('Hello. This is a test. It works!') ['Hello.', 'This is a test.', 'It works!'] It is important to note that there should be no space following the colon (':') in the URL; 'nltk: tokenizers/punkt/english.pickle' will not work! The ``nltk:`` protocol is used by default if no protocol is specified: >>> nltk.data.load('tokenizers/punkt/english.pickle') # doctest: +ELLIPSIS But it is also possible to load resources from ``http:``, ``ftp:``, and ``file:`` URLs, e.g. ``cfg = nltk.data.load('http://example.com/path/to/toy.cfg')`` >>> # Load a grammar using an absolute path. >>> url = 'file:%s' % nltk.data.find('grammars/sample_grammars/toy.cfg') >>> url # doctest: +ELLIPSIS 'file:/.../nltk_data/grammars/sample_grammars/toy.cfg' >>> print nltk.data.load(url) # doctest: +ELLIPSIS Grammar with 14 productions (start state = S) S -> NP VP PP -> P NP ... P -> 'on' P -> 'in' The second argument to the `nltk.data.load()` function specifies the file format, which determines how the file's contents are processed before they are returned by ``load()``. The formats that are currently supported by the data module are described by the dictionary `nltk.data.FORMATS`: >>> for format, descr in sorted(nltk.data.FORMATS.items()): ... print '%-7s %s' % (format, descr) # doctest: +NORMALIZE_WHITESPACE cfg A context free grammar, parsed by nltk.parse_cfg(). fcfg A feature CFG, parsed by nltk.parse_fcfg(). fol A list of first order logic expressions, parsed by nltk.sem.parse_fol() using nltk.sem.logic.LogicParser. logic A list of first order logic expressions, parsed by nltk.sem.parse_logic(). Requires an additional logic_parser parameter pcfg A probabilistic CFG, parsed by nltk.parse_pcfg(). pickle A serialized python object, stored using the pickle module. raw The raw (byte string) contents of a file. val A semantic valuation, parsed by nltk.sem.parse_valuation(). yaml A serialized python object, stored using the yaml module. `nltk.data.load()` will raise a ValueError if a bad format name is specified: >>> nltk.data.load('grammars/sample_grammars/toy.cfg', 'bar') Traceback (most recent call last): . . . ValueError: Unknown format type! By default, the ``"auto"`` format is used, which chooses a format based on the filename's extension. The mapping from file extensions to format names is specified by `nltk.data.AUTO_FORMATS`: >>> for ext, format in sorted(nltk.data.AUTO_FORMATS.items()): ... print '.%-7s -> %s' % (ext, format) .cfg -> cfg .fcfg -> fcfg .fol -> fol .logic -> logic .pcfg -> pcfg .pickle -> pickle .val -> val .yaml -> yaml If `nltk.data.load()` is unable to determine the format based on the filename's extension, it will raise a ValueError: >>> nltk.data.load('foo.bar') Traceback (most recent call last): . . . ValueError: Could not determine format for foo.bar based on its file extension; use the "format" argument to specify the format explicitly. Note that by explicitly specifying the ``format`` argument, you can override the load method's default processing behavior. For example, to get the raw contents of any file, simply use ``format="raw"``: >>> nltk.data.load('grammars/sample_grammars/toy.cfg', 'raw') # doctest: +ELLIPSIS "S -> NP VP\nPP -> P NP\nNP -> Det N | NP PP\nVP -> V NP | VP PP\n..." Making Local Copies ~~~~~~~~~~~~~~~~~~~ .. This will not be visible in the html output: create a tempdir to play in. >>> import tempfile, os >>> tempdir = tempfile.mkdtemp() >>> old_dir = os.path.abspath('.') >>> os.chdir(tempdir) The function `nltk.data.retrieve()` copies a given resource to a local file. This can be useful, for example, if you want to edit one of the sample grammars. >>> nltk.data.retrieve('grammars/sample_grammars/toy.cfg') Retrieving 'grammars/sample_grammars/toy.cfg', saving to 'toy.cfg' >>> # Simulate editing the grammar. >>> s = open('toy.cfg').read().replace('NP', 'DP') >>> out = open('toy.cfg', 'w'); out.write(s); out.close() >>> # Load the edited grammar, & display it. >>> cfg = nltk.data.load('file:toy.cfg') >>> print cfg # doctest: +ELLIPSIS Grammar with 14 productions (start state = S) S -> DP VP PP -> P DP ... P -> 'on' P -> 'in' The second argument to `nltk.data.retrieve()` specifies the filename for the new copy of the file. By default, the source file's filename is used. >>> nltk.data.retrieve('grammars/sample_grammars/toy.cfg', 'mytoy.cfg') Retrieving 'grammars/sample_grammars/toy.cfg', saving to 'mytoy.cfg' >>> os.path.isfile('./mytoy.cfg') True >>> nltk.data.retrieve('grammars/sample_grammars/np.fcfg') Retrieving 'grammars/sample_grammars/np.fcfg', saving to 'np.fcfg' >>> os.path.isfile('./np.fcfg') True If a file with the specified (or default) filename already exists in the current directory, then `nltk.data.retrieve()` will raise a ValueError exception. It will *not* overwrite the file: >>> os.path.isfile('./toy.cfg') True >>> nltk.data.retrieve('grammars/sample_grammars/toy.cfg') # doctest: +ELLIPSIS Traceback (most recent call last): . . . ValueError: File '.../toy.cfg' already exists! .. This will not be visible in the html output: clean up the tempdir. >>> os.chdir(old_dir) >>> for f in os.listdir(tempdir): ... os.remove(os.path.join(tempdir, f)) >>> os.rmdir(tempdir) Finding Files in the NLTK Data Package ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The `nltk.data.find()` function searches the NLTK data package for a given file, and returns a pointer to that file. This pointer can either be a `FileSystemPathPointer` (whose `path` attribute gives the absolute path of the file); or a `ZipFilePathPointer`, specifying a zipfile and the name of an entry within that zipfile. Both pointer types define the `open()` method, which can be used to read the string contents of the file. >>> path = nltk.data.find('corpora/abc/rural.txt') >>> str(path).endswith('/nltk_data/corpora/abc/rural.txt') # doctest: +ELLIPSIS True >>> path.open().read(60) 'PM denies knowledge of AWB kickbacks\nThe Prime Minister has ' Alternatively, the `nltk.data.load()` function can be used with the keyword argument ``format="raw"``: >>> nltk.data.load('corpora/abc/rural.txt', format='raw')[:60] 'PM denies knowledge of AWB kickbacks\nThe Prime Minister has ' Resource Caching ~~~~~~~~~~~~~~~~ NLTK uses a weakref dictionary to maintain a cache of resources that have been loaded. If you load a resource that is already stored in the cache, then the cached copy will be returned. This behavior can be seen by the trace output generated when verbose=True: >>> feat0 = nltk.data.load('grammars/book_grammars/feat0.fcfg', verbose=True) <> >>> feat0 = nltk.data.load('grammars/book_grammars/feat0.fcfg', verbose=True) <> If you wish to load a resource from its source, bypassing the cache, use the ``cache=False`` argument to `nltk.data.load()`. This can be useful, for example, if the resource is loaded from a local file, and you are actively editing that file: >>> feat0 = nltk.data.load('grammars/book_grammars/feat0.fcfg',cache=False,verbose=True) <> The cache *no longer* uses weak references. A resource will not be automatically expunged from the cache when no more objects are using it. In the following example, when we clear the variable ``feat0``, the reference count for the feature grammar object drops to zero. However, the object remains cached: >>> del feat0 >>> feat0 = nltk.data.load('grammars/book_grammars/feat0.fcfg', ... verbose=True) <> You can clear the entire contents of the cache, using `nltk.data.clear_cache()`: >>> nltk.data.clear_cache() Retrieving other Data Sources ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ >>> formulas = nltk.data.load('grammars/book_grammars/background.fol') >>> for f in formulas: print str(f) all x.(boxerdog(x) -> dog(x)) all x.(boxer(x) -> person(x)) all x.-(dog(x) & person(x)) all x.(married(x) <-> exists y.marry(x,y)) all x.(bark(x) -> dog(x)) all x y.(marry(x,y) -> (person(x) & person(y))) -(Vincent = Mia) -(Vincent = Fido) -(Mia = Fido) Regression Tests ~~~~~~~~~~~~~~~~ Create a temp dir for tests that write files: >>> import tempfile, os >>> tempdir = tempfile.mkdtemp() >>> old_dir = os.path.abspath('.') >>> os.chdir(tempdir) The `retrieve()` function accepts all url types: >>> urls = ['http://nltk.googlecode.com/svn/trunk/nltk/nltk/test/toy.cfg', ... 'file:%s' % nltk.data.find('grammars/sample_grammars/toy.cfg'), ... 'nltk:grammars/sample_grammars/toy.cfg', ... 'grammars/sample_grammars/toy.cfg'] >>> for i, url in enumerate(urls): ... nltk.data.retrieve(url, 'toy-%d.cfg' % i) # doctest: +ELLIPSIS Retrieving 'http://nltk.googlecode.com/svn/trunk/nltk/nltk/test/toy.cfg', saving to 'toy-0.cfg' Retrieving 'file:/.../nltk_data/grammars/sample_grammars/toy.cfg', saving to 'toy-1.cfg' Retrieving 'nltk:grammars/sample_grammars/toy.cfg', saving to 'toy-2.cfg' Retrieving 'grammars/sample_grammars/toy.cfg', saving to 'toy-3.cfg' Clean up the temp dir: >>> os.chdir(old_dir) >>> for f in os.listdir(tempdir): ... os.remove(os.path.join(tempdir, f)) >>> os.rmdir(tempdir) .. note:: load(..., format='yaml') is not currently tested, because we do not have any yaml files in our data distribution yet. We should probably have a trained brill tagger there. Lazy Loader ----------- A lazy loader is a wrapper object that defers loading a resource until it is accessed or used in any way. This is mainly intended for internal use by NLTK's corpus readers. >>> # Create a lazy loader for toy.cfg. >>> ll = nltk.data.LazyLoader('grammars/sample_grammars/toy.cfg') >>> # Show that it's not loaded yet: >>> object.__repr__(ll) # doctest: +ELLIPSIS '' >>> # printing it is enough to cause it to be loaded: >>> print ll >>> # Show that it's now been loaded: >>> object.__repr__(ll) # doctest: +ELLIPSIS '' >>> # Test that accessing an attribute also loads it: >>> ll = nltk.data.LazyLoader('grammars/sample_grammars/toy.cfg') >>> ll.start() S >>> object.__repr__(ll) # doctest: +ELLIPSIS '' nltk-2.0~b9/nltk/test/corpus.doctest0000644000175000017500000026744311423114516017425 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ================ Corpus Readers ================ The `nltk.corpus` package defines a collection of *corpus reader* classes, which can be used to access the contents of a diverse set of corpora. The list of available corpora is given at: http://nltk.googlecode.com/svn/trunk/nltk_data/index.xml Each corpus reader class is specialized to handle a specific corpus format. In addition, the `nltk.corpus` package automatically creates a set of corpus reader instances that can be used to access the corpora in the NLTK data package. Section `Corpus Reader Objects`_ ("Corpus Reader Objects") describes the corpus reader instances that can be used to read the corpora in the NLTK data package. Section `Corpus Reader Classes`_ ("Corpus Reader Classes") describes the corpus reader classes themselves, and discusses the issues involved in creating new corpus reader objects and new corpus reader classes. Section `Regression Tests`_ ("Regression Tests") contains regression tests for the corpus readers and associated functions and classes. .. contents:: **Table of Contents** :depth: 2 :backlinks: none --------------------- Corpus Reader Objects --------------------- Overview ======== NLTK includes a diverse set of corpora which can be read using the ``nltk.corpus`` package. Each corpus is accessed by means of a "corpus reader" object from ``nltk.corpus``: >>> import nltk.corpus >>> # The Brown corpus: >>> nltk.corpus.brown >>> # The Penn Treebank Corpus: >>> nltk.corpus.treebank >>> # The Name Genders Corpus: >>> nltk.corpus.names >>> # The Inaugural Address Corpus: >>> nltk.corpus.inaugural Most corpora consist of a set of files, each containing a document (or other pieces of text). A list of identifiers for these files is accessed via the ``fileids()`` method of the corpus reader: >>> nltk.corpus.treebank.fileids() # doctest: +ELLIPSIS ['wsj_0001.mrg', 'wsj_0002.mrg', 'wsj_0003.mrg', 'wsj_0004.mrg', ...] >>> nltk.corpus.inaugural.fileids() # doctest: +ELLIPSIS ['1789-Washington.txt', '1793-Washington.txt', '1797-Adams.txt', ...] Each corpus reader provides a variety of methods to read data from the corpus, depending on the format of the corpus. For example, plaintext corpora support methods to read the corpus as raw text, a list of words, a list of sentences, or a list of paragraphs. >>> from nltk.corpus import inaugural >>> inaugural.raw('1789-Washington.txt') # doctest: +ELLIPSIS 'Fellow-Citizens of the Senate ... >>> inaugural.words('1789-Washington.txt') ['Fellow', '-', 'Citizens', 'of', 'the', 'Senate', ...] >>> inaugural.sents('1789-Washington.txt') # doctest: +ELLIPSIS [['Fellow', '-', 'Citizens'...], ['Among', 'the', 'vicissitudes'...]...] >>> inaugural.paras('1789-Washington.txt') # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [[['Fellow', '-', 'Citizens'...]], [['Among', 'the', 'vicissitudes'...], ['On', 'the', 'one', 'hand', ',', 'I'...]...]...] Each of these reader methods may be given a single document's item name or a list of document item names. When given a list of document item names, the reader methods will concatenate together the contents of the individual documents. >>> l1 = len(inaugural.words('1789-Washington.txt')) >>> l2 = len(inaugural.words('1793-Washington.txt')) >>> l3 = len(inaugural.words(['1789-Washington.txt', '1793-Washington.txt'])) >>> print '%s+%s == %s' % (l1, l2, l3) 1538+147 == 1685 If the reader methods are called without any arguments, they will typically load all documents in the corpus. >>> len(inaugural.words()) 145735 If a corpus contains a README file, it can be accessed with a ``readme()`` method: >>> inaugural.readme()[:32] 'C-Span Inaugural Address Corpus\n' Plaintext Corpora ================= Here are the first few words from each of NLTK's plaintext corpora: >>> nltk.corpus.abc.words() ['PM', 'denies', 'knowledge', 'of', 'AWB', 'kickbacks', ...] >>> nltk.corpus.genesis.words() [u'In', u'the', u'beginning', u'God', u'created', ...] >>> nltk.corpus.gutenberg.words(fileids='austen-emma.txt') ['[', 'Emma', 'by', 'Jane', 'Austen', '1816', ']', ...] >>> nltk.corpus.inaugural.words() ['Fellow', '-', 'Citizens', 'of', 'the', 'Senate', ...] >>> nltk.corpus.state_union.words() ['PRESIDENT', 'HARRY', 'S', '.', 'TRUMAN', "'", 'S', ...] >>> nltk.corpus.udhr.words() # doctest: +NORMALIZE_WHITESPACE ['\xc0\xf3\xe0\xfe\xfb\xf2\xfa\xfb\xfe\xf1\xe0', '\xe8\xe7\xe8\xed6\xfa\xe0', ...] >>> nltk.corpus.webtext.words() ['Cookie', 'Manager', ':', '"', 'Don', "'", 't', ...] We can obtain character offsets for tokens in plaintext corpora using the ``sourced`` flag: >>> nltk.corpus.inaugural.words(sourced=True) ['Fellow'@[0:6], '-'@[6], 'Citizens'@[7:15], ...] Tagged Corpora ============== In addition to the plaintext corpora, NLTK's data package also contains a wide variety of annotated corpora. For example, the Brown Corpus is annotated with part-of-speech tags, and defines additional methods ``tagged_*()`` which words as `(word,tag)` tuples, rather than just bare word strings. >>> from nltk.corpus import brown >>> print brown.words() ['The', 'Fulton', 'County', 'Grand', 'Jury', 'said', ...] >>> print brown.tagged_words() [('The', 'AT'), ('Fulton', 'NP-TL'), ...] >>> print brown.sents() # doctest: +ELLIPSIS [['The', 'Fulton', 'County'...], ['The', 'jury', 'further'...], ...] >>> print brown.tagged_sents() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [[('The', 'AT'), ('Fulton', 'NP-TL')...], [('The', 'AT'), ('jury', 'NN'), ('further', 'RBR')...]...] >>> print brown.paras(categories='reviews') # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [[['It', 'is', 'not', 'news', 'that', 'Nathan', 'Milstein'...], ['Certainly', 'not', 'in', 'Orchestra', 'Hall', 'where'...]], [['There', 'was', 'about', 'that', 'song', 'something', ...], ['Not', 'the', 'noblest', 'performance', 'we', 'have', ...], ...], ...] >>> print brown.tagged_paras(categories='reviews') # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [[[('It', 'PPS'), ('is', 'BEZ'), ('not', '*'), ...], [('Certainly', 'RB'), ('not', '*'), ('in', 'IN'), ...]], [[('There', 'EX'), ('was', 'BEDZ'), ('about', 'IN'), ...], [('Not', '*'), ('the', 'AT'), ('noblest', 'JJT'), ...], ...], ...] Similarly, the Indian Langauge POS-Tagged Corpus includes samples of Indian text annotated with part-of-speech tags: >>> from nltk.corpus import indian >>> print indian.words() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE ['\xe0\xa6\xae\xe0\xa6\xb9\xe0\xa6\xbf\...', '\xe0\xa6\xb8\xe0\xa6\xa8\xe0\xa7\x8d\xe0...', ...] >>> print indian.tagged_words() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [('\xe0\xa6\xae\xe0\xa6\xb9\xe0\xa6\xbf...', 'NN'), ('\xe0\xa6\xb8\xe0\xa6\xa8\xe0\xa7\x8d\xe0...', 'NN'), ...] Tagged corpora support access to simplified tags, e.g. where all nouns tags are collapsed to a single category ``N``: >>> print brown.tagged_sents(simplify_tags=True) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [[('The', 'DET'), ('Fulton', 'NP'), ('County', 'N'), ('Grand', 'ADJ'), ...], [('The', 'DET'), ('jury', 'N'), ('further', 'ADV'), ('said', 'VD'), ...]...] Use ``nltk.draw.pos-concordance()`` to access a GUI for searching tagged corpora. Chunked Corpora =============== The CoNLL corpora also provide chunk structures, which are encoded as flat trees. The CoNLL 2000 Corpus includes phrasal chunks; and the CoNLL 2002 Corpus includes named entity chunks. >>> from nltk.corpus import conll2000, conll2002 >>> print conll2000.sents() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [['Confidence', 'in', 'the', 'pound', 'is', 'widely', ...], ['Chancellor', 'of', 'the', 'Exchequer', ...], ...] >>> for tree in conll2000.chunked_sents()[:2]: ... print tree # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE (S (NP Confidence/NN) (PP in/IN) (NP the/DT pound/NN) (VP is/VBZ widely/RB expected/VBN to/TO take/VB) (NP another/DT sharp/JJ dive/NN) if/IN ...) (S Chancellor/NNP (PP of/IN) (NP the/DT Exchequer/NNP) ...) >>> print conll2002.sents() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [[u'Sao', u'Paulo', u'(', u'Brasil', u')', u',', ...], [u'-'], ...] >>> for tree in conll2002.chunked_sents()[:2]: ... print tree # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE (S (LOC Sao/NC Paulo/VMI) (/Fpa (LOC Brasil/NC) )/Fpt ...) (S -/Fg) .. note:: Since the CONLL corpora do not contain paragraph break information, these readers do not support the ``para()`` method.) .. warning:: if you call the conll corpora reader methods without any arguments, they will return the contents of the entire corpus, *including* the 'test' portions of the corpus.) The IEER corpus is another chunked corpus. This corpus is unusual in that each corpus item contains multiple documents. (This reflects the fact that each corpus file contains multiple documents.) The IEER corpus defines the `parsed_docs` method, which returns the documents in a given item as `IEERDocument` objects: >>> from nltk.corpus import ieer >>> ieer.fileids() # doctest: +NORMALIZE_WHITESPACE ['APW_19980314', 'APW_19980424', 'APW_19980429', 'NYT_19980315', 'NYT_19980403', 'NYT_19980407'] >>> docs = ieer.parsed_docs('APW_19980314') >>> print docs[0] >>> print docs[0].docno APW19980314.0391 >>> print docs[0].doctype NEWS STORY >>> print docs[0].date_time 03/14/1998 10:36:00 >>> print docs[0].headline (DOCUMENT Kenyans protest tax hikes) >>> print docs[0].text # doctest: +ELLIPSIS (DOCUMENT (LOCATION NAIROBI) , (LOCATION Kenya) ( (ORGANIZATION AP) ) _ (CARDINAL Thousands) of laborers, ... on (DATE Saturday) ...) Parsed Corpora ============== The Treebank corpora provide a syntactic parse for each sentence. The NLTK data package includes a 10% sample of the Penn Treebank (in ``treebank``), as well as the Sinica Treebank (in ``sinica_treebank``). Reading the Penn Treebank: >>> from nltk.corpus import treebank >>> print treebank.fileids() # doctest: +ELLIPSIS ['wsj_0001.mrg', 'wsj_0002.mrg', 'wsj_0003.mrg', 'wsj_0004.mrg', ...] >>> print treebank.words('wsj_0003.mrg') ['A', 'form', 'of', 'asbestos', 'once', 'used', '*', ...] >>> print treebank.tagged_words('wsj_0003.mrg') [('A', 'DT'), ('form', 'NN'), ('of', 'IN'), ...] >>> print treebank.parsed_sents('wsj_0003.mrg')[0] # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE (S (S-TPC-1 (NP-SBJ (NP (NP (DT A) (NN form)) (PP (IN of) (NP (NN asbestos)))) (RRC ...)...)...) ... (VP (VBD reported) (SBAR (-NONE- 0) (S (-NONE- *T*-1)))) (. .)) Reading the Sinica Treebank: >>> from nltk.corpus import sinica_treebank >>> print sinica_treebank.sents() [['\xe4\xb8\x80'], ['\xe5\x8f\x8b\xe6\x83\x85'], ...] >>> sinica_treebank.parsed_sents()[25] # doctest: +NORMALIZE_WHITESPACE Tree('S', [Tree('NP', [Tree('Nba', ['\xe5\x98\x89\xe7\x8f\x8d'])]), Tree('V\xe2\x80\xa7\xe5\x9c\xb0', [Tree('VA11', ['\xe4\xb8\x8d\xe5\x81\x9c']), Tree('DE', ['\xe7\x9a\x84'])]), Tree('VA4', ['\xe5\x93\xad\xe6\xb3\xa3'])]) Reading the CoNLL 2007 Dependency Treebanks: >>> from nltk.corpus import conll2007 >>> conll2007.sents('cat.train') [['El', 'aumento', 'del', '\xc3\xadndice', 'de', 'desempleo', 'estadounidense', 'fortaleci\xc3\xb3', 'hoy', 'considerablemente', 'al', 'euro', ',', 'que', 'a', 'las', '15.35', 'GMT', 'se', 'cotizaba', 'en', 'el', 'mercado', 'de', 'divisas', 'de', 'Fr\xc3\xa1ncfort', 'a', '0,9452_d\xc3\xb3lares', ',', 'frente_a', 'los', '0,9349_d\xc3\xb3lares', 'de', 'esta', 'ma\xc3\xb1ana', '.'], ['El', 'Banco_Central_Europeo', '-', 'BCE', '-', 'fij\xc3\xb3', 'el', 'cambio', 'oficial', 'del', 'euro', 'en', 'los', '0,9355_d\xc3\xb3lares', '.'], ...] >>> conll2007.parsed_sents('cat.train')[0] >>> conll2007.parsed_sents('cat.train')[0].tree() # doctest: +NORMALIZE_WHITESPACE Tree('fortaleci\xc3\xb3', [Tree('aumento', ['El', Tree('del', [Tree('\xc3\xadndice', [Tree('de', [Tree('desempleo', ['estadounidense'])])])])]), 'hoy', 'considerablemente', Tree('al', [Tree('euro', [Tree('cotizaba', [',', 'que', Tree('a', [Tree('15.35', ['las', 'GMT'])]), 'se', Tree('en', [Tree('mercado', ['el', Tree('de', ['divisas']), Tree('de', ['Fr\xc3\xa1ncfort'])])]), Tree('a', ['0,9452_d\xc3\xb3lares']), Tree('frente_a', [',', Tree('0,9349_d\xc3\xb3lares', ['los', Tree('de', [Tree('ma\xc3\xb1ana', ['esta'])])])])])])]), '.']) NLTK also provides a corpus reader for the York-Toronto-Helsinki Parsed Corpus of Old English Prose (YCOE); but the corpus itself is not included in the NLTK data package. If you install it yourself, you can use NLTK to access it: >>> from nltk.corpus import ycoe >>> for tree in ycoe.parsed_sents('cocuraC')[:4]: ... print tree # doctest: +SKIP (CP-THT (C +D+atte) (IP-SUB ...) ... (. .)) (IP-MAT (IP-MAT-0 (PP (P On) (NP (ADJ o+dre) (N wisan)))...) ... (. .)) (IP-MAT (NP-NOM-x-2 *exp*) (NP-DAT-1 (D^D +D+am) (ADJ^D unge+dyldegum)) ... (. .)) (IP-MAT (ADVP (ADV Sw+a)) (NP-NOM-x (PRO^N hit)) (ADVP-TMP (ADV^T oft)) ... (. .)) If the YCOE corpus is not available, you will get an error message when you try to access it: >>> from nltk.corpus import ycoe >>> print ycoe # doctest: +SKIP Traceback (most recent call last): LookupError: ********************************************************************** Resource 'corpora/ycoe' not found. For installation instructions, please see . Searched in: - ... ********************************************************************** Word Lists and Lexicons ======================= The NLTK data package also includes a number of lexicons and word lists. These are accessed just like text corpora. The following examples illustrate the use of the wordlist corpora: >>> from nltk.corpus import names, stopwords, words >>> words.fileids() ['en', 'en-basic'] >>> words.words('en') # doctest: +ELLIPSIS ['A', 'a', 'aa', 'aal', 'aalii', 'aam', 'Aani', 'aardvark', 'aardwolf', ...] >>> stopwords.fileids() # doctest: +ELLIPSIS ['danish', 'dutch', 'english', 'finnish', 'french', 'german', 'hungarian', ...] >>> stopwords.words('portuguese') # doctest: +ELLIPSIS ['de', 'a', 'o', 'que', 'e', 'do', 'da', 'em', 'um', 'para', ...] >>> names.fileids() ['female.txt', 'male.txt'] >>> names.words('male.txt') # doctest: +ELLIPSIS ['Aamir', 'Aaron', 'Abbey', 'Abbie', 'Abbot', 'Abbott', ...] >>> names.words('female.txt') # doctest: +ELLIPSIS ['Abagael', 'Abagail', 'Abbe', 'Abbey', 'Abbi', 'Abbie', ...] The CMU Pronunciation Dictionary corpus contains pronounciation transcriptions for over 100,000 words. It can be accessed as a list of entries (where each entry consists of a word, an identifier, and a transcription) or as a dictionary from words to lists of transcriptions. Transcriptions are encoded as tuples of phoneme strings. >>> from nltk.corpus import cmudict >>> print cmudict.entries()[653:659] # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [('acetate', ['AE1', 'S', 'AH0', 'T', 'EY2', 'T']), ('acetic', ['AH0', 'S', 'EH1', 'T', 'IH0', 'K']), ('acetic', ['AH0', 'S', 'IY1', 'T', 'IH0', 'K']), ('aceto', ['AA0', 'S', 'EH1', 'T', 'OW0']), ('acetochlor', ['AA0', 'S', 'EH1', 'T', 'OW0', 'K', 'L', 'AO2', 'R']), ('acetone', ['AE1', 'S', 'AH0', 'T', 'OW2', 'N'])] >>> # Load the entire cmudict corpus into a Python dictionary: >>> transcr = cmudict.dict() >>> print [transcr[w][0] for w in 'Natural Language Tool Kit'.lower().split()] # doctest: +NORMALIZE_WHITESPACE [['N', 'AE1', 'CH', 'ER0', 'AH0', 'L'], ['L', 'AE1', 'NG', 'G', 'W', 'AH0', 'JH'], ['T', 'UW1', 'L'], ['K', 'IH1', 'T']] Categorized Corpora =================== Several corpora included with NLTK contain documents that have been categorized for topic, genre, polarity, etc. In addition to the standard corpus interface, these corpora provide access to the list of categories and the mapping between the documents and their categories (in both directions). Access the categories using the ``categories()`` method, e.g.: >>> from nltk.corpus import brown, movie_reviews, reuters >>> brown.categories() # doctest: +NORMALIZE_WHITESPACE ['adventure', 'belles_lettres', 'editorial', 'fiction', 'government', 'hobbies', 'humor', 'learned', 'lore', 'mystery', 'news', 'religion', 'reviews', 'romance', 'science_fiction'] >>> movie_reviews.categories() ['neg', 'pos'] >>> reuters.categories() # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS ['acq', 'alum', 'barley', 'bop', 'carcass', 'castor-oil', 'cocoa', 'coconut', 'coconut-oil', 'coffee', 'copper', 'copra-cake', 'corn', 'cotton', 'cotton-oil', 'cpi', 'cpu', 'crude', 'dfl', 'dlr', ...] This method has an optional argument that specifies a document or a list of documents, allowing us to map from (one or more) documents to (one or more) categories: >>> brown.categories('ca01') ['news'] >>> brown.categories(['ca01','cb01']) ['editorial', 'news'] >>> reuters.categories('training/9865') ['barley', 'corn', 'grain', 'wheat'] >>> reuters.categories(['training/9865', 'training/9880']) ['barley', 'corn', 'grain', 'money-fx', 'wheat'] We can go back the other way using the optional argument of the ``fileids()`` method: >>> reuters.fileids('barley') # doctest: +ELLIPSIS ['test/15618', 'test/15649', 'test/15676', 'test/15728', 'test/15871', ...] Both the ``categories()`` and ``fileids()`` methods return a sorted list containing no duplicates. In addition to mapping between categories and documents, these corpora permit direct access to their contents via the categories. Instead of accessing a subset of a corpus by specifying one or more fileids, we can identify one or more categories, e.g.: >>> brown.tagged_words(categories='news') [('The', 'AT'), ('Fulton', 'NP-TL'), ...] >>> brown.sents(categories=['editorial','reviews']) # doctest: +NORMALIZE_WHITESPACE [['Assembly', 'session', 'brought', 'much', 'good'], ['The', 'General', 'Assembly', ',', 'which', 'adjourns', 'today', ',', 'has', 'performed', 'in', 'an', 'atmosphere', 'of', 'crisis', 'and', 'struggle', 'from', 'the', 'day', 'it', 'convened', '.'], ...] Note that it is an error to specify both documents and categories. In the context of a text categorization system, we can easily test if the category assigned to a document is correct as follows: >>> def classify(doc): return 'news' # Trivial classifier >>> doc = 'ca01' >>> classify(doc) in brown.categories(doc) True Propbank Corpus =============== The Propbank corpus provides predicate-argument annotation for the entire treebank. Each verb in the treebank is annotated by a single instance in the propbank corpus, containing information about the location of the verb, and the location and identity of its arguments: >>> from nltk.corpus import propbank >>> pb_instances = propbank.instances() >>> print pb_instances # doctest: +NORMALIZE_WHITESPACE [, , ...] Each propbank instance defines the following member variables: - Location information: `fileid`, `sentnum`, `wordnum` - Annotator information: `tagger` - Inflection information: `inflection` - Roleset identifier: `roleset` - Verb (aka predicate) location: `predicate` - Argument locations and types: `arguments` The following examples show the types of these arguments: >>> inst = pb_instances[103] >>> (inst.fileid, inst.sentnum, inst.wordnum) ('wsj_0004.mrg', 8, 16) >>> inst.tagger 'gold' >>> inst.inflection >>> infl = inst.inflection >>> infl.form, infl.tense, infl.aspect, infl.person, infl.voice ('v', 'p', '-', '-', 'a') >>> inst.roleset 'rise.01' >>> inst.predicate PropbankTreePointer(16, 0) >>> inst.arguments # doctest: +NORMALIZE_WHITESPACE ((PropbankTreePointer(0, 2), 'ARG1'), (PropbankTreePointer(13, 1), 'ARGM-DIS'), (PropbankTreePointer(17, 1), 'ARG4-to'), (PropbankTreePointer(20, 1), 'ARG3-from')) The location of the predicate and of the arguments are encoded using `PropbankTreePointer` objects, as well as `PropbankChainTreePointer` objects and `PropbankSplitTreePointer` objects. A `PropbankTreePointer` consists of a `wordnum` and a `height`: >>> print inst.predicate.wordnum, inst.predicate.height 16 0 This identifies the tree constituent that is headed by the word that is the `wordnum`\ 'th token in the sentence, and whose span is found by going `height` nodes up in the tree. This type of pointer is only useful if we also have the corresponding tree structure, since it includes empty elements such as traces in the word number count. The trees for 10% of the standard propbank corpus are contained in the `treebank` corpus: >>> tree = inst.tree >>> from nltk.corpus import treebank >>> assert tree == treebank.parsed_sents(inst.fileid)[inst.sentnum] >>> inst.predicate.select(tree) Tree('VBD', ['rose']) >>> for (argloc, argid) in inst.arguments: ... print '%-10s %s' % (argid, argloc.select(tree).pprint(500)[:50]) ARG1 (NP-SBJ (NP (DT The) (NN yield)) (PP (IN on) (NP ( ARGM-DIS (PP (IN for) (NP (NN example))) ARG4-to (PP-DIR (TO to) (NP (CD 8.04) (NN %))) ARG3-from (PP-DIR (IN from) (NP (CD 7.90) (NN %))) Propbank tree pointers can be converted to standard tree locations, which are usually easier to work with, using the `treepos()` method: >>> treepos = inst.predicate.treepos(tree) >>> print treepos, tree[treepos] (4, 0) (VBD rose) In some cases, argument locations will be encoded using `PropbankChainTreePointer`\ s (for trace chains) or `PropbankSplitTreePointer`\ s (for discontinuous constituents). Both of these objects contain a single member variable, `pieces`, containing a list of the constituent pieces. They also define the method `select()`, which will return a tree containing all the elements of the argument. (A new head node is created, labeled "*CHAIN*" or "*SPLIT*", since the argument is not a single constituent in the original tree). Sentence #6 contains an example of an argument that is both discontinuous and contains a chain: >>> inst = pb_instances[6] >>> inst.roleset 'expose.01' >>> argloc, argid = inst.arguments[2] >>> argloc >>> argloc.pieces [, PropbankTreePointer(27, 0)] >>> argloc.pieces[0].pieces ... # doctest: +NORMALIZE_WHITESPACE [PropbankTreePointer(22, 1), PropbankTreePointer(24, 0), PropbankTreePointer(25, 1)] >>> print argloc.select(inst.tree) (*CHAIN* (*SPLIT* (NP (DT a) (NN group)) (IN of) (NP (NNS workers))) (-NONE- *)) The propbank corpus also provides access to the frameset files, which define the argument labels used by the annotations, on a per-verb basis. Each frameset file contains one or more predicates, such as 'turn' or 'turn_on', each of which is divided into coarse-grained word senses called rolesets. For each roleset, the frameset file provides descriptions of the argument roles, along with examples. >>> expose_01 = propbank.roleset('expose.01') >>> turn_01 = propbank.roleset('turn.01') >>> print turn_01 # doctest: +ELLIPSIS >>> for role in turn_01.findall("roles/role"): ... print role.attrib['n'], role.attrib['descr'] 0 turner 1 thing turning m direction, location >>> from nltk.etree import ElementTree >>> print ElementTree.tostring(turn_01.find('example')).strip() John turned the key in the lock. John turned the key in the lock Note that the standard corpus distribution only contains 10% of the treebank, so the parse trees are not availalbe for instances starting at 9353: >>> inst = pb_instances[9352] >>> inst.fileid 'wsj_0199.mrg' >>> print inst.tree # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS (S (NP-SBJ (NNP Trinity)) (VP (VBD said) (SBAR (-NONE- 0) ...)) >>> print inst.predicate.select(inst.tree) (VB begin) >>> inst = pb_instances[9353] >>> inst.fileid 'wsj_0200.mrg' >>> print inst.tree None >>> print inst.predicate.select(inst.tree) Traceback (most recent call last): . . . ValueError: Parse tree not avaialable However, if you supply your own version of the treebank corpus (by putting it before the nltk-provided version on `nltk.data.path`), then you can access the trees for all instances. A list of the verb lemmas contained in propbank is returned by the `propbank.verbs()` method: >>> propbank.verbs() ['abandon', 'abate', 'abdicate', 'abet', 'abide', ...] Other Corpora ============= senseval -------- The Senseval 2 corpus is a word sense disambiguation corpus. Each item in the corpus corresponds to a single ambiguous word. For each of these words, the corpus contains a list of instances, corresponding to occurences of that word. Each instance provides the word; a list of word senses that apply to the word occurrence; and the word's context. >>> from nltk.corpus import senseval >>> senseval.fileids() ['hard.pos', 'interest.pos', 'line.pos', 'serve.pos'] >>> senseval.instances('hard.pos') ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [SensevalInstance(word='hard-a', position=20, context=[('``', '``'), ('he', 'PRP'), ...('hard', 'JJ'), ...], senses=('HARD1',)), SensevalInstance(word='hard-a', position=10, context=[('clever', 'NNP'), ...('hard', 'JJ'), ('time', 'NN'), ...], senses=('HARD1',)), ...] The following code looks at instances of the word 'interest', and displays their local context (2 words on each side) and word sense(s): >>> for inst in senseval.instances('interest.pos')[:10]: ... p = inst.position ... left = ' '.join(w for (w,t) in inst.context[p-2:p]) ... word = ' '.join(w for (w,t) in inst.context[p:p+1]) ... right = ' '.join(w for (w,t) in inst.context[p+1:p+3]) ... senses = ' '.join(inst.senses) ... print '%20s |%10s | %-15s -> %s' % (left, word, right, senses) declines in | interest | rates . -> interest_6 indicate declining | interest | rates because -> interest_6 in short-term | interest | rates . -> interest_6 4 % | interest | in this -> interest_5 company with | interests | in the -> interest_5 , plus | interest | . -> interest_6 set the | interest | rate on -> interest_6 's own | interest | , prompted -> interest_4 principal and | interest | is the -> interest_6 increase its | interest | to 70 -> interest_5 ppattach -------- The Prepositional Phrase Attachment corpus is a corpus of prepositional phrase attachment decisions. Each instance in the corpus is encoded as a ``PPAttachment`` object: >>> from nltk.corpus import ppattach >>> ppattach.attachments('training') # doctest: +NORMALIZE_WHITESPACE [PPAttachment(sent='0', verb='join', noun1='board', prep='as', noun2='director', attachment='V'), PPAttachment(sent='1', verb='is', noun1='chairman', prep='of', noun2='N.V.', attachment='N'), ...] >>> inst = ppattach.attachments('training')[0] >>> (inst.sent, inst.verb, inst.noun1, inst.prep, inst.noun2) ('0', 'join', 'board', 'as', 'director') >>> inst.attachment 'V' semcor ------ The Brown Corpus, annotated with WordNet senses. >>> from nltk.corpus import semcor >>> semcor.words('brown2/tagfiles/br-n12.xml') # doctest: +ELLIPSIS ['When', 'several', 'minutes', 'had', 'passed', ...] >>> sent = semcor.xml('brown2/tagfiles/br-n12.xml').findall('context/p/s')[0] >>> for wordform in sent.getchildren(): ... print wordform.text, ... for key in wordform.keys(): ... print key + '=' + wordform.get(key), ... print ... Brenner pn=person cmd=done lexsn=1:03:00:: pos=NNP lemma=person rdf=person wnsn=1 re-entered lemma=re-enter cmd=done wnsn=1 pos=VB lexsn=2:38:00:: the cmd=ignore pos=DT hotel lemma=hotel cmd=done wnsn=1 pos=NN lexsn=1:06:00:: and cmd=ignore pos=CC faced lemma=face cmd=done wnsn=4 pos=VB lexsn=2:42:02:: Summers pn=person cmd=done lexsn=1:03:00:: pos=NNP lemma=person rdf=person wnsn=1 across cmd=ignore pos=IN the cmd=ignore pos=DT counter lemma=counter cmd=done wnsn=1 pos=NN lexsn=1:06:00:: shakespeare ----------- The Shakespeare corpus contains a set of Shakespeare plays, formatted as XML files. These corpora are returned as ElementTree objects: >>> from nltk.corpus import shakespeare >>> from nltk.etree import ElementTree >>> shakespeare.fileids() # doctest: +ELLIPSIS ['a_and_c.xml', 'dream.xml', 'hamlet.xml', 'j_caesar.xml', ...] >>> play = shakespeare.xml('dream.xml') >>> print play # doctest: +ELLIPSIS >>> print '%s: %s' % (play[0].tag, play[0].text) TITLE: A Midsummer Night's Dream >>> personae = [persona.text for persona in ... play.findall('PERSONAE/PERSONA')] >>> print personae # doctest: +ELLIPSIS ['THESEUS, Duke of Athens.', 'EGEUS, father to Hermia.', ...] >>> # Find and print speakers not listed as personae >>> names = [persona.split(',')[0] for persona in personae] >>> speakers = set(speaker.text for speaker in ... play.findall('*/*/*/SPEAKER')) >>> print sorted(speakers.difference(names)) # doctest: +NORMALIZE_WHITESPACE ['ALL', 'COBWEB', 'DEMETRIUS', 'Fairy', 'HERNIA', 'LYSANDER', 'Lion', 'MOTH', 'MUSTARDSEED', 'Moonshine', 'PEASEBLOSSOM', 'Prologue', 'Pyramus', 'Thisbe', 'Wall'] toolbox ------- The Toolbox corpus distributed with NLTK contains a sample lexicon and several sample texts from the Rotokas language. The Toolbox corpus reader returns Toolbox files as XML ElementTree objects. The following example loads the Rotokas dictionary, and figures out the distribution of part-of-speech tags for reduplicated words. >>> from nltk.corpus import toolbox >>> from nltk.etree import ElementTree >>> from nltk.probability import FreqDist >>> import re >>> rotokas = toolbox.xml('rotokas.dic') >>> redup_pos_freqdist = FreqDist() >>> # Note: we skip over the first record, which is actually >>> # the header. >>> for record in rotokas[1:]: ... lexeme = record.find('lx').text ... if re.match(r'(.*)\1$', lexeme): ... redup_pos_freqdist.inc(record.find('ps').text) >>> for item in redup_pos_freqdist.keys(): ... print item, redup_pos_freqdist[item] V 41 N 14 ??? 4 This example displays some records from a Rotokas text: >>> river = toolbox.xml('rotokas/river.txt', key='ref') >>> for record in river.findall('record')[:3]: ... for piece in record: ... if len(piece.text) > 60: ... print '%-6s %s...' % (piece.tag, piece.text[:57]) ... else: ... print '%-6s %s' % (piece.tag, piece.text) ref Paragraph 1 t ``Viapau oisio ra ovaupasi ... m viapau oisio ra ovau -pa -si ... g NEG this way/like this and forget -PROG -2/3.DL... p NEG ??? CONJ V.I -SUFF.V.3 -SUFF.V... f ``No ken lus tingting wanema samting papa i bin tok,'' Na... fe ``Don't forget what Dad said,'' yelled Naomi. ref 2 t Osa Ira ora Reviti viapau uvupasiva. m osa Ira ora Reviti viapau uvu -pa -si ... g as/like name and name NEG hear/smell -PROG -2/3... p CONJ N.PN CONJ N.PN NEG V.T -SUFF.V.3 -SUF... f Tasol Ila na David no bin harim toktok. fe But Ila and David took no notice. ref 3 t Ikaupaoro rokosiva ... m ikau -pa -oro roko -si -va ... g run/hurry -PROG -SIM go down -2/3.DL.M -RP ... p V.T -SUFF.V.3 -SUFF.V.4 ADV -SUFF.V.4 -SUFF.VT.... f Tupela i bin hariap i go long wara . fe They raced to the river. timit ----- The NLTK data package includes a fragment of the TIMIT Acoustic-Phonetic Continuous Speech Corpus. This corpus is broken down into small speech samples, each of which is available as a wave file, a phonetic transcription, and a tokenized word list. >>> from nltk.corpus import timit >>> print timit.utterances() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE ['dr1-fvmh0/sa1', 'dr1-fvmh0/sa2', 'dr1-fvmh0/si1466', 'dr1-fvmh0/si2096', 'dr1-fvmh0/si836', 'dr1-fvmh0/sx116', 'dr1-fvmh0/sx206', 'dr1-fvmh0/sx26', 'dr1-fvmh0/sx296', ...] >>> item = timit.utterances()[5] >>> print timit.phones(item) # doctest: +NORMALIZE_WHITESPACE ['h#', 'k', 'l', 'ae', 's', 'pcl', 'p', 'dh', 'ax', 's', 'kcl', 'k', 'r', 'ux', 'ix', 'nx', 'y', 'ax', 'l', 'eh', 'f', 'tcl', 't', 'hh', 'ae', 'n', 'dcl', 'd', 'h#'] >>> print timit.words(item) ['clasp', 'the', 'screw', 'in', 'your', 'left', 'hand'] >>> timit.play(item) # doctest: +SKIP The corpus reader can combine the word segmentation information with the phonemes to produce a single tree structure: >>> for tree in timit.phone_trees(item): ... print tree (S h# (clasp k l ae s pcl p) (the dh ax) (screw s kcl k r ux) (in ix nx) (your y ax) (left l eh f tcl t) (hand hh ae n dcl d) h#) The start time and stop time of each phoneme, word, and sentence are also available: >>> print timit.phone_times(item) # doctest: +ELLIPSIS [('h#', 0, 2190), ('k', 2190, 3430), ('l', 3430, 4326), ...] >>> print timit.word_times(item) # doctest: +ELLIPSIS [('clasp', 2190, 8804), ('the', 8804, 9734), ...] >>> print timit.sent_times(item) [('Clasp the screw in your left hand.', 0, 32154)] We can use these times to play selected pieces of a speech sample: >>> timit.play(item, 2190, 8804) # 'clasp' # doctest: +SKIP The corpus reader can also be queried for information about the speaker and sentence identifier for a given speech sample: >>> print timit.spkrid(item) dr1-fvmh0 >>> print timit.sentid(item) sx116 >>> print timit.spkrinfo(timit.spkrid(item)) # doctest: +NORMALIZE_WHITESPACE SpeakerInfo(id='VMH0', sex='F', dr='1', use='TRN', recdate='03/11/86', birthdate='01/08/60', ht='5\'05"', race='WHT', edu='BS', comments='BEST NEW ENGLAND ACCENT SO FAR') >>> # List the speech samples from the same speaker: >>> timit.utterances(spkrid=timit.spkrid(item)) # doctest: +ELLIPSIS ['dr1-fvmh0/sa1', 'dr1-fvmh0/sa2', 'dr1-fvmh0/si1466', ...] rte --- The RTE (Recognizing Textual Entailment) corpus was derived from the RTE1, RTE2 and RTE3 datasets (dev and test data), and consists of a list of XML-formatted 'text'/'hypothesis' pairs. >>> from nltk.corpus import rte >>> print rte.fileids() # doctest: +ELLIPSIS ['rte1_dev.xml', 'rte1_test.xml', 'rte2_dev.xml', ..., 'rte3_test.xml'] >>> rtepairs = rte.pairs(['rte2_test.xml', 'rte3_test.xml']) >>> print rtepairs # doctest: +ELLIPSIS [, , , ...] In the gold standard test sets, each pair is labeled according to whether or not the text 'entails' the hypothesis; the entailment value is mapped to an integer 1 (True) or 0 (False). >>> rtepairs[5] >>> rtepairs[5].text # doctest: +NORMALIZE_WHITESPACE 'His wife Strida won a seat in parliament after forging an alliance with the main anti-Syrian coalition in the recent election.' >>> rtepairs[5].hyp 'Strida elected to parliament.' >>> rtepairs[5].value 1 The RTE corpus also supports an ``xml()`` method which produces ElementTrees. >>> xmltree = rte.xml('rte3_dev.xml') >>> xmltree # doctest: +SKIP >>> xmltree[7].findtext('t') # doctest: +NORMALIZE_WHITESPACE "Mrs. Bush's approval ratings have remained very high, above 80%, even as her husband's have recently dropped below 50%." verbnet ------- The VerbNet corpus is a lexicon that divides verbs into classes, based on their syntax-semantics linking behavior. The basic elements in the lexicon are verb lemmas, such as 'abandon' and 'accept', and verb classes, which have identifiers such as 'remove-10.1' and 'admire-31.2-1'. These class identifiers consist of a representitive verb selected from the class, followed by a numerical identifier. The list of verb lemmas, and the list of class identifiers, can be retrieved with the following methods: >>> from nltk.corpus import verbnet >>> verbnet.lemmas()[20:25] ['accelerate', 'accept', 'acclaim', 'accompany', 'accrue'] >>> verbnet.classids()[:5] ['accompany-51.7', 'admire-31.2', 'admire-31.2-1', 'admit-65', 'adopt-93'] The `classids()` method may also be used to retrieve the classes that a given lemma belongs to: >>> verbnet.classids('accept') ['approve-77', 'characterize-29.2-1-1', 'obtain-13.5.2'] The primary object in the lexicon is a class record, which is stored as an ElementTree xml object. The class record for a given class identifier is returned by the `vnclass()` method: >>> verbnet.vnclass('remove-10.1') # doctest: +ELLIPSIS The `vnclass()` method also accepts "short" identifiers, such as '10.1': >>> verbnet.vnclass('10.1') # doctest: +ELLIPSIS See the Verbnet documentation, or the Verbnet files, for information about the structure of this xml. As an example, we can retrieve a list of thematic roles for a given Verbnet class: >>> vn_31_2 = verbnet.vnclass('admire-31.2') >>> for themrole in vn_31_2.findall('THEMROLES/THEMROLE'): ... print themrole.attrib['type'], ... for selrestr in themrole.findall('SELRESTRS/SELRESTR'): ... print '[%(Value)s%(type)s]' % selrestr.attrib, ... print Theme Experiencer [+animate] Predicate The Verbnet corpus also provides a variety of pretty printing functions that can be used to display the xml contents in a more consise form. The simplest such method is `pprint()`: >>> print verbnet.pprint('57') weather-57 Subclasses: (none) Members: blow clear drizzle fog freeze gust hail howl lightning mist mizzle pelt pour precipitate rain roar shower sleet snow spit spot sprinkle storm swelter teem thaw thunder Thematic roles: * Theme[+concrete +force] Frames: Intransitive (Expletive Subject) Syntax: LEX[it] LEX[[+be]] VERB Semantics: * weather(during(E), Weather_type, ?Theme) NP (Expletive Subject, Theme Object) Syntax: LEX[it] LEX[[+be]] VERB NP[Theme] Semantics: * weather(during(E), Weather_type, Theme) PP (Expletive Subject, Theme-PP) Syntax: LEX[it[+be]] VERB PREP[with] NP[Theme] Semantics: * weather(during(E), Weather_type, Theme) nps_chat -------- The NPS Chat Corpus, Release 1.0 consists of over 10,000 posts in age-specific chat rooms, which have been anonymized, POS-tagged and dialogue-act tagged. >>> print nltk.corpus.nps_chat.words() ['now', 'im', 'left', 'with', 'this', 'gay', 'name', ...] >>> print nltk.corpus.nps_chat.tagged_words() [('now', 'RB'), ('im', 'PRP'), ('left', 'VBD'), ...] >>> print nltk.corpus.nps_chat.tagged_posts() # doctest: +NORMALIZE_WHITESPACE [[('now', 'RB'), ('im', 'PRP'), ('left', 'VBD'), ('with', 'IN'), ('this', 'DT'), ('gay', 'JJ'), ('name', 'NN')], [(':P', 'UH')], ...] We can access the XML elements corresponding to individual posts. These elements have ``class`` and ``user`` attributes that we can access using ``p.attrib['class']`` and ``p.attrib['user']``. They also have text content, accessed using ``p.text``. >>> print nltk.corpus.nps_chat.xml_posts() # doctest: +ELLIPSIS [, , ...] >>> posts = nltk.corpus.nps_chat.xml_posts() >>> nltk.FreqDist(p.attrib['class'] for p in posts).keys() # doctest: +NORMALIZE_WHITESPACE ['Statement', 'System', 'Greet', 'Emotion', 'ynQuestion', 'whQuestion', 'Accept', 'Bye', 'Emphasis', 'Continuer', 'Reject', 'yAnswer', 'nAnswer', 'Clarify', 'Other'] >>> posts[0].text 'now im left with this gay name' In addition to the above methods for accessing tagged text, we can navigate the XML structure directly, as follows: >>> tokens = posts[0].findall('terminals/t') >>> [t.attrib['pos'] + "/" + t.attrib['word'] for t in tokens] ['RB/now', 'PRP/im', 'VBD/left', 'IN/with', 'DT/this', 'JJ/gay', 'NN/name'] --------------------- Corpus Reader Classes --------------------- NLTK's *corpus reader* classes are used to access the contents of a diverse set of corpora. Each corpus reader class is specialized to handle a specific corpus format. Examples include the `PlaintextCorpusReader`, which handles corpora that consist of a set of unannotated text files, and the `BracketParseCorpusReader`, which handles corpora that consist of files containing parenthesis-delineated parse trees. Automatically Created Corpus Reader Instances ============================================= When then `nltk.corpus` module is imported, it automatically creates a set of corpus reader instances that can be used to access the corpora in the NLTK data distribution. Here is a small sample of those corpus reader instances: >>> import nltk >>> nltk.corpus.brown # doctest: +ELLIPSIS >>> nltk.corpus.treebank # doctest: +ELLIPSIS >>> nltk.corpus.names # doctest: +ELLIPSIS >>> nltk.corpus.genesis # doctest: +ELLIPSIS >>> nltk.corpus.inaugural # doctest: +ELLIPSIS This sample illustrates that different corpus reader classes are used to read different corpora; but that the same corpus reader class may be used for more than one corpus (e.g., ``genesis`` and ``inaugural``). Creating New Corpus Reader Instances ==================================== Although the `nltk.corpus` module automatically creates corpus reader instances for the corpora in the NLTK data distribution, you may sometimes need to create your own corpus reader. In particular, you would need to create your own corpus reader if you want... - To access a corpus that is not included in the NLTK data distribution. - To access a full copy of a corpus for which the NLTK data distribution only provides a sample. - To access a corpus using a customized corpus reader (e.g., with a customized tokenizer). To create a new corpus reader, you will first need to look up the signature for that corpus reader's constructor. Different corpus readers have different constructor signatures, but most of the constructor signatures have the basic form:: SomeCorpusReader(root, files, ...options...) Where ``root`` is an absolute path to the directory containing the corpus data files; ``files`` is either a list of file names (relative to ``root``) or a regexp specifying which files should be included; and ``options`` are additional reader-specific options. For example, we can create a customized corpus reader for the genesis corpus that uses a different sentence tokenizer as follows: >>> # Find the directory where the corpus lives. >>> genesis_dir = nltk.data.find('corpora/genesis.zip').join('genesis/') >>> # Create our custom sentence tokenizer. >>> my_sent_tokenizer = nltk.RegexpTokenizer('[^.!?]+') >>> # Create the new corpus reader object. >>> my_genesis = nltk.corpus.PlaintextCorpusReader( ... genesis_dir, '.*\.txt', sent_tokenizer=my_sent_tokenizer) >>> # Use the new corpus reader object. >>> print my_genesis.sents('english-kjv.txt')[0] # doctest: +NORMALIZE_WHITESPACE ['In', 'the', 'beginning', 'God', 'created', 'the', 'heaven', 'and', 'the', 'earth'] If you wish to read your own plaintext corpus, which is stored in the directory '/usr/share/some-corpus', then you can create a corpus reader for it with:: >>> my_corpus = nltk.corpus.PlaintextCorpusReader( ... '/usr/share/some-corpus', '.*\.txt') # doctest: +SKIP For a complete list of corpus reader subclasses, see the API documentation for `nltk.corpus.CorpusReader`. Corpus Types ============ Corpora vary widely in the types of content they include. This is reflected in the fact that the base class `CorpusReader` only defines a few general-purpose methods for listing and accessing the files that make up a corpus. It is up to the subclasses to define *data access methods* that provide access to the information in the corpus. However, corpus reader subclasses should be consistent in their definitions of these data access methods wherever possible. At a high level, corpora can be divided into three basic types: - A *token corpus* contains information about specific occurences of language use (or linguistic tokens), such as dialogues or written texts. Examples of token corpora are collections of written text and collections of speech. - A *type corpus*, or *lexicon*, contains information about a coherent set of lexical items (or linguistic types). Examples of lexicons are dictionaries and word lists. - A *language description corpus* contains information about a set of non-lexical linguistic constructs, such as grammar rules. However, many individual corpora blur the distinctions between these types. For example, corpora that are primarily lexicons may include token data in the form of example sentences; and corpora that are primarily token corpora may be accompanied by one or more word lists or other lexical data sets. Because corpora vary so widely in their information content, we have decided that it would not be wise to use separate corpus reader base classes for different corpus types. Instead, we simply try to make the corpus readers consistent wherever possible, but let them differ where the underlying data itself differs. Common Corpus Reader Methods ============================ As mentioned above, there are only a handful of methods that all corpus readers are guaranteed to implement. These methods provide access to the files that contain the corpus data. Every corpus is assumed to consist of one or more files, all located in a common root directory (or in subdirectories of that root directory). The absolute path to the root directory is stored in the ``root`` property: >>> print nltk.corpus.genesis.root # doctest: +ELLIPSIS ZipFilePathPointer('/usr/share/nltk_data/corpora/genesis.zip', 'genesis/') Each file within the corpus is identified by a platform-independent identifier, which is basically a path string that uses ``/`` as the path seperator. I.e., this identifier can be converted to a relative path as follows: >>> some_corpus_file_id = nltk.corpus.reuters.fileids()[0] >>> import os.path >>> os.path.join(*some_corpus_file_id.split('/')) 'test/14826' To get a list of all data files that make up a corpus, use the ``fileids()`` method. In some corpora, these files will not all contain the same type of data; for example, for the ``nltk.corpus.timit`` corpus, ``fileids()`` will return a list including text files, word segmentation files, phonetic transcription files, sound files, and metadata files. For corpora with diverse file types, the ``fileids()`` method will often take one or more optional arguments, which can be used to get a list of the files with a specific file type: >>> nltk.corpus.timit.fileids() # doctest: +ELLIPSIS ['dr1-fvmh0/sa1.phn', 'dr1-fvmh0/sa1.txt', 'dr1-fvmh0/sa1.wav', ...] >>> nltk.corpus.timit.fileids('phn') # doctest: +ELLIPSIS ['dr1-fvmh0/sa1.phn', 'dr1-fvmh0/sa2.phn', 'dr1-fvmh0/si1466.phn', ...] In some corpora, the files are divided into distinct categories. For these corpora, the ``fileids()`` method takes an optional argument, which can be used to get a list of the files within a specific category: >>> nltk.corpus.brown.fileids('hobbies') # doctest: +ELLIPSIS ['ce01', 'ce02', 'ce03', 'ce04', 'ce05', 'ce06', 'ce07', ...] The ``abspath()`` method can be used to find the absolute path to a corpus file, given its file identifier: >>> nltk.corpus.brown.abspath('ce06') # doctest: +ELLIPSIS FileSystemPathPointer('.../corpora/brown/ce06') The ``abspaths()`` method can be used to find the absolute paths for one corpus file, a list of corpus files, or all corpus files, depending on its argument type: >>> nltk.corpus.brown.abspaths() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [FileSystemPathPointer('.../corpora/brown/ca01'), FileSystemPathPointer('.../corpora/brown/ca02'), ...] >>> nltk.corpus.brown.abspaths('ce06') # doctest: +ELLIPSIS [FileSystemPathPointer('.../corpora/brown/ce06')] >>> nltk.corpus.brown.abspaths(['ce06', 'ce07']) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [FileSystemPathPointer('.../corpora/brown/ce06'), FileSystemPathPointer('.../corpora/brown/ce07')] This method is mainly useful as a helper method when defining corpus data access methods, since data access methods can usually be called with a string argument (to get a view for a specific file), with a list argument (to get a view for a specific list of files), or with no argument (to get a view for the whole corpus). Data Access Methods =================== Individual corpus reader subclasses typically extend this basic set of file-access methods with one or more *data access methods*, which provide easy access to the data contained in the corpus. The signatures for data access methods often have the basic form:: corpus_reader.some_data access(fileids=None, ...options...) Where ``fileids`` can be a single file identifier string (to get a view for a specific file); a list of file identifier strings (to get a view for a specific list of files); or None (to get a view for the entire corpus). Some of the common data access methods, and their return types, are: - I{corpus}.words(): list of str - I{corpus}.sents(): list of (list of str) - I{corpus}.paras(): list of (list of (list of str)) - I{corpus}.tagged_words(): list of (str,str) tuple - I{corpus}.tagged_sents(): list of (list of (str,str)) - I{corpus}.tagged_paras(): list of (list of (list of (str,str))) - I{corpus}.chunked_sents(): list of (Tree w/ (str,str) leaves) - I{corpus}.parsed_sents(): list of (Tree with str leaves) - I{corpus}.parsed_paras(): list of (list of (Tree with str leaves)) - I{corpus}.xml(): A single xml ElementTree - I{corpus}.raw(): str (unprocessed corpus contents) For example, the `words()` method is supported by many different corpora, and returns a flat list of word strings: >>> nltk.corpus.brown.words() ['The', 'Fulton', 'County', 'Grand', 'Jury', 'said', ...] >>> nltk.corpus.treebank.words() ['Pierre', 'Vinken', ',', '61', 'years', 'old', ',', ...] >>> nltk.corpus.conll2002.words() [u'Sao', u'Paulo', u'(', u'Brasil', u')', u',', u'23', ...] >>> nltk.corpus.genesis.words() ['In', 'the', 'beginning', 'God', 'created', 'the', ...] On the other hand, the `tagged_words()` method is only supported by corpora that include part-of-speech annotations: >>> nltk.corpus.brown.tagged_words() [('The', 'AT'), ('Fulton', 'NP-TL'), ...] >>> nltk.corpus.treebank.tagged_words() [('Pierre', 'NNP'), ('Vinken', 'NNP'), (',', ','), ...] >>> nltk.corpus.conll2002.tagged_words() [(u'Sao', u'NC'), (u'Paulo', u'VMI'), (u'(', u'Fpa'), ...] >>> nltk.corpus.genesis.tagged_words() Traceback (most recent call last): ... AttributeError: 'PlaintextCorpusReader' object has no attribute 'tagged_words' Although most corpus readers use file identifiers to index their content, some corpora use different identifiers instead. For example, the data access methods for the ``timit`` corpus uses *utterance identifiers* to select which corpus items should be returned: >>> nltk.corpus.timit.utterances() # doctest: +ELLIPSIS ['dr1-fvmh0/sa1', 'dr1-fvmh0/sa2', 'dr1-fvmh0/si1466', ...] >>> nltk.corpus.timit.words('dr1-fvmh0/sa2') ["don't", 'ask', 'me', 'to', 'carry', 'an', 'oily', 'rag', 'like', 'that'] Attempting to call ``timit``\ 's data access methods with a file identifier will result in an exception: >>> nltk.corpus.timit.fileids() # doctest: +ELLIPSIS ['dr1-fvmh0/sa1.phn', 'dr1-fvmh0/sa1.txt', 'dr1-fvmh0/sa1.wav', ...] >>> nltk.corpus.timit.words('dr1-fvmh0/sa1.txt') # doctest: +ELLIPSIS Traceback (most recent call last): ... IOError:... No such file or directory: '.../dr1-fvmh0/sa1.txt.wrd' As another example, the ``propbank`` corpus defines the ``roleset()`` method, which expects a roleset identifier, not a file identifier: >>> roleset = nltk.corpus.propbank.roleset('eat.01') >>> from nltk.etree import ElementTree as ET >>> print ET.tostring(roleset) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE ...... ... ... Stream Backed Corpus Views ========================== An important feature of NLTK's corpus readers is that many of them access the underlying data files using "corpus views." A *corpus view* is an object that acts like a simple data structure (such as a list), but does not store the data elements in memory; instead, data elements are read from the underlying data files on an as-needed basis. By only loading items from the file on an as-needesd basis, corpus views maintain both memory efficiency and responsiveness. The memory efficiency of corpus readers is important because some corpora contain very large amounts of data, and storing the entire data set in memory could overwhelm many machines. The responsiveness is important when experimenting with corpora in interactive sessions and in in-class demonstrations. The most common corpus view is the `StreamBackedCorpusView`, which acts as a read-only list of tokens. Two additional corpus view classes, `ConcatenatedCorpusView` and `LazySubsequence`, make it possible to create concatenations and take slices of `StreamBackedCorpusView` objects without actually storing the resulting list-like object's elements in memory. In the future, we may add additional corpus views that act like other basic data structures, such as dictionaries. Writing New Corpus Readers ========================== In order to add support for new corpus formats, it is necessary to define new corpus reader classes. For many corpus formats, writing new corpus readers is relatively streight-forward. In this section, we'll describe what's involved in creating a new corpus reader. If you do create a new corpus reader, we encourage you to contribute it back to the NLTK project. Don't Reinvent the Wheel ------------------------ Before you start writing a new corpus reader, you should check to be sure that the desired format can't be read using an existing corpus reader with appropriate constructor arguments. For example, although the `TaggedCorpusReader` assumes that words and tags are separated by ``/`` characters by default, an alternative tag-separation character can be specified via the ``sep`` constructor argument. You should also check whether the new corpus format can be handled by subclassing an existing corpus reader, and tweaking a few methods or variables. Design ------ If you decide to write a new corpus reader from scratch, then you should first decide which data access methods you want the reader to provide, and what their signatures should be. You should look at existing corpus readers that process corpora with similar data contents, and try to be consistent with those corpus readers whenever possible. You should also consider what sets of identifiers are appropriate for the corpus format. Where it's practical, file identifiers should be used. However, for some corpora, it may make sense to use additional sets of identifiers. Each set of identifiers should have a distinct name (e.g., files, utterances, rolesets); and you should be consistent in using that name to refer to that identifier. Do not use parameter names like ``id``, which leave it unclear what type of identifier is required. Once you've decided what data access methods and identifiers are appropriate for your corpus, you should decide if there are any customizable parameters that you'd like the corpus reader to handle. These parameters make it possible to use a single corpus reader to handle a wider variety of corpora. The ``sep`` argument for `TaggedCorpusReader`, mentioned above, is an example of a customizable corpus reader parameter. Implementation -------------- Constructor ~~~~~~~~~~~ If your corpus reader implements any customizable parameters, then you'll need to override the constructor. Typically, the new constructor will first call its base class's constructor, and then store the customizable parameters. For example, the `ConllChunkCorpusReader`\ 's constructor is defined as follows: >>> def __init__(self, root, files, chunk_types): ... CorpusReader.__init__(self, root, files) ... self.chunk_types = tuple(chunk_types) If your corpus reader does not implement any customization parameters, then you can often just inherit the base class's constructor. Data Access Methods ~~~~~~~~~~~~~~~~~~~ The most common type of data access method takes an argument identifying which files to access, and returns a view covering those files. This argument may be a a single file identifier string (to get a view for a specific file); a list of file identifier strings (to get a view for a specific list of files); or None (to get a view for the entire corpus). The method's implementation converts this argument to a list of path names using the `abspaths()` method, which handles all three value types (string, list, and None): >>> nltk.corpus.brown.abspaths() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [FileSystemPathPointer('.../corpora/brown/ca01'), FileSystemPathPointer('.../corpora/brown/ca02'), ...] >>> nltk.corpus.brown.abspaths('ce06') # doctest: +ELLIPSIS [FileSystemPathPointer('.../corpora/brown/ce06')] >>> nltk.corpus.brown.abspaths(['ce06', 'ce07']) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [FileSystemPathPointer('.../corpora/brown/ce06'), FileSystemPathPointer('.../corpora/brown/ce07')] An example of this type of method is the `words()` method, defined by the `PlaintextCorpusReader` as follows: >>> def words(self, fileids=None): ... return concat([self.CorpusView(fileid, self._read_word_block) ... for fileid in self.abspaths(fileids)]) This method first uses `abspaths()` to convert ``fileids`` to a list of absolute paths. It then creates a corpus view for each file, using the `PlaintextCorpusReader._read_word_block()` method to read elements from the data file (see the discussion of corpus views below). Finally, it combines these corpus views using the `nltk.corpus.reader.util.concat()` function. When writing a corpus reader for a corpus that is never expected to be very large, it can sometimes be appropriate to read the files directly, rather than using a corpus view. For example, the `WordListCorpusView` class defines its `words()` method as follows: >>> def words(self, fileids=None): ... return concat([[w for w in open(fileid).read().split('\n') if w] ... for fileid in self.abspaths(fileids)]) (This is usually more appropriate for lexicons than for token corpora.) If the type of data returned by a data access method is one for which NLTK has a conventional representation (e.g., words, tagged words, and parse trees), then you should use that representation. Otherwise, you may find it necessary to define your own representation. For data structures that are relatively corpus-specific, it's usually best to define new classes for these elements. For example, the ``propbank`` corpus defines the `PropbankInstance` class to store the semantic role labeling instances described by the corpus; and the ``ppattach`` corpus defines the `PPAttachment` class to store the prepositional attachment instances described by the corpus. Corpus Views ~~~~~~~~~~~~ .. (Much of the content for this section is taken from the StreamBackedCorpusView docstring.) The heart of a `StreamBackedCorpusView` is its *block reader* function, which reads zero or more tokens from a stream, and returns them as a list. A very simple example of a block reader is: >>> def simple_block_reader(stream): ... return stream.readline().split() This simple block reader reads a single line at a time, and returns a single token (consisting of a string) for each whitespace-separated substring on the line. A `StreamBackedCorpusView` built from this block reader will act like a read-only list of all the whitespace-seperated tokens in an underlying file. When deciding how to define the block reader for a given corpus, careful consideration should be given to the size of blocks handled by the block reader. Smaller block sizes will increase the memory requirements of the corpus view's internal data structures (by 2 integers per block). On the other hand, larger block sizes may decrease performance for random access to the corpus. (But note that larger block sizes will *not* decrease performance for iteration.) Internally, the `StreamBackedCorpusView` class maintains a partial mapping from token index to file position, with one entry per block. When a token with a given index *i* is requested, the corpus view constructs it as follows: 1. First, it searches the toknum/filepos mapping for the token index closest to (but less than or equal to) *i*. 2. Then, starting at the file position corresponding to that index, it reads one block at a time using the block reader until it reaches the requested token. The toknum/filepos mapping is created lazily: it is initially empty, but every time a new block is read, the block's initial token is added to the mapping. (Thus, the toknum/filepos map has one entry per block.) You can create your own corpus view in one of two ways: 1. Call the `StreamBackedCorpusView` constructor, and provide your block reader function via the ``block_reader`` argument. 2. Subclass `StreamBackedCorpusView`, and override the `read_block()` method. The first option is usually easier, but the second option can allow you to write a single `read_block` method whose behavior can be customized by different parameters to the subclass's constructor. For an example of this design pattern, see the `TaggedCorpusView` class, which is used by `TaggedCorpusView`. ---------------- Regression Tests ---------------- The following helper functions are used to create and then delete testing corpora that are stored in temporary directories. These testing corpora are used to make sure the readers work correctly. >>> import tempfile, os.path, textwrap >>> def make_testcorpus(ext='', **fileids): ... root = tempfile.mkdtemp() ... for fileid, contents in fileids.items(): ... fileid += ext ... f = open(os.path.join(root, fileid), 'w') ... f.write(textwrap.dedent(contents)) ... f.close() ... return root >>> def del_testcorpus(root): ... for fileid in os.listdir(root): ... os.remove(os.path.join(root, fileid)) ... os.rmdir(root) Plaintext Corpus Reader ======================= The plaintext corpus reader is used to access corpora that consist of unprocessed plaintext data. It assumes that paragraph breaks are indicated by blank lines. Sentences and words can be tokenized using the default tokenizers, or by custom tokenizers specificed as parameters to the constructor. >>> root = make_testcorpus(ext='.txt', ... a="""\ ... This is the first sentence. Here is another ... sentence! And here's a third sentence. ... ... This is the second paragraph. Tokenization is currently ... fairly simple, so the period in Mr. gets tokenized. ... """, ... b="""This is the second file.""") >>> from nltk.corpus.reader.plaintext import PlaintextCorpusReader The list of documents can be specified explicitly, or implicitly (using a regexp). The ``ext`` argument specifies a file extension. >>> corpus = PlaintextCorpusReader(root, ['a.txt', 'b.txt']) >>> corpus.fileids() ['a.txt', 'b.txt'] >>> corpus = PlaintextCorpusReader(root, '.*\.txt') >>> corpus.fileids() ['a.txt', 'b.txt'] The directory containing the corpus is corpus.root: >>> str(corpus.root) == str(root) True We can get a list of words, or the raw string: >>> corpus.words() ['This', 'is', 'the', 'first', 'sentence', '.', 'Here', ...] >>> corpus.raw()[:40] 'This is the first sentence. Here is ano' Check that reading individual documents works, and reading all documents at once works: >>> len(corpus.words()), [len(corpus.words(d)) for d in corpus.fileids()] (46, [40, 6]) >>> corpus.words('a.txt') ['This', 'is', 'the', 'first', 'sentence', '.', 'Here', ...] >>> corpus.words('b.txt') ['This', 'is', 'the', 'second', 'file', '.'] >>> corpus.words()[:4], corpus.words()[-4:] (['This', 'is', 'the', 'first'], ['the', 'second', 'file', '.']) We're done with the test corpus: >>> del_testcorpus(root) Test the plaintext corpora that come with nltk: >>> from nltk.corpus import abc, genesis, inaugural >>> from nltk.corpus import state_union, webtext, udhr >>> for corpus in (abc, genesis, inaugural, state_union, ... webtext, udhr): ... print corpus ... print ' ', `corpus.fileids()`[:60] ... print ' ', `corpus.words()[:10]`[:60] ... # doctest: +ELLIPSIS ['rural.txt', 'science.txt'] ['PM', 'denies', 'knowledge', 'of', 'AWB', 'kickbacks', 'The ['english-kjv.txt', 'english-web.txt', 'finnish.txt', 'frenc ['In', 'the', 'beginning', 'God', 'created', 'the', 'heaven' ['1789-Washington.txt', '1793-Washington.txt', '1797-Adams.t ['Fellow', '-', 'Citizens', 'of', 'the', 'Senate', 'and', 'o ['1945-Truman.txt', '1946-Truman.txt', '1947-Truman.txt', '1 ['PRESIDENT', 'HARRY', 'S', '.', 'TRUMAN', "'", 'S', 'ADDRES ['firefox.txt', 'grail.txt', 'overheard.txt', 'pirates.txt', ['Cookie', 'Manager', ':', '"', 'Don', "'", 't', 'allow', 's ['Abkhaz-Cyrillic+Abkh', 'Abkhaz-UTF8', 'Achehnese-Latin1', ['\xc0\xf3\xe0\xfe\xfb\xf2\xfa\xfb\xfe\xf1\xe0', '\xe8\xe7\x Tagged Corpus Reader ==================== The Tagged Corpus reader can give us words, sentences, and paragraphs, each tagged or untagged. All of the read methods can take one item (in which case they return the contents of that file) or a list of documents (in which case they concatenate the contents of those files). By default, they apply to all documents in the corpus. >>> root = make_testcorpus( ... a="""\ ... This/det is/verb the/det first/adj sentence/noun ./punc ... Here/det is/verb another/adj sentence/noun ./punc ... Note/verb that/comp you/pron can/verb use/verb \ ... any/noun tag/noun set/noun ... ... This/det is/verb the/det second/adj paragraph/noun ./punc ... word/n without/adj a/det tag/noun :/: hello ./punc ... """, ... b="""\ ... This/det is/verb the/det second/adj file/noun ./punc ... """) >>> from nltk.corpus.reader.tagged import TaggedCorpusReader >>> corpus = TaggedCorpusReader(root, list('ab')) >>> corpus.fileids() ['a', 'b'] >>> str(corpus.root) == str(root) True >>> corpus.words() ['This', 'is', 'the', 'first', 'sentence', '.', 'Here', ...] >>> corpus.sents() # doctest: +ELLIPSIS [['This', 'is', 'the', 'first', ...], ['Here', 'is', 'another'...], ...] >>> corpus.paras() # doctest: +ELLIPSIS [[['This', ...], ['Here', ...], ...], [['This', ...], ...], ...] >>> corpus.tagged_words() # doctest: +ELLIPSIS [('This', 'DET'), ('is', 'VERB'), ('the', 'DET'), ...] >>> corpus.tagged_sents() # doctest: +ELLIPSIS [[('This', 'DET'), ('is', 'VERB'), ...], [('Here', 'DET'), ...], ...] >>> corpus.tagged_paras() # doctest: +ELLIPSIS [[[('This', 'DET'), ...], ...], [[('This', 'DET'), ...], ...], ...] >>> corpus.raw()[:40] 'This/det is/verb the/det first/adj sente' >>> len(corpus.words()), [len(corpus.words(d)) for d in corpus.fileids()] (38, [32, 6]) >>> len(corpus.sents()), [len(corpus.sents(d)) for d in corpus.fileids()] (6, [5, 1]) >>> len(corpus.paras()), [len(corpus.paras(d)) for d in corpus.fileids()] (3, [2, 1]) >>> corpus.words('a') ['This', 'is', 'the', 'first', 'sentence', '.', 'Here', ...] >>> corpus.words('b') ['This', 'is', 'the', 'second', 'file', '.'] >>> del_testcorpus(root) The Brown Corpus uses the tagged corpus reader: >>> from nltk.corpus import brown >>> brown.fileids() # doctest: +ELLIPSIS ['ca01', 'ca02', 'ca03', 'ca04', 'ca05', 'ca06', 'ca07', ...] >>> brown.categories() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE ['adventure', 'belles_lettres', 'editorial', 'fiction', 'government', 'hobbies', 'humor', 'learned', 'lore', 'mystery', 'news', 'religion', 'reviews', 'romance', 'science_fiction'] >>> brown.root # doctest: +ELLIPSIS FileSystemPathPointer('.../corpora/brown') >>> brown.words() ['The', 'Fulton', 'County', 'Grand', 'Jury', 'said', ...] >>> brown.sents() # doctest: +ELLIPSIS [['The', 'Fulton', 'County', 'Grand', ...], ...] >>> brown.paras() # doctest: +ELLIPSIS [[['The', 'Fulton', 'County', ...]], [['The', 'jury', ...]], ...] >>> brown.tagged_words() # doctest: +ELLIPSIS [('The', 'AT'), ('Fulton', 'NP-TL'), ...] >>> brown.tagged_sents() # doctest: +ELLIPSIS [[('The', 'AT'), ('Fulton', 'NP-TL'), ('County', 'NN-TL'), ...], ...] >>> brown.tagged_paras() # doctest: +ELLIPSIS [[[('The', 'AT'), ...]], [[('The', 'AT'), ...]], ...] Verbnet Corpus Reader ===================== Make sure we're picking up the right number of elements: >>> from nltk.corpus import verbnet >>> len(verbnet.lemmas()) 3621 >>> len(verbnet.wordnetids()) 4953 >>> len(verbnet.classids()) 429 Selecting classids based on various selectors: >>> verbnet.classids(lemma='take') # doctest: +NORMALIZE_WHITESPACE ['bring-11.3', 'characterize-29.2', 'convert-26.6.2', 'cost-54.2', 'fit-54.3', 'performance-26.7-2', 'steal-10.5'] >>> verbnet.classids(wordnetid='lead%2:38:01') ['accompany-51.7'] >>> verbnet.classids(fileid='approve-77.xml') ['approve-77'] >>> verbnet.classids(classid='admire-31.2') # subclasses ['admire-31.2-1'] vnclass() accepts filenames, long ids, and short ids: >>> a = ElementTree.tostring(verbnet.vnclass('admire-31.2.xml')) >>> b = ElementTree.tostring(verbnet.vnclass('admire-31.2')) >>> c = ElementTree.tostring(verbnet.vnclass('31.2')) >>> a == b == c True fileids() can be used to get files based on verbnet class ids: >>> verbnet.fileids('admire-31.2') ['admire-31.2.xml'] >>> verbnet.fileids(['admire-31.2', 'obtain-13.5.2']) ['admire-31.2.xml', 'obtain-13.5.2.xml'] >>> verbnet.fileids('badidentifier') Traceback (most recent call last): . . . ValueError: vnclass identifier 'badidentifier' not found longid() and shortid() can be used to convert identifiers: >>> verbnet.longid('31.2') 'admire-31.2' >>> verbnet.longid('admire-31.2') 'admire-31.2' >>> verbnet.shortid('31.2') '31.2' >>> verbnet.shortid('admire-31.2') '31.2' >>> verbnet.longid('badidentifier') Traceback (most recent call last): . . . ValueError: vnclass identifier 'badidentifier' not found >>> verbnet.shortid('badidentifier') Traceback (most recent call last): . . . ValueError: vnclass identifier 'badidentifier' not found Corpus View Regression Tests ============================ Select some corpus files to play with: >>> import nltk.data >>> # A very short file (160 chars): >>> f1 = nltk.data.find('corpora/inaugural/README') >>> # A relatively short file (791 chars): >>> f2 = nltk.data.find('corpora/inaugural/1793-Washington.txt') >>> # A longer file (32k chars): >>> f3 = nltk.data.find('corpora/inaugural/1909-Taft.txt') >>> fileids = [f1, f2, f3] Check that corpus views produce the correct sequence of values. >>> from nltk.corpus.reader.util import * >>> linetok = nltk.LineTokenizer(blanklines='discard-eof') >>> for f in fileids: ... v = StreamBackedCorpusView(f, read_whitespace_block) ... assert list(v) == open(f).read().split() ... v = StreamBackedCorpusView(f, read_line_block) ... assert list(v) == linetok.tokenize(open(f).read()) Check that the corpus views report the correct lengths: >>> for f in fileids: ... v = StreamBackedCorpusView(f, read_whitespace_block) ... assert len(v) == len(open(f).read().split()) ... v = StreamBackedCorpusView(f, read_line_block) ... assert len(v) == len(linetok.tokenize(open(f).read())) Concatenation ------------- Check that concatenation works as intended. >>> c1 = StreamBackedCorpusView(f1, read_whitespace_block) >>> c2 = StreamBackedCorpusView(f2, read_whitespace_block) >>> c3 = StreamBackedCorpusView(f3, read_whitespace_block) >>> c123 = c1+c2+c3 >>> print c123 ['C-Span', 'Inaugural', 'Address', 'Corpus', 'US', ...] >>> l1 = open(f1).read().split() >>> l2 = open(f2).read().split() >>> l3 = open(f3).read().split() >>> l123 = l1+l2+l3 >>> list(c123) == l123 True >>> (c1+c2+c3)[100] == l123[100] True Slicing ------- First, do some tests with fairly small slices. These will all generate tuple values. >>> from nltk.util import LazySubsequence >>> c1 = StreamBackedCorpusView(f1, read_whitespace_block) >>> l1 = open(f1).read().split() >>> print len(c1) 21 >>> len(c1) < LazySubsequence.MIN_SIZE True Choose a list of indices, based on the length, that covers the important corner cases: >>> indices = [-60, -30, -22, -21, -20, -1, ... 0, 1, 10, 20, 21, 22, 30, 60] Test slicing with explicit start & stop value: >>> for s in indices: ... for e in indices: ... assert list(c1[s:e]) == l1[s:e] Test slicing with stop=None: >>> for s in indices: ... assert list(c1[s:]) == l1[s:] Test slicing with start=None: >>> for e in indices: ... assert list(c1[:e]) == l1[:e] Test slicing with start=stop=None: >>> list(c1[:]) == list(l1[:]) True Next, we'll do some tests with much longer slices. These will generate LazySubsequence objects. >>> c3 = StreamBackedCorpusView(f3, read_whitespace_block) >>> l3 = open(f3).read().split() >>> print len(c3) 5430 >>> len(c3) > LazySubsequence.MIN_SIZE*2 True Choose a list of indices, based on the length, that covers the important corner cases: >>> indices = [-12000, -6000, -5431, -5430, -5429, -3000, -200, -1, ... 0, 1, 200, 3000, 5000, 5429, 5430, 5431, 6000, 12000] Test slicing with explicit start & stop value: >>> for s in indices: ... for e in indices: ... assert list(c3[s:e]) == l3[s:e] Test slicing with stop=None: >>> for s in indices: ... assert list(c3[s:]) == l3[s:] Test slicing with start=None: >>> for e in indices: ... assert list(c3[:e]) == l3[:e] Test slicing with start=stop=None: >>> list(c3[:]) == list(l3[:]) True Multiple Iterators ------------------ If multiple iterators are created for the same corpus view, their iteration can be interleaved: >>> c3 = StreamBackedCorpusView(f3, read_whitespace_block) >>> iterators = [c3.iterate_from(n) for n in [0,15,30,45]] >>> for i in range(15): ... for iterator in iterators: ... print '%-15s' % iterator.next(), ... print My a duties in fellow heavy of a citizens: weight the proper Anyone of office sense who responsibility. upon of has If which the taken not, he obligation the he is which oath has about the I no to oath have conception enter, imposes. just of or The taken the he office must powers is of feel and lacking an SeekableUnicodeStreamReader =========================== The file-like objects provided by the ``codecs`` module unfortunately suffer from a bug that prevents them from working correctly with corpus view objects. In particular, although the expose ``seek()`` and ``tell()`` methods, those methods do not exhibit the expected behavior, because they are not synchronized with the internal buffers that are kept by the file-like objects. For example, the ``tell()`` method will return the file position at the end of the buffers (whose contents have not yet been returned by the stream); and therefore this file position can not be used to return to the 'current' location in the stream (since ``seek()`` has no way to reconstruct the buffers). To get around these problems, we define a new class, `SeekableUnicodeStreamReader`, to act as a file-like interface to files containing encoded unicode data. This class is loosely based on the ``codecs.StreamReader`` class. To construct a new reader, we call the constructor with an underlying stream and an encoding name: >>> from cStringIO import StringIO >>> from nltk.data import SeekableUnicodeStreamReader >>> stream = StringIO(u"""\ ... This is a test file. ... It is encoded in ascii. ... """.encode('ascii')) >>> reader = SeekableUnicodeStreamReader(stream, 'ascii') `SeekableUnicodeStreamReader`\ s support all of the normal operations supplied by a read-only stream. Note that all of the read operations return ``unicode`` objects (not ``str`` objects). >>> reader.read() # read the entire file. u'This is a test file.\nIt is encoded in ascii.\n' >>> reader.seek(0) # rewind to the start. >>> reader.read(5) # read at most 5 bytes. u'This ' >>> reader.readline() # read to the end of the line. u'is a test file.\n' >>> reader.seek(0) # rewind to the start. >>> for line in reader: ... print `line` # iterate over lines u'This is a test file.\n' u'It is encoded in ascii.\n' >>> reader.seek(0) # rewind to the start. >>> reader.readlines() # read a list of line strings [u'This is a test file.\n', u'It is encoded in ascii.\n'] >>> reader.close() Size argument to ``read()`` --------------------------- The ``size`` argument to ``read()`` specifies the maximum number of *bytes* to read, not the maximum number of *characters*. Thus, for encodings that use multiple bytes per character, it may return fewer characters than the ``size`` argument: >>> stream = StringIO(u"""\ ... This is a test file. ... It is encoded in utf-16. ... """.encode('utf-16')) >>> reader = SeekableUnicodeStreamReader(stream, 'utf-16') >>> reader.read(10) u'This ' If a read block ends in the middle of the byte string encoding a single character, then that byte string is stored in an internal buffer, and re-used on the next call to ``read()``. However, if the size argument is too small to read even a single character, even though at least one character is available, then the ``read()`` method will read additional bytes until it can return a single character. This ensures that the ``read()`` method does not return an empty string, which could be mistaken for indicating the end of the file. >>> reader.seek(0) # rewind to the start. >>> reader.read(1) # we actually need to read 4 bytes u'T' >>> reader.tell() 4 The ``readline()`` method may read more than a single line of text, in which case it stores the text that it does not return in a buffer. If this buffer is not empty, then its contents will be included in the value returned by the next call to ``read()``, regardless of the ``size`` argument, since they are available without reading any new bytes from the stream: >>> reader.seek(0) # rewind to the start. >>> reader.readline() # stores extra text in a buffer u'This is a test file.\n' >>> print reader.linebuffer # examine the buffer contents [u'It is encoded i'] >>> reader.read(0) # returns the contents of the buffer u'It is encoded i' >>> print reader.linebuffer # examine the buffer contents None Seek and Tell ------------- In addition to these basic read operations, `SeekableUnicodeStreamReader` also supports the ``seek()`` and ``tell()`` operations. However, some care must still be taken when using these operations. In particular, the only file offsets that should be passed to ``seek()`` are ``0`` and any offset that has been returned by ``tell``. >>> stream = StringIO(u"""\ ... This is a test file. ... It is encoded in utf-16. ... """.encode('utf-16')) >>> reader = SeekableUnicodeStreamReader(stream, 'utf-16') >>> reader.read(20) u'This is a ' >>> pos = reader.tell(); print pos 22 >>> reader.read(20) u'test file.' >>> reader.seek(pos) # rewind to the position from tell. >>> reader.read(20) u'test file.' The ``seek()`` and ``tell()`` methods work property even when ``readline()`` is used. >>> stream = StringIO(u"""\ ... This is a test file. ... It is encoded in utf-16. ... """.encode('utf-16')) >>> reader = SeekableUnicodeStreamReader(stream, 'utf-16') >>> reader.readline() u'This is a test file.\n' >>> pos = reader.tell(); print pos 44 >>> reader.readline() u'It is encoded in utf-16.\n' >>> reader.seek(pos) # rewind to the position from tell. >>> reader.readline() u'It is encoded in utf-16.\n' The following test performs a random series of reads, seeks, and tells, and checks that the results are consistent: >>> import random >>> def reader_test(unicode_string, encoding, n=1000): ... try: bytestr = unicode_string.encode(encoding) ... except UnicodeEncodeError: ... return 'codec can not encode string' ... strlen = len(unicode_string) ... stream = StringIO(bytestr) ... reader = SeekableUnicodeStreamReader(stream, encoding) ... # Find all character positions ... chars = [] ... while True: ... pos = reader.tell() ... chars.append( (pos, reader.read(1)) ) ... if chars[-1][1] == '': break ... # Find all strings ... strings = dict( (pos,'') for (pos,c) in chars ) ... for pos1, char in chars: ... for pos2, _ in chars: ... if pos2 <= pos1: ... strings[pos2] += char ... while True: ... op = random.choice('tsrr') ... # Check our position? ... if op == 't': # tell ... reader.tell() ... # Perform a seek? ... if op == 's': # seek ... new_pos = random.choice([p for (p,c) in chars]) ... reader.seek(new_pos) ... # Perform a read? ... if op == 'r': # read ... if random.random() < .3: pos = reader.tell() ... else: pos = None ... if random.random() < .2: size = None ... elif random.random() < .8: ... size = random.randint(0, int(strlen/6)) ... else: size = random.randint(0, strlen+20) ... if random.random() < .8: ... s = reader.read(size) ... else: ... s = reader.readline(size) ... # check that everything's consistent ... if pos is not None: ... assert pos in strings ... assert strings[pos].startswith(s) ... n -= 1 ... if n == 0: ... return 'passed' Call the randomized test function `reader_test()` with a variety of input strings and encodings. >>> encodings = ['ascii', 'latin1', 'greek', 'hebrew', ... 'utf-16', 'utf-7', 'utf-8'] >>> strings = [ ... u"""\ ... This is a test file. ... It is fairly short.""", ... u"""\ ... This file can be encoded with latin1. \x83""", ... u"""\ ... This is a test file. ... Here's a blank line: ... ... And here's some unicode: \xee \u0123 \uffe3 ... """, ... u"""\ ... This is a test file. ... Unicode characters: \xf3 \u2222 \u3333\u4444 \u5555 ... """, ... u"""\ ... This is a larger file. It has some lines that are longer \ ... than 72 characters. It's got lots of repetition. Here's \ ... some unicode chars: \xee \u0123 \uffe3 \ueeee \u2345 ... ... How fun! Let's repeat it twenty times. ... """*20] >>> for i, string in enumerate(strings): ... print 'String %d: %s' % (i, `string[:45]`[:-1]+'...') ... for encoding in encodings: ... print (' - test for %-10s [%s]' % ... (encoding, reader_test(string, encoding))) String 0: u'This is a test file.\nIt is fairly short.... - test for ascii [passed] - test for latin1 [passed] - test for greek [passed] - test for hebrew [passed] - test for utf-16 [passed] - test for utf-7 [passed] - test for utf-8 [passed] String 1: u'This file can be encoded with latin1. \x83... - test for ascii [codec can not encode string] - test for latin1 [passed] - test for greek [passed] - test for hebrew [passed] - test for utf-16 [passed] - test for utf-7 [passed] - test for utf-8 [passed] String 2: u"This is a test file.\nHere's a blank line:\n\nAn... - test for ascii [codec can not encode string] - test for latin1 [codec can not encode string] - test for greek [codec can not encode string] - test for hebrew [codec can not encode string] - test for utf-16 [passed] - test for utf-7 [passed] - test for utf-8 [passed] String 3: u'This is a test file.\nUnicode characters: \xf3 \u2222 ... - test for ascii [codec can not encode string] - test for latin1 [codec can not encode string] - test for greek [codec can not encode string] - test for hebrew [codec can not encode string] - test for utf-16 [passed] - test for utf-7 [passed] - test for utf-8 [passed] String 4: u'This is a larger file. It has some lines tha... - test for ascii [codec can not encode string] - test for latin1 [codec can not encode string] - test for greek [codec can not encode string] - test for hebrew [codec can not encode string] - test for utf-16 [passed] - test for utf-7 [passed] - test for utf-8 [passed] Squashed Bugs ============= svn 5276 fixed a bug in the comment-stripping behavior of parse_sexpr_block. >>> from cStringIO import StringIO >>> from nltk.corpus.reader.util import read_sexpr_block >>> f = StringIO(""" ... (a b c) ... # This line is a comment. ... (d e f\ng h)""") >>> print read_sexpr_block(f, block_size=38, comment_char='#') ['(a b c)'] >>> print read_sexpr_block(f, block_size=38, comment_char='#') ['(d e f\ng h)'] svn 5277 fixed a bug in parse_sexpr_block, which would cause it to enter an infinite loop if a file ended mid-sexpr, or ended with a token that was not followed by whitespace. A related bug caused an infinite loop if the corpus ended in an unmatched close paren -- this was fixed in svn 5279 >>> f = StringIO(""" ... This file ends mid-sexpr ... (hello (world""") >>> for i in range(3): print read_sexpr_block(f) ['This', 'file', 'ends', 'mid-sexpr'] ['(hello (world'] [] >>> f = StringIO("""This file has no trailing whitespace.""") >>> for i in range(3): print read_sexpr_block(f) ['This', 'file', 'has', 'no', 'trailing'] ['whitespace.'] [] >>> # Bug fixed in 5279: >>> f = StringIO("""a b c)""") >>> for i in range(3): print read_sexpr_block(f) ['a', 'b'] ['c)'] [] svn 5624 & 5265 fixed a bug in ConcatenatedCorpusView, which caused it to return the wrong items when indexed starting at any index beyond the first file. >>> import nltk >>> sents = nltk.corpus.brown.sents() >>> print sents[6000] ['Cholesterol', 'and', 'thyroid'] >>> print sents[6000] ['Cholesterol', 'and', 'thyroid'] svn 5728 fixed a bug in Categorized*CorpusReader, which caused them to return words from *all* files when just one file was specified. >>> from nltk.corpus import reuters >>> reuters.words('training/13080') ['U', '.', 'S', '.', 'SCIENTISTS', 'SAY', 'TROPICAL', ...] >>> reuters.words('training/5082') ['SHEPPARD', 'RESOURCES', 'TO', 'MERGE', 'WITH', ...] svn 7227 fixed a bug in the qc corpus reader, which prevented access to its tuples() method >>> from nltk.corpus import qc >>> qc.tuples('test.txt') [('NUM:dist', 'How far is it from Denver to Aspen ?'), ('LOC:city', 'What county is Modesto , California in ?'), ...] nltk-2.0~b9/nltk/test/collocations.doctest0000644000175000017500000002401511331670235020570 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ============== Collocations ============== Overview ~~~~~~~~ Collocations are expressions of multiple words which commonly co-occur. For example, the top ten bigram collocations in Genesis are listed below, as measured using Pointwise Mutual Information. >>> import nltk >>> from nltk.collocations import * >>> bigram_measures = nltk.collocations.BigramAssocMeasures() >>> trigram_measures = nltk.collocations.TrigramAssocMeasures() >>> finder = BigramCollocationFinder.from_words( ... nltk.corpus.genesis.words('english-web.txt')) >>> finder.nbest(bigram_measures.pmi, 10) # doctest: +NORMALIZE_WHITESPACE [('cutting', 'instrument'), ('sewed', 'fig'), ('sweet', 'savor'), ('Ben', 'Ammi'), ('appoint', 'overseers'), ('olive', 'leaf'), ('months', 'later'), ('remaining', 'silent'), ('seek', 'occasion'), ('leaf', 'plucked')] While these words are highly collocated, the expressions are also very infrequent. Therefore it is useful to apply filters, such as ignoring all bigrams which occur less than three times in the corpus: >>> finder.apply_freq_filter(3) >>> finder.nbest(bigram_measures.pmi, 10) # doctest: +NORMALIZE_WHITESPACE [('Lahai', 'Roi'), ('gray', 'hairs'), ('Beer', 'Lahai'), ('Most', 'High'), ('ewe', 'lambs'), ('many', 'colors'), ('burnt', 'offering'), ('Paddan', 'Aram'), ('east', 'wind'), ('living', 'creature')] We may similarly find collocations among tagged words: >>> finder = BigramCollocationFinder.from_words( ... nltk.corpus.brown.tagged_words('ca01', simplify_tags=True)) >>> finder.nbest(bigram_measures.pmi, 5) # doctest: +NORMALIZE_WHITESPACE [(('weekend', 'N'), ('duty', 'N')), (('top', 'ADJ'), ('official', 'N')), (('George', 'NP'), ('P.', 'NP')), (('medical', 'ADJ'), ('intern', 'N')), (('1962', 'NUM'), ("governor's", 'N'))] Or tags alone: >>> finder = BigramCollocationFinder.from_words(t for w, t in ... nltk.corpus.brown.tagged_words('ca01', simplify_tags=True)) >>> finder.nbest(bigram_measures.pmi, 10) # doctest: +NORMALIZE_WHITESPACE [(':', '('), ('(', 'NUM'), ('NUM', ')'), (':', 'NUM'), (')', 'NUM'), ('-', 'WH'), ('VN', ':'), ('``', 'EX'), ('EX', 'MOD'), ('WH', 'VBZ')] Or spanning intervening words: >>> finder = BigramCollocationFinder.from_words( ... nltk.corpus.genesis.words('english-web.txt'), ... window_size = 20) >>> finder.apply_freq_filter(2) >>> ignored_words = nltk.corpus.stopwords.words('english') >>> finder.apply_word_filter(lambda w: len(w) < 3 or w.lower() in ignored_words) >>> finder.nbest(bigram_measures.likelihood_ratio, 10) # doctest: +NORMALIZE_WHITESPACE [('chief', 'chief'), ('hundred', 'years'), ('father', 'father'), ('lived', 'years'), ('years', 'father'), ('lived', 'father'), ('land', 'Egypt'), ('land', 'Canaan'), ('lived', 'hundred'), ('land', 'land')] Finders ~~~~~~~ The collocations package provides collocation finders which by default consider all ngrams in a text as candidate collocations: >>> text = "I do not like green eggs and ham, I do not like them Sam I am!" >>> tokens = nltk.wordpunct_tokenize(text) >>> finder = BigramCollocationFinder.from_words(tokens) >>> scored = finder.score_ngrams(bigram_measures.raw_freq) >>> sorted(bigram for bigram, score in scored) # doctest: +NORMALIZE_WHITESPACE [(',', 'I'), ('I', 'am'), ('I', 'do'), ('Sam', 'I'), ('am', '!'), ('and', 'ham'), ('do', 'not'), ('eggs', 'and'), ('green', 'eggs'), ('ham', ','), ('like', 'green'), ('like', 'them'), ('not', 'like'), ('them', 'Sam')] We could otherwise construct the collocation finder from manually-derived FreqDists: >>> word_fd = nltk.FreqDist(tokens) >>> bigram_fd = nltk.FreqDist(nltk.bigrams(tokens)) >>> finder = BigramCollocationFinder(word_fd, bigram_fd) >>> scored == finder.score_ngrams(bigram_measures.raw_freq) True A similar interface is provided for trigrams: >>> finder = TrigramCollocationFinder.from_words(tokens) >>> scored = finder.score_ngrams(trigram_measures.raw_freq) >>> set(trigram for trigram, score in scored) == set(nltk.trigrams(tokens)) True We may want to select only the top n results: >>> sorted(finder.nbest(trigram_measures.raw_freq, 2)) [('I', 'do', 'not'), ('do', 'not', 'like')] Alternatively, we can select those above a minimum score value: >>> sorted(finder.above_score(trigram_measures.raw_freq, ... 1.0 / len(nltk.trigrams(tokens)))) [('I', 'do', 'not'), ('do', 'not', 'like')] Filtering candidates ~~~~~~~~~~~~~~~~~~~~ All the ngrams in a text are often too many to be useful when finding collocations. It is generally useful to remove some words or punctuation, and to require a minimum frequency for candidate collocations. Given our sample text above, if we remove all trigrams containing personal pronouns from candidature, score_ngrams should return 6 less results, and 'do not like' will be the only candidate which occurs more than once: >>> len(finder.score_ngrams(trigram_measures.raw_freq)) 14 >>> finder.apply_word_filter(lambda w: w in ('I', 'me')) >>> len(finder.score_ngrams(trigram_measures.raw_freq)) 8 >>> sorted(finder.above_score(trigram_measures.raw_freq, ... 1.0 / len(nltk.trigrams(tokens)))) [('do', 'not', 'like')] Sometimes a filter is a function on the whole ngram, rather than each word, such as if we may permit 'and' to appear in the middle of a trigram, but not on either edge: >>> finder.apply_ngram_filter(lambda w1, w2, w3: 'and' in (w1, w3)) >>> len(finder.score_ngrams(trigram_measures.raw_freq)) 6 Finally, it is often important to remove low frequency candidates, as we lack sufficient evidence about their significance as collocations: >>> finder.apply_freq_filter(2) >>> len(finder.score_ngrams(trigram_measures.raw_freq)) 1 Association measures ~~~~~~~~~~~~~~~~~~~~ A number of measures are available to score collocations or other associations. The arguments to measure functions are marginals of a contingency table, in the bigram case (n_ii, (n_ix, n_xi), n_xx):: w1 ~w1 ------ ------ w2 | n_ii | n_oi | = n_xi ------ ------ ~w2 | n_io | n_oo | ------ ------ = n_ix TOTAL = n_xx We test their calculation using some known values presented in Manning and Schutze's text and other papers. Student's t: examples from Manning and Schutze 5.3.2 >>> print '%0.4f' % bigram_measures.student_t(8, (15828, 4675), 14307668) 0.9999 >>> print '%0.4f' % bigram_measures.student_t(20, (42, 20), 14307668) 4.4721 Chi-square: examples from Manning and Schutze 5.3.3 >>> print '%0.2f' % bigram_measures.chi_sq(8, (15828, 4675), 14307668) 1.55 >>> print '%0.0f' % bigram_measures.chi_sq(59, (67, 65), 571007) 456400 Likelihood ratios: examples from Dunning, CL, 1993 >>> print '%0.2f' % bigram_measures.likelihood_ratio(110, (2552, 221), 31777) 270.72 >>> print '%0.2f' % bigram_measures.likelihood_ratio(8, (13, 32), 31777) 95.29 Pointwise Mutual Information: examples from Manning and Schutze 5.4 >>> print '%0.2f' % bigram_measures.pmi(20, (42, 20), 14307668) 18.38 >>> print '%0.2f' % bigram_measures.pmi(20, (15019, 15629), 14307668) 0.29 TODO: Find authoritative results for trigrams. Using contingency table values ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ While frequency counts make marginals readily available for collocation finding, it is common to find published contingency table values. The collocations package therefore provides a wrapper, ContingencyMeasures, which wraps an association measures class, providing association measures which take contingency values as arguments, (n_ii, n_io, n_oi, n_oo) in the bigram case. >>> from nltk.metrics import ContingencyMeasures >>> cont_bigram_measures = ContingencyMeasures(bigram_measures) >>> print '%0.2f' % cont_bigram_measures.likelihood_ratio(8, 5, 24, 31740) 95.29 >>> print '%0.2f' % cont_bigram_measures.chi_sq(8, 15820, 4667, 14287173) 1.55 Ranking and correlation ~~~~~~~~~~~~~~~~~~~~~~~ It is useful to consider the results of finding collocations as a ranking, and the rankings output using different association measures can be compared using the Spearman correlation coefficient. Ranks can be assigned to a sorted list of results trivially by assigning strictly increasing ranks to each result: >>> from nltk.metrics.spearman import * >>> results_list = ['item1', 'item2', 'item3', 'item4', 'item5'] >>> print list(ranks_from_sequence(results_list)) [('item1', 0), ('item2', 1), ('item3', 2), ('item4', 3), ('item5', 4)] If scores are available for each result, we may allow sufficiently similar results (differing by no more than rank_gap) to be assigned the same rank: >>> results_scored = [('item1', 50.0), ('item2', 40.0), ('item3', 38.0), ... ('item4', 35.0), ('item5', 14.0)] >>> print list(ranks_from_scores(results_scored, rank_gap=5)) [('item1', 0), ('item2', 1), ('item3', 1), ('item4', 1), ('item5', 4)] The Spearman correlation coefficient gives a number from -1.0 to 1.0 comparing two rankings. A coefficient of 1.0 indicates identical rankings; -1.0 indicates exact opposite rankings. >>> print '%0.1f' % spearman_correlation( ... ranks_from_sequence(results_list), ... ranks_from_sequence(results_list)) 1.0 >>> print '%0.1f' % spearman_correlation( ... ranks_from_sequence(reversed(results_list)), ... ranks_from_sequence(results_list)) -1.0 >>> results_list2 = ['item2', 'item3', 'item1', 'item5', 'item4'] >>> print '%0.1f' % spearman_correlation( ... ranks_from_sequence(results_list), ... ranks_from_sequence(results_list2)) 0.6 >>> print '%0.1f' % spearman_correlation( ... ranks_from_sequence(reversed(results_list)), ... ranks_from_sequence(results_list2)) -0.6 nltk-2.0~b9/nltk/test/classify.doctest0000644000175000017500000001501311374105242017710 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ============= Classifiers ============= Classifiers label tokens with category labels (or *class labels*). Typically, labels are represented with strings (such as ``"health"`` or ``"sports"``. In NLTK, classifiers are defined using classes that implement the `ClassifyI` interface: >>> import nltk >>> nltk.usage(nltk.ClassifierI) ClassifierI supports the following operations: - self.batch_classify(featuresets) - self.batch_prob_classify(featuresets) - self.classify(featureset) - self.labels() - self.prob_classify(featureset) NLTK defines several classifier classes: - `ConditionalExponentialClassifier` - `DecisionTreeClassifier` - `MaxentClassifier` - `NaiveBayesClassifier` - `WekaClassifier` Classifiers are typically created by training them on a training corpus. Regression Tests ~~~~~~~~~~~~~~~~ We define a very simple training corpus with 3 binary features: ['a', 'b', 'c'], and are two labels: ['x', 'y']. We use a simple feature set so that the correct answers can be calculated analytically (although we haven't done this yet for all tests). >>> train = [ ... (dict(a=1,b=1,c=1), 'y'), ... (dict(a=1,b=1,c=1), 'x'), ... (dict(a=1,b=1,c=0), 'y'), ... (dict(a=0,b=1,c=1), 'x'), ... (dict(a=0,b=1,c=1), 'y'), ... (dict(a=0,b=0,c=1), 'y'), ... (dict(a=0,b=1,c=0), 'x'), ... (dict(a=0,b=0,c=0), 'x'), ... (dict(a=0,b=1,c=1), 'y'), ... ] >>> test = [ ... (dict(a=1,b=0,c=1)), # unseen ... (dict(a=1,b=0,c=0)), # unseen ... (dict(a=0,b=1,c=1)), # seen 3 times, labels=y,y,x ... (dict(a=0,b=1,c=0)), # seen 1 time, label=x ... ] Test the Naive Bayes classifier: >>> classifier = nltk.NaiveBayesClassifier.train(train) >>> sorted(classifier.labels()) ['x', 'y'] >>> classifier.batch_classify(test) ['y', 'x', 'y', 'x'] >>> for pdist in classifier.batch_prob_classify(test): ... print '%.4f %.4f' % (pdist.prob('x'), pdist.prob('y')) 0.3104 0.6896 0.5746 0.4254 0.3685 0.6315 0.6365 0.3635 >>> classifier.show_most_informative_features() Most Informative Features c = 0 x : y = 2.0 : 1.0 c = 1 y : x = 1.5 : 1.0 a = 1 y : x = 1.4 : 1.0 a = 0 x : y = 1.2 : 1.0 b = 0 x : y = 1.2 : 1.0 b = 1 y : x = 1.1 : 1.0 Test the Decision Tree classifier: >>> classifier = nltk.DecisionTreeClassifier.train( ... train, entropy_cutoff=0, ... support_cutoff=0) >>> sorted(classifier.labels()) ['x', 'y'] >>> print classifier c=0? .................................................. x a=0? ................................................ x a=1? ................................................ y c=1? .................................................. y >>> classifier.batch_classify(test) ['y', 'y', 'y', 'x'] >>> for pdist in classifier.batch_prob_classify(test): ... print '%.4f %.4f' % (pdist.prob('x'), pdist.prob('y')) Traceback (most recent call last): . . . NotImplementedError Test the Maximum Entropy classifier training algorithms; they should all generate the same results. >>> def test_maxent(algorithms): ... classifiers = {} ... for algorithm in nltk.classify.MaxentClassifier.ALGORITHMS: ... if algorithm.lower() == 'megam': ... try: nltk.classify.megam.config_megam() ... except: raise #continue ... try: ... classifiers[algorithm] = nltk.MaxentClassifier.train( ... train, algorithm, trace=0, max_iter=1000) ... except Exception, e: ... classifiers[algorithm] = e ... print ' '*11+''.join([' test[%s] ' % i ... for i in range(len(test))]) ... print ' '*11+' p(x) p(y)'*len(test) ... print '-'*(11+15*len(test)) ... for algorithm, classifier in classifiers.items(): ... print '%11s' % algorithm, ... if isinstance(classifier, Exception): ... print 'Error: %r' % classifier; continue ... for featureset in test: ... pdist = classifier.prob_classify(featureset) ... print '%8.2f%6.2f' % (pdist.prob('x'), pdist.prob('y')), ... print >>> test_maxent(nltk.classify.MaxentClassifier.ALGORITHMS) ... # doctest: +ELLIPSIS [Found megam: ...] test[0] test[1] test[2] test[3] p(x) p(y) p(x) p(y) p(x) p(y) p(x) p(y) ----------------------------------------------------------------------- LBFGSB 0.16 0.84 0.46 0.54 0.41 0.59 0.76 0.24 GIS 0.16 0.84 0.46 0.54 0.41 0.59 0.76 0.24 IIS 0.16 0.84 0.46 0.54 0.41 0.59 0.76 0.24 Nelder-Mead 0.16 0.84 0.46 0.54 0.41 0.59 0.76 0.24 CG 0.16 0.84 0.46 0.54 0.41 0.59 0.76 0.24 BFGS 0.16 0.84 0.46 0.54 0.41 0.59 0.76 0.24 MEGAM 0.16 0.84 0.46 0.54 0.41 0.59 0.76 0.24 Powell 0.16 0.84 0.46 0.54 0.41 0.59 0.76 0.24 Regression tests for TypedMaxentFeatureEncoding ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ >>> from nltk.classify import maxent >>> train = [ ... ({'a': 1, 'b': 1, 'c': 1}, 'y'), ... ({'a': 5, 'b': 5, 'c': 5}, 'x'), ... ({'a': 0.9, 'b': 0.9, 'c': 0.9}, 'y'), ... ({'a': 5.5, 'b': 5.4, 'c': 5.3}, 'x'), ... ({'a': 0.8, 'b': 1.2, 'c': 1}, 'y'), ... ({'a': 5.1, 'b': 4.9, 'c': 5.2}, 'x') ... ] >>> test = [ ... {'a': 1, 'b': 0.8, 'c': 1.2}, ... {'a': 5.2, 'b': 5.1, 'c': 5} ... ] >>> encoding = maxent.TypedMaxentFeatureEncoding.train( ... train, count_cutoff=3, alwayson_features=True) >>> classifier = maxent.MaxentClassifier.train( ... train, bernoulli=False, encoding=encoding, trace=0) >>> classifier.batch_classify(test) ['y', 'x'] nltk-2.0~b9/nltk/test/chunk.doctest0000644000175000017500000002566311331670242017217 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ========== Chunking ========== >>> from nltk.chunk import * >>> from nltk.chunk.util import * >>> from nltk.chunk.regexp import * >>> from nltk import Tree >>> tagged_text = "[ The/DT cat/NN ] sat/VBD on/IN [ the/DT mat/NN ] [ the/DT dog/NN ] chewed/VBD ./." >>> gold_chunked_text = tagstr2tree(tagged_text) >>> unchunked_text = gold_chunked_text.flatten() Chunking uses a special regexp syntax for rules that delimit the chunks. These rules must be converted to 'regular' regular expressions before a sentence can be chunked. >>> tag_pattern = "
?*" >>> regexp_pattern = tag_pattern2re_pattern(tag_pattern) >>> regexp_pattern '(<(DT)>)?(<(JJ)>)*(<(NN[^\\{\\}<>]*)>)' Construct some new chunking rules. >>> chunk_rule = ChunkRule("<.*>+", "Chunk everything") >>> chink_rule = ChinkRule("", "Chink on verbs/prepositions") >>> split_rule = SplitRule("
", "
", ... "Split successive determiner/noun pairs") Create and score a series of chunk parsers, successively more complex. >>> chunk_parser = RegexpChunkParser([chunk_rule], chunk_node='NP') >>> chunked_text = chunk_parser.parse(unchunked_text) >>> print chunked_text (S (NP The/DT cat/NN sat/VBD on/IN the/DT mat/NN the/DT dog/NN chewed/VBD ./.)) >>> chunkscore = ChunkScore() >>> chunkscore.score(gold_chunked_text, chunked_text) >>> chunkscore.precision() 0.0 >>> chunkscore.recall() 0.0 >>> chunkscore.f_measure() 0 >>> for chunk in chunkscore.missed(): print chunk (NP The/DT cat/NN) (NP the/DT mat/NN) (NP the/DT dog/NN) >>> for chunk in chunkscore.incorrect(): print chunk (NP The/DT cat/NN sat/VBD on/IN the/DT mat/NN the/DT dog/NN chewed/VBD ./.) >>> chunk_parser = RegexpChunkParser([chunk_rule, chink_rule], ... chunk_node='NP') >>> chunked_text = chunk_parser.parse(unchunked_text) >>> print chunked_text (S (NP The/DT cat/NN) sat/VBD on/IN (NP the/DT mat/NN the/DT dog/NN) chewed/VBD ./.) >>> assert chunked_text == chunk_parser.parse(list(unchunked_text)) >>> chunkscore = ChunkScore() >>> chunkscore.score(gold_chunked_text, chunked_text) >>> chunkscore.precision() 0.5 >>> chunkscore.recall() 0.33333333333333331 >>> chunkscore.f_measure() 0.40000000000000002 >>> for chunk in chunkscore.missed(): print chunk (NP the/DT mat/NN) (NP the/DT dog/NN) >>> for chunk in chunkscore.incorrect(): print chunk (NP the/DT mat/NN the/DT dog/NN) >>> chunk_parser = RegexpChunkParser([chunk_rule, chink_rule, split_rule], ... chunk_node='NP') >>> chunked_text = chunk_parser.parse(unchunked_text, trace=True) # Input:
<.> # Chunk everything: {
<.>} # Chink on verbs/prepositions: {
} {
} <.> # Split successive determiner/noun pairs: {
} {
}{
} <.> >>> print chunked_text (S (NP The/DT cat/NN) sat/VBD on/IN (NP the/DT mat/NN) (NP the/DT dog/NN) chewed/VBD ./.) >>> chunkscore = ChunkScore() >>> chunkscore.score(gold_chunked_text, chunked_text) >>> chunkscore.precision() 1.0 >>> chunkscore.recall() 1.0 >>> chunkscore.f_measure() 1.0 >>> chunkscore.missed() [] >>> chunkscore.incorrect() [] >>> chunk_parser.rules() # doctest: +NORMALIZE_WHITESPACE [+'>, '>, ', '
'>] Printing parsers: >>> print repr(chunk_parser) >>> print chunk_parser RegexpChunkParser with 3 rules: Chunk everything +'> Chink on verbs/prepositions '> Split successive determiner/noun pairs ', '
'> Regression Tests ~~~~~~~~~~~~~~~~ ChunkParserI ------------ `ChunkParserI` is an abstract interface -- it is not meant to be instantiated directly. >>> ChunkParserI().parse([]) Traceback (most recent call last): . . . AssertionError: ChunkParserI is an abstract interface ChunkString ----------- ChunkString can be built from a tree of tagged tuples, a tree of trees, or a mixed list of both: >>> t1 = Tree('S', [('w%d' % i, 't%d' % i) for i in range(10)]) >>> t2 = Tree('S', [Tree('t0', []), Tree('t1', ['c1'])]) >>> t3 = Tree('S', [('w0', 't0'), Tree('t1', ['c1'])]) >>> ChunkString(t1) '> >>> ChunkString(t2) '> >>> ChunkString(t3) '> Other values generate an error: >>> ChunkString(Tree('S', ['x'])) Traceback (most recent call last): . . . ValueError: chunk structures must contain tagged tokens or trees The `str()` for a chunk string adds spaces to it, which makes it line up with `str()` output for other chunk strings over the same underlying input. >>> cs = ChunkString(t1) >>> print cs >>> cs.xform('', '{}') >>> print cs {} The `_verify()` method makes sure that our transforms don't corrupt the chunk string. By setting debug_level=2, `_verify()` will be called at the end of every call to `xform`. >>> cs = ChunkString(t1, debug_level=3) >>> # tag not marked with <...>: >>> cs.xform('', 't3') Traceback (most recent call last): . . . ValueError: Transformation generated invalid chunkstring: t3 >>> # brackets not balanced: >>> cs.xform('', '{') Traceback (most recent call last): . . . ValueError: Transformation generated invalid chunkstring: { >>> # nested brackets: >>> cs.xform('', '{{}}') Traceback (most recent call last): . . . ValueError: Transformation generated invalid chunkstring: {{}} >>> # modified tags: >>> cs.xform('', '') Traceback (most recent call last): . . . ValueError: Transformation generated invalid chunkstring: tag changed >>> # added tags: >>> cs.xform('', '') Traceback (most recent call last): . . . ValueError: Transformation generated invalid chunkstring: tag changed Chunking Rules -------------- Test the different rule constructors & __repr__ methods: >>> r1 = RegexpChunkRule(''+ChunkString.IN_CHINK_PATTERN, ... '{}', 'chunk and ') >>> r2 = RegexpChunkRule(re.compile(''+ChunkString.IN_CHINK_PATTERN), ... '{}', 'chunk and ') >>> r3 = ChunkRule('', 'chunk and ') >>> r4 = ChinkRule('', 'chink and ') >>> r5 = UnChunkRule('', 'unchunk and ') >>> r6 = MergeRule('', '', 'merge w/ ') >>> r7 = SplitRule('', '', 'split from ') >>> r8 = ExpandLeftRule('', '', 'expand left ') >>> r9 = ExpandRightRule('', '', 'expand right ') >>> for rule in r1, r2, r3, r4, r5, r6, r7, r8, r9: ... print rule (?=[^\\}]*(\\{|$))'->'{}'> (?=[^\\}]*(\\{|$))'->'{}'> '> '> '> ', ''> ', ''> ', ''> ', ''> `tag_pattern2re_pattern()` complains if the tag pattern looks problematic: >>> tag_pattern2re_pattern('{}') Traceback (most recent call last): . . . ValueError: Bad tag pattern: '{}' RegexpChunkParser ----------------- A warning is printed when parsing an empty sentence: >>> parser = RegexpChunkParser([ChunkRule('', '')]) >>> parser.parse(Tree('S', [])) Warning: parsing empty text Tree('S', []) RegexpParser ------------ >>> parser = RegexpParser(''' ... NP: {
? * *} # NP ... P: {} # Preposition ... V: {} # Verb ... PP: {

} # PP -> P NP ... VP: { *} # VP -> V (NP|PP)* ... ''') >>> print repr(parser) >>> print parser chunk.RegexpParser with 5 stages: RegexpChunkParser with 1 rules: NP ? * *'> RegexpChunkParser with 1 rules: Preposition '> RegexpChunkParser with 1 rules: Verb '> RegexpChunkParser with 1 rules: PP -> P NP '> RegexpChunkParser with 1 rules: VP -> V (NP|PP)* *'> >>> print parser.parse(unchunked_text, trace=True) # Input:

<.> # NP: {
} {
}{
} <.> # Input: <.> # Preposition: {} <.> # Input:

<.> # Verb: {}

{} <.> # Input:

<.> # PP -> P NP: {

} <.> # Input: <.> # VP -> V (NP|PP)*: { }{} <.> (S (NP The/DT cat/NN) (VP (V sat/VBD) (PP (P on/IN) (NP the/DT mat/NN)) (NP the/DT dog/NN)) (VP (V chewed/VBD)) ./.) Test parsing of other rule types: >>> print RegexpParser(''' ... X: ... }{ # chink rule ... }{ # split rule ... {} # merge rule ... {} # chunk rule w/ context ... ''') chunk.RegexpParser with 1 stages: RegexpChunkParser with 4 rules: chink rule '> split rule ', ''> merge rule ', ''> chunk rule w/ context ', '', ''> Illegal patterns give an error message: >>> print RegexpParser('X: {} {}') Traceback (most recent call last): . . . ValueError: Illegal chunk pattern: {} {} nltk-2.0~b9/nltk/test/chat80.doctest0000644000175000017500000002061411331670245017170 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ======= Chat-80 ======= Chat-80 was a natural language system which allowed the user to interrogate a Prolog knowledge base in the domain of world geography. It was developed in the early '80s by Warren and Pereira; see ``_ for a description and ``_ for the source files. The ``chat80`` module contains functions to extract data from the Chat-80 relation files ('the world database'), and convert then into a format that can be incorporated in the FOL models of ``nltk.sem.evaluate``. The code assumes that the Prolog input files are available in the NLTK corpora directory. The Chat-80 World Database consists of the following files:: world0.pl rivers.pl cities.pl countries.pl contain.pl borders.pl This module uses a slightly modified version of ``world0.pl``, in which a set of Prolog rules have been omitted. The modified file is named ``world1.pl``. Currently, the file ``rivers.pl`` is not read in, since it uses a list rather than a string in the second field. Reading Chat-80 Files ===================== Chat-80 relations are like tables in a relational database. The relation acts as the name of the table; the first argument acts as the 'primary key'; and subsequent arguments are further fields in the table. In general, the name of the table provides a label for a unary predicate whose extension is all the primary keys. For example, relations in ``cities.pl`` are of the following form:: 'city(athens,greece,1368).' Here, ``'athens'`` is the key, and will be mapped to a member of the unary predicate *city*. By analogy with NLTK corpora, ``chat80`` defines a number of 'items' which correspond to the relations. >>> from nltk.sem import chat80 >>> print chat80.items # doctest: +ELLIPSIS ('borders', 'circle_of_lat', 'circle_of_long', 'city', ...) The fields in the table are mapped to binary predicates. The first argument of the predicate is the primary key, while the second argument is the data in the relevant field. Thus, in the above example, the third field is mapped to the binary predicate *population_of*, whose extension is a set of pairs such as ``'(athens, 1368)'``. An exception to this general framework is required by the relations in the files ``borders.pl`` and ``contains.pl``. These contain facts of the following form:: 'borders(albania,greece).' 'contains0(africa,central_africa).' We do not want to form a unary concept out the element in the first field of these records, and we want the label of the binary relation just to be ``'border'``/``'contain'`` respectively. In order to drive the extraction process, we use 'relation metadata bundles' which are Python dictionaries such as the following:: city = {'label': 'city', 'closures': [], 'schema': ['city', 'country', 'population'], 'filename': 'cities.pl'} According to this, the file ``city['filename']`` contains a list of relational tuples (or more accurately, the corresponding strings in Prolog form) whose predicate symbol is ``city['label']`` and whose relational schema is ``city['schema']``. The notion of a ``closure`` is discussed in the next section. Concepts ======== In order to encapsulate the results of the extraction, a class of ``Concept``\ s is introduced. A ``Concept`` object has a number of attributes, in particular a ``prefLabel``, an arity and ``extension``. >>> c1 = chat80.Concept('dog', arity=1, extension=set(['d1', 'd2'])) >>> print c1 Label = 'dog' Arity = 1 Extension = ['d2', 'd1'] The ``extension`` attribute makes it easier to inspect the output of the extraction. >>> schema = ['city', 'country', 'population'] >>> concepts = chat80.clause2concepts('cities.pl', 'city', schema) >>> concepts [Concept('city'), Concept('country_of'), Concept('population_of')] >>> for c in concepts: # doctest: +NORMALIZE_WHITESPACE ... print "%s:\n\t%s" % (c.prefLabel, c.extension[:4]) city: ['bucharest', 'hyderabad', 'mexico_city', 'los_angeles'] country_of: [('chungking', 'china'), ('karachi', 'pakistan'), ('singapore_city', 'singapore'), ('athens', 'greece')] population_of: [('sian', '629'), ('dairen', '544'), ('peking', '2031'), ('istanbul', '1215')] In addition, the ``extension`` can be further processed: in the case of the ``'border'`` relation, we check that the relation is **symmetric**, and in the case of the ``'contain'`` relation, we carry out the **transitive closure**. The closure properties associated with a concept is indicated in the relation metadata, as indicated earlier. >>> borders = set([('a1', 'a2'), ('a2', 'a3')]) >>> c2 = chat80.Concept('borders', arity=2, extension=borders) >>> print c2 Label = 'borders' Arity = 2 Extension = [('a1', 'a2'), ('a2', 'a3')] >>> c3 = chat80.Concept('borders', arity=2, closures=['symmetric'], extension=borders) >>> c3.close() >>> print c3 Label = 'borders' Arity = 2 Extension = [('a2', 'a1'), ('a1', 'a2'), ('a3', 'a2'), ('a2', 'a3')] The ``extension`` of a ``Concept`` object is then incorporated into a ``Valuation`` object. Persistence =========== The functions ``val_dump`` and ``val_load`` are provided to allow a valuation to be stored in a persistent database and re-loaded, rather than having to be re-computed each time. Individuals and Lexical Items ============================= As well as deriving relations from the Chat-80 data, we also create a set of individual constants, one for each entity in the domain. The individual constants are string-identical to the entities. For example, given a data item such as ``'zloty'``, we add to the valuation a pair ``('zloty', 'zloty')``. In order to parse English sentences that refer to these entities, we also create a lexical item such as the following for each individual constant:: PropN[num=sg, sem=<\P.(P zloty)>] -> 'Zloty' The set of rules is written to the file ``chat_pnames.fcfg`` in the current directory. SQL Query ========= The ``city`` relation is also available in RDB form and can be queried using SQL statements. >>> import nltk >>> q = "SELECT City, Population FROM city_table WHERE Country = 'china' and Population > 1000" >>> for answer in chat80.sql_query('corpora/city_database/city.db', q): ... print "%-10s %4s" % answer canton 1496 chungking 1100 mukden 1551 peking 2031 shanghai 5407 tientsin 1795 The (deliberately naive) grammar ``sql.fcfg`` translates from English to SQL: >>> nltk.data.show_cfg('grammars/book_grammars/sql0.fcfg') % start S S[SEM=(?np + WHERE + ?vp)] -> NP[SEM=?np] VP[SEM=?vp] VP[SEM=(?v + ?pp)] -> IV[SEM=?v] PP[SEM=?pp] VP[SEM=(?v + ?ap)] -> IV[SEM=?v] AP[SEM=?ap] NP[SEM=(?det + ?n)] -> Det[SEM=?det] N[SEM=?n] PP[SEM=(?p + ?np)] -> P[SEM=?p] NP[SEM=?np] AP[SEM=?pp] -> A[SEM=?a] PP[SEM=?pp] NP[SEM='Country="greece"'] -> 'Greece' NP[SEM='Country="china"'] -> 'China' Det[SEM='SELECT'] -> 'Which' | 'What' N[SEM='City FROM city_table'] -> 'cities' IV[SEM=''] -> 'are' A -> 'located' P[SEM=''] -> 'in' Given this grammar, we can express, and then execute, queries in English. >>> from nltk.parse import load_earley >>> from string import join >>> cp = load_earley('grammars/book_grammars/sql0.fcfg') >>> query = 'What cities are in China' >>> trees = cp.nbest_parse(query.split()) >>> answer = trees[0].node['SEM'] >>> q = join(answer) >>> print q SELECT City FROM city_table WHERE Country="china" >>> rows = chat80.sql_query('corpora/city_database/city.db', q) >>> for r in rows: print "%s" % r, canton chungking dairen harbin kowloon mukden peking shanghai sian tientsin Using Valuations ----------------- In order to convert such an extension into a valuation, we use the ``make_valuation()`` method; setting ``read=True`` creates and returns a new ``Valuation`` object which contains the results. >>> val = chat80.make_valuation(concepts, read=True) >>> 'calcutta' in val['city'] True >>> [town for (town, country) in val['country_of'] if country == 'india'] ['bombay', 'delhi', 'madras', 'hyderabad', 'calcutta'] >>> dom = val.domain >>> g = nltk.sem.Assignment(dom) >>> m = nltk.sem.Model(dom, val) >>> m.evaluate(r'population_of(jakarta, 533)', g) True nltk-2.0~b9/nltk/test/ccg.doctest0000644000175000017500000003560411331670250016636 0ustar bhavanibhavani.. Copyright (C) 2001-2010 NLTK Project .. For license information, see LICENSE.TXT ============================== Combinatory Categorial Grammar ============================== For more information, please see: http://nltk.googlecode.com/svn/trunk/doc/contrib/ccg/ccg.pdf Relative Clauses ---------------- >>> from nltk.ccg import chart, lexicon Construct a lexicon: >>> lex = lexicon.parseLexicon(''' ... :- S, NP, N, VP ... ... Det :: NP/N ... Pro :: NP ... Modal :: S\\NP/VP ... ... TV :: VP/NP ... DTV :: TV/NP ... ... the => Det ... ... that => Det ... that => NP ... ... I => Pro ... you => Pro ... we => Pro ... ... chef => N ... cake => N ... children => N ... dough => N ... ... will => Modal ... should => Modal ... might => Modal ... must => Modal ... ... and => var\\.,var/.,var ... ... to => VP[to]/VP ... ... without => (VP\\VP)/VP[ing] ... ... be => TV ... cook => TV ... eat => TV ... ... cooking => VP[ing]/NP ... ... give => DTV ... ... is => (S\\NP)/NP ... prefer => (S\\NP)/NP ... ... which => (N\\N)/(S/NP) ... ... persuade => (VP/VP[to])/NP ... ''') >>> parser = chart.CCGChartParser(lex, chart.DefaultRuleSet) >>> for parse in parser.nbest_parse("you prefer that cake".split(),1): ... chart.printCCGDerivation(parse) ... for parse in parser.nbest_parse("that is the cake which you prefer".split(), 1): ... chart.printCCGDerivation(parse) # doctest: +NORMALIZE_WHITESPACE ... you prefer that cake NP ((S\NP)/NP) (NP/N) N --------------------->B ((S\NP)/N) ---------------------------> (S\NP) --------------------------------< S that is the cake which you prefer NP ((S\NP)/NP) (NP/N) N ((N\N)/(S/NP)) NP ((S\NP)/NP) --------------------->B ((S\NP)/N) ------>T (N/(N\N)) --------------------------->B ((S\NP)/(N\N)) ------------------------------------------->B ((S\NP)/(S/NP)) ----->T (S/(S\NP)) ------------------>B (S/NP) -------------------------------------------------------------> (S\NP) -------------------------------------------------------------------< S Some other sentences to try: "that is the cake which we will persuade the chef to cook" "that is the cake which we will persuade the chef to give the children" >>> sent = "that is the dough which you will eat without cooking".split() >>> nosub_parser = chart.CCGChartParser(lex, chart.ApplicationRuleSet + ... chart.CompositionRuleSet + chart.TypeRaiseRuleSet) Without Substitution (no output) >>> for parse in nosub_parser.nbest_parse(sent,1): ... chart.printCCGDerivation(parse) With Substitution: >>> for parse in parser.nbest_parse(sent,1): ... chart.printCCGDerivation(parse) # doctest: +NORMALIZE_WHITESPACE that is the dough which you will eat without cooking NP ((S\NP)/NP) (NP/N) N ((N\N)/(S/NP)) NP ((S\NP)/VP) (VP/NP) ((VP\VP)/VP['ing']) (VP['ing']/NP) --------------------->B ((S\NP)/N) ------->T (N/(N\N)) ----->T (S/(S\NP)) ------------------>B (S/VP) ------------------------------------->B ((VP\VP)/NP) ----------------------------------------------B (S/NP) --------------------------------------------------------------------------------> (N\N) ---------------------------------------------------------------------------------------> N ------------------------------------------------------------------------------------------------------------> (S\NP) ------------------------------------------------------------------------------------------------------------------< S Conjunction ----------- >>> from nltk.ccg.chart import CCGChartParser, ApplicationRuleSet, CompositionRuleSet >>> from nltk.ccg.chart import SubstitutionRuleSet, TypeRaiseRuleSet, printCCGDerivation >>> from nltk.ccg import lexicon Lexicons for the tests: >>> test1_lex = ''' ... :- S,N,NP,VP ... I => NP ... you => NP ... will => S\\NP/VP ... cook => VP/NP ... which => (N\N)/(S/NP) ... and => var\\.,var/.,var ... might => S\\NP/VP ... eat => VP/NP ... the => NP/N ... mushrooms => N ... parsnips => N''' >>> test2_lex = ''' ... :- N, S, NP, VP ... articles => N ... the => NP/N ... and => var\\.,var/.,var ... which => (N\N)/(S/NP) ... I => NP ... anyone => NP ... will => (S/VP)\\NP ... file => VP/NP ... without => (VP\\VP)/VP[ing] ... forget => VP/NP ... reading => VP[ing]/NP ... ''' Tests handling of conjunctions. Note that while the two derivations are different, they are semantically equivalent. >>> lex = lexicon.parseLexicon(test1_lex) >>> parser = CCGChartParser(lex, ApplicationRuleSet + CompositionRuleSet + SubstitutionRuleSet) >>> for parse in parser.nbest_parse("I will cook and might eat the mushrooms and parsnips".split()): ... printCCGDerivation(parse) # doctest: +NORMALIZE_WHITESPACE I will cook and might eat the mushrooms and parsnips NP ((S\NP)/VP) (VP/NP) ((_var2\.,_var2)/.,_var2) ((S\NP)/VP) (VP/NP) (NP/N) N ((_var2\.,_var2)/.,_var2) N ---------------------->B ((S\NP)/NP) ---------------------->B ((S\NP)/NP) -------------------------------------------------> (((S\NP)/NP)\.,((S\NP)/NP)) -----------------------------------------------------------------------< ((S\NP)/NP) -------------------------------------> (N\.,N) ------------------------------------------------< N --------------------------------------------------------> NP -------------------------------------------------------------------------------------------------------------------------------> (S\NP) -----------------------------------------------------------------------------------------------------------------------------------< S I will cook and might eat the mushrooms and parsnips NP ((S\NP)/VP) (VP/NP) ((_var2\.,_var2)/.,_var2) ((S\NP)/VP) (VP/NP) (NP/N) N ((_var2\.,_var2)/.,_var2) N ---------------------->B ((S\NP)/NP) ---------------------->B ((S\NP)/NP) -------------------------------------------------> (((S\NP)/NP)\.,((S\NP)/NP)) -----------------------------------------------------------------------< ((S\NP)/NP) ------------------------------------------------------------------------------->B ((S\NP)/N) -------------------------------------> (N\.,N) ------------------------------------------------< N -------------------------------------------------------------------------------------------------------------------------------> (S\NP) -----------------------------------------------------------------------------------------------------------------------------------< S Tests handling subject extraction. Interesting to point that the two parses are clearly semantically different. >>> lex = lexicon.parseLexicon(test2_lex) >>> parser = CCGChartParser(lex, ApplicationRuleSet + CompositionRuleSet + SubstitutionRuleSet) >>> for parse in parser.nbest_parse("articles which I will file and forget without reading".split()): ... printCCGDerivation(parse) # doctest: +NORMALIZE_WHITESPACE articles which I will file and forget without reading N ((N\N)/(S/NP)) NP ((S/VP)\NP) (VP/NP) ((_var3\.,_var3)/.,_var3) (VP/NP) ((VP\VP)/VP['ing']) (VP['ing']/NP) -----------------< (S/VP) ------------------------------------->B ((VP\VP)/NP) ---------------------------------------------- ((VP/NP)\.,(VP/NP)) ----------------------------------------------------------------------------------< (VP/NP) --------------------------------------------------------------------------------------------------->B (S/NP) -------------------------------------------------------------------------------------------------------------------> (N\N) -----------------------------------------------------------------------------------------------------------------------------< N articles which I will file and forget without reading N ((N\N)/(S/NP)) NP ((S/VP)\NP) (VP/NP) ((_var3\.,_var3)/.,_var3) (VP/NP) ((VP\VP)/VP['ing']) (VP['ing']/NP) -----------------< (S/VP) ------------------------------------> ((VP/NP)\.,(VP/NP)) ---------------------------------------------< (VP/NP) ------------------------------------->B ((VP\VP)/NP) ----------------------------------------------------------------------------------B (S/NP) -------------------------------------------------------------------------------------------------------------------> (N\N) -----------------------------------------------------------------------------------------------------------------------------< N nltk-2.0~b9/nltk/tag/util.py0000644000175000017500000000562611361226640015642 0ustar bhavanibhavani# Natural Language Toolkit: Tagger Utilities # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT import re from nltk.internals import deprecated from nltk.metrics import accuracy as _accuracy def str2tuple(s, sep='/'): """ Given the string representation of a tagged token, return the corresponding tuple representation. The rightmost occurrence of C{sep} in C{s} will be used to divide C{s} into a word string and a tag string. If C{sep} does not occur in C{s}, return C{(s, None)}. @type s: C{str} @param s: The string representaiton of a tagged token. @type sep: C{str} @param sep: The separator string used to separate word strings from tags. """ loc = s.rfind(sep) if loc >= 0: return (s[:loc], s[loc+len(sep):].upper()) else: return (s, None) def tuple2str(tagged_token, sep='/'): """ Given the tuple representation of a tagged token, return the corresponding string representation. This representation is formed by concatenating the token's word string, followed by the separator, followed by the token's tag. (If the tag is None, then just return the bare word string.) @type tagged_token: C{(str, str)} @param tagged_token: The tuple representation of a tagged token. @type sep: C{str} @param sep: The separator string used to separate word strings from tags. """ word, tag = tagged_token if tag is None: return word else: assert sep not in tag, 'tag may not contain sep!' return '%s%s%s' % (word, sep, tag) def untag(tagged_sentence): """ Given a tagged sentence, return an untagged version of that sentence. I.e., return a list containing the first element of each tuple in C{tagged_sentence}. >>> untag([('John', 'NNP'), ('saw', 'VBD'), ('Mary', 'NNP')] ['John', 'saw', 'mary'] """ return [w for (w, t) in tagged_sentence] @deprecated("use tagger.evaluate(gold)") def accuracy(tagger, gold): return tagger.evaluate(gold) ###################################################################### #{ Deprecated ###################################################################### @deprecated("Use nltk.tag.str2tuple(s, sep) instead.") def tag2tuple(s, sep='/'): return str2tuple(s, sep) @deprecated("Use [nltk.tag.str2tuple(t, sep) for t in s.split()] instead.") def string2tags(s, sep='/'): return [str2tuple(t, sep) for t in s.split()] @deprecated("Use ' '.join(nltk.tag.tuple2str(w, sep) for w in t) instead.") def tags2string(t, sep='/'): return ' '.join(tuple2str(w, sep) for w in t) @deprecated("Use [nltk.tag.str2tuple(t, sep)[0] for t in s.split()] instead.") def string2words(s, sep='/'): return [str2tuple(t, sep)[0] for t in s.split()] nltk-2.0~b9/nltk/tag/tnt.py0000755000175000017500000004450111327451577015503 0ustar bhavanibhavani# Natural Language Toolkit: TnT Tagger # # Copyright (C) 2001-2010 NLTK Project # Author: Sam Huston # Steven Bird (modifications) # # URL: # For license information, see LICENSE.TXT ''' Implementation of 'TnT - A Statisical Part of Speech Tagger' by Thorsten Brants http://acl.ldc.upenn.edu/A/A00/A00-1031.pdf ''' import nltk from api import * class TnT(TaggerI): ''' TnT - Statistical POS tagger IMPORTANT NOTES: * DOES NOT AUTOMATICALLY DEAL WITH UNSEEN WORDS It is possible to provide an untrained POS tagger to create tags for unknown words, see __init__ function * SHOULD BE USED WITH SENTENCE-DELIMITED INPUT - Due to the nature of this tagger, it works best when trained over sentence delimited input. - However it still produces good results if the training data and testing data are separated on all punctuation eg: [,.?!] - Input for training is expected to be a list of sentences where each sentence is a list of (word, tag) tuples - Input for tag function is a single sentence Input for tagdata function is a list of sentences Output is of a similar form * Function provided to process text that is unsegmented - Please see basic_sent_chop() TnT uses a second order Markov model to produce tags for a sequence of input, specifically: argmax [Proj(P(t_i|t_i-1,t_i-2)P(w_i|t_i))] P(t_T+1 | t_T) IE: the maximum projection of a set of probabilities The set of possible tags for a given word is derived from the training data. It is the set of all tags that exact word has been assigned. The probability of a tag for a given word is the linear interpolation of 3 markov models; a zero-order, first-order, and a second order model. P(t_i| t_i-1, t_i-2) = l1*P(t_i) + l2*P(t_i| t_i-1) + l3*P(t_i| t_i-1, t_i-2) A beam search is used to limit the memory usage of the algorithm. The degree of the beam can be changed using N in the initialization. N represents the maximum number of possible solutions to maintain while tagging. It is possible to differentiate the tags which are assigned to capitalized words. However this does not result in a significant gain in the accuracy of the results. ''' def __init__(self, unk=None, Trained=False, N=1000, C=False): ''' Construct a TnT statistical tagger. Tagger must be trained before being used to tag input. @param unk: instance of a POS tagger, conforms to TaggerI @type unk:(TaggerI) @param Trained: Indication that the POS tagger is trained or not @type Trained: boolean @param N: Beam search degree (see above) @type N:(int) @param C: Capitalization flag @type C: boolean Initializer, creates frequency distributions to be used for tagging _lx values represent the portion of the tri/bi/uni taggers to be used to calculate the probability N value is the number of possible solutions to maintain while tagging. A good value for this is 1000 C is a boolean value which specifies to use or not use the Capitalization of the word as additional information for tagging. NOTE: using capitalization may not increase the accuracy of the tagger ''' self._uni = nltk.probability.FreqDist() self._bi = nltk.probability.ConditionalFreqDist() self._tri = nltk.probability.ConditionalFreqDist() self._wd = nltk.probability.ConditionalFreqDist() self._eos = nltk.probability.ConditionalFreqDist() self._l1 = 0.0 self._l2 = 0.0 self._l3 = 0.0 self._N = N self._C = C self._T = Trained self._unk = unk # statistical tools (ignore or delete me) self.unknown = 0 self.known = 0 def train(self, data): ''' Uses a set of tagged data to train the tagger. If an unknown word tagger is specified, it is trained on the same data. @param data: List of lists of (word, tag) tuples @type data: L{tuple} of L{str} ''' # Ensure that local C flag is initialized before use C = False if self._unk != None and self._T == False: self._unk.train(data) for sent in data: history = ['BOS', 'BOS'] for w, t in sent: # if capitalization is requested, # and the word begins with a capital # set local flag C to True if self._C and w[0].isupper(): C=True self._wd[w].inc(t) self._uni.inc((t,C)) self._bi[history[1]].inc((t,C)) self._tri[tuple(history)].inc((t,C)) history.append((t,C)) history.pop(0) # set local flag C to false for the next word C = False self._eos[t].inc('EOS') # compute lambda values from the trained frequency distributions self._compute_lambda() #(debugging -- ignore or delete me) #print "lambdas" #print i, self._l1, i, self._l2, i, self._l3 def _compute_lambda(self): ''' creates lambda values based upon training data NOTE: no need to explicitly reference C, it is contained within the tag variable :: tag == (tag,C) for each tag trigram (t1, t2, t3) depending on the maximum value of - f(t1,t2,t3)-1 / f(t1,t2)-1 - f(t2,t3)-1 / f(t2)-1 - f(t3)-1 / N-1 increment l3,l2, or l1 by f(t1,t2,t3) ISSUES -- Resolutions: if 2 values are equal, increment both lambda values by (f(t1,t2,t3) / 2) ''' # temporary lambda variables tl1 = 0.0 tl2 = 0.0 tl3 = 0.0 # for each t1,t2 in system for history in self._tri.conditions(): (h1, h2) = history # for each t3 given t1,t2 in system # (NOTE: tag actually represents (tag,C)) # However no effect within this function for tag in self._tri[history].samples(): # if there has only been 1 occurance of this tag in the data # then ignore this trigram. if self._uni[tag] == 1: continue # safe_div provides a safe floating point division # it returns -1 if the denominator is 0 c3 = self._safe_div((self._tri[history][tag]-1), (self._tri[history].N()-1)) c2 = self._safe_div((self._bi[h2][tag]-1), (self._bi[h2].N()-1)) c1 = self._safe_div((self._uni[tag]-1), (self._uni.N()-1)) # if c1 is the maximum value: if (c1 > c3) and (c1 > c2): tl1 += self._tri[history][tag] # if c2 is the maximum value elif (c2 > c3) and (c2 > c1): tl2 += self._tri[history][tag] # if c3 is the maximum value elif (c3 > c2) and (c3 > c1): tl3 += self._tri[history][tag] # if c3, and c2 are equal and larger than c1 elif (c3 == c2) and (c3 > c1): tl2 += float(self._tri[history][tag]) /2.0 tl3 += float(self._tri[history][tag]) /2.0 # if c1, and c2 are equal and larger than c3 # this might be a dumb thing to do....(not sure yet) elif (c2 == c1) and (c1 > c3): tl1 += float(self._tri[history][tag]) /2.0 tl2 += float(self._tri[history][tag]) /2.0 # otherwise there might be a problem # eg: all values = 0 else: #print "Problem", c1, c2 ,c3 pass # Lambda normalisation: # ensures that l1+l2+l3 = 1 self._l1 = tl1 / (tl1+tl2+tl3) self._l2 = tl2 / (tl1+tl2+tl3) self._l3 = tl3 / (tl1+tl2+tl3) def _safe_div(self, v1, v2): ''' Safe floating point division function, does not allow division by 0 returns -1 if the denominator is 0 ''' if v2 == 0: return -1 else: return float(v1) / float(v2) def tagdata(self, data): ''' Tags each sentence in a list of sentences @param data:list of list of words @type data: [[string,],] @return: list of list of (word, tag) tuples Invokes tag(sent) function for each sentence compiles the results into a list of tagged sentences each tagged sentence is a list of (word, tag) tuples ''' res = [] for sent in data: res1 = self.tag(sent) res.append(res1) return res def tag(self, data): ''' Tags a single sentence @param data: list of words @type data: [string,] @return: [(word, tag),] Calls recursive function '_tagword' to produce a list of tags Associates the sequence of returned tags with the correct words in the input sequence returns a list of (word, tag) tuples ''' current_state = [(['BOS', 'BOS'], 1.0)] sent = list(data) tags = self._tagword(sent, current_state) res = [] for i in range(len(sent)): # unpack and discard the C flags (t,C) = tags[i+2] res.append((sent[i], t)) return res def _tagword(self, sent, current_states): ''' @param sent : List of words remaining in the sentence @type sent : [word,] @param current_states : List of possible tag combinations for the sentence so far, and the probability associated with each tag combination @type current_states : [([tag, ],prob), ] Tags the first word in the sentence and recursively tags the reminder of sentence Uses formula specified above to calculate the probability of a particular tag ''' # if this word marks the end of the sentance, # return the most probable tag if sent == []: (h,p) = current_states[0] return h # otherwise there are more words to be tagged word = sent[0] sent = sent[1:] new_states = [] # if the Capitalisation is requested, # initalise the flag for this word C = False if self._C and word[0].isupper(): C=True # if word is known # compute the set of possible tags # and their associated probabilities if word in self._wd.conditions(): self.known += 1 for (history, curr_sent_prob) in current_states: probs = [] for t in self._wd[word].samples(): p_uni = self._uni.freq((t,C)) p_bi = self._bi[history[-1]].freq((t,C)) p_tri = self._tri[tuple(history[-2:])].freq((t,C)) p_wd = float(self._wd[word][t])/float(self._uni[(t,C)]) p = self._l1 *p_uni + self._l2 *p_bi + self._l3 *p_tri p2 = p * p_wd probs.append(((t,C), p2)) # compute the result of appending each tag to this history for (tag, prob) in probs: new_states.append((history + [tag], curr_sent_prob*prob)) # otherwise a new word, set of possible tags is unknown else: self.unknown += 1 # since a set of possible tags, # and the probability of each specific tag # can not be returned from most classifiers: # specify that any unknown words are tagged with certainty p = 1 # if no unknown word tagger has been specified # then use the tag 'Unk' if self._unk == None: tag = ('Unk',C) # otherwise apply the unknown word tagger else : [(_w, t)] = list(self._unk.tag([word])) tag = (t,C) for (history, prob) in current_states: history.append(tag) new_states = current_states # now have computed a set of possible new_states # sort states by prob # _cmp_tup is a comparison function, # set is now ordered greatest to least probability new_states.sort(self._cmp_tup) # del everything after N (threshold) # this is the beam search cut if len(new_states) > self._N: new_states = new_states[:self._N] # compute the tags for the rest of the sentence # return the best list of tags for the sentence return self._tagword(sent, new_states) def _cmp_tup(self, (_hq, p1), (_h2, p2)): ''' comparison function @params : (_, prob) @types : (_, int) tuple used to sort a list of these tuples into descending order ''' if (p2-p1) > 0: return 1 else: return -1 ######################################## # helper function -- basic sentence tokenizer ######################################## def basic_sent_chop(data, raw=True): ''' Basic method for tokenizing input into sentences for this tagger: @param data: list of tokens tokens can be either words or (word, tag) tuples @type data: [string,] or [(string, string),] @param raw: boolean flag marking the input data as a list of words or a list of tagged words @type raw: Boolean @ret : list of sentences sentences are a list of tokens tokens are the same as the input Function takes a list of tokens and separates the tokens into lists where each list represents a sentence fragment This function can separate both tagged and raw sequences into basic sentences. Sentence markers are the set of [,.!?] This is a simple method which enhances the performance of the TnT tagger. Better sentence tokenization will further enhance the results. ''' new_data = [] curr_sent = [] sent_mark = [',','.','?','!'] if raw: for word in data: if word in sent_mark: curr_sent.append(word) new_data.append(curr_sent) curr_sent = [] else: curr_sent.append(word) else: for (word,tag) in data: if word in sent_mark: curr_sent.append((word,tag)) new_data.append(curr_sent) curr_sent = [] else: curr_sent.append((word,tag)) return new_data def demo(): from nltk.tag import tnt from nltk.corpus import brown sents = list(brown.tagged_sents()) test = list(brown.sents()) # create and train the tagger tagger = tnt.TnT() tagger.train(sents[200:1000]) # tag some data tagged_data = tagger.tagdata(test[100:120]) # print results for j in range(len(tagged_data)): s = tagged_data[j] t = sents[j+100] for i in range(len(s)): print s[i],'--', t[i] print def demo2(): from nltk import tag from nltk.tag import tnt from nltk.corpus import treebank d = list(treebank.tagged_sents()) t = tnt.TnT(N=1000, C=False) s = tnt.TnT(N=1000, C=True) t.train(d[(11)*100:]) s.train(d[(11)*100:]) for i in range(10): tacc = tag.accuracy(t, d[i*100:((i+1)*100)]) tp_un = float(t.unknown) / float(t.known +t.unknown) tp_kn = float(t.known) / float(t.known + t.unknown) t.unknown = 0 t.known = 0 print 'Capitalization off:' print 'Accuracy:', tacc print 'Percentage known:', tp_kn print 'Percentage unknown:', tp_un print 'Accuracy over known words:', (tacc / tp_kn) sacc = tag.accuracy(s, d[i*100:((i+1)*100)]) sp_un = float(s.unknown) / float(s.known +s.unknown) sp_kn = float(s.known) / float(s.known + s.unknown) s.unknown = 0 s.known = 0 print 'Capitalization on:' print 'Accuracy:', sacc print 'Percentage known:', sp_kn print 'Percentage unknown:', sp_un print 'Accuracy over known words:', (sacc / sp_kn) def demo3(): from nltk import tag from nltk.corpus import treebank, brown from nltk.tag import tnt d = list(treebank.tagged_sents()) e = list(brown.tagged_sents()) d = d[:1000] e = e[:1000] d10 = int(len(d)*0.1) e10 = int(len(e)*0.1) tknacc = 0 sknacc = 0 tallacc = 0 sallacc = 0 tknown = 0 sknown = 0 for i in range(10): t = tnt.TnT(N=1000, C=False) s = tnt.TnT(N=1000, C=False) dtest = d[(i*d10):((i+1)*d10)] etest = e[(i*e10):((i+1)*e10)] dtrain = d[:(i*d10)] + d[((i+1)*d10):] etrain = e[:(i*e10)] + e[((i+1)*e10):] t.train(dtrain) s.train(etrain) tacc = tag.accuracy(t, dtest) tp_un = float(t.unknown) / float(t.known +t.unknown) tp_kn = float(t.known) / float(t.known + t.unknown) tknown += tp_kn t.unknown = 0 t.known = 0 sacc = tag.accuracy(s, etest) sp_un = float(s.unknown) / float(s.known + s.unknown) sp_kn = float(s.known) / float(s.known + s.unknown) sknown += sp_kn s.unknown = 0 s.known = 0 tknacc += (tacc / tp_kn) sknacc += (sacc / tp_kn) tallacc += tacc sallacc += sacc #print i+1, (tacc / tp_kn), i+1, (sacc / tp_kn), i+1, tacc, i+1, sacc print "brown: acc over words known:", 10 * tknacc print " : overall accuracy:", 10 * tallacc print " : words known:", 10 * tknown print "treebank: acc over words known:", 10 * sknacc print " : overall accuracy:", 10 * sallacc print " : words known:", 10 * sknown if __name__ == "__main__": demo() nltk-2.0~b9/nltk/tag/simplify.py0000644000175000017500000000733211361226333016514 0ustar bhavanibhavani# Natural Language Toolkit: POS Tag Simplification # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT ###################################################################### #{ Brown ###################################################################### # http://khnt.hit.uib.no/icame/manuals/brown/INDEX.HTM brown_mapping1 = { 'j': 'ADJ', 'p': 'PRO', 'm': 'MOD', 'q': 'DET', 'd': 'DET', 'w': 'WH', 'r': 'ADV', 'i': 'P', 'u': 'UH', 'e': 'EX', 'o': 'NUM', 'b': 'V', 'h': 'V', 'd': 'V', 'f': 'FW', 'a': 'DET', 't': 'TO', 'cc': 'CNJ', 'cs': 'CNJ', 'cd': 'NUM', 'nn': 'N', 'nr': 'N', 'np': 'NP', 'nc': 'N', } brown_mapping2 = { 'vb': 'V', 'vbd': 'VD', 'vbg': 'VG', 'vbn': 'VN' } def simplify_brown_tag(tag): tag = tag.lower() if tag[0] in brown_mapping1: return brown_mapping1[tag[0]] elif tag[:2] in brown_mapping1: return brown_mapping1[tag[:2]] try: if '-' in tag: tag = tag.split('-')[0] return brown_mapping2[tag] except KeyError: return tag.upper() ###################################################################### #{ Wall Street Journal tags (Penn Treebank) ###################################################################### wsj_mapping = { '-lrb-': '(', '-rrb-': ')', '-lsb-': '(', '-rsb-': ')', '-lcb-': '(', '-rcb-': ')', '-none-': '', 'cc': 'CNJ', 'cd': 'NUM', 'dt': 'DET', 'ex': 'EX', 'fw': 'FW', # existential "there", foreign word 'in': 'P', 'jj': 'ADJ', 'jjr': 'ADJ', 'jjs': 'ADJ', 'ls': 'L', 'md': 'MOD', # list item marker 'nn': 'N', 'nnp': 'NP', 'nnps': 'NP', 'nns': 'N', 'pdt': 'DET', 'pos': '', 'prp': 'PRO', 'prp$': 'PRO', 'rb': 'ADV', 'rbr': 'ADV', 'rbs': 'ADV', 'rp': 'PRO', 'sym': 'S', 'to': 'TO', 'uh': 'UH', 'vb': 'V', 'vbd': 'VD', 'vbg': 'VG', 'vbn': 'VN', 'vbp': 'V', 'vbz': 'V', 'wdt': 'WH', 'wp': 'WH', 'wp$': 'WH', 'wrb': 'WH', 'bes': 'V', 'hvs': 'V', 'prp^vbp': 'PRO' # additions for NPS Chat corpus } def simplify_wsj_tag(tag): if tag and tag[0] == '^': tag = tag[1:] try: tag = wsj_mapping[tag.lower()] except KeyError: pass return tag.upper() indian_mapping = { 'nn': 'N', 'vm': 'MOD', 'jj': 'ADJ', 'nnp': 'NP', 'prp': 'PRO', 'prep': 'PRE', 'vaux': 'V', 'vfm': 'V', 'cc': 'CNJ', 'nnpc': 'NP', 'nnc': 'N', 'qc': 'QC', 'dem': 'DET', 'vrb': 'V', 'qfnum': 'NUM', 'rb': 'ADV', 'qf': 'DET', 'punc': '.', 'rp': 'PRT', 'psp': 'PSP', 'nst': 'N', 'nvb': 'N', 'vjj': 'V', 'neg': 'NEG', 'vnn': 'V', 'xc': 'XC', 'intf': 'INTF', 'nloc': 'N', 'jvb': 'ADJ', 'wq': 'WH', 'qw': 'WH', 'jj:?': 'ADJ', '"cc': 'CNJ', 'nnp,': 'NP', 'sym\xc0\xa7\xb7': 'SYM', 'symc': 'SYM'} def simplify_indian_tag(tag): if ':' in tag: tag = tag.split(':')[0] try: tag = indian_mapping[tag.lower()] except KeyError: pass return tag.upper() ###################################################################### #{ Alpino tags ###################################################################### alpino_mapping = { 'noun':'N', 'name': 'NP', 'vg': 'VG', 'punct':'.', 'verb':'V', 'pron': 'PRO', 'prep':'P' } def simplify_alpino_tag(tag): try: tag = alpino_mapping[tag] except KeyError: pass return tag.upper() ###################################################################### #{ Default tag simplification ###################################################################### def simplify_tag(tag): return tag[0].upper() nltk-2.0~b9/nltk/tag/sequential.py0000644000175000017500000005420711327451577017051 0ustar bhavanibhavani# Natural Language Toolkit: Sequential Backoff Taggers # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird (minor additions) # Tiago Tresoldi (original affix tagger) # URL: # For license information, see LICENSE.TXT """ Classes for tagging sentences sequentially, left to right. The abstract base class L{SequentialBackoffTagger} serves as the base class for all the taggers in this module. Tagging of individual words is performed by the method L{choose_tag() }, which is defined by subclasses of L{SequentialBackoffTagger}. If a tagger is unable to determine a tag for the specified token, then its I{backoff tagger} is consulted instead. Any C{SequentialBackoffTagger} may serve as a backoff tagger for any other C{SequentialBackoffTagger}. """ import re, yaml from nltk.probability import FreqDist, ConditionalFreqDist from nltk.classify.naivebayes import NaiveBayesClassifier from api import * from util import * ###################################################################### #{ Abstract Base Classes ###################################################################### class SequentialBackoffTagger(TaggerI): """ An abstract base class for taggers that tags words sequentially, left to right. Tagging of individual words is performed by the method L{choose_tag()}, which should be defined by subclasses. If a tagger is unable to determine a tag for the specified token, then its backoff tagger is consulted. @ivar _taggers: A list of all the taggers that should be tried to tag a token (i.e., C{self} and its backoff taggers). """ def __init__(self, backoff=None): if backoff is None: self._taggers = [self] else: self._taggers = [self] + backoff._taggers def _get_backoff(self): if len(self._taggers) < 2: return None else: return self._taggers[1] backoff = property(_get_backoff, doc=''' The backoff tagger for this tagger.''') def tag(self, tokens): # docs inherited from TaggerI tags = [] for i in range(len(tokens)): tags.append(self.tag_one(tokens, i, tags)) return zip(tokens, tags) def tag_one(self, tokens, index, history): """ Determine an appropriate tag for the specified token, and return that tag. If this tagger is unable to determine a tag for the specified token, then its backoff tagger is consulted. @rtype: C{str} @type tokens: C{list} @param tokens: The list of words that are being tagged. @type index: C{int} @param index: The index of the word whose tag should be returned. @type history: C{list} of C{str} @param history: A list of the tags for all words before C{index}. """ tag = None for tagger in self._taggers: tag = tagger.choose_tag(tokens, index, history) if tag is not None: break return tag def choose_tag(self, tokens, index, history): """ Decide which tag should be used for the specified token, and return that tag. If this tagger is unable to determine a tag for the specified token, return C{None} -- do I{not} consult the backoff tagger. This method should be overridden by subclasses of C{SequentialBackoffTagger}. @rtype: C{str} @type tokens: C{list} @param tokens: The list of words that are being tagged. @type index: C{int} @param index: The index of the word whose tag should be returned. @type history: C{list} of C{str} @param history: A list of the tags for all words before C{index}. """ raise AssertionError('SequentialBackoffTagger is an abstract class') class ContextTagger(SequentialBackoffTagger): """ An abstract base class for sequential backoff taggers that choose a tag for a token based on the value of its "context". Different subclasses are used to define different contexts. A C{ContextTagger} chooses the tag for a token by calculating the token's context, and looking up the corresponding tag in a table. This table can be constructed manually; or it can be automatically constructed based on a training corpus, using the L{_train()} factory method. @ivar _context_to_tag: Dictionary mapping contexts to tags. """ def __init__(self, context_to_tag, backoff=None): """ @param context_to_tag: A dictionary mapping contexts to tags. @param backoff: The backoff tagger that should be used for this tagger. """ SequentialBackoffTagger.__init__(self, backoff) if context_to_tag: self._context_to_tag = context_to_tag else: self._context_to_tag = {} def context(self, tokens, index, history): """ @return: the context that should be used to look up the tag for the specified token; or C{None} if the specified token should not be handled by this tagger. @rtype: (hashable) """ raise AssertionError('Abstract base class') def choose_tag(self, tokens, index, history): context = self.context(tokens, index, history) return self._context_to_tag.get(context) def size(self): """ @return: The number of entries in the table used by this tagger to map from contexts to tags. """ return len(self._context_to_tag) def __repr__(self): return '<%s: size=%d>' % (self.__class__.__name__, self.size()) def _train(self, tagged_corpus, cutoff=0, verbose=False): """ Initialize this C{ContextTagger}'s L{_context_to_tag} table based on the given training data. In particular, for each context C{I{c}} in the training data, set C{_context_to_tag[I{c}]} to the most frequent tag for that context. However, exclude any contexts that are already tagged perfectly by the backoff tagger(s). The old value of C{self._context_to_tag} (if any) is discarded. @param tagged_corpus: A tagged corpus. Each item should be a C{list} of C{(word, tag)} tuples. @param cutoff: If the most likely tag for a context occurs fewer than C{cutoff} times, then exclude it from the context-to-tag table for the new tagger. """ token_count = hit_count = 0 # A context is considered 'useful' if it's not already tagged # perfectly by the backoff tagger. useful_contexts = set() # Count how many times each tag occurs in each context. fd = ConditionalFreqDist() for sentence in tagged_corpus: tokens, tags = zip(*sentence) for index, (token, tag) in enumerate(sentence): # Record the event. token_count += 1 context = self.context(tokens, index, tags[:index]) if context is None: continue fd[context].inc(tag) # If the backoff got it wrong, this context is useful: if (self.backoff is None or tag != self.backoff.tag_one(tokens, index, tags[:index])): useful_contexts.add(context) # Build the context_to_tag table -- for each context, figure # out what the most likely tag is. Only include contexts that # we've seen at least `cutoff` times. for context in useful_contexts: best_tag = fd[context].max() hits = fd[context][best_tag] if hits > cutoff: self._context_to_tag[context] = best_tag hit_count += hits # Display some stats, if requested. if verbose: size = len(self._context_to_tag) backoff = 100 - (hit_count * 100.0)/ token_count pruning = 100 - (size * 100.0) / len(fd.conditions()) print "[Trained Unigram tagger:", print "size=%d, backoff=%.2f%%, pruning=%.2f%%]" % ( size, backoff, pruning) ###################################################################### #{ Tagger Classes ###################################################################### class DefaultTagger(SequentialBackoffTagger, yaml.YAMLObject): """ A tagger that assigns the same tag to every token. """ yaml_tag = '!nltk.DefaultTagger' def __init__(self, tag): """ Construct a new tagger that assigns C{tag} to all tokens. """ self._tag = tag SequentialBackoffTagger.__init__(self, None) def choose_tag(self, tokens, index, history): return self._tag # ignore token and history def __repr__(self): return '' % self._tag class NgramTagger(ContextTagger, yaml.YAMLObject): """ A tagger that chooses a token's tag based on its word string and on the preceeding I{n} word's tags. In particular, a tuple C{(tags[i-n:i-1], words[i])} is looked up in a table, and the corresponding tag is returned. N-gram taggers are typically trained on a tagged corpus. """ yaml_tag = '!nltk.NgramTagger' def __init__(self, n, train=None, model=None, backoff=None, cutoff=0, verbose=False): """ Train a new C{NgramTagger} using the given training data or the supplied model. In particular, construct a new tagger whose table maps from each context C{(tag[i-n:i-1], word[i])} to the most frequent tag for that context. But exclude any contexts that are already tagged perfectly by the backoff tagger. @param train: A tagged corpus consisting of a C{list} of tagged sentences, where each sentence is a C{list} of C{(word, tag)} tuples. @param backoff: A backoff tagger, to be used by the new tagger if it encounters an unknown context. @param cutoff: If the most likely tag for a context occurs fewer than C{cutoff} times, then exclude it from the context-to-tag table for the new tagger. """ self._n = n self._check_params(train, model) ContextTagger.__init__(self, model, backoff) if train: self._train(train, cutoff, verbose) def context(self, tokens, index, history): tag_context = tuple(history[max(0,index-self._n+1):index]) return (tag_context, tokens[index]) class UnigramTagger(NgramTagger): """ A tagger that chooses a token's tag based its word string. Unigram taggers are typically trained on a tagged corpus. """ yaml_tag = '!nltk.UnigramTagger' def __init__(self, train=None, model=None, backoff=None, cutoff=0, verbose=False): NgramTagger.__init__(self, 1, train, model, backoff, cutoff, verbose) def context(self, tokens, index, history): return tokens[index] class BigramTagger(NgramTagger): """ A tagger that chooses a token's tag based its word string and on the preceeding words' tag. In particular, a tuple consisting of the previous tag and the word is looked up in a table, and the corresponding tag is returned. Bigram taggers are typically trained on a tagged corpus. """ yaml_tag = '!nltk.BigramTagger' def __init__(self, train, model=None, backoff=None, cutoff=0, verbose=False): NgramTagger.__init__(self, 2, train, model, backoff, cutoff, verbose) class TrigramTagger(NgramTagger): """ A tagger that chooses a token's tag based its word string and on the preceeding two words' tags. In particular, a tuple consisting of the previous two tags and the word is looked up in a table, and the corresponding tag is returned. Trigram taggers are typically trained them on a tagged corpus. """ yaml_tag = '!nltk.TrigramTagger' def __init__(self, train=None, model=None, backoff=None, cutoff=0, verbose=False): NgramTagger.__init__(self, 3, train, model, backoff, cutoff, verbose) class AffixTagger(ContextTagger, yaml.YAMLObject): """ A tagger that chooses a token's tag based on a leading or trailing substring of its word string. (It is important to note that these substrings are not necessarily "true" morphological affixes). In particular, a fixed-length substring of the word is looked up in a table, and the corresponding tag is returned. Affix taggers are typically constructed by training them on a tagged corpus; see L{the constructor <__init__>}. """ yaml_tag = '!nltk.AffixTagger' def __init__(self, train=None, model=None, affix_length=-3, min_stem_length=2, backoff=None, cutoff=0, verbose=False): """ Construct a new affix tagger. @param affix_length: The length of the affixes that should be considered during training and tagging. Use negative numbers for suffixes. @param min_stem_length: Any words whose length is less than C{min_stem_length+abs(affix_length)} will be assigned a tag of C{None} by this tagger. """ self._check_params(train, model) ContextTagger.__init__(self, model, backoff) self._affix_length = affix_length self._min_word_length = min_stem_length + abs(affix_length) if train: self._train(train, cutoff, verbose) def context(self, tokens, index, history): token = tokens[index] if len(token) < self._min_word_length: return None elif self._affix_length > 0: return token[:self._affix_length] else: return token[self._affix_length:] class RegexpTagger(SequentialBackoffTagger, yaml.YAMLObject): """ A tagger that assigns tags to words based on regular expressions over word strings. """ yaml_tag = '!nltk.RegexpTagger' def __init__(self, regexps, backoff=None): """ Construct a new regexp tagger. @type regexps: C{list} of C{(str, str)} @param regexps: A list of C{(regexp, tag)} pairs, each of which indicates that a word matching C{regexp} should be tagged with C{tag}. The pairs will be evalutated in order. If none of the regexps match a word, then the optional backoff tagger is invoked, else it is assigned the tag C{None}. """ self._regexps = regexps SequentialBackoffTagger.__init__(self, backoff) def choose_tag(self, tokens, index, history): for regexp, tag in self._regexps: if re.match(regexp, tokens[index]): # ignore history return tag return None def __repr__(self): return '' % len(self._regexps) class ClassifierBasedTagger(SequentialBackoffTagger, FeaturesetTaggerI): """ A sequential tagger that uses a classifier to choose the tag for each token in a sentence. The featureset input for the classifier is generated by a feature detector function:: feature_detector(tokens, index, history) -> featureset Where C{tokens} is the list of unlabeled tokens in the sentence; C{index} is the index of the token for which feature detection should be performed; and C{history} is list of the tags for all tokens before C{index}. """ def __init__(self, feature_detector=None, train=None, classifier_builder=NaiveBayesClassifier.train, classifier=None, backoff=None, cutoff_prob=None, verbose=False): """ Construct a new classifier-based sequential tagger. @param feature_detector: A function used to generate the featureset input for the classifier:: feature_detector(tokens, index, history) -> featureset @param train: A tagged corpus consisting of a C{list} of tagged sentences, where each sentence is a C{list} of C{(word, tag)} tuples. @param backoff: A backoff tagger, to be used by the new tagger if it encounters an unknown context. @param classifier_builder: A function used to train a new classifier based on the data in C{train}. It should take one argument, a list of labeled featuresets (i.e., C{(featureset, label)} tuples). @param classifier: The classifier that should be used by the tagger. This is only useful if you want to manually construct the classifier; normally, you would use C{train} instead. @param backoff: A backoff tagger, used if this tagger is unable to determine a tag for a given token. @param cutoff_prob: If specified, then this tagger will fall back on its backoff tagger if the probability of the most likely tag is less than C{cutoff_prob}. """ self._check_params(train, classifier) SequentialBackoffTagger.__init__(self, backoff) if (train and classifier) or (not train and not classifier): raise ValueError('Must specify either training data or ' 'trained classifier.') if feature_detector is not None: self._feature_detector = feature_detector # The feature detector function, used to generate a featureset # or each token: feature_detector(tokens, index, history) -> featureset self._cutoff_prob = cutoff_prob """Cutoff probability for tagging -- if the probability of the most likely tag is less than this, then use backoff.""" self._classifier = classifier """The classifier used to choose a tag for each token.""" if train: self._train(train, classifier_builder, verbose) def choose_tag(self, tokens, index, history): # Use our feature detector to get the featureset. featureset = self.feature_detector(tokens, index, history) # Use the classifier to pick a tag. If a cutoff probability # was specified, then check that the tag's probability is # higher than that cutoff first; otherwise, return None. if self._cutoff_prob is None: return self._classifier.classify(featureset) else: pdist = self._classifier.prob_classify(featureset) tag = pdist.max() if pdist.prob(tag) >= self._cutoff_prob: return tag else: return None def _train(self, tagged_corpus, classifier_builder, verbose): """ Build a new classifier, based on the given training data (C{tagged_corpus}). """ classifier_corpus = [] if verbose: print 'Constructing training corpus for classifier.' for sentence in tagged_corpus: history = [] untagged_sentence, tags = zip(*sentence) for index in range(len(sentence)): featureset = self.feature_detector(untagged_sentence, index, history) classifier_corpus.append( (featureset, tags[index]) ) history.append(tags[index]) if verbose: print 'Training classifier (%d instances)' % len(classifier_corpus) self._classifier = classifier_builder(classifier_corpus) def __repr__(self): return '' % self._classifier def feature_detector(self, tokens, index, history): """ Return the feature detector that this tagger uses to generate featuresets for its classifier. The feature detector is a function with the signature:: feature_detector(tokens, index, history) -> featureset @see: L{classifier()} """ return self._feature_detector(tokens, index, history) def classifier(self): """ Return the classifier that this tagger uses to choose a tag for each word in a sentence. The input for this classifier is generated using this tagger's feature detector. @see: L{feature_detector()} """ return self._classifier class ClassifierBasedPOSTagger(ClassifierBasedTagger): """ A classifier based part of speech tagger. """ def feature_detector(self, tokens, index, history): word = tokens[index] if index == 0: prevword = prevprevword = None prevtag = prevprevtag = None elif index == 1: prevword = tokens[index-1].lower() prevprevword = None prevtag = history[index-1] prevprevtag = None else: prevword = tokens[index-1].lower() prevprevword = tokens[index-2].lower() prevtag = history[index-1] prevprevtag = history[index-2] if re.match('[0-9]+(\.[0-9]*)?|[0-9]*\.[0-9]+$', word): shape = 'number' elif re.match('\W+$', word): shape = 'punct' elif re.match('[A-Z][a-z]+$', word): shape = 'upcase' elif re.match('[a-z]+$', word): shape = 'downcase' elif re.match('\w+$', word): shape = 'mixedcase' else: shape = 'other' features = { 'prevtag': prevtag, 'prevprevtag': prevprevtag, 'word': word, 'word.lower': word.lower(), 'suffix3': word.lower()[-3:], 'suffix2': word.lower()[-2:], 'suffix1': word.lower()[-1:], 'prevprevword': prevprevword, 'prevword': prevword, 'prevtag+word': '%s+%s' % (prevtag, word.lower()), 'prevprevtag+word': '%s+%s' % (prevprevtag, word.lower()), 'prevword+word': '%s+%s' % (prevword, word.lower()), 'shape': shape, } return features nltk-2.0~b9/nltk/tag/hunpos.py0000644000175000017500000000523511360522135016172 0ustar bhavanibhavani# -*- coding: utf-8 -*- # Natural Language Toolkit: Interface to the HunPos POS-tagger # # Copyright (C) 2001-2010 NLTK Project # Author: Peter Ljunglöf # URL: # For license information, see LICENSE.TXT # # $Id: hunpos.py $ """ A module for interfacing with the HunPos open-source POS-tagger. """ import os from subprocess import Popen, PIPE import nltk from api import * _hunpos_url = 'http://code.google.com/p/hunpos/' class HunposTagger(TaggerI): """ A class for pos tagging with HunPos. The input is the paths to: - a model trained on training data - (optionally) the path to the hunpos-tag binary - (optionally) the encoding of the training data (default: ASCII) Example: >>> ht = HunposTagger('english.model') >>> ht.tag('What is the airspeed of an unladen swallow ?'.split()) [('What', 'WP'), ('is', 'VBZ'), ('the', 'DT'), ('airspeed', 'NN'), ('of', 'IN'), ('an', 'DT'), ('unladen', 'NN'), ('swallow', 'VB'), ('?', '.')] """ def __init__(self, path_to_model, path_to_bin=None, encoding=None, verbose=False): hunpos_paths = ['.', '/usr/bin', '/usr/local/bin', '/opt/local/bin', '/Applications/bin', '~/bin', '~/Applications/bin'] hunpos_paths = map(os.path.expanduser, hunpos_paths) self._hunpos_bin = nltk.internals.find_binary( 'hunpos-tag', path_to_bin, env_vars=('HUNPOS', 'HUNPOS_HOME'), searchpath=hunpos_paths, url=_hunpos_url, verbose=verbose) assert os.path.isfile(path_to_model), \ "Hunpos model file not found: %s" % (model_file) self._hunpos_model = path_to_model self._encoding = encoding def tag(self, tokens): return self.batch_tag([tokens])[0] def batch_tag(self, sentences): encoding = self._encoding hunpos = Popen([self._hunpos_bin, self._hunpos_model], shell=False, stdin=PIPE, stdout=PIPE, stderr=PIPE) hunpos_input = "\n\n".join("\n".join(sentence) for sentence in sentences) if encoding: hunpos_input = hunpos_input.encode(encoding) hunpos_output, _stderr = hunpos.communicate(hunpos_input) if encoding: hunpos_output = hunpos_output.decode(encoding) tagged_sentences = [] for tagged_sentence in hunpos_output.strip().split("\n\n"): sentence = [tuple(tagged_word.strip().split("\t")) for tagged_word in tagged_sentence.strip().split("\n")] tagged_sentences.append(sentence) return tagged_sentences nltk-2.0~b9/nltk/tag/hmm.py0000644000175000017500000014441511327451577015461 0ustar bhavanibhavani# Natural Language Toolkit: Hidden Markov Model # # Copyright (C) 2001-2010 NLTK Project # Author: Trevor Cohn # Philip Blunsom # Tiago Tresoldi (fixes) # Steven Bird (fixes) # Joseph Frazee (fixes) # URL: # For license information, see LICENSE.TXT # # $Id: hmm.py 8479 2010-01-13 05:40:34Z StevenBird1 $ """ Hidden Markov Models (HMMs) largely used to assign the correct label sequence to sequential data or assess the probability of a given label and data sequence. These models are finite state machines characterised by a number of states, transitions between these states, and output symbols emitted while in each state. The HMM is an extension to the Markov chain, where each state corresponds deterministically to a given event. In the HMM the observation is a probabilistic function of the state. HMMs share the Markov chain's assumption, being that the probability of transition from one state to another only depends on the current state - i.e. the series of states that led to the current state are not used. They are also time invariant. The HMM is a directed graph, with probability weighted edges (representing the probability of a transition between the source and sink states) where each vertex emits an output symbol when entered. The symbol (or observation) is non-deterministically generated. For this reason, knowing that a sequence of output observations was generated by a given HMM does not mean that the corresponding sequence of states (and what the current state is) is known. This is the 'hidden' in the hidden markov model. Formally, a HMM can be characterised by: - the output observation alphabet. This is the set of symbols which may be observed as output of the system. - the set of states. - the transition probabilities M{a_{ij} = P(s_t = j | s_{t-1} = i)}. These represent the probability of transition to each state from a given state. - the output probability matrix M{b_i(k) = P(X_t = o_k | s_t = i)}. These represent the probability of observing each symbol in a given state. - the initial state distribution. This gives the probability of starting in each state. To ground this discussion, take a common NLP application, part-of-speech (POS) tagging. An HMM is desirable for this task as the highest probability tag sequence can be calculated for a given sequence of word forms. This differs from other tagging techniques which often tag each word individually, seeking to optimise each individual tagging greedily without regard to the optimal combination of tags for a larger unit, such as a sentence. The HMM does this with the Viterbi algorithm, which efficiently computes the optimal path through the graph given the sequence of words forms. In POS tagging the states usually have a 1:1 correspondence with the tag alphabet - i.e. each state represents a single tag. The output observation alphabet is the set of word forms (the lexicon), and the remaining three parameters are derived by a training regime. With this information the probability of a given sentence can be easily derived, by simply summing the probability of each distinct path through the model. Similarly, the highest probability tagging sequence can be derived with the Viterbi algorithm, yielding a state sequence which can be mapped into a tag sequence. This discussion assumes that the HMM has been trained. This is probably the most difficult task with the model, and requires either MLE estimates of the parameters or unsupervised learning using the Baum-Welch algorithm, a variant of EM. """ import re import types from numpy import * from nltk.probability import FreqDist, ConditionalFreqDist, \ ConditionalProbDist, DictionaryProbDist, DictionaryConditionalProbDist, \ LidstoneProbDist, MutableProbDist, MLEProbDist from nltk.internals import deprecated from nltk.metrics import accuracy as _accuracy from nltk.util import LazyMap, LazyConcatenation, LazyZip from api import * # _NINF = float('-inf') # won't work on Windows _NINF = float('-1e300') _TEXT = 0 # index of text in a tuple _TAG = 1 # index of tag in a tuple class HiddenMarkovModelTagger(TaggerI): """ Hidden Markov model class, a generative model for labelling sequence data. These models define the joint probability of a sequence of symbols and their labels (state transitions) as the product of the starting state probability, the probability of each state transition, and the probability of each observation being generated from each state. This is described in more detail in the module documentation. This implementation is based on the HMM description in Chapter 8, Huang, Acero and Hon, Spoken Language Processing and includes an extension for training shallow HMM parsers or specializaed HMMs as in Molina et. al, 2002. A specialized HMM modifies training data by applying a specialization function to create a new training set that is more appropriate for sequential tagging with an HMM. A typical use case is chunking. """ def __init__(self, symbols, states, transitions, outputs, priors, **kwargs): """ Creates a hidden markov model parametised by the the states, transition probabilities, output probabilities and priors. @param symbols: the set of output symbols (alphabet) @type symbols: seq of any @param states: a set of states representing state space @type states: seq of any @param transitions: transition probabilities; Pr(s_i | s_j) is the probability of transition from state i given the model is in state_j @type transitions: C{ConditionalProbDistI} @param outputs: output probabilities; Pr(o_k | s_i) is the probability of emitting symbol k when entering state i @type outputs: C{ConditionalProbDistI} @param priors: initial state distribution; Pr(s_i) is the probability of starting in state i @type priors: C{ProbDistI} @kwparam transform: an optional function for transforming training instances, defaults to the identity function. @type transform: C{function} or C{HiddenMarkovModelTaggerTransform} """ self._states = states self._transitions = transitions self._symbols = symbols self._outputs = outputs self._priors = priors self._cache = None self._transform = kwargs.get('transform', IdentityTransform()) if isinstance(self._transform, types.FunctionType): self._transform = LambdaTransform(self._transform) elif not isinstance(self._transform, HiddenMarkovModelTaggerTransformI): raise @classmethod def _train(cls, labeled_sequence, test_sequence=None, unlabeled_sequence=None, **kwargs): transform = kwargs.get('transform', IdentityTransform()) if isinstance(transform, types.FunctionType): transform = LambdaTransform(transform) elif \ not isinstance(transform, HiddenMarkovModelTaggerTransformI): raise estimator = kwargs.get('estimator', lambda fd, bins: \ LidstoneProbDist(fd, 0.1, bins)) labeled_sequence = LazyMap(transform.transform, labeled_sequence) symbols = list(set(word for sent in labeled_sequence for word, tag in sent)) tag_set = list(set(tag for sent in labeled_sequence for word, tag in sent)) trainer = HiddenMarkovModelTrainer(tag_set, symbols) hmm = trainer.train_supervised(labeled_sequence, estimator=estimator) hmm = cls(hmm._symbols, hmm._states, hmm._transitions, hmm._outputs, hmm._priors, transform=transform) if test_sequence: hmm.test(test_sequence, verbose=kwargs.get('verbose', False)) if unlabeled_sequence: max_iterations = kwargs.get('max_iterations', 5) hmm = trainer.train_unsupervised(unlabeled_sequence, model=hmm, max_iterations=max_iterations) if test_sequence: hmm.test(test_sequence, verbose=kwargs.get('verbose', False)) return hmm @classmethod def train(cls, labeled_sequence, test_sequence=None, unlabeled_sequence=None, **kwargs): """ Train a new C{HiddenMarkovModelTagger} using the given labeled and unlabeled training instances. Testing will be performed if test instances are provided. @return: a hidden markov model tagger @rtype: C{HiddenMarkovModelTagger} @param labeled_sequence: a sequence of labeled training instances, i.e. a list of sentences represented as tuples @type labeled_sequence: C{list} of C{list} @param test_sequence: a sequence of labeled test instances @type test_sequence: C{list} of C{list} @param unlabeled_sequence: a sequence of unlabeled training instances, i.e. a list of sentences represented as words @type unlabeled_sequence: C{list} of C{list} @kwparam transform: an optional function for transforming training instances, defaults to the identity function, see L{transform()} @type transform: C{function} @kwparam estimator: an optional function or class that maps a condition's frequency distribution to its probability distribution, defaults to a Lidstone distribution with gamma = 0.1 @type estimator: C{class} or C{function} @kwparam verbose: boolean flag indicating whether training should be verbose or include printed output @type verbose: C{bool} @kwparam max_iterations: number of Baum-Welch interations to perform @type max_iterations: C{int} """ return cls._train(labeled_sequence, test_sequence, unlabeled_sequence, **kwargs) def probability(self, sequence): """ Returns the probability of the given symbol sequence. If the sequence is labelled, then returns the joint probability of the symbol, state sequence. Otherwise, uses the forward algorithm to find the probability over all label sequences. @return: the probability of the sequence @rtype: float @param sequence: the sequence of symbols which must contain the TEXT property, and optionally the TAG property @type sequence: Token """ return 2**(self.log_probability(self._transform.transform(sequence))) def log_probability(self, sequence): """ Returns the log-probability of the given symbol sequence. If the sequence is labelled, then returns the joint log-probability of the symbol, state sequence. Otherwise, uses the forward algorithm to find the log-probability over all label sequences. @return: the log-probability of the sequence @rtype: float @param sequence: the sequence of symbols which must contain the TEXT property, and optionally the TAG property @type sequence: Token """ sequence = self._transform.transform(sequence) T = len(sequence) N = len(self._states) if T > 0 and sequence[0][_TAG]: last_state = sequence[0][_TAG] p = self._priors.logprob(last_state) + \ self._outputs[last_state].logprob(sequence[0][_TEXT]) for t in range(1, T): state = sequence[t][_TAG] p += self._transitions[last_state].logprob(state) + \ self._outputs[state].logprob(sequence[t][_TEXT]) last_state = state return p else: alpha = self._forward_probability(sequence) p = _log_add(*alpha[T-1, :]) return p def tag(self, unlabeled_sequence): """ Tags the sequence with the highest probability state sequence. This uses the best_path method to find the Viterbi path. @return: a labelled sequence of symbols @rtype: list @param unlabeled_sequence: the sequence of unlabeled symbols @type unlabeled_sequence: list """ unlabeled_sequence = self._transform.transform(unlabeled_sequence) return self._tag(unlabeled_sequence) def _tag(self, unlabeled_sequence): path = self._best_path(unlabeled_sequence) return zip(unlabeled_sequence, path) def _output_logprob(self, state, symbol): """ @return: the log probability of the symbol being observed in the given state @rtype: float """ return self._outputs[state].logprob(symbol) def _create_cache(self): """ The cache is a tuple (P, O, X, S) where: - S maps symbols to integers. I.e., it is the inverse mapping from self._symbols; for each symbol s in self._symbols, the following is true:: self._symbols[S[s]] == s - O is the log output probabilities:: O[i,k] = log( P(token[t]=sym[k]|tag[t]=state[i]) ) - X is the log transition probabilities:: X[i,j] = log( P(tag[t]=state[j]|tag[t-1]=state[i]) ) - P is the log prior probabilities:: P[i] = log( P(tag[0]=state[i]) ) """ if not self._cache: N = len(self._states) M = len(self._symbols) P = zeros(N, float32) X = zeros((N, N), float32) O = zeros((N, M), float32) for i in range(N): si = self._states[i] P[i] = self._priors.logprob(si) for j in range(N): X[i, j] = self._transitions[si].logprob(self._states[j]) for k in range(M): O[i, k] = self._outputs[si].logprob(self._symbols[k]) S = {} for k in range(M): S[self._symbols[k]] = k self._cache = (P, O, X, S) def _update_cache(self, symbols): # add new symbols to the symbol table and repopulate the output # probabilities and symbol table mapping if symbols: self._create_cache() P, O, X, S = self._cache for symbol in symbols: if symbol not in self._symbols: self._cache = None self._symbols.append(symbol) # don't bother with the work if there aren't any new symbols if not self._cache: N = len(self._states) M = len(self._symbols) Q = O.shape[1] # add new columns to the output probability table without # destroying the old probabilities O = hstack([O, zeros((N, M - Q), float32)]) for i in range(N): si = self._states[i] # only calculate probabilities for new symbols for k in range(Q, M): O[i, k] = self._outputs[si].logprob(self._symbols[k]) # only create symbol mappings for new symbols for k in range(Q, M): S[self._symbols[k]] = k self._cache = (P, O, X, S) def best_path(self, unlabeled_sequence): """ Returns the state sequence of the optimal (most probable) path through the HMM. Uses the Viterbi algorithm to calculate this part by dynamic programming. @return: the state sequence @rtype: sequence of any @param unlabeled_sequence: the sequence of unlabeled symbols @type unlabeled_sequence: list """ unlabeled_sequence = self._transform.transform(unlabeled_sequence) return self._best_path(unlabeled_sequence) def _best_path(self, unlabeled_sequence): T = len(unlabeled_sequence) N = len(self._states) self._create_cache() self._update_cache(unlabeled_sequence) P, O, X, S = self._cache V = zeros((T, N), float32) B = ones((T, N), int) * -1 V[0] = P + O[:, S[unlabeled_sequence[0]]] for t in range(1, T): for j in range(N): vs = V[t-1, :] + X[:, j] best = argmax(vs) V[t, j] = vs[best] + O[j, S[unlabeled_sequence[t]]] B[t, j] = best current = argmax(V[T-1,:]) sequence = [current] for t in range(T-1, 0, -1): last = B[t, current] sequence.append(last) current = last sequence.reverse() return map(self._states.__getitem__, sequence) def best_path_simple(self, unlabeled_sequence): """ Returns the state sequence of the optimal (most probable) path through the HMM. Uses the Viterbi algorithm to calculate this part by dynamic programming. This uses a simple, direct method, and is included for teaching purposes. @return: the state sequence @rtype: sequence of any @param unlabeled_sequence: the sequence of unlabeled symbols @type unlabeled_sequence: list """ unlabeled_sequence = self._transform.transform(unlabeled_sequence) return self._best_path_simple(unlabeled_sequence) def _best_path_simple(self, unlabeled_sequence): T = len(unlabeled_sequence) N = len(self._states) V = zeros((T, N), float64) B = {} # find the starting log probabilities for each state symbol = unlabeled_sequence[0] for i, state in enumerate(self._states): V[0, i] = self._priors.logprob(state) + \ self._output_logprob(state, symbol) B[0, state] = None # find the maximum log probabilities for reaching each state at time t for t in range(1, T): symbol = unlabeled_sequence[t] for j in range(N): sj = self._states[j] best = None for i in range(N): si = self._states[i] va = V[t-1, i] + self._transitions[si].logprob(sj) if not best or va > best[0]: best = (va, si) V[t, j] = best[0] + self._output_logprob(sj, symbol) B[t, sj] = best[1] # find the highest probability final state best = None for i in range(N): val = V[T-1, i] if not best or val > best[0]: best = (val, self._states[i]) # traverse the back-pointers B to find the state sequence current = best[1] sequence = [current] for t in range(T-1, 0, -1): last = B[t, current] sequence.append(last) current = last sequence.reverse() return sequence def random_sample(self, rng, length): """ Randomly sample the HMM to generate a sentence of a given length. This samples the prior distribution then the observation distribution and transition distribution for each subsequent observation and state. This will mostly generate unintelligible garbage, but can provide some amusement. @return: the randomly created state/observation sequence, generated according to the HMM's probability distributions. The SUBTOKENS have TEXT and TAG properties containing the observation and state respectively. @rtype: list @param rng: random number generator @type rng: Random (or any object with a random() method) @param length: desired output length @type length: int """ # sample the starting state and symbol prob dists tokens = [] state = self._sample_probdist(self._priors, rng.random(), self._states) symbol = self._sample_probdist(self._outputs[state], rng.random(), self._symbols) tokens.append((symbol, state)) for i in range(1, length): # sample the state transition and symbol prob dists state = self._sample_probdist(self._transitions[state], rng.random(), self._states) symbol = self._sample_probdist(self._outputs[state], rng.random(), self._symbols) tokens.append((symbol, state)) return tokens def _sample_probdist(self, probdist, p, samples): cum_p = 0 for sample in samples: add_p = probdist.prob(sample) if cum_p <= p <= cum_p + add_p: return sample cum_p += add_p raise Exception('Invalid probability distribution - ' 'does not sum to one') def entropy(self, unlabeled_sequence): """ Returns the entropy over labellings of the given sequence. This is given by:: H(O) = - sum_S Pr(S | O) log Pr(S | O) where the summation ranges over all state sequences, S. Let M{Z = Pr(O) = sum_S Pr(S, O)} where the summation ranges over all state sequences and O is the observation sequence. As such the entropy can be re-expressed as:: H = - sum_S Pr(S | O) log [ Pr(S, O) / Z ] = log Z - sum_S Pr(S | O) log Pr(S, 0) = log Z - sum_S Pr(S | O) [ log Pr(S_0) + sum_t Pr(S_t | S_{t-1}) + sum_t Pr(O_t | S_t) ] The order of summation for the log terms can be flipped, allowing dynamic programming to be used to calculate the entropy. Specifically, we use the forward and backward probabilities (alpha, beta) giving:: H = log Z - sum_s0 alpha_0(s0) beta_0(s0) / Z * log Pr(s0) + sum_t,si,sj alpha_t(si) Pr(sj | si) Pr(O_t+1 | sj) beta_t(sj) / Z * log Pr(sj | si) + sum_t,st alpha_t(st) beta_t(st) / Z * log Pr(O_t | st) This simply uses alpha and beta to find the probabilities of partial sequences, constrained to include the given state(s) at some point in time. """ unlabeled_sequence = self._transform.transform(unlabeled_sequence) T = len(unlabeled_sequence) N = len(self._states) alpha = self._forward_probability(unlabeled_sequence) beta = self._backward_probability(unlabeled_sequence) normalisation = _log_add(*alpha[T-1, :]) entropy = normalisation # starting state, t = 0 for i, state in enumerate(self._states): p = 2**(alpha[0, i] + beta[0, i] - normalisation) entropy -= p * self._priors.logprob(state) #print 'p(s_0 = %s) =' % state, p # state transitions for t0 in range(T - 1): t1 = t0 + 1 for i0, s0 in enumerate(self._states): for i1, s1 in enumerate(self._states): p = 2**(alpha[t0, i0] + self._transitions[s0].logprob(s1) + self._outputs[s1].logprob( unlabeled_sequence[t1][_TEXT]) + beta[t1, i1] - normalisation) entropy -= p * self._transitions[s0].logprob(s1) #print 'p(s_%d = %s, s_%d = %s) =' % (t0, s0, t1, s1), p # symbol emissions for t in range(T): for i, state in enumerate(self._states): p = 2**(alpha[t, i] + beta[t, i] - normalisation) entropy -= p * self._outputs[state].logprob( unlabeled_sequence[t][_TEXT]) #print 'p(s_%d = %s) =' % (t, state), p return entropy def point_entropy(self, unlabeled_sequence): """ Returns the pointwise entropy over the possible states at each position in the chain, given the observation sequence. """ unlabeled_sequence = self._transform.transform(unlabeled_sequence) T = len(unlabeled_sequence) N = len(self._states) alpha = self._forward_probability(unlabeled_sequence) beta = self._backward_probability(unlabeled_sequence) normalisation = _log_add(*alpha[T-1, :]) entropies = zeros(T, float64) probs = zeros(N, float64) for t in range(T): for s in range(N): probs[s] = alpha[t, s] + beta[t, s] - normalisation for s in range(N): entropies[t] -= 2**(probs[s]) * probs[s] return entropies def _exhaustive_entropy(self, unlabeled_sequence): unlabeled_sequence = self._transform.transform(unlabeled_sequence) T = len(unlabeled_sequence) N = len(self._states) labellings = [[state] for state in self._states] for t in range(T - 1): current = labellings labellings = [] for labelling in current: for state in self._states: labellings.append(labelling + [state]) log_probs = [] for labelling in labellings: labelled_sequence = unlabeled_sequence[:] for t, label in enumerate(labelling): labelled_sequence[t] = (labelled_sequence[t][_TEXT], label) lp = self.log_probability(labelled_sequence) log_probs.append(lp) normalisation = _log_add(*log_probs) #ps = zeros((T, N), float64) #for labelling, lp in zip(labellings, log_probs): #for t in range(T): #ps[t, self._states.index(labelling[t])] += \ # 2**(lp - normalisation) #for t in range(T): #print 'prob[%d] =' % t, ps[t] entropy = 0 for lp in log_probs: lp -= normalisation entropy -= 2**(lp) * lp return entropy def _exhaustive_point_entropy(self, unlabeled_sequence): unlabeled_sequence = self._transform.transform(unlabeled_sequence) T = len(unlabeled_sequence) N = len(self._states) labellings = [[state] for state in self._states] for t in range(T - 1): current = labellings labellings = [] for labelling in current: for state in self._states: labellings.append(labelling + [state]) log_probs = [] for labelling in labellings: labelled_sequence = unlabeled_sequence[:] for t, label in enumerate(labelling): labelled_sequence[t] = (labelled_sequence[t][_TEXT], label) lp = self.log_probability(labelled_sequence) log_probs.append(lp) normalisation = _log_add(*log_probs) probabilities = zeros((T, N), float64) probabilities[:] = _NINF for labelling, lp in zip(labellings, log_probs): lp -= normalisation for t, label in enumerate(labelling): index = self._states.index(label) probabilities[t, index] = _log_add(probabilities[t, index], lp) entropies = zeros(T, float64) for t in range(T): for s in range(N): entropies[t] -= 2**(probabilities[t, s]) * probabilities[t, s] return entropies def _forward_probability(self, unlabeled_sequence): """ Return the forward probability matrix, a T by N array of log-probabilities, where T is the length of the sequence and N is the number of states. Each entry (t, s) gives the probability of being in state s at time t after observing the partial symbol sequence up to and including t. @param unlabeled_sequence: the sequence of unlabeled symbols @type unlabeled_sequence: list @return: the forward log probability matrix @rtype: array """ T = len(unlabeled_sequence) N = len(self._states) alpha = zeros((T, N), float64) symbol = unlabeled_sequence[0][_TEXT] for i, state in enumerate(self._states): alpha[0, i] = self._priors.logprob(state) + \ self._outputs[state].logprob(symbol) for t in range(1, T): symbol = unlabeled_sequence[t][_TEXT] for i, si in enumerate(self._states): alpha[t, i] = _NINF for j, sj in enumerate(self._states): alpha[t, i] = _log_add(alpha[t, i], alpha[t-1, j] + self._transitions[sj].logprob(si)) alpha[t, i] += self._outputs[si].logprob(symbol) return alpha def _backward_probability(self, unlabeled_sequence): """ Return the backward probability matrix, a T by N array of log-probabilities, where T is the length of the sequence and N is the number of states. Each entry (t, s) gives the probability of being in state s at time t after observing the partial symbol sequence from t .. T. @return: the backward log probability matrix @rtype: array @param unlabeled_sequence: the sequence of unlabeled symbols @type unlabeled_sequence: list """ T = len(unlabeled_sequence) N = len(self._states) beta = zeros((T, N), float64) # initialise the backward values beta[T-1, :] = log2(1) # inductively calculate remaining backward values for t in range(T-2, -1, -1): symbol = unlabeled_sequence[t+1][_TEXT] for i, si in enumerate(self._states): beta[t, i] = _NINF for j, sj in enumerate(self._states): beta[t, i] = _log_add(beta[t, i], self._transitions[si].logprob(sj) + self._outputs[sj].logprob(symbol) + beta[t + 1, j]) return beta def test(self, test_sequence, **kwargs): """ Tests the C{HiddenMarkovModelTagger} instance. @param test_sequence: a sequence of labeled test instances @type test_sequence: C{list} of C{list} @kwparam verbose: boolean flag indicating whether training should be verbose or include printed output @type verbose: C{bool} """ def words(sent): return [word for (word, tag) in sent] def tags(sent): return [tag for (word, tag) in sent] test_sequence = LazyMap(self._transform.transform, test_sequence) predicted_sequence = LazyMap(self._tag, LazyMap(words, test_sequence)) if kwargs.get('verbose', False): # This will be used again later for accuracy so there's no sense # in tagging it twice. test_sequence = list(test_sequence) predicted_sequence = list(predicted_sequence) for test_sent, predicted_sent in zip(test_sequence, predicted_sequence): print 'Test:', \ ' '.join(['%s/%s' % (str(token), str(tag)) for (token, tag) in test_sent]) print print 'Untagged:', \ ' '.join([str(token) for (token, tag) in test_sent]) print print 'HMM-tagged:', \ ' '.join(['%s/%s' % (str(token), str(tag)) for (token, tag) in predicted_sent]) print print 'Entropy:', \ self.entropy([(token, None) for (token, tag) in predicted_sent]) print print '-' * 60 test_tags = LazyConcatenation(LazyMap(tags, test_sequence)) predicted_tags = LazyConcatenation(LazyMap(tags, predicted_sequence)) acc = _accuracy(test_tags, predicted_tags) count = sum([len(sent) for sent in test_sequence]) print 'accuracy over %d tokens: %.2f' % (count, acc * 100) def __repr__(self): return ('' % (len(self._states), len(self._symbols))) class HiddenMarkovModelTrainer(object): """ Algorithms for learning HMM parameters from training data. These include both supervised learning (MLE) and unsupervised learning (Baum-Welch). """ def __init__(self, states=None, symbols=None): """ Creates an HMM trainer to induce an HMM with the given states and output symbol alphabet. A supervised and unsupervised training method may be used. If either of the states or symbols are not given, these may be derived from supervised training. @param states: the set of state labels @type states: sequence of any @param symbols: the set of observation symbols @type symbols: sequence of any """ if states: self._states = states else: self._states = [] if symbols: self._symbols = symbols else: self._symbols = [] def train(self, labelled_sequences=None, unlabeled_sequences=None, **kwargs): """ Trains the HMM using both (or either of) supervised and unsupervised techniques. @return: the trained model @rtype: HiddenMarkovModelTagger @param labelled_sequences: the supervised training data, a set of labelled sequences of observations @type labelled_sequences: list @param unlabeled_sequences: the unsupervised training data, a set of sequences of observations @type unlabeled_sequences: list @param kwargs: additional arguments to pass to the training methods """ assert labelled_sequences or unlabeled_sequences model = None if labelled_sequences: model = self.train_supervised(labelled_sequences, **kwargs) if unlabeled_sequences: if model: kwargs['model'] = model model = self.train_unsupervised(unlabeled_sequences, **kwargs) return model def train_unsupervised(self, unlabeled_sequences, **kwargs): """ Trains the HMM using the Baum-Welch algorithm to maximise the probability of the data sequence. This is a variant of the EM algorithm, and is unsupervised in that it doesn't need the state sequences for the symbols. The code is based on 'A Tutorial on Hidden Markov Models and Selected Applications in Speech Recognition', Lawrence Rabiner, IEEE, 1989. @return: the trained model @rtype: HiddenMarkovModelTagger @param unlabeled_sequences: the training data, a set of sequences of observations @type unlabeled_sequences: list @param kwargs: may include the following parameters:: model - a HiddenMarkovModelTagger instance used to begin the Baum-Welch algorithm max_iterations - the maximum number of EM iterations convergence_logprob - the maximum change in log probability to allow convergence """ N = len(self._states) M = len(self._symbols) symbol_dict = dict((self._symbols[i], i) for i in range(M)) # create a uniform HMM, which will be iteratively refined, unless # given an existing model model = kwargs.get('model') if not model: priors = UniformProbDist(self._states) transitions = DictionaryConditionalProbDist( dict((state, UniformProbDist(self._states)) for state in self._states)) output = DictionaryConditionalProbDist( dict((state, UniformProbDist(self._symbols)) for state in self._states)) model = HiddenMarkovModelTagger(self._symbols, self._states, transitions, output, priors) # update model prob dists so that they can be modified model._priors = MutableProbDist(model._priors, self._states) model._transitions = DictionaryConditionalProbDist( dict((s, MutableProbDist(model._transitions[s], self._states)) for s in self._states)) model._outputs = DictionaryConditionalProbDist( dict((s, MutableProbDist(model._outputs[s], self._symbols)) for s in self._states)) # iterate until convergence converged = False last_logprob = None iteration = 0 max_iterations = kwargs.get('max_iterations', 1000) epsilon = kwargs.get('convergence_logprob', 1e-6) while not converged and iteration < max_iterations: A_numer = ones((N, N), float64) * _NINF B_numer = ones((N, M), float64) * _NINF A_denom = ones(N, float64) * _NINF B_denom = ones(N, float64) * _NINF logprob = 0 for sequence in unlabeled_sequences: sequence = list(sequence) if not sequence: continue # compute forward and backward probabilities alpha = model._forward_probability(sequence) beta = model._backward_probability(sequence) # find the log probability of the sequence T = len(sequence) lpk = _log_add(*alpha[T-1, :]) logprob += lpk # now update A and B (transition and output probabilities) # using the alpha and beta values. Please refer to Rabiner's # paper for details, it's too hard to explain in comments local_A_numer = ones((N, N), float64) * _NINF local_B_numer = ones((N, M), float64) * _NINF local_A_denom = ones(N, float64) * _NINF local_B_denom = ones(N, float64) * _NINF # for each position, accumulate sums for A and B for t in range(T): x = sequence[t][_TEXT] #not found? FIXME if t < T - 1: xnext = sequence[t+1][_TEXT] #not found? FIXME xi = symbol_dict[x] for i in range(N): si = self._states[i] if t < T - 1: for j in range(N): sj = self._states[j] local_A_numer[i, j] = \ _log_add(local_A_numer[i, j], alpha[t, i] + model._transitions[si].logprob(sj) + model._outputs[sj].logprob(xnext) + beta[t+1, j]) local_A_denom[i] = _log_add(local_A_denom[i], alpha[t, i] + beta[t, i]) else: local_B_denom[i] = _log_add(local_A_denom[i], alpha[t, i] + beta[t, i]) local_B_numer[i, xi] = _log_add(local_B_numer[i, xi], alpha[t, i] + beta[t, i]) # add these sums to the global A and B values for i in range(N): for j in range(N): A_numer[i, j] = _log_add(A_numer[i, j], local_A_numer[i, j] - lpk) for k in range(M): B_numer[i, k] = _log_add(B_numer[i, k], local_B_numer[i, k] - lpk) A_denom[i] = _log_add(A_denom[i], local_A_denom[i] - lpk) B_denom[i] = _log_add(B_denom[i], local_B_denom[i] - lpk) # use the calculated values to update the transition and output # probability values for i in range(N): si = self._states[i] for j in range(N): sj = self._states[j] model._transitions[si].update(sj, A_numer[i,j] - A_denom[i]) for k in range(M): ok = self._symbols[k] model._outputs[si].update(ok, B_numer[i,k] - B_denom[i]) # Rabiner says the priors don't need to be updated. I don't # believe him. FIXME # test for convergence if iteration > 0 and abs(logprob - last_logprob) < epsilon: converged = True print 'iteration', iteration, 'logprob', logprob iteration += 1 last_logprob = logprob return model def train_supervised(self, labelled_sequences, **kwargs): """ Supervised training maximising the joint probability of the symbol and state sequences. This is done via collecting frequencies of transitions between states, symbol observations while within each state and which states start a sentence. These frequency distributions are then normalised into probability estimates, which can be smoothed if desired. @return: the trained model @rtype: HiddenMarkovModelTagger @param labelled_sequences: the training data, a set of labelled sequences of observations @type labelled_sequences: list @param kwargs: may include an 'estimator' parameter, a function taking a C{FreqDist} and a number of bins and returning a C{ProbDistI}; otherwise a MLE estimate is used """ # default to the MLE estimate estimator = kwargs.get('estimator') if estimator == None: estimator = lambda fdist, bins: MLEProbDist(fdist) # count occurences of starting states, transitions out of each state # and output symbols observed in each state starting = FreqDist() transitions = ConditionalFreqDist() outputs = ConditionalFreqDist() for sequence in labelled_sequences: lasts = None for token in sequence: state = token[_TAG] symbol = token[_TEXT] if lasts == None: starting.inc(state) else: transitions[lasts].inc(state) outputs[state].inc(symbol) lasts = state # update the state and symbol lists if state not in self._states: self._states.append(state) if symbol not in self._symbols: self._symbols.append(symbol) # create probability distributions (with smoothing) N = len(self._states) pi = estimator(starting, N) A = ConditionalProbDist(transitions, estimator, N) B = ConditionalProbDist(outputs, estimator, len(self._symbols)) return HiddenMarkovModelTagger(self._symbols, self._states, A, B, pi) class HiddenMarkovModelTaggerTransform(HiddenMarkovModelTaggerTransformI): """ An abstract subclass of C{HiddenMarkovModelTaggerTransformI}. """ def __init__(self): if self.__class__ == HiddenMarkovModelTaggerTransform: raise AssertionError, "Abstract classes can't be instantiated" class LambdaTransform(HiddenMarkovModelTaggerTransform): """ A subclass of C{HiddenMarkovModelTaggerTransform} that is backed by an arbitrary user-defined function, instance method, or lambda function. """ def __init__(self, transform): """ @param func: a user-defined or lambda transform function @type func: C{function} """ self._transform = transform def transform(self, labeled_symbols): return self._transform(labeled_symbols) class IdentityTransform(HiddenMarkovModelTaggerTransform): """ A subclass of C{HiddenMarkovModelTaggerTransform} that implements L{transform()} as the identity function, i.e. symbols passed to C{transform()} are returned unmodified. """ def transform(self, labeled_symbols): return labeled_symbols def _log_add(*values): """ Adds the logged values, returning the logarithm of the addition. """ x = max(values) if x > _NINF: sum_diffs = 0 for value in values: sum_diffs += 2**(value - x) return x + log2(sum_diffs) else: return x def demo(): # demonstrates HMM probability calculation print print "HMM probability calculation demo" print # example taken from page 381, Huang et al symbols = ['up', 'down', 'unchanged'] states = ['bull', 'bear', 'static'] def pd(values, samples): d = {} for value, item in zip(values, samples): d[item] = value return DictionaryProbDist(d) def cpd(array, conditions, samples): d = {} for values, condition in zip(array, conditions): d[condition] = pd(values, samples) return DictionaryConditionalProbDist(d) A = array([[0.6, 0.2, 0.2], [0.5, 0.3, 0.2], [0.4, 0.1, 0.5]], float64) A = cpd(A, states, states) B = array([[0.7, 0.1, 0.2], [0.1, 0.6, 0.3], [0.3, 0.3, 0.4]], float64) B = cpd(B, states, symbols) pi = array([0.5, 0.2, 0.3], float64) pi = pd(pi, states) model = HiddenMarkovModelTagger(symbols=symbols, states=states, transitions=A, outputs=B, priors=pi) print 'Testing', model for test in [['up', 'up'], ['up', 'down', 'up'], ['down'] * 5, ['unchanged'] * 5 + ['up']]: sequence = [(t, None) for t in test] print 'Testing with state sequence', test print 'probability =', model.probability(sequence) print 'tagging = ', model.tag([word for (word,tag) in sequence]) print 'p(tagged) = ', model.probability(sequence) print 'H = ', model.entropy(sequence) print 'H_exh = ', model._exhaustive_entropy(sequence) print 'H(point) = ', model.point_entropy(sequence) print 'H_exh(point)=', model._exhaustive_point_entropy(sequence) print def load_pos(num_sents): from nltk.corpus import brown sentences = brown.tagged_sents(categories='news')[:num_sents] tag_re = re.compile(r'[*]|--|[^+*-]+') tag_set = set() symbols = set() cleaned_sentences = [] for sentence in sentences: for i in range(len(sentence)): word, tag = sentence[i] word = word.lower() # normalize symbols.add(word) # log this word # Clean up the tag. tag = tag_re.match(tag).group() tag_set.add(tag) sentence[i] = (word, tag) # store cleaned-up tagged token cleaned_sentences += [sentence] return cleaned_sentences, list(tag_set), list(symbols) @deprecated("Use model.test(sentences, **kwargs) instead.") def test_pos(model, sentences, display=False): return model.test(sentences, verbose=display) def demo_pos(): # demonstrates POS tagging using supervised training print print "HMM POS tagging demo" print print 'Training HMM...' labelled_sequences, tag_set, symbols = load_pos(200) trainer = HiddenMarkovModelTrainer(tag_set, symbols) hmm = trainer.train_supervised(labelled_sequences[10:], estimator=lambda fd, bins: LidstoneProbDist(fd, 0.1, bins)) print 'Testing...' hmm.test(labelled_sequences[:10], verbose=True) def _untag(sentences): unlabeled = [] for sentence in sentences: unlabeled.append((token[_TEXT], None) for token in sentence) return unlabeled def demo_pos_bw(): # demonstrates the Baum-Welch algorithm in POS tagging print print "Baum-Welch demo for POS tagging" print print 'Training HMM (supervised)...' sentences, tag_set, symbols = load_pos(210) symbols = set() for sentence in sentences: for token in sentence: symbols.add(token[_TEXT]) trainer = HiddenMarkovModelTrainer(tag_set, list(symbols)) hmm = trainer.train_supervised(sentences[10:200], estimator=lambda fd, bins: LidstoneProbDist(fd, 0.1, bins)) print 'Training (unsupervised)...' # it's rather slow - so only use 10 samples unlabeled = _untag(sentences[200:210]) hmm = trainer.train_unsupervised(unlabeled, model=hmm, max_iterations=5) hmm.test(sentences[:10], verbose=True) def demo_bw(): # demo Baum Welch by generating some sequences and then performing # unsupervised training on them print print "Baum-Welch demo for market example" print # example taken from page 381, Huang et al symbols = ['up', 'down', 'unchanged'] states = ['bull', 'bear', 'static'] def pd(values, samples): d = {} for value, item in zip(values, samples): d[item] = value return DictionaryProbDist(d) def cpd(array, conditions, samples): d = {} for values, condition in zip(array, conditions): d[condition] = pd(values, samples) return DictionaryConditionalProbDist(d) A = array([[0.6, 0.2, 0.2], [0.5, 0.3, 0.2], [0.4, 0.1, 0.5]], float64) A = cpd(A, states, states) B = array([[0.7, 0.1, 0.2], [0.1, 0.6, 0.3], [0.3, 0.3, 0.4]], float64) B = cpd(B, states, symbols) pi = array([0.5, 0.2, 0.3], float64) pi = pd(pi, states) model = HiddenMarkovModelTagger(symbols=symbols, states=states, transitions=A, outputs=B, priors=pi) # generate some random sequences training = [] import random rng = random.Random() for i in range(10): item = model.random_sample(rng, 5) training.append((i[0], None) for i in item) # train on those examples, starting with the model that generated them trainer = HiddenMarkovModelTrainer(states, symbols) hmm = trainer.train_unsupervised(training, model=model, max_iterations=1000) if __name__ == '__main__': demo() demo_pos() demo_pos_bw() # demo_bw() nltk-2.0~b9/nltk/tag/crf.py0000644000175000017500000007615211327451577015454 0ustar bhavanibhavani# Natural Language Toolkit: Conditional Random Fields # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # # $Id: hmm.py 5994 2008-06-02 12:07:07Z stevenbird $ """ An interface to U{Mallet }'s Linear Chain Conditional Random Field (LC-CRF) implementation. A user-supplied I{feature detector function} is used to convert each token to a featureset. Each feature/value pair is then encoded as a single binary feature for Mallet. """ from tempfile import * import textwrap import re import time import subprocess import sys import zipfile import pickle from nltk.classify.maxent import * from nltk.classify.mallet import call_mallet from nltk.etree import ElementTree from api import * class MalletCRF(FeaturesetTaggerI): """ A conditional random field tagger, which is trained and run by making external calls to Mallet. Tokens are converted to featuresets using a feature detector function:: feature_detector(tokens, index) -> featureset These featuresets are then encoded into feature vectors by converting each feature (name, value) pair to a unique binary feature. Ecah C{MalletCRF} object is backed by a X{crf model file}. This model file is actually a zip file, and it contains one file for the serialized model (C{crf-model.ser}) and one file for information about the structure of the CRF (C{crf-info.xml}). """ def __init__(self, filename, feature_detector=None): """ Create a new C{MalletCRF}. @param filename: The filename of the model file that backs this CRF. @param feature_detector: The feature detector function that is used to convert tokens to featuresets. This parameter only needs to be given if the model file does not contain a pickled pointer to the feature detector (e.g., if the feature detector was a lambda function). """ # Read the CRFInfo from the model file. zf = zipfile.ZipFile(filename) crf_info = CRFInfo.fromstring(zf.read('crf-info.xml')) zf.close() self.crf_info = crf_info """A L{CRFInfo} object describing this CRF.""" # Ensure that our crf_info object has a feature detector. if crf_info.feature_detector is not None: if (feature_detector is not None and self.crf_info.feature_detector != feature_detector): raise ValueError('Feature detector mismatch: %r vs %r' % (feature_detector, self.crf_info.feature_detector)) elif feature_detector is None: raise ValueError('Feature detector not found; supply it manually.') elif feature_detector.__name__ != crf_info.feature_detector_name: raise ValueError('Feature detector name mismatch: %r vs %r' % (feature_detector.__name__, crf_info.feature_detector_name)) else: self.crf_info.feature_detector = feature_detector #///////////////////////////////////////////////////////////////// # Convenience accessors (info also available via self.crf_info) #///////////////////////////////////////////////////////////////// def _get_filename(self): return self.crf_info.model_filename filename = property(_get_filename , doc=""" The filename of the crf model file that backs this C{MalletCRF}. The crf model file is actually a zip file, and it contains one file for the serialized model (C{crf-model.ser}) and one file for information about the structure of the CRF (C{crf-info.xml}).""") def _get_feature_detector(self): return self.crf_info.model_feature_detector feature_detector = property(_get_feature_detector , doc=""" The feature detector function that is used to convert tokens to featuresets. This function has the signature:: feature_detector(tokens, index) -> featureset""") #///////////////////////////////////////////////////////////////// # Tagging #///////////////////////////////////////////////////////////////// #: The name of the java script used to run MalletCRFs. _RUN_CRF = "org.nltk.mallet.RunCRF" def batch_tag(self, sentences): # Write the test corpus to a temporary file (fd, test_file) = mkstemp('.txt', 'test') self.write_test_corpus(sentences, os.fdopen(fd, 'w')) try: # Run mallet on the test file. stdout, stderr = call_mallet([self._RUN_CRF, '--model-file', os.path.abspath(self.crf_info.model_filename), '--test-file', test_file], stdout='pipe') # Decode the output labels = self.parse_mallet_output(stdout) # strip __start__ and __end__ if self.crf_info.add_start_state and self.crf_info.add_end_state: labels = [labs[1:-1] for labs in labels] elif self.crf_info.add_start_state: labels = [labs[1:] for labs in labels] elif self.crf_info.add_end_state: labels = [labs[:-1] for labs in labels] # Combine the labels and the original sentences. return [zip(sent, label) for (sent,label) in zip(sentences, labels)] finally: os.remove(test_file) #///////////////////////////////////////////////////////////////// # Training #///////////////////////////////////////////////////////////////// #: The name of the java script used to train MalletCRFs. _TRAIN_CRF = "org.nltk.mallet.TrainCRF" @classmethod def train(cls, feature_detector, corpus, filename=None, weight_groups=None, gaussian_variance=1, default_label='O', transduction_type='VITERBI', max_iterations=500, add_start_state=True, add_end_state=True, trace=1): """ Train a new linear chain CRF tagger based on the given corpus of training sequences. This tagger will be backed by a I{crf model file}, containing both a serialized Mallet model and information about the CRF's structure. This crf model file will I{not} be automatically deleted -- if you wish to delete it, you must delete it manually. The filename of the model file for a MalletCRF C{crf} is available as C{crf.filename}. @type corpus: C{list} of C{tuple} @param corpus: Training data, represented as a list of sentences, where each sentence is a list of (token, tag) tuples. @type filename: C{str} @param filename: The filename that should be used for the crf model file that backs the new C{MalletCRF}. If no filename is given, then a new filename will be chosen automatically. @type weight_groups: C{list} of L{CRFInfo.WeightGroup} @param weight_groups: Specifies how input-features should be mapped to joint-features. See L{CRFInfo.WeightGroup} for more information. @type gaussian_variance: C{float} @param gaussian_variance: The gaussian variance of the prior that should be used to train the new CRF. @type default_label: C{str} @param default_label: The "label for initial context and uninteresting tokens" (from Mallet's SimpleTagger.java.) It's unclear whether this currently has any effect. @type transduction_type: C{str} @param transduction_type: The type of transduction used by the CRF. Can be VITERBI, VITERBI_FBEAM, VITERBI_BBEAM, VITERBI_FBBEAM, or VITERBI_FBEAMKL. @type max_iterations: C{int} @param max_iterations: The maximum number of iterations that should be used for training the CRF. @type add_start_state: C{bool} @param add_start_state: If true, then NLTK will add a special start state, named C{'__start__'}. The initial cost for the start state will be set to 0; and the initial cost for all other states will be set to +inf. @type add_end_state: C{bool} @param add_end_state: If true, then NLTK will add a special end state, named C{'__end__'}. The final cost for the end state will be set to 0; and the final cost for all other states will be set to +inf. @type trace: C{int} @param trace: Controls the verbosity of trace output generated while training the CRF. Higher numbers generate more verbose output. """ t0 = time.time() # Record starting time. # If they did not supply a model filename, then choose one. if filename is None: (fd, filename) = mkstemp('.crf', 'model') os.fdopen(fd).close() # Ensure that the filename ends with '.zip' if not filename.endswith('.crf'): filename += '.crf' if trace >= 1: print '[MalletCRF] Training a new CRF: %s' % filename # Create crf-info object describing the new CRF. crf_info = MalletCRF._build_crf_info( corpus, gaussian_variance, default_label, max_iterations, transduction_type, weight_groups, add_start_state, add_end_state, filename, feature_detector) # Create a zipfile, and write crf-info to it. if trace >= 2: print '[MalletCRF] Adding crf-info.xml to %s' % filename zf = zipfile.ZipFile(filename, mode='w') zf.writestr('crf-info.xml', crf_info.toxml()+'\n') zf.close() # Create the CRF object. crf = MalletCRF(filename, feature_detector) # Write the Training corpus to a temporary file. if trace >= 2: print '[MalletCRF] Writing training corpus...' (fd, train_file) = mkstemp('.txt', 'train') crf.write_training_corpus(corpus, os.fdopen(fd, 'w')) try: if trace >= 1: print '[MalletCRF] Calling mallet to train CRF...' cmd = [MalletCRF._TRAIN_CRF, '--model-file', os.path.abspath(filename), '--train-file', train_file] if trace > 3: call_mallet(cmd) else: p = call_mallet(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, blocking=False) MalletCRF._filter_training_output(p, trace) finally: # Delete the temp file containing the training corpus. os.remove(train_file) if trace >= 1: print '[MalletCRF] Training complete.' print '[MalletCRF] Model stored in: %s' % filename if trace >= 2: dt = time.time()-t0 print '[MalletCRF] Total training time: %d seconds' % dt # Return the completed CRF. return crf @staticmethod def _build_crf_info(corpus, gaussian_variance, default_label, max_iterations, transduction_type, weight_groups, add_start_state, add_end_state, model_filename, feature_detector): """ Construct a C{CRFInfo} object describing a CRF with a given set of configuration parameters, and based on the contents of a given corpus. """ state_info_list = [] labels = set() if add_start_state: labels.add('__start__') if add_end_state: labels.add('__end__') transitions = set() # not necessary to find this? for sent in corpus: prevtag = default_label for (tok,tag) in sent: labels.add(tag) transitions.add( (prevtag, tag) ) prevtag = tag if add_start_state: transitions.add( ('__start__', sent[0][1]) ) if add_end_state: transitions.add( (sent[-1][1], '__end__') ) labels = sorted(labels) # 0th order default: if weight_groups is None: weight_groups = [CRFInfo.WeightGroup(name=l, src='.*', dst=re.escape(l)) for l in labels] # Check that weight group names are unique if len(weight_groups) != len(set(wg.name for wg in weight_groups)): raise ValueError("Weight group names must be unique") # Construct a list of state descriptions. Currently, we make # these states fully-connected, with one parameter per # transition. for src in labels: if add_start_state: if src == '__start__': initial_cost = 0 else: initial_cost = '+inf' if add_end_state: if src == '__end__': final_cost = 0 else: final_cost = '+inf' state_info = CRFInfo.State(src, initial_cost, final_cost, []) for dst in labels: state_weight_groups = [wg.name for wg in weight_groups if wg.match(src, dst)] state_info.transitions.append( CRFInfo.Transition(dst, dst, state_weight_groups)) state_info_list.append(state_info) return CRFInfo(state_info_list, gaussian_variance, default_label, max_iterations, transduction_type, weight_groups, add_start_state, add_end_state, model_filename, feature_detector) #: A table used to filter the output that mallet generates during #: training. By default, mallet generates very verbose output. #: This table is used to select which lines of output are actually #: worth displaying to the user, based on the level of the C{trace} #: parameter. Each entry of this table is a tuple #: C{(min_trace_level, regexp)}. A line will be displayed only if #: C{trace>=min_trace_level} and the line matches C{regexp} for at #: least one table entry. _FILTER_TRAINING_OUTPUT = [ (1, r'DEBUG:.*'), (1, r'Number of weights.*'), (1, r'CRF about to train.*'), (1, r'CRF finished.*'), (1, r'CRF training has converged.*'), (2, r'CRF weights.*'), (2, r'getValue\(\) \(loglikelihood\) .*'), ] @staticmethod def _filter_training_output(p, trace): """ Filter the (very verbose) output that is generated by mallet, and only display the interesting lines. The lines that are selected for display are determined by L{_FILTER_TRAINING_OUTPUT}. """ out = [] while p.poll() is None: while True: line = p.stdout.readline() if not line: break out.append(line) for (t, regexp) in MalletCRF._FILTER_TRAINING_OUTPUT: if t <= trace and re.match(regexp, line): indent = ' '*t print '[MalletCRF] %s%s' % (indent, line.rstrip()) break if p.returncode != 0: print "\nError encountered! Mallet's most recent output:" print ''.join(out[-100:]) raise OSError('Mallet command failed') #///////////////////////////////////////////////////////////////// # Communication w/ mallet #///////////////////////////////////////////////////////////////// def write_training_corpus(self, corpus, stream, close_stream=True): """ Write a given training corpus to a given stream, in a format that can be read by the java script C{org.nltk.mallet.TrainCRF}. """ feature_detector = self.crf_info.feature_detector for sentence in corpus: if self.crf_info.add_start_state: stream.write('__start__ __start__\n') unlabeled_sent = [tok for (tok,tag) in sentence] for index in range(len(unlabeled_sent)): featureset = feature_detector(unlabeled_sent, index) for (fname, fval) in featureset.items(): stream.write(self._format_feature(fname, fval)+" ") stream.write(sentence[index][1]+'\n') if self.crf_info.add_end_state: stream.write('__end__ __end__\n') stream.write('\n') if close_stream: stream.close() def write_test_corpus(self, corpus, stream, close_stream=True): """ Write a given test corpus to a given stream, in a format that can be read by the java script C{org.nltk.mallet.TestCRF}. """ feature_detector = self.crf_info.feature_detector for sentence in corpus: if self.crf_info.add_start_state: stream.write('__start__ __start__\n') for index in range(len(sentence)): featureset = feature_detector(sentence, index) for (fname, fval) in featureset.items(): stream.write(self._format_feature(fname, fval)+" ") stream.write('\n') if self.crf_info.add_end_state: stream.write('__end__ __end__\n') stream.write('\n') if close_stream: stream.close() def parse_mallet_output(self, s): """ Parse the output that is generated by the java script C{org.nltk.mallet.TestCRF}, and convert it to a labeled corpus. """ if re.match(r'\s*<>', s): assert 0, 'its a lattice' corpus = [[]] for line in s.split('\n'): line = line.strip() # Label with augmentations? if line: corpus[-1].append(line.strip()) # Start of new instance? elif corpus[-1] != []: corpus.append([]) if corpus[-1] == []: corpus.pop() return corpus _ESCAPE_RE = re.compile('[^a-zA-Z0-9]') @staticmethod def _escape_sub(m): return '%' + ('%02x' % ord(m.group())) @staticmethod def _format_feature(fname, fval): """ Return a string name for a given feature (name, value) pair, appropriate for consumption by mallet. We escape every character in fname or fval that's not a letter or a number, just to be conservative. """ fname = MalletCRF._ESCAPE_RE.sub(MalletCRF._escape_sub, fname) if isinstance(fval, basestring): fval = "'%s'" % MalletCRF._ESCAPE_RE.sub( MalletCRF._escape_sub, fval) else: fval = MalletCRF._ESCAPE_RE.sub(MalletCRF._escape_sub, '%r'%fval) return fname+'='+fval #///////////////////////////////////////////////////////////////// # String Representation #///////////////////////////////////////////////////////////////// def __repr__(self): return 'MalletCRF(%r)' % self.crf_info.model_filename ########################################################################### ## Serializable CRF Information Object ########################################################################### class CRFInfo(object): """ An object used to record configuration information about a MalletCRF object. This configuration information can be serialized to an XML file, which can then be read by NLTK's custom interface to Mallet's CRF. CRFInfo objects are typically created by the L{MalletCRF.train()} method. Advanced users may wish to directly create custom C{CRFInfo.WeightGroup} objects and pass them to the L{MalletCRF.train()} function. See L{CRFInfo.WeightGroup} for more information. """ def __init__(self, states, gaussian_variance, default_label, max_iterations, transduction_type, weight_groups, add_start_state, add_end_state, model_filename, feature_detector): self.gaussian_variance = float(gaussian_variance) self.default_label = default_label self.states = states self.max_iterations = max_iterations self.transduction_type = transduction_type self.weight_groups = weight_groups self.add_start_state = add_start_state self.add_end_state = add_end_state self.model_filename = model_filename if isinstance(feature_detector, basestring): self.feature_detector_name = feature_detector self.feature_detector = None else: self.feature_detector_name = feature_detector.__name__ self.feature_detector = feature_detector _XML_TEMPLATE = ( '\n' ' %(model_filename)s\n' ' %(gaussian_variance)d\n' ' %(default_label)s\n' ' %(max_iterations)s\n' ' %(transduction_type)s\n' ' \n' ' %(feature_detector)s\n' ' \n' ' %(add_start_state)s\n' ' %(add_end_state)s\n' ' \n' '%(states)s\n' ' \n' ' \n' '%(w_groups)s\n' ' \n' '\n') def toxml(self): info = self.__dict__.copy() info['states'] = '\n'.join(state.toxml() for state in self.states) info['w_groups'] = '\n'.join(wg.toxml() for wg in self.weight_groups) info['feature_detector_name'] = (info['feature_detector_name'] .replace('&', '&') .replace('<', '<')) try: fd = pickle.dumps(self.feature_detector) fd = fd.replace('&', '&').replace('<', '<') fd = fd.replace('\n', ' ') # put pickle data all on 1 line. info['feature_detector'] = '%s' % fd except pickle.PicklingError: info['feature_detector'] = '' return self._XML_TEMPLATE % info @staticmethod def fromstring(s): return CRFInfo._read(ElementTree.fromstring(s)) @staticmethod def _read(etree): states = [CRFInfo.State._read(et) for et in etree.findall('states/state')] weight_groups = [CRFInfo.WeightGroup._read(et) for et in etree.findall('weightGroups/weightGroup')] fd = etree.find('featureDetector') feature_detector = fd.get('name') if fd.find('pickle') is not None: try: feature_detector = pickle.loads(fd.find('pickle').text) except pickle.PicklingError, e: pass # unable to unpickle it. return CRFInfo(states, float(etree.find('gaussianVariance').text), etree.find('defaultLabel').text, int(etree.find('maxIterations').text), etree.find('transductionType').text, weight_groups, bool(etree.find('addStartState').text), bool(etree.find('addEndState').text), etree.find('modelFile').text, feature_detector) def write(self, filename): out = open(filename, 'w') out.write(self.toxml()) out.write('\n') out.close() class State(object): """ A description of a single CRF state. """ def __init__(self, name, initial_cost, final_cost, transitions): if initial_cost != '+inf': initial_cost = float(initial_cost) if final_cost != '+inf': final_cost = float(final_cost) self.name = name self.initial_cost = initial_cost self.final_cost = final_cost self.transitions = transitions _XML_TEMPLATE = ( ' \n' ' \n' '%(transitions)s\n' ' \n' ' \n') def toxml(self): info = self.__dict__.copy() info['transitions'] = '\n'.join(transition.toxml() for transition in self.transitions) return self._XML_TEMPLATE % info @staticmethod def _read(etree): transitions = [CRFInfo.Transition._read(et) for et in etree.findall('transitions/transition')] return CRFInfo.State(etree.get('name'), etree.get('initialCost'), etree.get('finalCost'), transitions) class Transition(object): """ A description of a single CRF transition. """ def __init__(self, destination, label, weightgroups): """ @param destination: The name of the state that this transition connects to. @param label: The tag that is generated when traversing this transition. @param weightgroups: A list of L{WeightGroup} names, indicating which weight groups should be used to calculate the cost of traversing this transition. """ self.destination = destination self.label = label self.weightgroups = weightgroups _XML_TEMPLATE = (' ') def toxml(self): info = self.__dict__ info['w_groups'] = ' '.join(wg for wg in self.weightgroups) return self._XML_TEMPLATE % info @staticmethod def _read(etree): return CRFInfo.Transition(etree.get('destination'), etree.get('label'), etree.get('weightGroups').split()) class WeightGroup(object): """ A configuration object used by C{MalletCRF} to specify how input-features (which are a function of only the input) should be mapped to joint-features (which are a function of both the input and the output tags). Each weight group specifies that a given set of input features should be paired with all transitions from a given set of source tags to a given set of destination tags. """ def __init__(self, name, src, dst, features='.*'): """ @param name: A unique name for this weight group. @param src: The set of source tags that should be used for this weight group, specified as either a list of state names or a regular expression. @param dst: The set of destination tags that should be used for this weight group, specified as either a list of state names or a regular expression. @param features: The set of input feature that should be used for this weight group, specified as either a list of feature names or a regular expression. WARNING: currently, this regexp is passed streight to java -- i.e., it must be a java-style regexp! """ if re.search('\s', name): raise ValueError('weight group name may not ' 'contain whitespace.') if re.search('"', name): raise ValueError('weight group name may not contain \'"\'.') self.name = name self.src = src self.dst = dst self.features = features self._src_match_cache = {} self._dst_match_cache = {} _XML_TEMPLATE = (' ') def toxml(self): return self._XML_TEMPLATE % self.__dict__ @staticmethod def _read(etree): return CRFInfo.WeightGroup(etree.get('name'), etree.get('src'), etree.get('dst'), etree.get('features')) # [xx] feature name???? def match(self, src, dst): # Check if the source matches src_match = self._src_match_cache.get(src) if src_match is None: if isinstance(self.src, basestring): src_match = bool(re.match(self.src+'\Z', src)) else: src_match = src in self.src self._src_match_cache[src] = src_match # Check if the dest matches dst_match = self._dst_match_cache.get(dst) if dst_match is None: if isinstance(self.dst, basestring): dst_match = bool(re.match(self.dst+'\Z', dst)) else: dst_match = dst in self.dst self._dst_match_cache[dst] = dst_match # Return true if both matched. return src_match and dst_match ########################################################################### ## Demonstration code ########################################################################### def demo(train_size=100, test_size=100, java_home='/usr/local/jdk1.5.0/', mallet_home='/usr/local/mallet-0.4'): from nltk.corpus import brown import textwrap # Define a very simple feature detector def fd(sentence, index): word = sentence[index] return dict(word=word, suffix=word[-2:], len=len(word)) # Let nltk know where java & mallet are. nltk.internals.config_java(java_home) nltk.classify.mallet.config_mallet(mallet_home) # Get the training & test corpus. We simplify the tagset a little: # just the first 2 chars. def strip(corpus): return [[(w, t[:2]) for (w,t) in sent] for sent in corpus] brown_train = strip(brown.tagged_sents(categories='news')[:train_size]) brown_test = strip(brown.tagged_sents(categories='editorial')[:test_size]) crf = MalletCRF.train(fd, brown_train, #'/tmp/crf-model', transduction_type='VITERBI') sample_output = crf.tag([w for (w,t) in brown_test[5]]) acc = nltk.tag.accuracy(crf, brown_test) print '\nAccuracy: %.1f%%' % (acc*100) print 'Sample output:' print textwrap.fill(' '.join('%s/%s' % w for w in sample_output), initial_indent=' ', subsequent_indent=' ')+'\n' # Clean up print 'Clean-up: deleting', crf.filename os.remove(crf.filename) return crf if __name__ == '__main__': crf = demo(train_size=100) nltk-2.0~b9/nltk/tag/brill.py0000644000175000017500000015420511327451577016002 0ustar bhavanibhavani# Natural Language Toolkit: Brill Tagger # # Copyright (C) 2001-2010 NLTK Project # Authors: Christopher Maloof # Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT """ Brill's transformational rule-based tagger. """ import bisect # for binary search through a subset of indices import random # for shuffling WSJ files import yaml # to save and load taggers in files import textwrap from nltk.compat import defaultdict from util import untag from api import * ###################################################################### ## The Brill Tagger ###################################################################### class BrillTagger(TaggerI, yaml.YAMLObject): """ Brill's transformational rule-based tagger. Brill taggers use an X{initial tagger} (such as L{tag.DefaultTagger}) to assign an initial tag sequence to a text; and then apply an ordered list of transformational rules to correct the tags of individual tokens. These transformation rules are specified by the L{BrillRule} interface. Brill taggers can be created directly, from an initial tagger and a list of transformational rules; but more often, Brill taggers are created by learning rules from a training corpus, using either L{BrillTaggerTrainer} or L{FastBrillTaggerTrainer}. """ yaml_tag = '!nltk.BrillTagger' def __init__(self, initial_tagger, rules): """ @param initial_tagger: The initial tagger @type initial_tagger: L{TaggerI} @param rules: An ordered list of transformation rules that should be used to correct the initial tagging. @type rules: C{list} of L{BrillRule} """ self._initial_tagger = initial_tagger self._rules = tuple(rules) def rules(self): return self._rules def tag(self, tokens): # Inherit documentation from TaggerI # Run the initial tagger. tagged_tokens = self._initial_tagger.tag(tokens) # Create a dictionary that maps each tag to a list of the # indices of tokens that have that tag. tag_to_positions = defaultdict(set) for i, (token, tag) in enumerate(tagged_tokens): tag_to_positions[tag].add(i) # Apply each rule, in order. Only try to apply rules at # positions that have the desired original tag. for rule in self._rules: # Find the positions where it might apply positions = tag_to_positions.get(rule.original_tag, []) # Apply the rule at those positions. changed = rule.apply(tagged_tokens, positions) # Update tag_to_positions with the positions of tags that # were modified. for i in changed: tag_to_positions[rule.original_tag].remove(i) tag_to_positions[rule.replacement_tag].add(i) return tagged_tokens ###################################################################### ## Brill Rules ###################################################################### class BrillRule(yaml.YAMLObject): """ An interface for tag transformations on a tagged corpus, as performed by brill taggers. Each transformation finds all tokens in the corpus that are tagged with a specific X{original tag} and satisfy a specific X{condition}, and replaces their tags with a X{replacement tag}. For any given transformation, the original tag, replacement tag, and condition are fixed. Conditions may depend on the token under consideration, as well as any other tokens in the corpus. Brill rules must be comparable and hashable. """ def __init__(self, original_tag, replacement_tag): assert self.__class__ != BrillRule, \ "BrillRule is an abstract base class" self.original_tag = original_tag """The tag which this C{BrillRule} may cause to be replaced.""" self.replacement_tag = replacement_tag """The tag with which this C{BrillRule} may replace another tag.""" def apply(self, tokens, positions=None): """ Apply this rule at every position in C{positions} where it applies to the given sentence. I.e., for each position M{p} in C{positions}, if C{tokens[M{p}]} is tagged with this rule's original tag, and satisfies this rule's condition, then set its tag to be this rule's replacement tag. @param tokens: The tagged sentence @type tokens: list of Token @type positions: C{list} of C{int} @param positions: The positions where the transformation is to be tried. If not specified, try it at all positions. @return: The indices of tokens whose tags were changed by this rule. @rtype: C{int} """ if positions is None: positions = range(len(tokens)) # Determine the indices at which this rule applies. change = [i for i in positions if self.applies(tokens, i)] # Make the changes. Note: this must be done in a separate # step from finding applicable locations, since we don't want # the rule to interact with itself. for i in change: tokens[i] = (tokens[i][0], self.replacement_tag) return change def applies(self, tokens, index): """ @return: True if the rule would change the tag of C{tokens[index]}, False otherwise @rtype: Boolean @param tokens: A tagged sentence @type tokens: C{list} of C{str} @param index: The index to check @type index: C{int} """ assert False, "Brill rules must define applies()" # Rules must be comparable and hashable for the algorithm to work def __eq__(self): assert False, "Brill rules must be comparable" def __ne__(self): assert False, "Brill rules must be comparable" def __hash__(self): assert False, "Brill rules must be hashable" class ProximateTokensRule(BrillRule): """ An abstract base class for brill rules whose condition checks for the presence of tokens with given properties at given ranges of positions, relative to the token. Each subclass of proximate tokens brill rule defines a method M{extract_property}, which extracts a specific property from the the token, such as its text or tag. Each instance is parameterized by a set of tuples, specifying ranges of positions and property values to check for in those ranges: - (M{start}, M{end}, M{value}) The brill rule is then applicable to the M{n}th token iff: - The M{n}th token is tagged with the rule's original tag; and - For each (M{start}, M{end}, M{value}) triple: - The property value of at least one token between M{n+start} and M{n+end} (inclusive) is M{value}. For example, a proximate token brill template with M{start=end=-1} generates rules that check just the property of the preceding token. Note that multiple properties may be included in a single rule; the rule applies if they all hold. """ def __init__(self, original_tag, replacement_tag, *conditions): """ Construct a new brill rule that changes a token's tag from C{original_tag} to C{replacement_tag} if all of the properties specified in C{conditions} hold. @type conditions: C{tuple} of C{(int, int, *)} @param conditions: A list of 3-tuples C{(start, end, value)}, each of which specifies that the property of at least one token between M{n}+C{start} and M{n}+C{end} (inclusive) is C{value}. @raise ValueError: If C{start}>C{end} for any condition. """ assert self.__class__ != ProximateTokensRule, \ "ProximateTokensRule is an abstract base class" BrillRule.__init__(self, original_tag, replacement_tag) self._conditions = conditions for (s,e,v) in conditions: if s>e: raise ValueError('Condition %s has an invalid range' % ((s,e,v),)) # Make Brill rules look nice in YAML. @classmethod def to_yaml(cls, dumper, data): node = dumper.represent_mapping(cls.yaml_tag, dict( description=str(data), conditions=list(list(x) for x in data._conditions), original=data.original_tag, replacement=data.replacement_tag)) return node @classmethod def from_yaml(cls, loader, node): map = loader.construct_mapping(node, deep=True) return cls(map['original'], map['replacement'], *(tuple(x) for x in map['conditions'])) @staticmethod def extract_property(token): """ Returns some property characterizing this token, such as its base lexical item or its tag. Each implentation of this method should correspond to an implementation of the method with the same name in a subclass of L{ProximateTokensTemplate}. @param token: The token @type token: Token @return: The property @rtype: any """ assert False, "ProximateTokenRules must define extract_property()" def applies(self, tokens, index): # Inherit docs from BrillRule # Does the given token have this rule's "original tag"? if tokens[index][1] != self.original_tag: return False # Check to make sure that every condition holds. for (start, end, val) in self._conditions: # Find the (absolute) start and end indices. s = max(0, index+start) e = min(index+end+1, len(tokens)) # Look for *any* token that satisfies the condition. for i in range(s, e): if self.extract_property(tokens[i]) == val: break else: # No token satisfied the condition; return false. return False # Every condition checked out, so the rule is applicable. return True def __eq__(self, other): return (self is other or (other is not None and other.__class__ == self.__class__ and self.original_tag == other.original_tag and self.replacement_tag == other.replacement_tag and self._conditions == other._conditions)) def __ne__(self, other): return not (self==other) def __hash__(self): # Cache our hash value (justified by profiling.) try: return self.__hash except: self.__hash = hash( (self.original_tag, self.replacement_tag, self._conditions, self.__class__.__name__) ) return self.__hash def __repr__(self): # Cache our repr (justified by profiling -- this is used as # a sort key when deterministic=True.) try: return self.__repr except: conditions = ' and '.join(['%s in %d...%d' % (v,s,e) for (s,e,v) in self._conditions]) self.__repr = ('<%s: %s->%s if %s>' % (self.__class__.__name__, self.original_tag, self.replacement_tag, conditions)) return self.__repr def __str__(self): replacement = '%s -> %s' % (self.original_tag, self.replacement_tag) if len(self._conditions) == 0: conditions = '' else: conditions = ' if '+ ', and '.join([self._condition_to_str(c) for c in self._conditions]) return replacement+conditions def _condition_to_str(self, condition): """ Return a string representation of the given condition. This helper method is used by L{__str__}. """ (start, end, value) = condition return ('the %s of %s is %r' % (self.PROPERTY_NAME, self._range_to_str(start, end), value)) def _range_to_str(self, start, end): """ Return a string representation for the given range. This helper method is used by L{__str__}. """ if start == end == 0: return 'this word' if start == end == -1: return 'the preceding word' elif start == end == 1: return 'the following word' elif start == end and start < 0: return 'word i-%d' % -start elif start == end and start > 0: return 'word i+%d' % start else: if start >= 0: start = '+%d' % start if end >= 0: end = '+%d' % end return 'words i%s...i%s' % (start, end) class ProximateTagsRule(ProximateTokensRule): """ A rule which examines the tags of nearby tokens. @see: superclass L{ProximateTokensRule} for details. @see: L{SymmetricProximateTokensTemplate}, which generates these rules. """ PROPERTY_NAME = 'tag' # for printing. yaml_tag = '!ProximateTagsRule' @staticmethod def extract_property(token): """@return: The given token's tag.""" return token[1] class ProximateWordsRule(ProximateTokensRule): """ A rule which examines the base types of nearby tokens. @see: L{ProximateTokensRule} for details. @see: L{SymmetricProximateTokensTemplate}, which generates these rules. """ PROPERTY_NAME = 'text' # for printing. yaml_tag = '!ProximateWordsRule' @staticmethod def extract_property(token): """@return: The given token's text.""" return token[0] ###################################################################### ## Brill Templates ###################################################################### class BrillTemplateI(object): """ An interface for generating lists of transformational rules that apply at given sentence positions. C{BrillTemplateI} is used by C{Brill} training algorithms to generate candidate rules. """ def __init__(self): raise AssertionError, "BrillTemplateI is an abstract interface" def applicable_rules(self, tokens, i, correctTag): """ Return a list of the transformational rules that would correct the C{i}th subtoken's tag in the given token. In particular, return a list of zero or more rules that would change C{tagged_tokens[i][1]} to C{correctTag}, if applied to C{token}. If the C{i}th subtoken already has the correct tag (i.e., if C{tagged_tokens[i][1]} == C{correctTag}), then C{applicable_rules} should return the empty list. @param tokens: The tagged tokens being tagged. @type tokens: C{list} of C{tuple} @param i: The index of the token whose tag should be corrected. @type i: C{int} @param correctTag: The correct tag for the C{i}th token. @type correctTag: (any) @rtype: C{list} of L{BrillRule} """ raise AssertionError, "BrillTemplateI is an abstract interface" def get_neighborhood(self, token, index): """ Returns the set of indices C{i} such that C{applicable_rules(token, i, ...)} depends on the value of the C{index}th subtoken of C{token}. This method is used by the \"fast\" Brill tagger trainer. @param token: The tokens being tagged. @type token: C{list} of C{tuple} @param index: The index whose neighborhood should be returned. @type index: C{int} @rtype: C{Set} """ raise AssertionError, "BrillTemplateI is an abstract interface" class ProximateTokensTemplate(BrillTemplateI): """ An brill templates that generates a list of L{ProximateTokensRule}s that apply at a given sentence position. In particular, each C{ProximateTokensTemplate} is parameterized by a proximate token brill rule class and a list of boundaries, and generates all rules that: - use the given brill rule class - use the given list of boundaries as the C{start} and C{end} points for their conditions - are applicable to the given token. """ def __init__(self, rule_class, *boundaries): """ Construct a template for generating proximate token brill rules. @type rule_class: C{class} @param rule_class: The proximate token brill rule class that should be used to generate new rules. This class must be a subclass of L{ProximateTokensRule}. @type boundaries: C{tuple} of C{(int, int)} @param boundaries: A list of tuples C{(start, end)}, each of which specifies a range for which a condition should be created by each rule. @raise ValueError: If C{start}>C{end} for any boundary. """ self._rule_class = rule_class self._boundaries = boundaries for (s,e) in boundaries: if s>e: raise ValueError('Boundary %s has an invalid range' % ((s,e),)) def applicable_rules(self, tokens, index, correct_tag): if tokens[index][1] == correct_tag: return [] # For each of this template's boundaries, Find the conditions # that are applicable for the given token. applicable_conditions = \ [self._applicable_conditions(tokens, index, start, end) for (start, end) in self._boundaries] # Find all combinations of these applicable conditions. E.g., # if applicable_conditions=[[A,B], [C,D]], then this will # generate [[A,C], [A,D], [B,C], [B,D]]. condition_combos = [[]] for conditions in applicable_conditions: condition_combos = [old_conditions+[new_condition] for old_conditions in condition_combos for new_condition in conditions] # Translate the condition sets into rules. return [self._rule_class(tokens[index][1], correct_tag, *conds) for conds in condition_combos] def _applicable_conditions(self, tokens, index, start, end): """ @return: A set of all conditions for proximate token rules that are applicable to C{tokens[index]}, given boundaries of C{(start, end)}. I.e., return a list of all tuples C{(start, end, M{value})}, such the property value of at least one token between M{index+start} and M{index+end} (inclusive) is M{value}. """ conditions = [] s = max(0, index+start) e = min(index+end+1, len(tokens)) for i in range(s, e): value = self._rule_class.extract_property(tokens[i]) conditions.append( (start, end, value) ) return conditions def get_neighborhood(self, tokens, index): # inherit docs from BrillTemplateI # applicable_rules(tokens, index, ...) depends on index. neighborhood = set([index]) # applicable_rules(tokens, i, ...) depends on index if # i+start < index <= i+end. for (start, end) in self._boundaries: s = max(0, index+(-end)) e = min(index+(-start)+1, len(tokens)) for i in range(s, e): neighborhood.add(i) return neighborhood class SymmetricProximateTokensTemplate(BrillTemplateI): """ Simulates two L{ProximateTokensTemplate}s which are symmetric across the location of the token. For rules of the form \"If the M{n}th token is tagged C{A}, and any tag preceding B{or} following the M{n}th token by a distance between M{x} and M{y} is C{B}, and ... , then change the tag of the nth token from C{A} to C{C}.\" One C{ProximateTokensTemplate} is formed by passing in the same arguments given to this class's constructor: tuples representing intervals in which a tag may be found. The other C{ProximateTokensTemplate} is constructed with the negative of all the arguments in reversed order. For example, a C{SymmetricProximateTokensTemplate} using the pair (-2,-1) and the constructor C{SymmetricProximateTokensTemplate} generates the same rules as a C{SymmetricProximateTokensTemplate} using (-2,-1) plus a second C{SymmetricProximateTokensTemplate} using (1,2). This is useful because we typically don't want templates to specify only \"following\" or only \"preceding\"; we'd like our rules to be able to look in either direction. """ def __init__(self, rule_class, *boundaries): """ Construct a template for generating proximate token brill rules. @type rule_class: C{class} @param rule_class: The proximate token brill rule class that should be used to generate new rules. This class must be a subclass of L{ProximateTokensRule}. @type boundaries: C{tuple} of C{(int, int)} @param boundaries: A list of tuples C{(start, end)}, each of which specifies a range for which a condition should be created by each rule. @raise ValueError: If C{start}>C{end} for any boundary. """ self._ptt1 = ProximateTokensTemplate(rule_class, *boundaries) reversed = [(-e,-s) for (s,e) in boundaries] self._ptt2 = ProximateTokensTemplate(rule_class, *reversed) # Generates lists of a subtype of ProximateTokensRule. def applicable_rules(self, tokens, index, correctTag): """ See L{BrillTemplateI} for full specifications. @rtype: list of ProximateTokensRule """ return (self._ptt1.applicable_rules(tokens, index, correctTag) + self._ptt2.applicable_rules(tokens, index, correctTag)) def get_neighborhood(self, tokens, index): # inherit docs from BrillTemplateI n1 = self._ptt1.get_neighborhood(tokens, index) n2 = self._ptt2.get_neighborhood(tokens, index) return n1.union(n2) ###################################################################### ## Brill Tagger Trainer ###################################################################### class BrillTaggerTrainer(object): """ A trainer for brill taggers. """ def __init__(self, initial_tagger, templates, trace=0, deterministic=None): """ @param deterministic: If true, then choose between rules that have the same score by picking the one whose __repr__ is lexicographically smaller. If false, then just pick the first rule we find with a given score -- this will depend on the order in which keys are returned from dictionaries, and so may not be the same from one run to the next. If not specified, treat as true iff trace > 0. """ if deterministic is None: deterministic = (trace > 0) self._initial_tagger = initial_tagger self._templates = templates self._trace = trace self._deterministic = deterministic #//////////////////////////////////////////////////////////// # Training #//////////////////////////////////////////////////////////// def train(self, train_sents, max_rules=200, min_score=2): """ Trains the Brill tagger on the corpus C{train_token}, producing at most C{max_rules} transformations, each of which reduces the net number of errors in the corpus by at least C{min_score}. @type train_sents: C{list} of C{list} of L{tuple} @param train_sents: The corpus of tagged tokens @type max_rules: C{int} @param max_rules: The maximum number of transformations to be created @type min_score: C{int} @param min_score: The minimum acceptable net error reduction that each transformation must produce in the corpus. """ if self._trace > 0: print ("Training Brill tagger on %d " "sentences..." % len(train_sents)) # Create a new copy of the training corpus, and run the # initial tagger on it. We will progressively update this # test corpus to look more like the training corpus. test_sents = [self._initial_tagger.tag(untag(sent)) for sent in train_sents] if self._trace > 2: self._trace_header() # Look for useful rules. rules = [] try: while len(rules) < max_rules: (rule, score, fixscore) = self._best_rule(test_sents, train_sents) if rule is None or score < min_score: if self._trace > 1: print 'Insufficient improvement; stopping' break else: # Add the rule to our list of rules. rules.append(rule) # Use the rules to update the test corpus. Keep # track of how many times the rule applied (k). k = 0 for sent in test_sents: k += len(rule.apply(sent)) # Display trace output. if self._trace > 1: self._trace_rule(rule, score, fixscore, k) # The user can also cancel training manually: except KeyboardInterrupt: print "Training stopped manually -- %d rules found" % len(rules) # Create and return a tagger from the rules we found. return BrillTagger(self._initial_tagger, rules) #//////////////////////////////////////////////////////////// # Finding the best rule #//////////////////////////////////////////////////////////// # Finds the rule that makes the biggest net improvement in the corpus. # Returns a (rule, score) pair. def _best_rule(self, test_sents, train_sents): # Create a dictionary mapping from each tag to a list of the # indices that have that tag in both test_sents and # train_sents (i.e., where it is correctly tagged). correct_indices = defaultdict(list) for sentnum, sent in enumerate(test_sents): for wordnum, tagged_word in enumerate(sent): if tagged_word[1] == train_sents[sentnum][wordnum][1]: tag = tagged_word[1] correct_indices[tag].append( (sentnum, wordnum) ) # Find all the rules that correct at least one token's tag, # and the number of tags that each rule corrects (in # descending order of number of tags corrected). rules = self._find_rules(test_sents, train_sents) # Keep track of the current best rule, and its score. best_rule, best_score, best_fixscore = None, 0, 0 # Consider each rule, in descending order of fixscore (the # number of tags that the rule corrects, not including the # number that it breaks). for (rule, fixscore) in rules: # The actual score must be <= fixscore; so if best_score # is bigger than fixscore, then we already have the best # rule. if best_score > fixscore or (best_score == fixscore and not self._deterministic): return best_rule, best_score, best_fixscore # Calculate the actual score, by decrementing fixscore # once for each tag that the rule changes to an incorrect # value. score = fixscore if rule.original_tag in correct_indices: for (sentnum, wordnum) in correct_indices[rule.original_tag]: if rule.applies(test_sents[sentnum], wordnum): score -= 1 # If the score goes below best_score, then we know # that this isn't the best rule; so move on: if score < best_score or (score == best_score and not self._deterministic): break # If the actual score is better than the best score, then # update best_score and best_rule. if score > best_score or (score == best_score and self._deterministic and repr(rule) < repr(best_rule)): best_rule, best_score, best_fixscore = rule, score, fixscore # Return the best rule, and its score. return best_rule, best_score, best_fixscore def _find_rules(self, test_sents, train_sents): """ Find all rules that correct at least one token's tag in C{test_sents}. @return: A list of tuples C{(rule, fixscore)}, where C{rule} is a brill rule and C{fixscore} is the number of tokens whose tag the rule corrects. Note that C{fixscore} does I{not} include the number of tokens whose tags are changed to incorrect values. """ # Create a list of all indices that are incorrectly tagged. error_indices = [] for sentnum, sent in enumerate(test_sents): for wordnum, tagged_word in enumerate(sent): if tagged_word[1] != train_sents[sentnum][wordnum][1]: error_indices.append( (sentnum, wordnum) ) # Create a dictionary mapping from rules to their positive-only # scores. rule_score_dict = defaultdict(int) for (sentnum, wordnum) in error_indices: test_sent = test_sents[sentnum] train_sent = train_sents[sentnum] for rule in self._find_rules_at(test_sent, train_sent, wordnum): rule_score_dict[rule] += 1 # Convert the dictionary into a list of (rule, score) tuples, # sorted in descending order of score. return sorted(rule_score_dict.items(), key=lambda (rule,score): -score) def _find_rules_at(self, test_sent, train_sent, i): """ @rtype: C{Set} @return: the set of all rules (based on the templates) that correct token C{i}'s tag in C{test_sent}. """ applicable_rules = set() if test_sent[i][1] != train_sent[i][1]: correct_tag = train_sent[i][1] for template in self._templates: new_rules = template.applicable_rules(test_sent, i, correct_tag) applicable_rules.update(new_rules) return applicable_rules #//////////////////////////////////////////////////////////// # Tracing #//////////////////////////////////////////////////////////// def _trace_header(self): print """ B | S F r O | Score = Fixed - Broken c i o t | R Fixed = num tags changed incorrect -> correct o x k h | u Broken = num tags changed correct -> incorrect r e e e | l Other = num tags changed incorrect -> incorrect e d n r | e ------------------+------------------------------------------------------- """.rstrip() def _trace_rule(self, rule, score, fixscore, numchanges): if self._trace > 2: print ('%4d%4d%4d%4d ' % (score, fixscore, fixscore-score, numchanges-fixscore*2+score)), '|', print textwrap.fill(str(rule), initial_indent=' '*20, width=79, subsequent_indent=' '*18+'| ').strip() else: print rule ###################################################################### ## Fast Brill Tagger Trainer ###################################################################### class FastBrillTaggerTrainer(object): """ A faster trainer for brill taggers. """ def __init__(self, initial_tagger, templates, trace=0, deterministic=False): if not deterministic: deterministic = (trace > 0) self._initial_tagger = initial_tagger self._templates = templates self._trace = trace self._deterministic = deterministic self._tag_positions = None """Mapping from tags to lists of positions that use that tag.""" self._rules_by_position = None """Mapping from positions to the set of rules that are known to occur at that position. Position is (sentnum, wordnum). Initially, this will only contain positions where each rule applies in a helpful way; but when we examine a rule, we'll extend this list to also include positions where each rule applies in a harmful or neutral way.""" self._positions_by_rule = None """Mapping from rule to position to effect, specifying the effect that each rule has on the overall score, at each position. Position is (sentnum, wordnum); and effect is -1, 0, or 1. As with _rules_by_position, this mapping starts out only containing rules with positive effects; but when we examine a rule, we'll extend this mapping to include the positions where the rule is harmful or neutral.""" self._rules_by_score = None """Mapping from scores to the set of rules whose effect on the overall score is upper bounded by that score. Invariant: rulesByScore[s] will contain r iff the sum of _positions_by_rule[r] is s.""" self._rule_scores = None """Mapping from rules to upper bounds on their effects on the overall score. This is the inverse mapping to _rules_by_score. Invariant: ruleScores[r] = sum(_positions_by_rule[r])""" self._first_unknown_position = None """Mapping from rules to the first position where we're unsure if the rule applies. This records the next position we need to check to see if the rule messed anything up.""" #//////////////////////////////////////////////////////////// # Training #//////////////////////////////////////////////////////////// def train(self, train_sents, max_rules=200, min_score=2): # Basic idea: Keep track of the rules that apply at each position. # And keep track of the positions to which each rule applies. if self._trace > 0: print ("Training Brill tagger on %d " "sentences..." % len(train_sents)) # Create a new copy of the training corpus, and run the # initial tagger on it. We will progressively update this # test corpus to look more like the training corpus. test_sents = [self._initial_tagger.tag(untag(sent)) for sent in train_sents] # Initialize our mappings. This will find any errors made # by the initial tagger, and use those to generate repair # rules, which are added to the rule mappings. if self._trace > 0: print "Finding initial useful rules..." self._init_mappings(test_sents, train_sents) if self._trace > 0: print (" Found %d useful rules." % len(self._rule_scores)) # Let the user know what we're up to. if self._trace > 2: self._trace_header() elif self._trace == 1: print "Selecting rules..." # Repeatedly select the best rule, and add it to `rules`. rules = [] try: while (len(rules) < max_rules): # Find the best rule, and add it to our rule list. rule = self._best_rule(train_sents, test_sents, min_score) if rule: rules.append(rule) else: break # No more good rules left! # Report the rule that we found. if self._trace > 1: self._trace_rule(rule) # Apply the new rule at the relevant sites self._apply_rule(rule, test_sents) # Update _tag_positions[rule.original_tag] and # _tag_positions[rule.replacement_tag] for the affected # positions (i.e., self._positions_by_rule[rule]). self._update_tag_positions(rule) # Update rules that were affected by the change. self._update_rules(rule, train_sents, test_sents) # The user can cancel training manually: except KeyboardInterrupt: print "Training stopped manually -- %d rules found" % len(rules) # Discard our tag position mapping & rule mappings. self._clean() # Create and return a tagger from the rules we found. return BrillTagger(self._initial_tagger, rules) def _init_mappings(self, test_sents, train_sents): """ Initialize the tag position mapping & the rule related mappings. For each error in test_sents, find new rules that would correct them, and add them to the rule mappings. """ self._tag_positions = defaultdict(list) self._rules_by_position = defaultdict(set) self._positions_by_rule = defaultdict(dict) self._rules_by_score = defaultdict(set) self._rule_scores = defaultdict(int) self._first_unknown_position = defaultdict(int) # Scan through the corpus, initializing the tag_positions # mapping and all the rule-related mappings. for sentnum, sent in enumerate(test_sents): for wordnum, (word, tag) in enumerate(sent): # Initialize tag_positions self._tag_positions[tag].append( (sentnum,wordnum) ) # If it's an error token, update the rule-related mappings. correct_tag = train_sents[sentnum][wordnum][1] if tag != correct_tag: for rule in self._find_rules(sent, wordnum, correct_tag): self._update_rule_applies(rule, sentnum, wordnum, train_sents) def _clean(self): self._tag_positions = None self._rules_by_position = None self._positions_by_rule = None self._rules_by_score = None self._rule_scores = None self._first_unknown_position = None def _find_rules(self, sent, wordnum, new_tag): """ Use the templates to find rules that apply at index C{wordnum} in the sentence C{sent} and generate the tag C{new_tag}. """ for template in self._templates: for rule in template.applicable_rules(sent, wordnum, new_tag): yield rule def _update_rule_applies(self, rule, sentnum, wordnum, train_sents): """ Update the rule data tables to reflect the fact that C{rule} applies at the position C{(sentnum, wordnum)}. """ pos = sentnum, wordnum # If the rule is already known to apply here, ignore. # (This only happens if the position's tag hasn't changed.) if pos in self._positions_by_rule[rule]: return # Update self._positions_by_rule. correct_tag = train_sents[sentnum][wordnum][1] if rule.replacement_tag == correct_tag: self._positions_by_rule[rule][pos] = 1 elif rule.original_tag == correct_tag: self._positions_by_rule[rule][pos] = -1 else: # was wrong, remains wrong self._positions_by_rule[rule][pos] = 0 # Update _rules_by_position self._rules_by_position[pos].add(rule) # Update _rule_scores. old_score = self._rule_scores[rule] self._rule_scores[rule] += self._positions_by_rule[rule][pos] # Update _rules_by_score. self._rules_by_score[old_score].discard(rule) self._rules_by_score[self._rule_scores[rule]].add(rule) def _update_rule_not_applies(self, rule, sentnum, wordnum): """ Update the rule data tables to reflect the fact that C{rule} does not apply at the position C{(sentnum, wordnum)}. """ pos = sentnum, wordnum # Update _rule_scores. old_score = self._rule_scores[rule] self._rule_scores[rule] -= self._positions_by_rule[rule][pos] # Update _rules_by_score. self._rules_by_score[old_score].discard(rule) self._rules_by_score[self._rule_scores[rule]].add(rule) # Update _positions_by_rule del self._positions_by_rule[rule][pos] self._rules_by_position[pos].remove(rule) # Optional addition: if the rule now applies nowhere, delete # all its dictionary entries. def _best_rule(self, train_sents, test_sents, min_score): """ Find the next best rule. This is done by repeatedly taking a rule with the highest score and stepping through the corpus to see where it applies. When it makes an error (decreasing its score) it's bumped down, and we try a new rule with the highest score. When we find a rule which has the highest score AND which has been tested against the entire corpus, we can conclude that it's the next best rule. """ if self._rules_by_score == {}: return None max_score = max(self._rules_by_score) while max_score >= min_score: best_rules = list(self._rules_by_score[max_score]) if self._deterministic: best_rules.sort(key=repr) for rule in best_rules: positions = self._tag_positions[rule.original_tag] unk = self._first_unknown_position.get(rule, (0,-1)) start = bisect.bisect_left(positions, unk) for i in range(start, len(positions)): sentnum, wordnum = positions[i] if rule.applies(test_sents[sentnum], wordnum): self._update_rule_applies(rule, sentnum, wordnum, train_sents) if self._rule_scores[rule] < max_score: self._first_unknown_position[rule] = (sentnum, wordnum+1) break # The update demoted the rule. if self._rule_scores[rule] == max_score: self._first_unknown_position[rule] = (len(train_sents)+1,0) return rule # We demoted all the rules with score==max_score. assert not self._rules_by_score[max_score] del self._rules_by_score[max_score] if len(self._rules_by_score) == 0: return None max_score = max(self._rules_by_score) # We reached the min-score threshold. return None def _apply_rule(self, rule, test_sents): """ Update C{test_sents} by applying C{rule} everywhere where its conditions are meet. """ update_positions = set(self._positions_by_rule[rule]) old_tag = rule.original_tag new_tag = rule.replacement_tag if self._trace > 3: self._trace_apply(len(update_positions)) # Update test_sents. for (sentnum, wordnum) in update_positions: text = test_sents[sentnum][wordnum][0] test_sents[sentnum][wordnum] = (text, new_tag) def _update_tag_positions(self, rule): """ Update _tag_positions to reflect the changes to tags that are made by C{rule}. """ # Update the tag index. for pos in self._positions_by_rule[rule]: # Delete the old tag. old_tag_positions = self._tag_positions[rule.original_tag] old_index = bisect.bisect_left(old_tag_positions, pos) del old_tag_positions[old_index] # Insert the new tag. new_tag_positions = self._tag_positions[rule.replacement_tag] bisect.insort_left(new_tag_positions, pos) def _update_rules(self, rule, train_sents, test_sents): """ Check if we should add or remove any rules from consideration, given the changes made by C{rule}. """ # Collect a list of all positions that might be affected. neighbors = set() for sentnum, wordnum in self._positions_by_rule[rule]: for template in self._templates: n = template.get_neighborhood(test_sents[sentnum], wordnum) neighbors.update([(sentnum, i) for i in n]) # Update the rules at each position. num_obsolete = num_new = num_unseen = 0 for sentnum, wordnum in neighbors: test_sent = test_sents[sentnum] correct_tag = train_sents[sentnum][wordnum][1] # Check if the change causes any rule at this position to # stop matching; if so, then update our rule mappings # accordingly. old_rules = set(self._rules_by_position[sentnum, wordnum]) for old_rule in old_rules: if not old_rule.applies(test_sent, wordnum): num_obsolete += 1 self._update_rule_not_applies(old_rule, sentnum, wordnum) # Check if the change causes our templates to propose any # new rules for this position. site_rules = set() for template in self._templates: for new_rule in template.applicable_rules(test_sent, wordnum, correct_tag): if new_rule not in old_rules: num_new += 1 if new_rule not in self._rule_scores: num_unseen += 1 old_rules.add(new_rule) self._update_rule_applies(new_rule, sentnum, wordnum, train_sents) # We may have caused other rules to match here, that are # not proposed by our templates -- in particular, rules # that are harmful or neutral. We therefore need to # update any rule whose first_unknown_position is past # this rule. for new_rule, pos in self._first_unknown_position.items(): if pos > (sentnum, wordnum): if new_rule not in old_rules: num_new += 1 if new_rule.applies(test_sent, wordnum): self._update_rule_applies(new_rule, sentnum, wordnum, train_sents) if self._trace > 3: self._trace_update_rules(num_obsolete, num_new, num_unseen) #//////////////////////////////////////////////////////////// # Tracing #//////////////////////////////////////////////////////////// def _trace_header(self): print """ B | S F r O | Score = Fixed - Broken c i o t | R Fixed = num tags changed incorrect -> correct o x k h | u Broken = num tags changed correct -> incorrect r e e e | l Other = num tags changed incorrect -> incorrect e d n r | e ------------------+------------------------------------------------------- """.rstrip() def _trace_rule(self, rule): assert self._rule_scores[rule] == \ sum(self._positions_by_rule[rule].values()) changes = self._positions_by_rule[rule].values() num_changed = len(changes) num_fixed = len([c for c in changes if c==1]) num_broken = len([c for c in changes if c==-1]) num_other = len([c for c in changes if c==0]) score = self._rule_scores[rule] if self._trace > 2: print '%4d%4d%4d%4d |' % (score,num_fixed,num_broken,num_other), print textwrap.fill(str(rule), initial_indent=' '*20, subsequent_indent=' '*18+'| ').strip() else: print rule def _trace_apply(self, num_updates): prefix = ' '*18+'|' print prefix print prefix, 'Applying rule to %d positions.' % num_updates def _trace_update_rules(self, num_obsolete, num_new, num_unseen): prefix = ' '*18+'|' print prefix, 'Updated rule tables:' print prefix, (' - %d rule applications removed' % num_obsolete) print prefix, (' - %d rule applications added (%d novel)' % (num_new, num_unseen)) print prefix ###################################################################### ## Testing ###################################################################### # returns a list of errors in string format def error_list (train_sents, test_sents, radius=2): """ Returns a list of human-readable strings indicating the errors in the given tagging of the corpus. @param train_sents: The correct tagging of the corpus @type train_sents: C{list} of C{tuple} @param test_sents: The tagged corpus @type test_sents: C{list} of C{tuple} @param radius: How many tokens on either side of a wrongly-tagged token to include in the error string. For example, if C{radius}=2, each error string will show the incorrect token plus two tokens on either side. @type radius: int """ hdr = (('%25s | %s | %s\n' + '-'*26+'+'+'-'*24+'+'+'-'*26) % ('left context', 'word/test->gold'.center(22), 'right context')) errors = [hdr] for (train_sent, test_sent) in zip(train_sents, test_sents): for wordnum, (word, train_pos) in enumerate(train_sent): test_pos = test_sent[wordnum][1] if train_pos != test_pos: left = ' '.join('%s/%s' % w for w in train_sent[:wordnum]) right = ' '.join('%s/%s' % w for w in train_sent[wordnum+1:]) mid = '%s/%s->%s' % (word, test_pos, train_pos) errors.append('%25s | %s | %s' % (left[-25:], mid.center(22), right[:25])) return errors ###################################################################### # Demonstration ###################################################################### def demo(num_sents=2000, max_rules=200, min_score=3, error_output="errors.out", rule_output="rules.yaml", randomize=False, train=.8, trace=3): """ Brill Tagger Demonstration @param num_sents: how many sentences of training and testing data to use @type num_sents: L{int} @param max_rules: maximum number of rule instances to create @type max_rules: L{int} @param min_score: the minimum score for a rule in order for it to be considered @type min_score: L{int} @param error_output: the file where errors will be saved @type error_output: C{string} @param rule_output: the file where rules will be saved @type rule_output: C{string} @param randomize: whether the training data should be a random subset of the corpus @type randomize: boolean @param train: the fraction of the the corpus to be used for training (1=all) @type train: L{float} @param trace: the level of diagnostic tracing output to produce (0-4) @type trace: L{int} """ from nltk.corpus import treebank from nltk import tag from nltk.tag import brill nn_cd_tagger = tag.RegexpTagger([(r'^-?[0-9]+(.[0-9]+)?$', 'CD'), (r'.*', 'NN')]) # train is the proportion of data used in training; the rest is reserved # for testing. print "Loading tagged data... " tagged_data = treebank.tagged_sents() if randomize: random.seed(len(sents)) random.shuffle(sents) cutoff = int(num_sents*train) training_data = tagged_data[:cutoff] gold_data = tagged_data[cutoff:num_sents] testing_data = [[t[0] for t in sent] for sent in gold_data] print "Done loading." # Unigram tagger print "Training unigram tagger:" unigram_tagger = tag.UnigramTagger(training_data, backoff=nn_cd_tagger) if gold_data: print " [accuracy: %f]" % unigram_tagger.evaluate(gold_data) # Bigram tagger print "Training bigram tagger:" bigram_tagger = tag.BigramTagger(training_data, backoff=unigram_tagger) if gold_data: print " [accuracy: %f]" % bigram_tagger.evaluate(gold_data) # Brill tagger templates = [ brill.SymmetricProximateTokensTemplate(brill.ProximateTagsRule, (1,1)), brill.SymmetricProximateTokensTemplate(brill.ProximateTagsRule, (2,2)), brill.SymmetricProximateTokensTemplate(brill.ProximateTagsRule, (1,2)), brill.SymmetricProximateTokensTemplate(brill.ProximateTagsRule, (1,3)), brill.SymmetricProximateTokensTemplate(brill.ProximateWordsRule, (1,1)), brill.SymmetricProximateTokensTemplate(brill.ProximateWordsRule, (2,2)), brill.SymmetricProximateTokensTemplate(brill.ProximateWordsRule, (1,2)), brill.SymmetricProximateTokensTemplate(brill.ProximateWordsRule, (1,3)), brill.ProximateTokensTemplate(brill.ProximateTagsRule, (-1, -1), (1,1)), brill.ProximateTokensTemplate(brill.ProximateWordsRule, (-1, -1), (1,1)), ] trainer = brill.FastBrillTaggerTrainer(bigram_tagger, templates, trace) #trainer = brill.BrillTaggerTrainer(u, templates, trace) brill_tagger = trainer.train(training_data, max_rules, min_score) if gold_data: print("\nBrill accuracy: %f" % brill_tagger.evaluate(gold_data)) if trace <= 1: print("\nRules: ") for rule in brill_tagger.rules(): print(str(rule)) print_rules = file(rule_output, 'w') yaml.dump(brill_tagger, print_rules) print_rules.close() testing_data = brill_tagger.batch_tag(testing_data) error_file = file(error_output, 'w') error_file.write('Errors for Brill Tagger %r\n\n' % rule_output) for e in error_list(gold_data, testing_data): error_file.write(e+'\n') error_file.close() print ("Done; rules and errors saved to %s and %s." % (rule_output, error_output)) if __name__ == '__main__': demo() #demo(num_sents=1000000, train=1.0, max_rules=10000) nltk-2.0~b9/nltk/tag/api.py0000644000175000017500000000677111327451577015453 0ustar bhavanibhavani# Natural Language Toolkit: Tagger Interface # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird (minor additions) # URL: # For license information, see LICENSE.TXT """ Interface for tagging each token in a sentence with supplementary information, such as its part of speech. """ from nltk.internals import overridden, deprecated from nltk.metrics import accuracy as _accuracy from util import untag class TaggerI(object): """ A processing interface for assigning a tag to each token in a list. Tags are case sensitive strings that identify some property of each token, such as its part of speech or its sense. Some taggers require specific types for their tokens. This is generally indicated by the use of a sub-interface to C{TaggerI}. For example, I{featureset taggers}, which are subclassed from L{FeaturesetTaggerI}, require that each token be a I{featureset}. Subclasses must define: - either L{tag()} or L{batch_tag()} (or both) """ def tag(self, tokens): """ Determine the most appropriate tag sequence for the given token sequence, and return a corresponding list of tagged tokens. A tagged token is encoded as a tuple C{(token, tag)}. @rtype: C{list} of C{(token, tag)} """ if overridden(self.batch_tag): return self.batch_tag([tokens])[0] else: raise NotImplementedError() def batch_tag(self, sentences): """ Apply L{self.tag()} to each element of C{sentences}. I.e.: >>> return [self.tag(sent) for sent in sentences] """ return [self.tag(sent) for sent in sentences] def evaluate(self, gold): """ Score the accuracy of the tagger against the gold standard. Strip the tags from the gold standard text, retag it using the tagger, then compute the accuracy score. @type gold: C{list} of C{list} of C{(token, tag)} @param gold: The list of tagged sentences to score the tagger on. @rtype: C{float} """ tagged_sents = self.batch_tag([untag(sent) for sent in gold]) gold_tokens = sum(gold, []) test_tokens = sum(tagged_sents, []) return _accuracy(gold_tokens, test_tokens) def _check_params(self, train, model): if (train and model) or (not train and not model): raise ValueError('Must specify either training data or trained model.') class FeaturesetTaggerI(TaggerI): """ A tagger that requires tokens to be I{featuresets}. A featureset is a dictionary that maps from I{feature names} to I{feature values}. See L{nltk.classify} for more information about features and featuresets. """ class HiddenMarkovModelTaggerTransformI(object): """ An interface for a transformation to be used as the transform parameter of C{HiddenMarkovModelTagger}. """ def __init__(self): if self.__class__ == HiddenMarkovModelTaggerTransformI: raise AssertionError, "Interfaces can't be instantiated" def transform(self, labeled_symbols): """ @return: a C{list} of transformed symbols @rtype: C{list} @param labeled_symbols: a C{list} of labeled untransformed symbols, i.e. symbols that are not (token, tag) or (word, tag) @type labeled_symbols: C{list} """ raise NotImplementedError() nltk-2.0~b9/nltk/tag/__init__.py0000644000175000017500000000405211360522135016411 0ustar bhavanibhavani# Natural Language Toolkit: Taggers # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird (minor additions) # URL: # For license information, see LICENSE.TXT """ Classes and interfaces for tagging each token of a sentence with supplementary information, such as its part of speech. This task, which is known as X{tagging}, is defined by the L{TaggerI} interface. """ from api import * from util import * from simplify import * from sequential import * from brill import * from tnt import * from hunpos import * import nltk __all__ = [ # Tagger interface 'TaggerI', # Standard POS tagger 'pos_tag', 'batch_pos_tag', # Should these be included:? #'SequentialBackoffTagger', 'ContextTagger', # Sequential backoff taggers. 'DefaultTagger', 'UnigramTagger', 'BigramTagger', 'TrigramTagger', 'NgramTagger', 'AffixTagger', 'RegexpTagger', # Brill tagger -- trainer names? 'BrillTagger', 'BrillTaggerTrainer', 'FastBrillTaggerTrainer', # Utilities. Note: conversion functions x2y are intentionally # left out; they should be accessed as nltk.tag.x2y(). Similarly # for nltk.tag.accuracy. 'untag', ] # Import hmm module if numpy is installed try: import numpy from hmm import * __all__ += ['HiddenMarkovModelTagger', 'HiddenMarkovModelTrainer',] # [xx] deprecated HiddenMarkovModel etc objects? except ImportError: pass # Standard treebank POS tagger _POS_TAGGER = 'taggers/maxent_treebank_pos_tagger/english.pickle' def pos_tag(tokens): """ Use NLTK's currently recommended part of speech tagger to tag the given list of tokens. """ tagger = nltk.data.load(_POS_TAGGER) return tagger.tag(tokens) def batch_pos_tag(sentences): """ Use NLTK's currently recommended part of speech tagger to tag the given list of sentences, each consisting of a list of tokens. """ tagger = nltk.data.load(_POS_TAGGER) return tagger.batch_tag(sentences) nltk-2.0~b9/nltk/stem/wordnet.py0000644000175000017500000000276011363770700016543 0ustar bhavanibhavani# Natural Language Toolkit: WordNet stemmer interface # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT from nltk.corpus.reader.wordnet import NOUN from nltk.corpus import wordnet as _wordnet from nltk.internals import Deprecated class WordNetLemmatizer(object): """ A lemmatizer that uses WordNet's built-in morphy function. """ def __init__(self): """ Create a new WordNet stemmer. """ pass def lemmatize(self, word, pos=NOUN): lemmas = _wordnet._morphy(word, pos) if not lemmas: return word lemmas.sort(key=len) return lemmas[0] def __repr__(self): return '' if __name__ == '__main__': from nltk import stem wnl = stem.WordNetLemmatizer() print 'dogs ->', wnl.lemmatize('dogs') print 'churches ->', wnl.lemmatize('churches') print 'aardwolves ->', wnl.lemmatize('aardwolves') print 'abaci ->', wnl.lemmatize('abaci') print 'hardrock ->', wnl.lemmatize('hardrock') class WordnetStemmer(Deprecated, WordNetLemmatizer): """Use WordNetLemmatizer instead.""" def __init__(self): WordNetLemmatizer.__init__(self) class WordNetStemmer(Deprecated, WordNetLemmatizer): """Use WordNetLemmatizer instead.""" def __init__(self): WordNetLemmatizer.__init__(self) nltk-2.0~b9/nltk/stem/snowball.py0000644000175000017500000041165011423114517016677 0ustar bhavanibhavani# -*- coding: utf-8 -*- # # Natural Language Toolkit: Snowball Stemmer # # Copyright (C) 2001-2010 NLTK Project # Author: Peter Michael Stahl # Algorithms: Dr Martin Porter # URL: # For license information, see LICENSE.TXT u""" Snowball stemmers and appendant demo function This module provides a port of the Snowball stemmers developed by U{Dr Martin Porter}. There is also a demo function demonstrating the different algorithms. It can be invoked directly on the command line. For more information take a look into the class C{SnowballStemmer}. @author: Peter Michael Stahl @contact: pemistahl@gmail.com @contact: U{http://twitter.com/pemistahl} """ from api import * from nltk.corpus import stopwords class SnowballStemmer(StemmerI): u""" A word stemmer based on the Snowball stemming algorithms. At the moment, this port is able to stem words from thirteen languages: Danish, Dutch, Finnish, French, German, Hungarian, Italian, Norwegian, Portuguese, Romanian, Russian, Spanish and Swedish. The algorithms have been developed by U{Dr Martin Porter}. These stemmers are called Snowball, because he invented a programming language with this name for creating new stemming algorithms. There is more information available on the U{Snowball Website}. The stemmer is invoked as shown below: >>> from snowball import SnowballStemmer >>> SnowballStemmer.languages # See which languages are supported ('danish', 'dutch', 'finnish', 'french', 'german', 'hungarian', 'italian', 'norwegian', 'portuguese", 'romanian', 'russian', 'spanish', 'swedish') >>> stemmer = SnowballStemmer("german") # Choose a language >>> stemmer.stem(u"Autobahnen") # Stem a word u'autobahn' @author: Peter Michael Stahl @contact: pemistahl@gmail.com @contact: U{http://twitter.com/pemistahl} @cvar languages: A tuple that contains the available language names @type languages: C{tuple} @ivar stopwords: A list that contains stopwords for the respective language in Unicode format. @type stopwords: C{list} """ languages = ("danish", "dutch", "finnish", "french", "german", "hungarian", "italian", "norwegian", "portuguese", "romanian", "russian", "spanish", "swedish") def __new__(cls, language, **kwargs): u""" Override the constructor of class L{SnowballStemmer} in order to create an instance of the language's respective subclass. @param language: The language whose subclass is instantiated. @type language: C{str, unicode} @param kwargs: An arbitrary argument list for keyword arguments. @type kwargs: C{dict} @return: An instance of the language's respective subclass. @rtype: C{class} @raise ValueError: If there is no stemmer for the specified language, a C{ValueError} is raised. """ if language == "danish": return StemmerI.__new__(DanishStemmer) elif language == "dutch": return StemmerI.__new__(DutchStemmer) elif language == "finnish": return StemmerI.__new__(FinnishStemmer) elif language == "french": return StemmerI.__new__(FrenchStemmer) elif language == "german": return StemmerI.__new__(GermanStemmer) elif language == "hungarian": return StemmerI.__new__(HungarianStemmer) elif language == "italian": return StemmerI.__new__(ItalianStemmer) elif language == "norwegian": return StemmerI.__new__(NorwegianStemmer) elif language == "portuguese": return StemmerI.__new__(PortugueseStemmer) elif language == "romanian": return StemmerI.__new__(RomanianStemmer) elif language == "russian": return StemmerI.__new__(RussianStemmer) elif language == "spanish": return StemmerI.__new__(SpanishStemmer) elif language == "swedish": return StemmerI.__new__(SwedishStemmer) else: raise ValueError(u"The language '%s' is not supported." % language) def __init__(self, language, ignore_stopwords=False): u""" Create an instance of the Snowball stemmer. @param language: The language that is applied for stemming. @type language: C{str, unicode} @param ignore_stopwords: If set to C{True}, stopwords are not stemmed and returned unchanged. Set to C{False} by default. @type ignore_stopwords: C{bool} """ if ignore_stopwords: if language == "romanian": raise ValueError(u"The Romanian stemmer has not yet" + u" a list of stopwords. Please set" + u" u'ignore_stopwords' to u'False'.") else: self.stopwords = [word.decode("utf-8") for word in stopwords.words(language)] else: self.stopwords = set() def __repr__(self): u""" Print out the string representation of the respective class. """ return "<%s>" % type(self).__name__ class _ScandinavianStemmer(SnowballStemmer): u""" This subclass encapsulates a method for defining the string region R1. It is used by the Danish, Norwegian, and Swedish stemmer. """ def _r1_scandinavian(self, word, vowels): u""" Return the region R1 that is used by the Scandinavian stemmers. R1 is the region after the first non-vowel following a vowel, or is the null region at the end of the word if there is no such non-vowel. But then R1 is adjusted so that the region before it contains at least three letters. @param word: The word whose region R1 is determined. @type word: C{str, unicode} @param vowels: The vowels of the respective language that are used to determine the region R1. @type vowels: C{unicode} @return: C{r1}, the region R1 for the respective word. @rtype: C{unicode} @note: This helper method is invoked by the respective stem method of the subclasses L{DanishStemmer}, L{NorwegianStemmer}, and L{SwedishStemmer}. It is not to be invoked directly! """ r1 = u"" for i in xrange(1, len(word)): if word[i] not in vowels and word[i-1] in vowels: if len(word[:i+1]) < 3 and len(word[:i+1]) > 0: r1 = word[3:] elif len(word[:i+1]) >= 3: r1 = word[i+1:] else: return word break return r1 class _StandardStemmer(SnowballStemmer): u""" This subclass encapsulates two methods for defining the standard versions of the string regions R1, R2, and RV. """ def _r1r2_standard(self, word, vowels): u""" Return the standard interpretations of the string regions R1 and R2. R1 is the region after the first non-vowel following a vowel, or is the null region at the end of the word if there is no such non-vowel. R2 is the region after the first non-vowel following a vowel in R1, or is the null region at the end of the word if there is no such non-vowel. @param word: The word whose regions R1 and R2 are determined. @type word: C{str, unicode} @param vowels: The vowels of the respective language that are used to determine the regions R1 and R2. @type vowels: C{unicode} @return: C{(r1,r2)}, the regions R1 and R2 for the respective word. @rtype: C{tuple} @note: This helper method is invoked by the respective stem method of the subclasses L{DutchStemmer}, L{FinnishStemmer}, L{FrenchStemmer}, L{GermanStemmer}, L{ItalianStemmer}, L{PortugueseStemmer}, L{RomanianStemmer}, and L{SpanishStemmer}. It is not to be invoked directly! @note: A detailed description of how to define R1 and R2 can be found under U{http://snowball.tartarus.org/ texts/r1r2.html}. """ r1 = u"" r2 = u"" for i in xrange(1, len(word)): if word[i] not in vowels and word[i-1] in vowels: r1 = word[i+1:] break for i in xrange(1, len(r1)): if r1[i] not in vowels and r1[i-1] in vowels: r2 = r1[i+1:] break return (r1, r2) def _rv_standard(self, word, vowels): u""" Return the standard interpretation of the string region RV. If the second letter is a consonant, RV is the region after the next following vowel. If the first two letters are vowels, RV is the region after the next following consonant. Otherwise, RV is the region after the third letter. @param word: The word whose region RV is determined. @type word: C{str, unicode} @param vowels: The vowels of the respective language that are used to determine the region RV. @type vowels: C{unicode} @return: C{rv}, the region RV for the respective word. @rtype: C{unicode} @note: This helper method is invoked by the respective stem method of the subclasses L{ItalianStemmer}, L{PortugueseStemmer}, L{RomanianStemmer}, and L{SpanishStemmer}. It is not to be invoked directly! """ rv = u"" if len(word) >= 2: if word[1] not in vowels: for i in xrange(2, len(word)): if word[i] in vowels: rv = word[i+1:] break elif word[:2] in vowels: for i in xrange(2, len(word)): if word[i] not in vowels: rv = word[i+1:] break else: rv = word[3:] return rv class DanishStemmer(_ScandinavianStemmer): u""" The Danish Snowball stemmer. @cvar __vowels: The Danish vowels. @type __vowels: C{unicode} @cvar __consonants: The Danish consonants. @type __consonants: C{unicode} @cvar __double_consonants: The Danish double consonants. @type __double_consonants: C{tuple} @cvar __s_ending: Letters that may directly appear before a word final 's'. @type __s_ending: C{unicode} @cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. @type __step1_suffixes: C{tuple} @cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. @type __step2_suffixes: C{tuple} @cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. @type __step3_suffixes: C{tuple} @note: A detailed description of the Danish stemming algorithm can be found under U{http://snowball.tartarus.org/algorithms /danish/stemmer.html}. """ # The language's vowels and other important characters are defined. __vowels = u"aeiouy\xE6\xE5\xF8" __consonants = u"bcdfghjklmnpqrstvwxz" __double_consonants = (u"bb", u"cc", u"dd", u"ff", u"gg", u"hh", u"jj", u"kk", u"ll", u"mm", u"nn", u"pp", u"qq", u"rr", u"ss", u"tt", u"vv", u"ww", u"xx", u"zz") __s_ending = u"abcdfghjklmnoprtvyz\xE5" # The different suffixes, divided into the algorithm's steps # and organized by length, are listed in tuples. __step1_suffixes = (u"erendes", u"erende", u"hedens", u"ethed", u"erede", u"heden", u"heder", u"endes", u"ernes", u"erens", u"erets", u"ered", u"ende", u"erne", u"eren", u"erer", u"heds", u"enes", u"eres", u"eret", u"hed", u"ene", u"ere", u"ens", u"ers", u"ets", u"en", u"er", u"es", u"et", u"e", u"s") __step2_suffixes = (u"gd", u"dt", u"gt", u"kt") __step3_suffixes = (u"elig", u"l\xF8st", u"lig", u"els", u"ig") def stem(self, word): u""" Stem a Danish word and return the stemmed form. @param word: The word that is stemmed. @type word: C{str, unicode} @return: The stemmed form. @rtype: C{unicode} """ if word in self.stopwords: return word # Every word is put into lower case for normalization. word = word.lower() # After this, the required regions are generated # by the respective helper method. r1 = self._r1_scandinavian(word, self.__vowels) # Then the actual stemming process starts. # Every new step is explicitly indicated # according to the descriptions on the Snowball website. # STEP 1 for suffix in self.__step1_suffixes: if r1.endswith(suffix): if suffix == u"s": if word[-2] in self.__s_ending: word = word[:-1] r1 = r1[:-1] else: word = word[:-len(suffix)] r1 = r1[:-len(suffix)] break # STEP 2 for suffix in self.__step2_suffixes: if r1.endswith(suffix): word = word[:-1] r1 = r1[:-1] break # STEP 3 if r1.endswith(u"igst"): word = word[:-2] r1 = r1[:-2] for suffix in self.__step3_suffixes: if r1.endswith(suffix): if suffix == u"l\xF8st": word = word[:-1] r1 = r1[:-1] else: word = word[:-len(suffix)] r1 = r1[:-len(suffix)] if r1.endswith(self.__step2_suffixes): word = word[:-1] r1 = r1[:-1] break # STEP 4: Undouble for double_cons in self.__double_consonants: if word.endswith(double_cons) and len(word) > 3: word = word[:-1] break return word class DutchStemmer(_StandardStemmer): u""" The Dutch Snowball stemmer. @cvar __vowels: The Dutch vowels. @type __vowels: C{unicode} @cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. @type __step1_suffixes: C{tuple} @cvar __step3b_suffixes: Suffixes to be deleted in step 3b of the algorithm. @type __step3b_suffixes: C{tuple} @note: A detailed description of the Dutch stemming algorithm can be found under U{http://snowball.tartarus.org/algorithms /dutch/stemmer.html}. """ __vowels = u"aeiouy\xE8" __step1_suffixes = (u"heden", u"ene", u"en", u"se", u"s") __step3b_suffixes = (u"baar", u"lijk", u"bar", u"end", u"ing", u"ig") def stem(self, word): u""" Stem a Dutch word and return the stemmed form. @param word: The word that is stemmed. @type word: C{str, unicode} @return: The stemmed form. @rtype: C{unicode} """ if word in self.stopwords: return word step2_success = False # Vowel accents are removed. word = (word.lower().replace(u"\xE4", u"a").replace(u"\xE1", u"a") .replace(u"\xEB", u"e").replace(u"\xE9", u"e") .replace(u"\xED", u"i").replace(u"\xEF", u"i") .replace(u"\xF6", u"o").replace(u"\xF3", u"o") .replace(u"\xFC", u"u").replace(u"\xFA", u"u")) # An initial 'y', a 'y' after a vowel, # and an 'i' between self.__vowels is put into upper case. # As from now these are treated as consonants. if word.startswith(u"y"): word = u"".join((u"Y", word[1:])) for i in xrange(1, len(word)): if word[i-1] in self.__vowels and word[i] == u"y": word = u"".join((word[:i], u"Y", word[i+1:])) for i in xrange(1, len(word)-1): if (word[i-1] in self.__vowels and word[i] == u"i" and word[i+1] in self.__vowels): word = u"".join((word[:i], u"I", word[i+1:])) r1, r2 = self._r1r2_standard(word, self.__vowels) # R1 is adjusted so that the region before it # contains at least 3 letters. for i in xrange(1, len(word)): if word[i] not in self.__vowels and word[i-1] in self.__vowels: if len(word[:i+1]) < 3 and len(word[:i+1]) > 0: r1 = word[3:] elif len(word[:i+1]) == 0: return word break # STEP 1 for suffix in self.__step1_suffixes: if r1.endswith(suffix): if suffix == u"heden": word = u"".join((word[:-5], u"heid")) r1 = u"".join((r1[:-5], u"heid")) if r2.endswith(u"heden"): r2 = u"".join((r2[:-5], u"heid")) elif (suffix in (u"ene", u"en") and not word.endswith(u"heden") and word[-len(suffix)-1] not in self.__vowels and word[-len(suffix)-3:-len(suffix)] != u"gem"): word = word[:-len(suffix)] r1 = r1[:-len(suffix)] r2 = r2[:-len(suffix)] if word.endswith((u"kk", u"dd", u"tt")): word = word[:-1] r1 = r1[:-1] r2 = r2[:-1] elif (suffix in (u"se", u"s") and word[-len(suffix)-1] not in self.__vowels and word[-len(suffix)-1] != u"j"): word = word[:-len(suffix)] r1 = r1[:-len(suffix)] r2 = r2[:-len(suffix)] break # STEP 2 if r1.endswith(u"e") and word[-2] not in self.__vowels: step2_success = True word = word[:-1] r1 = r1[:-1] r2 = r2[:-1] if word.endswith((u"kk", u"dd", u"tt")): word = word[:-1] r1 = r1[:-1] r2 = r2[:-1] # STEP 3a if r2.endswith(u"heid") and word[-5] != u"c": word = word[:-4] r1 = r1[:-4] r2 = r2[:-4] if (r1.endswith(u"en") and word[-3] not in self.__vowels and word[-5:-2] != u"gem"): word = word[:-2] r1 = r1[:-2] r2 = r2[:-2] if word.endswith((u"kk", u"dd", u"tt")): word = word[:-1] r1 = r1[:-1] r2 = r2[:-1] # STEP 3b: Derivational suffixes for suffix in self.__step3b_suffixes: if r2.endswith(suffix): if suffix in (u"end", u"ing"): word = word[:-3] r2 = r2[:-3] if r2.endswith(u"ig") and word[-3] != u"e": word = word[:-2] else: if word.endswith((u"kk", u"dd", u"tt")): word = word[:-1] elif suffix == u"ig" and word[-3] != u"e": word = word[:-2] elif suffix == u"lijk": word = word[:-4] r1 = r1[:-4] if r1.endswith(u"e") and word[-2] not in self.__vowels: word = word[:-1] if word.endswith((u"kk", u"dd", u"tt")): word = word[:-1] elif suffix == u"baar": word = word[:-4] elif suffix == u"bar" and step2_success: word = word[:-3] break # STEP 4: Undouble vowel if len(word) >= 4: if word[-1] not in self.__vowels and word[-1] != u"I": if word[-3:-1] in (u"aa", u"ee", u"oo", u"uu"): if word[-4] not in self.__vowels: word = u"".join((word[:-3], word[-3], word[-1])) # All occurrences of 'I' and 'Y' are put back into lower case. word = word.replace(u"I", u"i").replace(u"Y", u"y") return word class FinnishStemmer(_StandardStemmer): u""" The Finnish Snowball stemmer. @cvar __vowels: The Finnish vowels. @type __vowels: C{unicode} @cvar __restricted_vowels: A subset of the Finnish vowels. @type __restricted_vowels: C{unicode} @cvar __long_vowels: The Finnish vowels in their long forms. @type __long_vowels: C{tuple} @cvar __consonants: The Finnish consonants. @type __consonants: C{unicode} @cvar __double_consonants: The Finnish double consonants. @type __double_consonants: C{tuple} @cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. @type __step1_suffixes: C{tuple} @cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. @type __step2_suffixes: C{tuple} @cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. @type __step3_suffixes: C{tuple} @cvar __step4_suffixes: Suffixes to be deleted in step 4 of the algorithm. @type __step4_suffixes: C{tuple} @note: A detailed description of the Finnish stemming algorithm can be found under U{http://snowball.tartarus.org/algorithms /finnish/stemmer.html}. """ __vowels = u"aeiouy\xE4\xF6" __restricted_vowels = u"aeiou\xE4\xF6" __long_vowels = (u"aa", u"ee", u"ii", u"oo", u"uu", u"\xE4\xE4", u"\xF6\xF6") __consonants = u"bcdfghjklmnpqrstvwxz" __double_consonants = (u"bb", u"cc", u"dd", u"ff", u"gg", u"hh", u"jj", u"kk", u"ll", u"mm", u"nn", u"pp", u"qq", u"rr", u"ss", u"tt", u"vv", u"ww", u"xx", u"zz") __step1_suffixes = (u'kaan', u'k\xE4\xE4n', u'sti', u'kin', u'han', u'h\xE4n', u'ko', u'k\xF6', u'pa', u'p\xE4') __step2_suffixes = (u'nsa', u'ns\xE4', u'mme', u'nne', u'si', u'ni', u'an', u'\xE4n', u'en') __step3_suffixes = (u'siin', u'tten', u'seen', u'han', u'hen', u'hin', u'hon', u'h\xE4n', u'h\xF6n', u'den', u'tta', u'tt\xE4', u'ssa', u'ss\xE4', u'sta', u'st\xE4', u'lla', u'll\xE4', u'lta', u'lt\xE4', u'lle', u'ksi', u'ine', u'ta', u't\xE4', u'na', u'n\xE4', u'a', u'\xE4', u'n') __step4_suffixes = (u'impi', u'impa', u'imp\xE4', u'immi', u'imma', u'imm\xE4', u'mpi', u'mpa', u'mp\xE4', u'mmi', u'mma', u'mm\xE4', u'eja', u'ej\xE4') def stem(self, word): u""" Stem a Finnish word and return the stemmed form. @param word: The word that is stemmed. @type word: C{str, unicode} @return: The stemmed form. @rtype: C{unicode} """ if word in self.stopwords: return word step3_success = False word = word.lower() r1, r2 = self._r1r2_standard(word, self.__vowels) # STEP 1: Particles etc. for suffix in self.__step1_suffixes: if r1.endswith(suffix): if suffix == u"sti": if suffix in r2: word = word[:-3] r1 = r1[:-3] r2 = r2[:-3] else: if word[-len(suffix)-1] in u"ntaeiouy\xE4\xF6": word = word[:-len(suffix)] r1 = r1[:-len(suffix)] r2 = r2[:-len(suffix)] break # STEP 2: Possessives for suffix in self.__step2_suffixes: if r1.endswith(suffix): if suffix == u"si": if word[-3] != u"k": word = word[:-2] r1 = r1[:-2] r2 = r2[:-2] elif suffix == u"ni": word = word[:-2] r1 = r1[:-2] r2 = r2[:-2] if word.endswith(u"kse"): word = u"".join((word[:-3], u"ksi")) if r1.endswith(u"kse"): r1 = u"".join((r1[:-3], u"ksi")) if r2.endswith(u"kse"): r2 = u"".join((r2[:-3], u"ksi")) elif suffix == u"an": if (word[-4:-2] in (u"ta", u"na") or word[-5:-2] in (u"ssa", u"sta", u"lla", u"lta")): word = word[:-2] r1 = r1[:-2] r2 = r2[:-2] elif suffix == u"\xE4n": if (word[-4:-2] in (u"t\xE4", u"n\xE4") or word[-5:-2] in (u"ss\xE4", u"st\xE4", u"ll\xE4", u"lt\xE4")): word = word[:-2] r1 = r1[:-2] r2 = r2[:-2] elif suffix == u"en": if word[-5:-2] in (u"lle", u"ine"): word = word[:-2] r1 = r1[:-2] r2 = r2[:-2] else: word = word[:-3] r1 = r1[:-3] r2 = r2[:-3] break # STEP 3: Cases for suffix in self.__step3_suffixes: if r1.endswith(suffix): if suffix in (u"han", u"hen", u"hin", u"hon", u"h\xE4n", u"h\xF6n"): if ((suffix == u"han" and word[-4] == u"a") or (suffix == u"hen" and word[-4] == u"e") or (suffix == u"hin" and word[-4] == u"i") or (suffix == u"hon" and word[-4] == u"o") or (suffix == u"h\xE4n" and word[-4] == u"\xE4") or (suffix == u"h\xF6n" and word[-4] == u"\xF6")): word = word[:-3] r1 = r1[:-3] r2 = r2[:-3] step3_success = True elif suffix in (u"siin", u"den", u"tten"): if (word[-len(suffix)-1] == u"i" and word[-len(suffix)-2] in self.__restricted_vowels): word = word[:-len(suffix)] r1 = r1[:-len(suffix)] r2 = r2[:-len(suffix)] step3_success = True else: continue elif suffix == u"seen": if word[-6:-4] in self.__long_vowels: word = word[:-4] r1 = r1[:-4] r2 = r2[:-4] step3_success = True else: continue elif suffix in (u"a", u"\xE4"): if word[-2] in self.__vowels and word[-3] in self.__consonants: word = word[:-1] r1 = r1[:-1] r2 = r2[:-1] step3_success = True elif suffix in (u"tta", u"tt\xE4"): if word[-4] == u"e": word = word[:-3] r1 = r1[:-3] r2 = r2[:-3] step3_success = True elif suffix == u"n": word = word[:-1] r1 = r1[:-1] r2 = r2[:-1] step3_success = True if word[-2:] == u"ie" or word[-2:] in self.__long_vowels: word = word[:-1] r1 = r1[:-1] r2 = r2[:-1] else: word = word[:-len(suffix)] r1 = r1[:-len(suffix)] r2 = r2[:-len(suffix)] step3_success = True break # STEP 4: Other endings for suffix in self.__step4_suffixes: if r2.endswith(suffix): if suffix in (u"mpi", u"mpa", u"mp\xE4", u"mmi", u"mma", u"mm\xE4"): if word[-5:-3] != u"po": word = word[:-3] r1 = r1[:-3] r2 = r2[:-3] else: word = word[:-len(suffix)] r1 = r1[:-len(suffix)] r2 = r2[:-len(suffix)] break # STEP 5: Plurals if step3_success and len(r1) >= 1 and r1[-1] in u"ij": word = word[:-1] r1 = r1[:-1] elif (not step3_success and len(r1) >= 2 and r1[-1] == u"t" and r1[-2] in self.__vowels): word = word[:-1] r1 = r1[:-1] r2 = r2[:-1] if r2.endswith(u"imma"): word = word[:-4] r1 = r1[:-4] elif r2.endswith(u"mma") and r2[-5:-3] != u"po": word = word[:-3] r1 = r1[:-3] # STEP 6: Tidying up if r1[-2:] in self.__long_vowels: word = word[:-1] r1 = r1[:-1] if (len(r1) >= 2 and r1[-2] in self.__consonants and r1[-1] in u"a\xE4ei"): word = word[:-1] r1 = r1[:-1] if r1.endswith((u"oj", u"uj")): word = word[:-1] r1 = r1[:-1] if r1.endswith(u"jo"): word = word[:-1] r1 = r1[:-1] # If the word ends with a double consonant # followed by zero or more vowels, the last consonant is removed. for i in xrange(1, len(word)): if word[-i] in self.__vowels: continue else: if i == 1: if word[-i-1:] in self.__double_consonants: word = word[:-1] else: if word[-i-1:-i+1] in self.__double_consonants: word = u"".join((word[:-i], word[-i+1:])) break return word class FrenchStemmer(_StandardStemmer): u""" The French Snowball stemmer. @cvar __vowels: The French vowels. @type __vowels: C{unicode} @cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. @type __step1_suffixes: C{tuple} @cvar __step2a_suffixes: Suffixes to be deleted in step 2a of the algorithm. @type __step2a_suffixes: C{tuple} @cvar __step2b_suffixes: Suffixes to be deleted in step 2b of the algorithm. @type __step2b_suffixes: C{tuple} @cvar __step4_suffixes: Suffixes to be deleted in step 4 of the algorithm. @type __step4_suffixes: C{tuple} @note: A detailed description of the French stemming algorithm can be found under U{http://snowball.tartarus.org/algorithms /french/stemmer.html}. """ __vowels = u"aeiouy\xE2\xE0\xEB\xE9\xEA\xE8\xEF\xEE\xF4\xFB\xF9" __step1_suffixes = (u'issements', u'issement', u'atrices', u'atrice', u'ateurs', u'ations', u'logies', u'usions', u'utions', u'ements', u'amment', u'emment', u'ances', u'iqUes', u'ismes', u'ables', u'istes', u'ateur', u'ation', u'logie', u'usion', u'ution', u'ences', u'ement', u'euses', u'ments', u'ance', u'iqUe', u'isme', u'able', u'iste', u'ence', u'it\xE9s', u'ives', u'eaux', u'euse', u'ment', u'eux', u'it\xE9', u'ive', u'ifs', u'aux', u'if') __step2a_suffixes = (u'issaIent', u'issantes', u'iraIent', u'issante', u'issants', u'issions', u'irions', u'issais', u'issait', u'issant', u'issent', u'issiez', u'issons', u'irais', u'irait', u'irent', u'iriez', u'irons', u'iront', u'isses', u'issez', u'\xEEmes', u'\xEEtes', u'irai', u'iras', u'irez', u'isse', u'ies', u'ira', u'\xEEt', u'ie', u'ir', u'is', u'it', u'i') __step2b_suffixes = (u'eraIent', u'assions', u'erions', u'assent', u'assiez', u'\xE8rent', u'erais', u'erait', u'eriez', u'erons', u'eront', u'aIent', u'antes', u'asses', u'ions', u'erai', u'eras', u'erez', u'\xE2mes', u'\xE2tes', u'ante', u'ants', u'asse', u'\xE9es', u'era', u'iez', u'ais', u'ait', u'ant', u'\xE9e', u'\xE9s', u'er', u'ez', u'\xE2t', u'ai', u'as', u'\xE9', u'a') __step4_suffixes = (u'i\xE8re', u'I\xE8re', u'ion', u'ier', u'Ier', u'e', u'\xEB') def stem(self, word): u""" Stem a French word and return the stemmed form. @param word: The word that is stemmed. @type word: C{str, unicode} @return: The stemmed form. @rtype: C{unicode} """ if word in self.stopwords: return word step1_success = False rv_ending_found = False step2a_success = False step2b_success = False word = word.lower() # Every occurrence of 'u' after 'q' is put into upper case. for i in xrange(1, len(word)): if word[i-1] == u"q" and word[i] == u"u": word = u"".join((word[:i], u"U", word[i+1:])) # Every occurrence of 'u' and 'i' # between vowels is put into upper case. # Every occurrence of 'y' preceded or # followed by a vowel is also put into upper case. for i in xrange(1, len(word)-1): if word[i-1] in self.__vowels and word[i+1] in self.__vowels: if word[i] == u"u": word = u"".join((word[:i], u"U", word[i+1:])) elif word[i] == u"i": word = u"".join((word[:i], u"I", word[i+1:])) if word[i-1] in self.__vowels or word[i+1] in self.__vowels: if word[i] == u"y": word = u"".join((word[:i], u"Y", word[i+1:])) r1, r2 = self._r1r2_standard(word, self.__vowels) rv = self.__rv_french(word, self.__vowels) # STEP 1: Standard suffix removal for suffix in self.__step1_suffixes: if word.endswith(suffix): if suffix == u"eaux": word = word[:-1] step1_success = True elif suffix in (u"euse", u"euses"): if suffix in r2: word = word[:-len(suffix)] step1_success = True elif suffix in r1: word = u"".join((word[:-len(suffix)], u"eux")) step1_success = True elif suffix in (u"ement", u"ements") and suffix in rv: word = word[:-len(suffix)] step1_success = True if word[-2:] == u"iv" and u"iv" in r2: word = word[:-2] if word[-2:] == u"at" and u"at" in r2: word = word[:-2] elif word[-3:] == u"eus": if u"eus" in r2: word = word[:-3] elif u"eus" in r1: word = u"".join((word[:-1], u"x")) elif word[-3:] in (u"abl", u"iqU"): if u"abl" in r2 or u"iqU" in r2: word = word[:-3] elif word[-3:] in (u"i\xE8r", u"I\xE8r"): if u"i\xE8r" in rv or u"I\xE8r" in rv: word = u"".join((word[:-3], u"i")) elif suffix == u"amment" and suffix in rv: word = u"".join((word[:-6], u"ant")) rv = u"".join((rv[:-6], u"ant")) rv_ending_found = True elif suffix == u"emment" and suffix in rv: word = u"".join((word[:-6], u"ent")) rv_ending_found = True elif (suffix in (u"ment", u"ments") and suffix in rv and not rv.startswith(suffix) and rv[rv.rindex(suffix)-1] in self.__vowels): word = word[:-len(suffix)] rv = rv[:-len(suffix)] rv_ending_found = True elif suffix == u"aux" and suffix in r1: word = u"".join((word[:-2], u"l")) step1_success = True elif (suffix in (u"issement", u"issements") and suffix in r1 and word[-len(suffix)-1] not in self.__vowels): word = word[:-len(suffix)] step1_success = True elif suffix in (u"ance", u"iqUe", u"isme", u"able", u"iste", u"eux", u"ances", u"iqUes", u"ismes", u"ables", u"istes") and suffix in r2: word = word[:-len(suffix)] step1_success = True elif suffix in (u"atrice", u"ateur", u"ation", u"atrices", u"ateurs", u"ations") and suffix in r2: word = word[:-len(suffix)] step1_success = True if word[-2:] == u"ic": if u"ic" in r2: word = word[:-2] else: word = u"".join((word[:-2], u"iqU")) elif suffix in (u"logie", u"logies") and suffix in r2: word = u"".join((word[:-len(suffix)], u"log")) step1_success = True elif (suffix in (u"usion", u"ution", u"usions", u"utions") and suffix in r2): word = u"".join((word[:-len(suffix)], u"u")) step1_success = True elif suffix in (u"ence", u"ences") and suffix in r2: word = u"".join((word[:-len(suffix)], u"ent")) step1_success = True elif suffix in (u"it\xE9", u"it\xE9s") and suffix in r2: word = word[:-len(suffix)] step1_success = True if word[-4:] == u"abil": if u"abil" in r2: word = word[:-4] else: word = u"".join((word[:-2], u"l")) elif word[-2:] == u"ic": if u"ic" in r2: word = word[:-2] else: word = u"".join((word[:-2], u"iqU")) elif word[-2:] == u"iv": if u"iv" in r2: word = word[:-2] elif (suffix in (u"if", u"ive", u"ifs", u"ives") and suffix in r2): word = word[:-len(suffix)] step1_success = True if word[-2:] == u"at" and u"at" in r2: word = word[:-2] if word[-2:] == u"ic": if u"ic" in r2: word = word[:-2] else: word = u"".join((word[:-2], u"iqU")) break # STEP 2a: Verb suffixes beginning 'i' if not step1_success or rv_ending_found: for suffix in self.__step2a_suffixes: if word.endswith(suffix): if (suffix in rv and len(rv) > len(suffix) and rv[rv.rindex(suffix)-1] not in self.__vowels): word = word[:-len(suffix)] step2a_success = True break # STEP 2b: Other verb suffixes if not step2a_success: for suffix in self.__step2b_suffixes: if rv.endswith(suffix): if suffix == u"ions" and u"ions" in r2: word = word[:-4] step2b_success = True elif suffix in (u'eraIent', u'erions', u'\xE8rent', u'erais', u'erait', u'eriez', u'erons', u'eront', u'erai', u'eras', u'erez', u'\xE9es', u'era', u'iez', u'\xE9e', u'\xE9s', u'er', u'ez', u'\xE9'): word = word[:-len(suffix)] step2b_success = True elif suffix in (u'assions', u'assent', u'assiez', u'aIent', u'antes', u'asses', u'\xE2mes', u'\xE2tes', u'ante', u'ants', u'asse', u'ais', u'ait', u'ant', u'\xE2t', u'ai', u'as', u'a'): word = word[:-len(suffix)] rv = rv[:-len(suffix)] step2b_success = True if rv.endswith(u"e"): word = word[:-1] break # STEP 3 if step1_success or step2a_success or step2b_success: if word[-1] == u"Y": word = u"".join((word[:-1], u"i")) elif word[-1] == u"\xE7": word = u"".join((word[:-1], u"c")) # STEP 4: Residual suffixes else: if (len(word) >= 2 and word[-1] == u"s" and word[-2] not in u"aiou\xE8s"): word = word[:-1] for suffix in self.__step4_suffixes: if word.endswith(suffix): if suffix in rv: if (suffix == u"ion" and suffix in r2 and rv[-4] in u"st"): word = word[:-3] elif suffix in (u"ier", u"i\xE8re", u"Ier", u"I\xE8re"): word = u"".join((word[:-len(suffix)], u"i")) elif suffix == u"e": word = word[:-1] elif suffix == u"\xEB" and word[-3:-1] == u"gu": word = word[:-1] break # STEP 5: Undouble if word.endswith((u"enn", u"onn", u"ett", u"ell", u"eill")): word = word[:-1] # STEP 6: Un-accent for i in xrange(1, len(word)): if word[-i] not in self.__vowels: i += 1 else: if i != 1 and word[-i] in (u"\xE9", u"\xE8"): word = u"".join((word[:-i], u"e", word[-i+1:])) break word = (word.replace(u"I", u"i") .replace(u"U", u"u") .replace(u"Y", u"y")) return word def __rv_french(self, word, vowels): u""" Return the region RV that is used by the French stemmer. If the word begins with two vowels, RV is the region after the third letter. Otherwise, it is the region after the first vowel not at the beginning of the word, or the end of the word if these positions cannot be found. (Exceptionally, u'par', u'col' or u'tap' at the beginning of a word is also taken to define RV as the region to their right.) @param word: The French word whose region RV is determined. @type word: C{str, unicode} @param vowels: The French vowels that are used to determine the region RV. @type vowels: C{unicode} @return: C{rv}, the region RV for the respective French word. @rtype: C{unicode} @note: This helper method is invoked by the stem method of the subclass L{FrenchStemmer}. It is not to be invoked directly! """ rv = u"" if len(word) >= 2: if (word.startswith((u"par", u"col", u"tap")) or (word[0] in vowels and word[1] in vowels)): rv = word[3:] else: for i in xrange(1, len(word)): if word[i] in vowels: rv = word[i+1:] break return rv class GermanStemmer(_StandardStemmer): u""" The German Snowball stemmer. @cvar __vowels: The German vowels. @type __vowels: C{unicode} @cvar __s_ending: Letters that may directly appear before a word final 's'. @type __s_ending: C{unicode} @cvar __st_ending: Letter that may directly appear before a word final 'st'. @type __st_ending: C{unicode} @cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. @type __step1_suffixes: C{tuple} @cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. @type __step2_suffixes: C{tuple} @cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. @type __step3_suffixes: C{tuple} @note: A detailed description of the German stemming algorithm can be found under U{http://snowball.tartarus.org/algorithms /german/stemmer.html}. """ __vowels = u"aeiouy\xE4\xF6\xFC" __s_ending = u"bdfghklmnrt" __st_ending = u"bdfghklmnt" __step1_suffixes = (u"ern", u"em", u"er", u"en", u"es", u"e", u"s") __step2_suffixes = (u"est", u"en", u"er", u"st") __step3_suffixes = (u"isch", u"lich", u"heit", u"keit", u"end", u"ung", u"ig", u"ik") def stem(self, word): u""" Stem a German word and return the stemmed form. @param word: The word that is stemmed. @type word: C{str, unicode} @return: The stemmed form. @rtype: C{unicode} """ if word in self.stopwords: return word word = word.lower().replace(u"\xDF", u"ss") # Every occurrence of 'u' and 'y' # between vowels is put into upper case. for i in xrange(1, len(word)-1): if word[i-1] in self.__vowels and word[i+1] in self.__vowels: if word[i] == u"u": word = u"".join((word[:i], u"U", word[i+1:])) elif word[i] == u"y": word = u"".join((word[:i], u"Y", word[i+1:])) r1, r2 = self._r1r2_standard(word, self.__vowels) # R1 is adjusted so that the region before it # contains at least 3 letters. for i in xrange(1, len(word)): if word[i] not in self.__vowels and word[i-1] in self.__vowels: if len(word[:i+1]) < 3 and len(word[:i+1]) > 0: r1 = word[3:] elif len(word[:i+1]) == 0: return word break # STEP 1 for suffix in self.__step1_suffixes: if r1.endswith(suffix): if (suffix in (u"en", u"es", u"e") and word[-len(suffix)-4:-len(suffix)] == u"niss"): word = word[:-len(suffix)-1] r1 = r1[:-len(suffix)-1] r2 = r2[:-len(suffix)-1] elif suffix == u"s": if word[-2] in self.__s_ending: word = word[:-1] r1 = r1[:-1] r2 = r2[:-1] else: word = word[:-len(suffix)] r1 = r1[:-len(suffix)] r2 = r2[:-len(suffix)] break # STEP 2 for suffix in self.__step2_suffixes: if r1.endswith(suffix): if suffix == u"st": if word[-3] in self.__st_ending and len(word[:-3]) >= 3: word = word[:-2] r1 = r1[:-2] r2 = r2[:-2] else: word = word[:-len(suffix)] r1 = r1[:-len(suffix)] r2 = r2[:-len(suffix)] break # STEP 3: Derivational suffixes for suffix in self.__step3_suffixes: if r2.endswith(suffix): if suffix in (u"end", u"ung"): if (u"ig" in r2[-len(suffix)-2:-len(suffix)] and u"e" not in r2[-len(suffix)-3:-len(suffix)-2]): word = word[:-len(suffix)-2] else: word = word[:-len(suffix)] elif (suffix in (u"ig", u"ik", u"isch") and u"e" not in r2[-len(suffix)-1:-len(suffix)]): word = word[:-len(suffix)] elif suffix in (u"lich", u"heit"): if (u"er" in r1[-len(suffix)-2:-len(suffix)] or u"en" in r1[-len(suffix)-2:-len(suffix)]): word = word[:-len(suffix)-2] else: word = word[:-len(suffix)] elif suffix == u"keit": if u"lich" in r2[-len(suffix)-4:-len(suffix)]: word = word[:-len(suffix)-4] elif u"ig" in r2[-len(suffix)-2:-len(suffix)]: word = word[:-len(suffix)-2] else: word = word[:-len(suffix)] break # Umlaut accents are removed and # 'u' and 'y' are put back into lower case. word = (word.replace(u"\xE4", u"a").replace(u"\xF6", u"o") .replace(u"\xFC", u"u").replace(u"U", u"u") .replace(u"Y", u"y")) return word class HungarianStemmer(SnowballStemmer): u""" The Hungarian Snowball stemmer. @cvar __vowels: The Hungarian vowels. @type __vowels: C{unicode} @cvar __digraphs: The Hungarian digraphs. @type __digraphs: C{tuple} @cvar __double_consonants: The Hungarian double consonants. @type __double_consonants: C{tuple} @cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. @type __step1_suffixes: C{tuple} @cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. @type __step2_suffixes: C{tuple} @cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. @type __step3_suffixes: C{tuple} @cvar __step4_suffixes: Suffixes to be deleted in step 4 of the algorithm. @type __step4_suffixes: C{tuple} @cvar __step5_suffixes: Suffixes to be deleted in step 5 of the algorithm. @type __step5_suffixes: C{tuple} @cvar __step6_suffixes: Suffixes to be deleted in step 6 of the algorithm. @type __step6_suffixes: C{tuple} @cvar __step7_suffixes: Suffixes to be deleted in step 7 of the algorithm. @type __step7_suffixes: C{tuple} @cvar __step8_suffixes: Suffixes to be deleted in step 8 of the algorithm. @type __step8_suffixes: C{tuple} @cvar __step9_suffixes: Suffixes to be deleted in step 9 of the algorithm. @type __step9_suffixes: C{tuple} @note: A detailed description of the Hungarian stemming algorithm can be found under U{http://snowball.tartarus.org/algorithms /hungarian/stemmer.html}. """ __vowels = u"aeiou\xF6\xFC\xE1\xE9\xED\xF3\xF5\xFA\xFB" __digraphs = (u"cs", u"dz", u"dzs", u"gy", u"ly", u"ny", u"ty", u"zs") __double_consonants = (u"bb", u"cc", u"ccs", u"dd", u"ff", u"gg", u"ggy", u"jj", u"kk", u"ll", u"lly", u"mm", u"nn", u"nny", u"pp", u"rr", u"ss", u"ssz", u"tt", u"tty", u"vv", u"zz", u"zzs") __step1_suffixes = (u"al", u"el") __step2_suffixes = (u'k\xE9ppen', u'onk\xE9nt', u'enk\xE9nt', u'ank\xE9nt', u'k\xE9pp', u'k\xE9nt', u'ban', u'ben', u'nak', u'nek', u'val', u'vel', u't\xF3l', u't\xF5l', u'r\xF3l', u'r\xF5l', u'b\xF3l', u'b\xF5l', u'hoz', u'hez', u'h\xF6z', u'n\xE1l', u'n\xE9l', u'\xE9rt', u'kor', u'ba', u'be', u'ra', u're', u'ig', u'at', u'et', u'ot', u'\xF6t', u'ul', u'\xFCl', u'v\xE1', u'v\xE9', u'en', u'on', u'an', u'\xF6n', u'n', u't') __step3_suffixes = (u"\xE1nk\xE9nt", u"\xE1n", u"\xE9n") __step4_suffixes = (u'astul', u'est\xFCl', u'\xE1stul', u'\xE9st\xFCl', u'stul', u'st\xFCl') __step5_suffixes = (u"\xE1", u"\xE9") __step6_suffixes = (u'ok\xE9', u'\xF6k\xE9', u'ak\xE9', u'ek\xE9', u'\xE1k\xE9', u'\xE1\xE9i', u'\xE9k\xE9', u'\xE9\xE9i', u'k\xE9', u'\xE9i', u'\xE9\xE9', u'\xE9') __step7_suffixes = (u'\xE1juk', u'\xE9j\xFCk', u'\xFCnk', u'unk', u'juk', u'j\xFCk', u'\xE1nk', u'\xE9nk', u'nk', u'uk', u'\xFCk', u'em', u'om', u'am', u'od', u'ed', u'ad', u'\xF6d', u'ja', u'je', u'\xE1m', u'\xE1d', u'\xE9m', u'\xE9d', u'm', u'd', u'a', u'e', u'o', u'\xE1', u'\xE9') __step8_suffixes = (u'jaitok', u'jeitek', u'jaink', u'jeink', u'aitok', u'eitek', u'\xE1itok', u'\xE9itek', u'jaim', u'jeim', u'jaid', u'jeid', u'eink', u'aink', u'itek', u'jeik', u'jaik', u'\xE1ink', u'\xE9ink', u'aim', u'eim', u'aid', u'eid', u'jai', u'jei', u'ink', u'aik', u'eik', u'\xE1im', u'\xE1id', u'\xE1ik', u'\xE9im', u'\xE9id', u'\xE9ik', u'im', u'id', u'ai', u'ei', u'ik', u'\xE1i', u'\xE9i', u'i') __step9_suffixes = (u"\xE1k", u"\xE9k", u"\xF6k", u"ok", u"ek", u"ak", u"k") def stem(self, word): u""" Stem an Hungarian word and return the stemmed form. @param word: The word that is stemmed. @type word: C{str, unicode} @return: The stemmed form. @rtype: C{unicode} """ if word in self.stopwords: return word word = word.lower() r1 = self.__r1_hungarian(word, self.__vowels, self.__digraphs) # STEP 1: Remove instrumental case if r1.endswith(self.__step1_suffixes): for double_cons in self.__double_consonants: if word[-2-len(double_cons):-2] == double_cons: word = u"".join((word[:-4], word[-3])) if r1[-2-len(double_cons):-2] == double_cons: r1 = u"".join((r1[:-4], r1[-3])) break # STEP 2: Remove frequent cases for suffix in self.__step2_suffixes: if word.endswith(suffix): if r1.endswith(suffix): word = word[:-len(suffix)] r1 = r1[:-len(suffix)] if r1.endswith(u"\xE1"): word = u"".join((word[:-1], u"a")) r1 = u"".join((r1[:-1], u"a")) elif r1.endswith(u"\xE9"): word = u"".join((word[:-1], u"e")) r1 = u"".join((r1[:-1], u"e")) break # STEP 3: Remove special cases for suffix in self.__step3_suffixes: if r1.endswith(suffix): if suffix == u"\xE9n": word = u"".join((word[:-2], u"e")) r1 = u"".join((r1[:-2], u"e")) else: word = u"".join((word[:-len(suffix)], u"a")) r1 = u"".join((r1[:-len(suffix)], u"a")) break # STEP 4: Remove other cases for suffix in self.__step4_suffixes: if r1.endswith(suffix): if suffix == u"\xE1stul": word = u"".join((word[:-5], u"a")) r1 = u"".join((r1[:-5], u"a")) elif suffix == u"\xE9st\xFCl": word = u"".join((word[:-5], u"e")) r1 = u"".join((r1[:-5], u"e")) else: word = word[:-len(suffix)] r1 = r1[:-len(suffix)] break # STEP 5: Remove factive case for suffix in self.__step5_suffixes: if r1.endswith(suffix): for double_cons in self.__double_consonants: if word[-1-len(double_cons):-1] == double_cons: word = u"".join((word[:-3], word[-2])) if r1[-1-len(double_cons):-1] == double_cons: r1 = u"".join((r1[:-3], r1[-2])) break # STEP 6: Remove owned for suffix in self.__step6_suffixes: if r1.endswith(suffix): if suffix in (u"\xE1k\xE9", u"\xE1\xE9i"): word = u"".join((word[:-3], u"a")) r1 = u"".join((r1[:-3], u"a")) elif suffix in (u"\xE9k\xE9", u"\xE9\xE9i", u"\xE9\xE9"): word = u"".join((word[:-len(suffix)], u"e")) r1 = u"".join((r1[:-len(suffix)], u"e")) else: word = word[:-len(suffix)] r1 = r1[:-len(suffix)] break # STEP 7: Remove singular owner suffixes for suffix in self.__step7_suffixes: if word.endswith(suffix): if r1.endswith(suffix): if suffix in (u"\xE1nk", u"\xE1juk", u"\xE1m", u"\xE1d", u"\xE1"): word = u"".join((word[:-len(suffix)], u"a")) r1 = u"".join((r1[:-len(suffix)], u"a")) elif suffix in (u"\xE9nk", u"\xE9j\xFCk", u"\xE9m", u"\xE9d", u"\xE9"): word = u"".join((word[:-len(suffix)], u"e")) r1 = u"".join((r1[:-len(suffix)], u"e")) else: word = word[:-len(suffix)] r1 = r1[:-len(suffix)] break # STEP 8: Remove plural owner suffixes for suffix in self.__step8_suffixes: if word.endswith(suffix): if r1.endswith(suffix): if suffix in (u"\xE1im", u"\xE1id", u"\xE1i", u"\xE1ink", u"\xE1itok", u"\xE1ik"): word = u"".join((word[:-len(suffix)], u"a")) r1 = u"".join((r1[:-len(suffix)], u"a")) elif suffix in (u"\xE9im", u"\xE9id", u"\xE9i", u"\xE9ink", u"\xE9itek", u"\xE9ik"): word = u"".join((word[:-len(suffix)], u"e")) r1 = u"".join((r1[:-len(suffix)], u"e")) else: word = word[:-len(suffix)] r1 = r1[:-len(suffix)] break # STEP 9: Remove plural suffixes for suffix in self.__step9_suffixes: if word.endswith(suffix): if r1.endswith(suffix): if suffix == u"\xE1k": word = u"".join((word[:-2], u"a")) elif suffix == u"\xE9k": word = u"".join((word[:-2], u"e")) else: word = word[:-len(suffix)] break return word def __r1_hungarian(self, word, vowels, digraphs): u""" Return the region R1 that is used by the Hungarian stemmer. If the word begins with a vowel, R1 is defined as the region after the first consonant or digraph (= two letters stand for one phoneme) in the word. If the word begins with a consonant, it is defined as the region after the first vowel in the word. If the word does not contain both a vowel and consonant, R1 is the null region at the end of the word. @param word: The Hungarian word whose region R1 is determined. @type word: C{str, unicode} @param vowels: The Hungarian vowels that are used to determine the region R1. @type vowels: C{unicode} @param digraphs: The digraphs that are used to determine the region R1. @type digraphs: C{tuple} @return: C{r1}, the region R1 for the respective word. @rtype: C{unicode} @note: This helper method is invoked by the stem method of the subclass L{HungarianStemmer}. It is not to be invoked directly! """ r1 = u"" if word[0] in vowels: for digraph in digraphs: if digraph in word[1:]: r1 = word[word.index(digraph[-1])+1:] return r1 for i in xrange(1, len(word)): if word[i] not in vowels: r1 = word[i+1:] break else: for i in xrange(1, len(word)): if word[i] in vowels: r1 = word[i+1:] break return r1 class ItalianStemmer(_StandardStemmer): u""" The Italian Snowball stemmer. @cvar __vowels: The Italian vowels. @type __vowels: C{unicode} @cvar __step0_suffixes: Suffixes to be deleted in step 0 of the algorithm. @type __step0_suffixes: C{tuple} @cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. @type __step1_suffixes: C{tuple} @cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. @type __step2_suffixes: C{tuple} @note: A detailed description of the Italian stemming algorithm can be found under U{http://snowball.tartarus.org/algorithms /italian/stemmer.html}. """ __vowels = u"aeiou\xE0\xE8\xEC\xF2\xF9" __step0_suffixes = (u'gliela', u'gliele', u'glieli', u'glielo', u'gliene', u'sene', u'mela', u'mele', u'meli', u'melo', u'mene', u'tela', u'tele', u'teli', u'telo', u'tene', u'cela', u'cele', u'celi', u'celo', u'cene', u'vela', u'vele', u'veli', u'velo', u'vene', u'gli', u'ci', u'la', u'le', u'li', u'lo', u'mi', u'ne', u'si', u'ti', u'vi') __step1_suffixes = (u'atrice', u'atrici', u'azione', u'azioni', u'uzione', u'uzioni', u'usione', u'usioni', u'amento', u'amenti', u'imento', u'imenti', u'amente', u'abile', u'abili', u'ibile', u'ibili', u'mente', u'atore', u'atori', u'logia', u'logie', u'anza', u'anze', u'iche', u'ichi', u'ismo', u'ismi', u'ista', u'iste', u'isti', u'ist\xE0', u'ist\xE8', u'ist\xEC', u'ante', u'anti', u'enza', u'enze', u'ico', u'ici', u'ica', u'ice', u'oso', u'osi', u'osa', u'ose', u'it\xE0', u'ivo', u'ivi', u'iva', u'ive') __step2_suffixes = (u'erebbero', u'irebbero', u'assero', u'assimo', u'eranno', u'erebbe', u'eremmo', u'ereste', u'eresti', u'essero', u'iranno', u'irebbe', u'iremmo', u'ireste', u'iresti', u'iscano', u'iscono', u'issero', u'arono', u'avamo', u'avano', u'avate', u'eremo', u'erete', u'erono', u'evamo', u'evano', u'evate', u'iremo', u'irete', u'irono', u'ivamo', u'ivano', u'ivate', u'ammo', u'ando', u'asse', u'assi', u'emmo', u'enda', u'ende', u'endi', u'endo', u'erai', u'erei', u'Yamo', u'iamo', u'immo', u'irai', u'irei', u'isca', u'isce', u'isci', u'isco', u'ano', u'are', u'ata', u'ate', u'ati', u'ato', u'ava', u'avi', u'avo', u'er\xE0', u'ere', u'er\xF2', u'ete', u'eva', u'evi', u'evo', u'ir\xE0', u'ire', u'ir\xF2', u'ita', u'ite', u'iti', u'ito', u'iva', u'ivi', u'ivo', u'ono', u'uta', u'ute', u'uti', u'uto', u'ar', u'ir') def stem(self, word): u""" Stem an Italian word and return the stemmed form. @param word: The word that is stemmed. @type word: C{str, unicode} @return: The stemmed form. @rtype: C{unicode} """ if word in self.stopwords: return word step1_success = False # All acute accents are replaced by grave accents. word = (word.lower().replace(u"\xE1", u"\xE0") .replace(u"\xE9", u"\xE8") .replace(u"\xED", u"\xEC") .replace(u"\xF3", u"\xF2") .replace(u"\xFA", u"\xF9")) # Every occurrence of 'u' after 'q' # is put into upper case. for i in xrange(1, len(word)): if word[i-1] == u"q" and word[i] == u"u": word = u"".join((word[:i], u"U", word[i+1:])) # Every occurrence of 'u' and 'i' # between vowels is put into upper case. for i in xrange(1, len(word)-1): if word[i-1] in self.__vowels and word[i+1] in self.__vowels: if word[i] == u"u": word = u"".join((word[:i], u"U", word[i+1:])) elif word [i] == u"i": word = u"".join((word[:i], u"I", word[i+1:])) r1, r2 = self._r1r2_standard(word, self.__vowels) rv = self._rv_standard(word, self.__vowels) # STEP 0: Attached pronoun for suffix in self.__step0_suffixes: if rv.endswith(suffix): if rv[-len(suffix)-4:-len(suffix)] in (u"ando", u"endo"): word = word[:-len(suffix)] r1 = r1[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] elif (rv[-len(suffix)-2:-len(suffix)] in (u"ar", u"er", u"ir")): word = u"".join((word[:-len(suffix)], u"e")) r1 = u"".join((r1[:-len(suffix)], u"e")) r2 = u"".join((r2[:-len(suffix)], u"e")) rv = u"".join((rv[:-len(suffix)], u"e")) break # STEP 1: Standard suffix removal for suffix in self.__step1_suffixes: if word.endswith(suffix): if suffix == u"amente" and r1.endswith(suffix): step1_success = True word = word[:-6] r2 = r2[:-6] rv = rv[:-6] if r2.endswith(u"iv"): word = word[:-2] r2 = r2[:-2] rv = rv[:-2] if r2.endswith(u"at"): word = word[:-2] rv = rv[:-2] elif r2.endswith((u"os", u"ic")): word = word[:-2] rv = rv[:-2] elif r2 .endswith(u"abil"): word = word[:-4] rv = rv[:-4] elif (suffix in (u"amento", u"amenti", u"imento", u"imenti") and rv.endswith(suffix)): step1_success = True word = word[:-6] rv = rv[:-6] elif r2.endswith(suffix): step1_success = True if suffix in (u"azione", u"azioni", u"atore", u"atori"): word = word[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] if r2.endswith(u"ic"): word = word[:-2] rv = rv[:-2] elif suffix in (u"logia", u"logie"): word = word[:-2] rv = word[:-2] elif suffix in (u"uzione", u"uzioni", u"usione", u"usioni"): word = word[:-5] rv = rv[:-5] elif suffix in (u"enza", u"enze"): word = u"".join((word[:-2], u"te")) rv = u"".join((rv[:-2], u"te")) elif suffix == u"it\xE0": word = word[:-3] r2 = r2[:-3] rv = rv[:-3] if r2.endswith((u"ic", u"iv")): word = word[:-2] rv = rv[:-2] elif r2.endswith(u"abil"): word = word[:-4] rv = rv[:-4] elif suffix in (u"ivo", u"ivi", u"iva", u"ive"): word = word[:-3] r2 = r2[:-3] rv = rv[:-3] if r2.endswith(u"at"): word = word[:-2] r2 = r2[:-2] rv = rv[:-2] if r2.endswith(u"ic"): word = word[:-2] rv = rv[:-2] else: word = word[:-len(suffix)] rv = rv[:-len(suffix)] break # STEP 2: Verb suffixes if not step1_success: for suffix in self.__step2_suffixes: if rv.endswith(suffix): word = word[:-len(suffix)] rv = rv[:-len(suffix)] break # STEP 3a if rv.endswith((u"a", u"e", u"i", u"o", u"\xE0", u"\xE8", u"\xEC", u"\xF2")): word = word[:-1] rv = rv[:-1] if rv.endswith(u"i"): word = word[:-1] rv = rv[:-1] # STEP 3b if rv.endswith((u"ch", u"gh")): word = word[:-1] word = word.replace(u"I", u"i").replace(u"U", u"u") return word class NorwegianStemmer(_ScandinavianStemmer): u""" The Norwegian Snowball stemmer. @cvar __vowels: The Norwegian vowels. @type __vowels: C{unicode} @cvar __s_ending: Letters that may directly appear before a word final 's'. @type __s_ending: C{unicode} @cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. @type __step1_suffixes: C{tuple} @cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. @type __step2_suffixes: C{tuple} @cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. @type __step3_suffixes: C{tuple} @note: A detailed description of the Norwegian stemming algorithm can be found under U{http://snowball.tartarus.org/algorithms /norwegian/stemmer.html}. """ __vowels = u"aeiouy\xE6\xE5\xF8" __s_ending = u"bcdfghjlmnoprtvyz" __step1_suffixes = (u"hetenes", u"hetene", u"hetens", u"heter", u"heten", u"endes", u"ande", u"ende", u"edes", u"enes", u"erte", u"ede", u"ane", u"ene", u"ens", u"ers", u"ets", u"het", u"ast", u"ert", u"en", u"ar", u"er", u"as", u"es", u"et", u"a", u"e", u"s") __step2_suffixes = (u"dt", u"vt") __step3_suffixes = (u"hetslov", u"eleg", u"elig", u"elov", u"slov", u"leg", u"eig", u"lig", u"els", u"lov", u"ig") def stem(self, word): u""" Stem a Norwegian word and return the stemmed form. @param word: The word that is stemmed. @type word: C{str, unicode} @return: The stemmed form. @rtype: C{unicode} """ if word in self.stopwords: return word word = word.lower() r1 = self._r1_scandinavian(word, self.__vowels) # STEP 1 for suffix in self.__step1_suffixes: if r1.endswith(suffix): if suffix in (u"erte", u"ert"): word = u"".join((word[:-len(suffix)], u"er")) r1 = u"".join((r1[:-len(suffix)], u"er")) elif suffix == u"s": if (word[-2] in self.__s_ending or (word[-2] == u"k" and word[-3] not in self.__vowels)): word = word[:-1] r1 = r1[:-1] else: word = word[:-len(suffix)] r1 = r1[:-len(suffix)] break # STEP 2 for suffix in self.__step2_suffixes: if r1.endswith(suffix): word = word[:-1] r1 = r1[:-1] break # STEP 3 for suffix in self.__step3_suffixes: if r1.endswith(suffix): word = word[:-len(suffix)] break return word class PortugueseStemmer(_StandardStemmer): u""" The Portuguese Snowball stemmer. @cvar __vowels: The Portuguese vowels. @type __vowels: C{unicode} @cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. @type __step1_suffixes: C{tuple} @cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. @type __step2_suffixes: C{tuple} @cvar __step4_suffixes: Suffixes to be deleted in step 4 of the algorithm. @type __step4_suffixes: C{tuple} @note: A detailed description of the Portuguese stemming algorithm can be found under U{http://snowball.tartarus.org/algorithms /portuguese/stemmer.html}. """ __vowels = u"aeiou\xE1\xE9\xED\xF3\xFA\xE2\xEA\xF4" __step1_suffixes = (u'amentos', u'imentos', u'uciones', u'amento', u'imento', u'adoras', u'adores', u'a\xE7o~es', u'log\xEDas', u'\xEAncias', u'amente', u'idades', u'ismos', u'istas', u'adora', u'a\xE7a~o', u'antes', u'\xE2ncia', u'log\xEDa', u'uci\xF3n', u'\xEAncia', u'mente', u'idade', u'ezas', u'icos', u'icas', u'ismo', u'\xE1vel', u'\xEDvel', u'ista', u'osos', u'osas', u'ador', u'ante', u'ivas', u'ivos', u'iras', u'eza', u'ico', u'ica', u'oso', u'osa', u'iva', u'ivo', u'ira') __step2_suffixes = (u'ar\xEDamos', u'er\xEDamos', u'ir\xEDamos', u'\xE1ssemos', u'\xEAssemos', u'\xEDssemos', u'ar\xEDeis', u'er\xEDeis', u'ir\xEDeis', u'\xE1sseis', u'\xE9sseis', u'\xEDsseis', u'\xE1ramos', u'\xE9ramos', u'\xEDramos', u'\xE1vamos', u'aremos', u'eremos', u'iremos', u'ariam', u'eriam', u'iriam', u'assem', u'essem', u'issem', u'ara~o', u'era~o', u'ira~o', u'arias', u'erias', u'irias', u'ardes', u'erdes', u'irdes', u'asses', u'esses', u'isses', u'astes', u'estes', u'istes', u'\xE1reis', u'areis', u'\xE9reis', u'ereis', u'\xEDreis', u'ireis', u'\xE1veis', u'\xEDamos', u'armos', u'ermos', u'irmos', u'aria', u'eria', u'iria', u'asse', u'esse', u'isse', u'aste', u'este', u'iste', u'arei', u'erei', u'irei', u'aram', u'eram', u'iram', u'avam', u'arem', u'erem', u'irem', u'ando', u'endo', u'indo', u'adas', u'idas', u'ar\xE1s', u'aras', u'er\xE1s', u'eras', u'ir\xE1s', u'avas', u'ares', u'eres', u'ires', u'\xEDeis', u'ados', u'idos', u'\xE1mos', u'amos', u'emos', u'imos', u'iras', u'ada', u'ida', u'ar\xE1', u'ara', u'er\xE1', u'era', u'ir\xE1', u'ava', u'iam', u'ado', u'ido', u'ias', u'ais', u'eis', u'ira', u'ia', u'ei', u'am', u'em', u'ar', u'er', u'ir', u'as', u'es', u'is', u'eu', u'iu', u'ou') __step4_suffixes = (u"os", u"a", u"i", u"o", u"\xE1", u"\xED", u"\xF3") def stem(self, word): u""" Stem a Portuguese word and return the stemmed form. @param word: The word that is stemmed. @type word: C{str, unicode} @return: The stemmed form. @rtype: C{unicode} """ if word in self.stopwords: return word step1_success = False step2_success = False word = (word.lower() .replace(u"\xE3", u"a~") .replace(u"\xF5", u"o~")) r1, r2 = self._r1r2_standard(word, self.__vowels) rv = self._rv_standard(word, self.__vowels) # STEP 1: Standard suffix removal for suffix in self.__step1_suffixes: if word.endswith(suffix): if suffix == u"amente" and r1.endswith(suffix): step1_success = True word = word[:-6] r2 = r2[:-6] rv = rv[:-6] if r2.endswith(u"iv"): word = word[:-2] r2 = r2[:-2] rv = rv[:-2] if r2.endswith(u"at"): word = word[:-2] rv = rv[:-2] elif r2.endswith((u"os", u"ic", u"ad")): word = word[:-2] rv = rv[:-2] elif (suffix in (u"ira", u"iras") and rv.endswith(suffix) and word[-len(suffix)-1:-len(suffix)] == u"e"): step1_success = True word = u"".join((word[:-len(suffix)], u"ir")) rv = u"".join((rv[:-len(suffix)], u"ir")) elif r2.endswith(suffix): step1_success = True if suffix in (u"log\xEDa", u"log\xEDas"): word = word[:-2] rv = rv[:-2] elif suffix in (u"uci\xF3n", u"uciones"): word = u"".join((word[:-len(suffix)], u"u")) rv = u"".join((rv[:-len(suffix)], u"u")) elif suffix in (u"\xEAncia", u"\xEAncias"): word = u"".join((word[:-len(suffix)], u"ente")) rv = u"".join((rv[:-len(suffix)], u"ente")) elif suffix == u"mente": word = word[:-5] r2 = r2[:-5] rv = rv[:-5] if r2.endswith((u"ante", u"avel", u"\xEDvel")): word = word[:-4] rv = rv[:-4] elif suffix in (u"idade", u"idades"): word = word[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] if r2.endswith((u"ic", u"iv")): word = word[:-2] rv = rv[:-2] elif r2.endswith(u"abil"): word = word[:-4] rv = rv[:-4] elif suffix in (u"iva", u"ivo", u"ivas", u"ivos"): word = word[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] if r2.endswith(u"at"): word = word[:-2] rv = rv[:-2] else: word = word[:-len(suffix)] rv = rv[:-len(suffix)] break # STEP 2: Verb suffixes if not step1_success: for suffix in self.__step2_suffixes: if rv.endswith(suffix): step2_success = True word = word[:-len(suffix)] rv = rv[:-len(suffix)] break # STEP 3 if step1_success or step2_success: if rv.endswith(u"i") and word[-2] == u"c": word = word[:-1] rv = rv[:-1] ### STEP 4: Residual suffix if not step1_success and not step2_success: for suffix in self.__step4_suffixes: if rv.endswith(suffix): word = word[:-len(suffix)] rv = rv[:-len(suffix)] break # STEP 5 if rv.endswith((u"e", u"\xE9", u"\xEA")): word = word[:-1] rv = rv[:-1] if ((word.endswith(u"gu") and rv.endswith(u"u")) or (word.endswith(u"ci") and rv.endswith(u"i"))): word = word[:-1] elif word.endswith(u"\xE7"): word = u"".join((word[:-1], u"c")) word = word.replace(u"a~", u"\xE3").replace(u"o~", u"\xF5") return word class RomanianStemmer(_StandardStemmer): u""" The Romanian Snowball stemmer. @cvar __vowels: The Romanian vowels. @type __vowels: C{unicode} @cvar __step0_suffixes: Suffixes to be deleted in step 0 of the algorithm. @type __step0_suffixes: C{tuple} @cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. @type __step1_suffixes: C{tuple} @cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. @type __step2_suffixes: C{tuple} @cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. @type __step3_suffixes: C{tuple} @note: A detailed description of the Romanian stemming algorithm can be found under U{http://snowball.tartarus.org/algorithms /romanian/stemmer.html}. """ __vowels = u"aeiou\u0103\xE2\xEE" __step0_suffixes = (u'iilor', u'ului', u'elor', u'iile', u'ilor', u'atei', u'a\u0163ie', u'a\u0163ia', u'aua', u'ele', u'iua', u'iei', u'ile', u'ul', u'ea', u'ii') __step1_suffixes = (u'abilitate', u'abilitati', u'abilit\u0103\u0163i', u'ibilitate', u'abilit\u0103i', u'ivitate', u'ivitati', u'ivit\u0103\u0163i', u'icitate', u'icitati', u'icit\u0103\u0163i', u'icatori', u'ivit\u0103i', u'icit\u0103i', u'icator', u'a\u0163iune', u'atoare', u'\u0103toare', u'i\u0163iune', u'itoare', u'iciva', u'icive', u'icivi', u'iciv\u0103', u'icala', u'icale', u'icali', u'ical\u0103', u'ativa', u'ative', u'ativi', u'ativ\u0103', u'atori', u'\u0103tori', u'itiva', u'itive', u'itivi', u'itiv\u0103', u'itori', u'iciv', u'ical', u'ativ', u'ator', u'\u0103tor', u'itiv', u'itor') __step2_suffixes = (u'abila', u'abile', u'abili', u'abil\u0103', u'ibila', u'ibile', u'ibili', u'ibil\u0103', u'atori', u'itate', u'itati', u'it\u0103\u0163i', u'abil', u'ibil', u'oasa', u'oas\u0103', u'oase', u'anta', u'ante', u'anti', u'ant\u0103', u'ator', u'it\u0103i', u'iune', u'iuni', u'isme', u'ista', u'iste', u'isti', u'ist\u0103', u'i\u015Fti', u'ata', u'at\u0103', u'ati', u'ate', u'uta', u'ut\u0103', u'uti', u'ute', u'ita', u'it\u0103', u'iti', u'ite', u'ica', u'ice', u'ici', u'ic\u0103', u'osi', u'o\u015Fi', u'ant', u'iva', u'ive', u'ivi', u'iv\u0103', u'ism', u'ist', u'at', u'ut', u'it', u'ic', u'os', u'iv') __step3_suffixes = (u'seser\u0103\u0163i', u'aser\u0103\u0163i', u'iser\u0103\u0163i', u'\xE2ser\u0103\u0163i', u'user\u0103\u0163i', u'seser\u0103m', u'aser\u0103m', u'iser\u0103m', u'\xE2ser\u0103m', u'user\u0103m', u'ser\u0103\u0163i', u'sese\u015Fi', u'seser\u0103', u'easc\u0103', u'ar\u0103\u0163i', u'ur\u0103\u0163i', u'ir\u0103\u0163i', u'\xE2r\u0103\u0163i', u'ase\u015Fi', u'aser\u0103', u'ise\u015Fi', u'iser\u0103', u'\xe2se\u015Fi', u'\xE2ser\u0103', u'use\u015Fi', u'user\u0103', u'ser\u0103m', u'sesem', u'indu', u'\xE2ndu', u'eaz\u0103', u'e\u015Fti', u'e\u015Fte', u'\u0103\u015Fti', u'\u0103\u015Fte', u'ea\u0163i', u'ia\u0163i', u'ar\u0103m', u'ur\u0103m', u'ir\u0103m', u'\xE2r\u0103m', u'asem', u'isem', u'\xE2sem', u'usem', u'se\u015Fi', u'ser\u0103', u'sese', u'are', u'ere', u'ire', u'\xE2re', u'ind', u'\xE2nd', u'eze', u'ezi', u'esc', u'\u0103sc', u'eam', u'eai', u'eau', u'iam', u'iai', u'iau', u'a\u015Fi', u'ar\u0103', u'u\u015Fi', u'ur\u0103', u'i\u015Fi', u'ir\u0103', u'\xE2\u015Fi', u'\xe2r\u0103', u'ase', u'ise', u'\xE2se', u'use', u'a\u0163i', u'e\u0163i', u'i\u0163i', u'\xe2\u0163i', u'sei', u'ez', u'am', u'ai', u'au', u'ea', u'ia', u'ui', u'\xE2i', u'\u0103m', u'em', u'im', u'\xE2m', u'se') def stem(self, word): u""" Stem a Romanian word and return the stemmed form. @param word: The word that is stemmed. @type word: C{str, unicode} @return: The stemmed form. @rtype: C{unicode} """ step1_success = False step2_success = False word = word.lower() for i in xrange(1, len(word)-1): if word[i-1] in self.__vowels and word[i+1] in self.__vowels: if word[i] == u"u": word = u"".join((word[:i], u"U", word[i+1:])) elif word[i] == u"i": word = u"".join((word[:i], u"I", word[i+1:])) r1, r2 = self._r1r2_standard(word, self.__vowels) rv = self._rv_standard(word, self.__vowels) # STEP 0: Removal of plurals and other simplifications for suffix in self.__step0_suffixes: if word.endswith(suffix): if suffix in r1: if suffix in (u"ul", u"ului"): word = word[:-len(suffix)] if suffix in rv: rv = rv[:-len(suffix)] else: rv = u"" elif (suffix == u"aua" or suffix == u"atei" or (suffix == u"ile" and word[-5:-3] != u"ab")): word = word[:-2] elif suffix in (u"ea", u"ele", u"elor"): word = u"".join((word[:-len(suffix)], u"e")) if suffix in rv: rv = u"".join((rv[:-len(suffix)], u"e")) else: rv = u"" elif suffix in (u"ii", u"iua", u"iei", u"iile", u"iilor", u"ilor"): word = u"".join((word[:-len(suffix)], u"i")) if suffix in rv: rv = u"".join((rv[:-len(suffix)], u"i")) else: rv = u"" elif suffix in (u"a\u0163ie", u"a\u0163ia"): word = word[:-1] break # STEP 1: Reduction of combining suffixes while True: replacement_done = False for suffix in self.__step1_suffixes: if word.endswith(suffix): if suffix in r1: step1_success = True replacement_done = True if suffix in (u"abilitate", u"abilitati", u"abilit\u0103i", u"abilit\u0103\u0163i"): word = u"".join((word[:-len(suffix)], u"abil")) elif suffix == u"ibilitate": word = word[:-5] elif suffix in (u"ivitate", u"ivitati", u"ivit\u0103i", u"ivit\u0103\u0163i"): word = u"".join((word[:-len(suffix)], u"iv")) elif suffix in (u"icitate", u"icitati", u"icit\u0103i", u"icit\u0103\u0163i", u"icator", u"icatori", u"iciv", u"iciva", u"icive", u"icivi", u"iciv\u0103", u"ical", u"icala", u"icale", u"icali", u"ical\u0103"): word = u"".join((word[:-len(suffix)], u"ic")) elif suffix in (u"ativ", u"ativa", u"ative", u"ativi", u"ativ\u0103", u"a\u0163iune", u"atoare", u"ator", u"atori", u"\u0103toare", u"\u0103tor", u"\u0103tori"): word = u"".join((word[:-len(suffix)], u"at")) if suffix in r2: r2 = u"".join((r2[:-len(suffix)], u"at")) elif suffix in (u"itiv", u"itiva", u"itive", u"itivi", u"itiv\u0103", u"i\u0163iune", u"itoare", u"itor", u"itori"): word = u"".join((word[:-len(suffix)], u"it")) if suffix in r2: r2 = u"".join((r2[:-len(suffix)], u"it")) else: step1_success = False break if not replacement_done: break # STEP 2: Removal of standard suffixes for suffix in self.__step2_suffixes: if word.endswith(suffix): if suffix in r2: step2_success = True if suffix in (u"iune", u"iuni"): if word[-5] == u"\u0163": word = u"".join((word[:-5], u"t")) elif suffix in (u"ism", u"isme", u"ist", u"ista", u"iste", u"isti", u"ist\u0103", u"i\u015Fti"): word = u"".join((word[:-len(suffix)], u"ist")) else: word = word[:-len(suffix)] break # STEP 3: Removal of verb suffixes if not step1_success and not step2_success: for suffix in self.__step3_suffixes: if word.endswith(suffix): if suffix in rv: if suffix in (u'seser\u0103\u0163i', u'seser\u0103m', u'ser\u0103\u0163i', u'sese\u015Fi', u'seser\u0103', u'ser\u0103m', u'sesem', u'se\u015Fi', u'ser\u0103', u'sese', u'a\u0163i', u'e\u0163i', u'i\u0163i', u'\xE2\u0163i', u'sei', u'\u0103m', u'em', u'im', u'\xE2m', u'se'): word = word[:-len(suffix)] rv = rv[:-len(suffix)] else: if (not rv.startswith(suffix) and rv[rv.index(suffix)-1] not in u"aeio\u0103\xE2\xEE"): word = word[:-len(suffix)] break # STEP 4: Removal of final vowel for suffix in (u"ie", u"a", u"e", u"i", u"\u0103"): if word.endswith(suffix): if suffix in rv: word = word[:-len(suffix)] break word = word.replace(u"I", u"i").replace(u"U", u"u") return word class RussianStemmer(SnowballStemmer): u""" The Russian Snowball stemmer. @cvar __perfective_gerund_suffixes: Suffixes to be deleted. @type __perfective_gerund_suffixes: C{tuple} @cvar __adjectival_suffixes: Suffixes to be deleted. @type __adjectival_suffixes: C{tuple} @cvar __reflexive_suffixes: Suffixes to be deleted. @type __reflexive_suffixes: C{tuple} @cvar __verb_suffixes: Suffixes to be deleted. @type __verb_suffixes: C{tuple} @cvar __noun_suffixes: Suffixes to be deleted. @type __noun_suffixes: C{tuple} @cvar __superlative_suffixes: Suffixes to be deleted. @type __superlative_suffixes: C{tuple} @cvar __derivational_suffixes: Suffixes to be deleted. @type __derivational_suffixes: C{tuple} @note: A detailed description of the Russian stemming algorithm can be found under U{http://snowball.tartarus.org/algorithms /russian/stemmer.html}. """ __perfective_gerund_suffixes = (u"ivshis'", u"yvshis'", u"vshis'", u"ivshi", u"yvshi", u"vshi", u"iv", u"yv", u"v") __adjectival_suffixes = (u'ui^ushchi^ui^u', u'ui^ushchi^ai^a', u'ui^ushchimi', u'ui^ushchymi', u'ui^ushchego', u'ui^ushchogo', u'ui^ushchemu', u'ui^ushchomu', u'ui^ushchikh', u'ui^ushchykh', u'ui^ushchui^u', u'ui^ushchaia', u'ui^ushchoi^u', u'ui^ushchei^u', u'i^ushchi^ui^u', u'i^ushchi^ai^a', u'ui^ushchee', u'ui^ushchie', u'ui^ushchye', u'ui^ushchoe', u'ui^ushchei`', u'ui^ushchii`', u'ui^ushchyi`', u'ui^ushchoi`', u'ui^ushchem', u'ui^ushchim', u'ui^ushchym', u'ui^ushchom', u'i^ushchimi', u'i^ushchymi', u'i^ushchego', u'i^ushchogo', u'i^ushchemu', u'i^ushchomu', u'i^ushchikh', u'i^ushchykh', u'i^ushchui^u', u'i^ushchai^a', u'i^ushchoi^u', u'i^ushchei^u', u'i^ushchee', u'i^ushchie', u'i^ushchye', u'i^ushchoe', u'i^ushchei`', u'i^ushchii`', u'i^ushchyi`', u'i^ushchoi`', u'i^ushchem', u'i^ushchim', u'i^ushchym', u'i^ushchom', u'shchi^ui^u', u'shchi^ai^a', u'ivshi^ui^u', u'ivshi^ai^a', u'yvshi^ui^u', u'yvshi^ai^a', u'shchimi', u'shchymi', u'shchego', u'shchogo', u'shchemu', u'shchomu', u'shchikh', u'shchykh', u'shchui^u', u'shchai^a', u'shchoi^u', u'shchei^u', u'ivshimi', u'ivshymi', u'ivshego', u'ivshogo', u'ivshemu', u'ivshomu', u'ivshikh', u'ivshykh', u'ivshui^u', u'ivshai^a', u'ivshoi^u', u'ivshei^u', u'yvshimi', u'yvshymi', u'yvshego', u'yvshogo', u'yvshemu', u'yvshomu', u'yvshikh', u'yvshykh', u'yvshui^u', u'yvshai^a', u'yvshoi^u', u'yvshei^u', u'vshi^ui^u', u'vshi^ai^a', u'shchee', u'shchie', u'shchye', u'shchoe', u'shchei`', u'shchii`', u'shchyi`', u'shchoi`', u'shchem', u'shchim', u'shchym', u'shchom', u'ivshee', u'ivshie', u'ivshye', u'ivshoe', u'ivshei`', u'ivshii`', u'ivshyi`', u'ivshoi`', u'ivshem', u'ivshim', u'ivshym', u'ivshom', u'yvshee', u'yvshie', u'yvshye', u'yvshoe', u'yvshei`', u'yvshii`', u'yvshyi`', u'yvshoi`', u'yvshem', u'yvshim', u'yvshym', u'yvshom', u'vshimi', u'vshymi', u'vshego', u'vshogo', u'vshemu', u'vshomu', u'vshikh', u'vshykh', u'vshui^u', u'vshai^a', u'vshoi^u', u'vshei^u', u'emi^ui^u', u'emi^ai^a', u'nni^ui^u', u'nni^ai^a', u'vshee', u'vshie', u'vshye', u'vshoe', u'vshei`', u'vshii`', u'vshyi`', u'vshoi`', u'vshem', u'vshim', u'vshym', u'vshom', u'emimi', u'emymi', u'emego', u'emogo', u'ememu', u'emomu', u'emikh', u'emykh', u'emui^u', u'emai^a', u'emoi^u', u'emei^u', u'nnimi', u'nnymi', u'nnego', u'nnogo', u'nnemu', u'nnomu', u'nnikh', u'nnykh', u'nnui^u', u'nnai^a', u'nnoi^u', u'nnei^u', u'emee', u'emie', u'emye', u'emoe', u'emei`', u'emii`', u'emyi`', u'emoi`', u'emem', u'emim', u'emym', u'emom', u'nnee', u'nnie', u'nnye', u'nnoe', u'nnei`', u'nnii`', u'nnyi`', u'nnoi`', u'nnem', u'nnim', u'nnym', u'nnom', u'i^ui^u', u'i^ai^a', u'imi', u'ymi', u'ego', u'ogo', u'emu', u'omu', u'ikh', u'ykh', u'ui^u', u'ai^a', u'oi^u', u'ei^u', u'ee', u'ie', u'ye', u'oe', u'ei`', u'ii`', u'yi`', u'oi`', u'em', u'im', u'ym', u'om') __reflexive_suffixes = (u"si^a", u"s'") __verb_suffixes = (u"esh'", u'ei`te', u'ui`te', u'ui^ut', u"ish'", u'ete', u'i`te', u'i^ut', u'nno', u'ila', u'yla', u'ena', u'ite', u'ili', u'yli', u'ilo', u'ylo', u'eno', u'i^at', u'uet', u'eny', u"it'", u"yt'", u'ui^u', u'la', u'na', u'li', u'em', u'lo', u'no', u'et', u'ny', u"t'", u'ei`', u'ui`', u'il', u'yl', u'im', u'ym', u'en', u'it', u'yt', u'i^u', u'i`', u'l', u'n') __noun_suffixes = (u'ii^ami', u'ii^akh', u'i^ami', u'ii^am', u'i^akh', u'ami', u'iei`', u'i^am', u'iem', u'akh', u'ii^u', u"'i^u", u'ii^a', u"'i^a", u'ev', u'ov', u'ie', u"'e", u'ei', u'ii', u'ei`', u'oi`', u'ii`', u'em', u'am', u'om', u'i^u', u'i^a', u'a', u'e', u'i', u'i`', u'o', u'u', u'y', u"'") __superlative_suffixes = (u"ei`she", u"ei`sh") __derivational_suffixes = (u"ost'", u"ost") def stem(self, word): u""" Stem a Russian word and return the stemmed form. @param word: The word that is stemmed. @type word: C{str, unicode} @return: The stemmed form. @rtype: C{unicode} """ if word in self.stopwords: return word chr_exceeded = False for i in xrange(len(word)): if ord(word[i]) not in xrange(256): chr_exceeded = True break if chr_exceeded: word = self.__cyrillic_to_roman(word) step1_success = False adjectival_removed = False verb_removed = False undouble_success = False superlative_removed = False rv, r2 = self.__regions_russian(word) # Step 1 for suffix in self.__perfective_gerund_suffixes: if rv.endswith(suffix): if suffix in (u"v", u"vshi", u"vshis'"): if (rv[-len(suffix)-3:-len(suffix)] == "i^a" or rv[-len(suffix)-1:-len(suffix)] == "a"): word = word[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] step1_success = True break else: word = word[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] step1_success = True break if not step1_success: for suffix in self.__reflexive_suffixes: if rv.endswith(suffix): word = word[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] break for suffix in self.__adjectival_suffixes: if rv.endswith(suffix): if suffix in (u'i^ushchi^ui^u', u'i^ushchi^ai^a', u'i^ushchui^u', u'i^ushchai^a', u'i^ushchoi^u', u'i^ushchei^u', u'i^ushchimi', u'i^ushchymi', u'i^ushchego', u'i^ushchogo', u'i^ushchemu', u'i^ushchomu', u'i^ushchikh', u'i^ushchykh', u'shchi^ui^u', u'shchi^ai^a', u'i^ushchee', u'i^ushchie', u'i^ushchye', u'i^ushchoe', u'i^ushchei`', u'i^ushchii`', u'i^ushchyi`', u'i^ushchoi`', u'i^ushchem', u'i^ushchim', u'i^ushchym', u'i^ushchom', u'vshi^ui^u', u'vshi^ai^a', u'shchui^u', u'shchai^a', u'shchoi^u', u'shchei^u', u'emi^ui^u', u'emi^ai^a', u'nni^ui^u', u'nni^ai^a', u'shchimi', u'shchymi', u'shchego', u'shchogo', u'shchemu', u'shchomu', u'shchikh', u'shchykh', u'vshui^u', u'vshai^a', u'vshoi^u', u'vshei^u', u'shchee', u'shchie', u'shchye', u'shchoe', u'shchei`', u'shchii`', u'shchyi`', u'shchoi`', u'shchem', u'shchim', u'shchym', u'shchom', u'vshimi', u'vshymi', u'vshego', u'vshogo', u'vshemu', u'vshomu', u'vshikh', u'vshykh', u'emui^u', u'emai^a', u'emoi^u', u'emei^u', u'nnui^u', u'nnai^a', u'nnoi^u', u'nnei^u', u'vshee', u'vshie', u'vshye', u'vshoe', u'vshei`', u'vshii`', u'vshyi`', u'vshoi`', u'vshem', u'vshim', u'vshym', u'vshom', u'emimi', u'emymi', u'emego', u'emogo', u'ememu', u'emomu', u'emikh', u'emykh', u'nnimi', u'nnymi', u'nnego', u'nnogo', u'nnemu', u'nnomu', u'nnikh', u'nnykh', u'emee', u'emie', u'emye', u'emoe', u'emei`', u'emii`', u'emyi`', u'emoi`', u'emem', u'emim', u'emym', u'emom', u'nnee', u'nnie', u'nnye', u'nnoe', u'nnei`', u'nnii`', u'nnyi`', u'nnoi`', u'nnem', u'nnim', u'nnym', u'nnom'): if (rv[-len(suffix)-3:-len(suffix)] == "i^a" or rv[-len(suffix)-1:-len(suffix)] == "a"): word = word[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] adjectival_removed = True break else: word = word[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] adjectival_removed = True break if not adjectival_removed: for suffix in self.__verb_suffixes: if rv.endswith(suffix): if suffix in (u"la", u"na", u"ete", u"i`te", u"li", u"i`", u"l", u"em", u"n", u"lo", u"no", u"et", u"i^ut", u"ny", u"t'", u"esh'", u"nno"): if (rv[-len(suffix)-3:-len(suffix)] == "i^a" or rv[-len(suffix)-1:-len(suffix)] == "a"): word = word[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] verb_removed = True break else: word = word[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] verb_removed = True break if not adjectival_removed and not verb_removed: for suffix in self.__noun_suffixes: if rv.endswith(suffix): word = word[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] break # Step 2 if rv.endswith("i"): word = word[:-1] r2 = r2[:-1] # Step 3 for suffix in self.__derivational_suffixes: if r2.endswith(suffix): word = word[:-len(suffix)] break # Step 4 if word.endswith("nn"): word = word[:-1] undouble_success = True if not undouble_success: for suffix in self.__superlative_suffixes: if word.endswith(suffix): word = word[:-len(suffix)] superlative_removed = True break if word.endswith("nn"): word = word[:-1] if not undouble_success and not superlative_removed: if word.endswith("'"): word = word[:-1] if chr_exceeded: word = self.__roman_to_cyrillic(word) return word def __regions_russian(self, word): u""" Return the regions RV and R2 which are used by the Russian stemmer. In any word, RV is the region after the first vowel, or the end of the word if it contains no vowel. R2 is the region after the first non-vowel following a vowel in R1, or the end of the word if there is no such non-vowel. R1 is the region after the first non-vowel following a vowel, or the end of the word if there is no such non-vowel. @param word: The Russian word whose regions RV and R2 are determined. @type word: C{str, unicode} @return: C{(rv, r2)}, the regions RV and R2 for the respective Russian word. @rtype: C{tuple} @note: This helper method is invoked by the stem method of the subclass L{RussianStemmer}. It is not to be invoked directly! """ r1 = u"" r2 = u"" rv = u"" vowels = (u"A", u"U", u"E", u"a", u"e", u"i", u"o", u"u", u"y") word = (word.replace(u"i^a", u"A") .replace(u"i^u", u"U") .replace(u"e`", u"E")) for i in xrange(1, len(word)): if word[i] not in vowels and word[i-1] in vowels: r1 = word[i+1:] break for i in xrange(1, len(r1)): if r1[i] not in vowels and r1[i-1] in vowels: r2 = r1[i+1:] break for i in xrange(len(word)): if word[i] in vowels: rv = word[i+1:] break r2 = (r2.replace(u"A", u"i^a") .replace(u"U", u"i^u") .replace(u"E", u"e`")) rv = (rv.replace(u"A", u"i^a") .replace(u"U", u"i^u") .replace(u"E", u"e`")) return (rv, r2) def __cyrillic_to_roman(self, word): u""" Transliterate a Russian word into the Roman alphabet. A Russian word whose letters consist of the Cyrillic alphabet are transliterated into the Roman alphabet in order to ease the forthcoming stemming process. @param word: The word that is transliterated. @type word: C{unicode} @return: C{word}, the transliterated word. @rtype: C{unicode} @note: This helper method is invoked by the stem method of the subclass L{RussianStemmer}. It is not to be invoked directly! """ word = (word.replace(u"\u0410", u"a").replace(u"\u0430", u"a") .replace(u"\u0411", u"b").replace(u"\u0431", u"b") .replace(u"\u0412", u"v").replace(u"\u0432", u"v") .replace(u"\u0413", u"g").replace(u"\u0433", u"g") .replace(u"\u0414", u"d").replace(u"\u0434", u"d") .replace(u"\u0415", u"e").replace(u"\u0435", u"e") .replace(u"\u0401", u"e").replace(u"\u0451", u"e") .replace(u"\u0416", u"zh").replace(u"\u0436", u"zh") .replace(u"\u0417", u"z").replace(u"\u0437", u"z") .replace(u"\u0418", u"i").replace(u"\u0438", u"i") .replace(u"\u0419", u"i`").replace(u"\u0439", u"i`") .replace(u"\u041A", u"k").replace(u"\u043A", u"k") .replace(u"\u041B", u"l").replace(u"\u043B", u"l") .replace(u"\u041C", u"m").replace(u"\u043C", u"m") .replace(u"\u041D", u"n").replace(u"\u043D", u"n") .replace(u"\u041E", u"o").replace(u"\u043E", u"o") .replace(u"\u041F", u"p").replace(u"\u043F", u"p") .replace(u"\u0420", u"r").replace(u"\u0440", u"r") .replace(u"\u0421", u"s").replace(u"\u0441", u"s") .replace(u"\u0422", u"t").replace(u"\u0442", u"t") .replace(u"\u0423", u"u").replace(u"\u0443", u"u") .replace(u"\u0424", u"f").replace(u"\u0444", u"f") .replace(u"\u0425", u"kh").replace(u"\u0445", u"kh") .replace(u"\u0426", u"t^s").replace(u"\u0446", u"t^s") .replace(u"\u0427", u"ch").replace(u"\u0447", u"ch") .replace(u"\u0428", u"sh").replace(u"\u0448", u"sh") .replace(u"\u0429", u"shch").replace(u"\u0449", u"shch") .replace(u"\u042A", u"''").replace(u"\u044A", u"''") .replace(u"\u042B", u"y").replace(u"\u044B", u"y") .replace(u"\u042C", u"'").replace(u"\u044C", u"'") .replace(u"\u042D", u"e`").replace(u"\u044D", u"e`") .replace(u"\u042E", u"i^u").replace(u"\u044E", u"i^u") .replace(u"\u042F", u"i^a").replace(u"\u044F", u"i^a")) return word def __roman_to_cyrillic(self, word): u""" Transliterate a Russian word back into the Cyrillic alphabet. A Russian word formerly transliterated into the Roman alphabet in order to ease the stemming process, is transliterated back into the Cyrillic alphabet, its original form. @param word: The word that is transliterated. @type word: C{str, unicode} @return: C{word}, the transliterated word. @rtype: C{unicode} @note: This helper method is invoked by the stem method of the subclass L{RussianStemmer}. It is not to be invoked directly! """ word = (word.replace(u"i^u", u"\u044E").replace(u"i^a", u"\u044F") .replace(u"shch", u"\u0449").replace(u"kh", u"\u0445") .replace(u"t^s", u"\u0446").replace(u"ch", u"\u0447") .replace(u"e`", u"\u044D").replace(u"i`", u"\u0439") .replace(u"sh", u"\u0448").replace(u"k", u"\u043A") .replace(u"e", u"\u0435").replace(u"zh", u"\u0436") .replace(u"a", u"\u0430").replace(u"b", u"\u0431") .replace(u"v", u"\u0432").replace(u"g", u"\u0433") .replace(u"d", u"\u0434").replace(u"e", u"\u0435") .replace(u"z", u"\u0437").replace(u"i", u"\u0438") .replace(u"l", u"\u043B").replace(u"m", u"\u043C") .replace(u"n", u"\u043D").replace(u"o", u"\u043E") .replace(u"p", u"\u043F").replace(u"r", u"\u0440") .replace(u"s", u"\u0441").replace(u"t", u"\u0442") .replace(u"u", u"\u0443").replace(u"f", u"\u0444") .replace(u"''", u"\u044A").replace(u"y", u"\u044B") .replace(u"'", u"\u044C")) return word class SpanishStemmer(_StandardStemmer): u""" The Spanish Snowball stemmer. @cvar __vowels: The Spanish vowels. @type __vowels: C{unicode} @cvar __step0_suffixes: Suffixes to be deleted in step 0 of the algorithm. @type __step0_suffixes: C{tuple} @cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. @type __step1_suffixes: C{tuple} @cvar __step2a_suffixes: Suffixes to be deleted in step 2a of the algorithm. @type __step2a_suffixes: C{tuple} @cvar __step2b_suffixes: Suffixes to be deleted in step 2b of the algorithm. @type __step2b_suffixes: C{tuple} @cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. @type __step3_suffixes: C{tuple} @note: A detailed description of the Spanish stemming algorithm can be found under U{http://snowball.tartarus.org/algorithms /spanish/stemmer.html}. """ __vowels = u"aeiou\xE1\xE9\xED\xF3\xFA\xFC" __step0_suffixes = (u"selas", u"selos", u"sela", u"selo", u"las", u"les", u"los", u"nos", u"me", u"se", u"la", u"le", u"lo") __step1_suffixes = (u'amientos', u'imientos', u'amiento', u'imiento', u'aciones', u'uciones', u'adoras', u'adores', u'ancias', u'log\xEDas', u'encias', u'amente', u'idades', u'anzas', u'ismos', u'ables', u'ibles', u'istas', u'adora', u'aci\xF3n', u'antes', u'ancia', u'log\xEDa', u'uci\xf3n', u'encia', u'mente', u'anza', u'icos', u'icas', u'ismo', u'able', u'ible', u'ista', u'osos', u'osas', u'ador', u'ante', u'idad', u'ivas', u'ivos', u'ico', u'ica', u'oso', u'osa', u'iva', u'ivo') __step2a_suffixes = (u'yeron', u'yendo', u'yamos', u'yais', u'yan', u'yen', u'yas', u'yes', u'ya', u'ye', u'yo', u'y\xF3') __step2b_suffixes = (u'ar\xEDamos', u'er\xEDamos', u'ir\xEDamos', u'i\xE9ramos', u'i\xE9semos', u'ar\xEDais', u'aremos', u'er\xEDais', u'eremos', u'ir\xEDais', u'iremos', u'ierais', u'ieseis', u'asteis', u'isteis', u'\xE1bamos', u'\xE1ramos', u'\xE1semos', u'ar\xEDan', u'ar\xEDas', u'ar\xE9is', u'er\xEDan', u'er\xEDas', u'er\xE9is', u'ir\xEDan', u'ir\xEDas', u'ir\xE9is', u'ieran', u'iesen', u'ieron', u'iendo', u'ieras', u'ieses', u'abais', u'arais', u'aseis', u'\xE9amos', u'ar\xE1n', u'ar\xE1s', u'ar\xEDa', u'er\xE1n', u'er\xE1s', u'er\xEDa', u'ir\xE1n', u'ir\xE1s', u'ir\xEDa', u'iera', u'iese', u'aste', u'iste', u'aban', u'aran', u'asen', u'aron', u'ando', u'abas', u'adas', u'idas', u'aras', u'ases', u'\xEDais', u'ados', u'idos', u'amos', u'imos', u'emos', u'ar\xE1', u'ar\xE9', u'er\xE1', u'er\xE9', u'ir\xE1', u'ir\xE9', u'aba', u'ada', u'ida', u'ara', u'ase', u'\xEDan', u'ado', u'ido', u'\xEDas', u'\xE1is', u'\xE9is', u'\xEDa', u'ad', u'ed', u'id', u'an', u'i\xF3', u'ar', u'er', u'ir', u'as', u'\xEDs', u'en', u'es') __step3_suffixes = (u"os", u"a", u"e", u"o", u"\xE1", u"\xE9", u"\xED", u"\xF3") def stem(self, word): u""" Stem a Spanish word and return the stemmed form. @param word: The word that is stemmed. @type word: C{str, unicode} @return: The stemmed form. @rtype: C{unicode} """ if word in self.stopwords: return word step1_success = False word = word.lower() r1, r2 = self._r1r2_standard(word, self.__vowels) rv = self._rv_standard(word, self.__vowels) # STEP 0: Attached pronoun for suffix in self.__step0_suffixes: if word.endswith(suffix): if rv.endswith(suffix): if rv[:-len(suffix)].endswith((u"i\xE9ndo", u"\xE1ndo", u"\xE1r", u"\xE9r", u"\xEDr")): word = (word[:-len(suffix)].replace(u"\xE1", u"a") .replace(u"\xE9", u"e") .replace(u"\xED", u"i")) r1 = (r1[:-len(suffix)].replace(u"\xE1", u"a") .replace(u"\xE9", u"e") .replace(u"\xED", u"i")) r2 = (r2[:-len(suffix)].replace(u"\xE1", u"a") .replace(u"\xE9", u"e") .replace(u"\xED", u"i")) rv = (rv[:-len(suffix)].replace(u"\xE1", u"a") .replace(u"\xE9", u"e") .replace(u"\xED", u"i")) elif rv[:-len(suffix)].endswith((u"ando", u"iendo", u"ar", u"er", u"ir")): word = word[:-len(suffix)] r1 = r1[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] elif (rv[:-len(suffix)].endswith(u"yendo") and word[:-len(suffix)].endswith(u"uyendo")): word = word[:-len(suffix)] r1 = r1[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] break # STEP 1: Standard suffix removal for suffix in self.__step1_suffixes: if word.endswith(suffix): if suffix == u"amente" and r1.endswith(suffix): step1_success = True word = word[:-6] r2 = r2[:-6] rv = rv[:-6] if r2.endswith(u"iv"): word = word[:-2] r2 = r2[:-2] rv = rv[:-2] if r2.endswith(u"at"): word = word[:-2] rv = rv[:-2] elif r2.endswith((u"os", u"ic", u"ad")): word = word[:-2] rv = rv[:-2] elif r2.endswith(suffix): step1_success = True if suffix in (u"adora", u"ador", u"aci\xF3n", u"adoras", u"adores", u"aciones", u"ante", u"antes", u"ancia", u"ancias"): word = word[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] if r2.endswith(u"ic"): word = word[:-2] rv = rv[:-2] elif suffix in (u"log\xEDa", u"log\xEDas"): word = word.replace(suffix, u"log") rv = rv.replace(suffix, u"log") elif suffix in (u"uci\xF3n", u"uciones"): word = word.replace(suffix, u"u") rv = rv.replace(suffix, u"u") elif suffix in (u"encia", u"encias"): word = word.replace(suffix, u"ente") rv = rv.replace(suffix, u"ente") elif suffix == u"mente": word = word[:-5] r2 = r2[:-5] rv = rv[:-5] if r2.endswith((u"ante", u"able", u"ible")): word = word[:-4] rv = rv[:-4] elif suffix in (u"idad", u"idades"): word = word[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] for pre_suff in (u"abil", u"ic", u"iv"): if r2.endswith(pre_suff): word = word[:-len(pre_suff)] rv = rv[:-len(pre_suff)] elif suffix in (u"ivo", u"iva", u"ivos", u"ivas"): word = word[:-len(suffix)] r2 = r2[:-len(suffix)] rv = rv[:-len(suffix)] if r2.endswith(u"at"): word = word[:-2] rv = rv[:-2] else: word = word[:-len(suffix)] rv = rv[:-len(suffix)] break # STEP 2a: Verb suffixes beginning 'y' if not step1_success: for suffix in self.__step2a_suffixes: if (rv.endswith(suffix) and word[-len(suffix)-1:-len(suffix)] == u"u"): word = word[:-len(suffix)] rv = rv[:-len(suffix)] break # STEP 2b: Other verb suffixes for suffix in self.__step2b_suffixes: if rv.endswith(suffix): if suffix in (u"en", u"es", u"\xE9is", u"emos"): word = word[:-len(suffix)] rv = rv[:-len(suffix)] if word.endswith(u"gu"): word = word[:-1] if rv.endswith(u"gu"): rv = rv[:-1] else: word = word[:-len(suffix)] rv = rv[:-len(suffix)] break # STEP 3: Residual suffix for suffix in self.__step3_suffixes: if rv.endswith(suffix): if suffix in (u"e", u"\xE9"): word = word[:-len(suffix)] rv = rv[:-len(suffix)] if word[-2:] == u"gu" and rv[-1] == u"u": word = word[:-1] else: word = word[:-len(suffix)] break word = (word.replace(u"\xE1", u"a").replace(u"\xE9", u"e") .replace(u"\xED", u"i").replace(u"\xF3", u"o") .replace(u"\xFA", u"u")) return word class SwedishStemmer(_ScandinavianStemmer): u""" The Swedish Snowball stemmer. @cvar __vowels: The Swedish vowels. @type __vowels: C{unicode} @cvar __s_ending: Letters that may directly appear before a word final 's'. @type __s_ending: C{unicode} @cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. @type __step1_suffixes: C{tuple} @cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. @type __step2_suffixes: C{tuple} @cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. @type __step3_suffixes: C{tuple} @note: A detailed description of the Swedish stemming algorithm can be found under U{http://snowball.tartarus.org/algorithms /swedish/stemmer.html}. """ __vowels = u"aeiouy\xE4\xE5\xF6" __s_ending = u"bcdfghjklmnoprtvy" __step1_suffixes = (u"heterna", u"hetens", u"heter", u"heten", u"anden", u"arnas", u"ernas", u"ornas", u"andes", u"andet", u"arens", u"arna", u"erna", u"orna", u"ande", u"arne", u"aste", u"aren", u"ades", u"erns", u"ade", u"are", u"ern", u"ens", u"het", u"ast", u"ad", u"en", u"ar", u"er", u"or", u"as", u"es", u"at", u"a", u"e", u"s") __step2_suffixes = (u"dd", u"gd", u"nn", u"dt", u"gt", u"kt", u"tt") __step3_suffixes = (u"fullt", u"l\xF6st", u"els", u"lig", u"ig") def stem(self, word): u""" Stem a Swedish word and return the stemmed form. @param word: The word that is stemmed. @type word: C{str, unicode} @return: The stemmed form. @rtype: C{unicode} """ if word in self.stopwords: return word word = word.lower() r1 = self._r1_scandinavian(word, self.__vowels) # STEP 1 for suffix in self.__step1_suffixes: if r1.endswith(suffix): if suffix == u"s": if word[-2] in self.__s_ending: word = word[:-1] r1 = r1[:-1] else: word = word[:-len(suffix)] r1 = r1[:-len(suffix)] break # STEP 2 for suffix in self.__step2_suffixes: if r1.endswith(suffix): word = word[:-1] r1 = r1[:-1] break # STEP 3 for suffix in self.__step3_suffixes: if r1.endswith(suffix): if suffix in (u"els", u"lig", u"ig"): word = word[:-len(suffix)] elif suffix in (u"fullt", u"l\xF6st"): word = word[:-1] break return word def demo(): u""" This function provides a demonstration of the Snowball stemmers. After invoking this function and specifying a language, it stems an excerpt of the Universal Declaration of Human Rights (which is a part of the NLTK corpus collection) and then prints out the original and the stemmed text. """ import re from nltk.corpus import udhr print u"\n" print u"******************************" print u"Demo for the Snowball stemmers" print u"******************************" while True: language = raw_input(u"Please enter the name of the language " + u"to be demonstrated\n" + u"/".join(SnowballStemmer.languages) + u"\n" + u"(enter 'exit' in order to leave): ") if language == u"exit": break elif language == u"danish": stemmer = SnowballStemmer(u"danish") excerpt = udhr.words(u"Danish_Dansk-Latin1")[:300] elif language == u"dutch": stemmer = SnowballStemmer(u"dutch") excerpt = udhr.words(u"Dutch_Nederlands-Latin1")[:300] elif language == u"finnish": stemmer = SnowballStemmer(u"finnish") excerpt = udhr.words(u"Finnish_Suomi-Latin1")[:300] elif language == u"french": stemmer = SnowballStemmer(u"french") excerpt = udhr.words(u"French_Francais-Latin1")[:300] elif language == u"german": stemmer = SnowballStemmer(u"german") excerpt = udhr.words(u"German_Deutsch-Latin1")[:300] elif language == u"hungarian": stemmer = SnowballStemmer(u"hungarian") excerpt = udhr.words(u"Hungarian_Magyar-UTF8")[:300] elif language == u"italian": stemmer = SnowballStemmer(u"italian") excerpt = udhr.words(u"Italian_Italiano-Latin1")[:300] elif language == u"norwegian": stemmer = SnowballStemmer(u"norwegian") excerpt = udhr.words(u"Norwegian-Latin1")[:300] elif language == u"portuguese": stemmer = SnowballStemmer(u"portuguese") excerpt = udhr.words(u"Portuguese_Portugues-Latin1")[:300] elif language == u"romanian": stemmer = SnowballStemmer(u"romanian") excerpt = udhr.words(u"Romanian_Romana-Latin2")[:300] elif language == u"russian": stemmer = SnowballStemmer(u"russian") excerpt = udhr.words(u"Russian-UTF8")[:300] elif language == u"spanish": stemmer = SnowballStemmer(u"spanish") excerpt = udhr.words(u"Spanish-Latin1")[:300] elif language == u"swedish": stemmer = SnowballStemmer(u"swedish") excerpt = udhr.words(u"Swedish_Svenska-Latin1")[:300] else: print (u"\nOops, there is no stemmer for this language. " + u"Please try again.\n") continue stemmed = u" ".join([stemmer.stem(word) for word in excerpt]) stemmed = re.sub(r"(.{,70})\s", r'\1\n', stemmed+u' ').rstrip() excerpt = u" ".join(excerpt) excerpt = re.sub(r"(.{,70})\s", r'\1\n', excerpt+u' ').rstrip() print u"\n" print u'-' * 70 print u'ORIGINAL'.center(70) print excerpt print u"\n\n" print u'STEMMED RESULTS'.center(70) print stemmed print u'-' * 70 print u"\n" if __name__ == u"__main__": demo() nltk-2.0~b9/nltk/stem/rslp.py0000644000175000017500000001324011327451602016031 0ustar bhavanibhavani# -*- coding: utf-8 -*- # Natural Language Toolkit: RSLP Stemmer # # Copyright (C) 2001-2010 NLTK Project # Author: Tiago Tresoldi # URL: # For license information, see LICENSE.TXT # This code is based on the algorithm presented in the paper "A Stemming # Algorithm for the Portuguese Language" by Viviane Moreira Orengo and # Christian Huyck, which unfortunately I had no access to. The code is a # Python version, with some minor modifications of mine, to the description # presented at http://www.webcitation.org/5NnvdIzOb and to the C source code # available at http://www.inf.ufrgs.br/~arcoelho/rslp/integrando_rslp.html. # Please note that this stemmer is intended for demostration and educational # purposes only. Feel free to write me for any comments, including the # development of a different and/or better stemmer for Portuguese. I also # suggest using NLTK's mailing list for Portuguese for any discussion. # Este código é baseado no algoritmo apresentado no artigo "A Stemming # Algorithm for the Portuguese Language" de Viviane Moreira Orengo e # Christian Huyck, o qual infelizmente não tive a oportunidade de ler. O # código é uma conversão para Python, com algumas pequenas modificações # minhas, daquele apresentado em http://www.webcitation.org/5NnvdIzOb e do # código para linguagem C disponível em # http://www.inf.ufrgs.br/~arcoelho/rslp/integrando_rslp.html. Por favor, # lembre-se de que este stemmer foi desenvolvido com finalidades unicamente # de demonstração e didáticas. Sinta-se livre para me escrever para qualquer # comentário, inclusive sobre o desenvolvimento de um stemmer diferente # e/ou melhor para o português. Também sugiro utilizar-se a lista de discussão # do NLTK para o português para qualquer debate. import nltk.data from api import * class RSLPStemmer(StemmerI): """ A stemmer for Portuguese. """ def __init__ (self): self._model = [] self._model.append( self.read_rule("step0.pt") ) self._model.append( self.read_rule("step1.pt") ) self._model.append( self.read_rule("step2.pt") ) self._model.append( self.read_rule("step3.pt") ) self._model.append( self.read_rule("step4.pt") ) self._model.append( self.read_rule("step5.pt") ) self._model.append( self.read_rule("step6.pt") ) def read_rule (self, filename): rules = nltk.data.load('nltk:stemmers/rslp/' + filename, format='raw').decode("utf8") lines = rules.split("\n") lines = [line for line in lines if line != u""] # remove blank lines lines = [line for line in lines if line[0] != "#"] # remove comments # NOTE: a simple but ugly hack to make this parser happy with double '\t's lines = [line.replace("\t\t", "\t") for line in lines] # parse rules rules = [] for line in lines: rule = [] tokens = line.split("\t") # text to be searched for at the end of the string rule.append( tokens[0][1:-1] ) # remove quotes # minimum stem size to perform the replacement rule.append( int(tokens[1]) ) # text to be replaced into rule.append( tokens[2][1:-1] ) # remove quotes # exceptions to this rule rule.append( [token[1:-1] for token in tokens[3].split(",")] ) # append to the results rules.append(rule) return rules def stem(self, word): word = word.lower() # the word ends in 's'? apply rule for plural reduction if word[-1] == "s": word = self.apply_rule(word, 0) # the word ends in 'a'? apply rule for feminine reduction if word[-1] == "a": word = self.apply_rule(word, 1) # augmentative reduction word = self.apply_rule(word, 3) # adverb reduction word = self.apply_rule(word, 2) # noun reduction prev_word = word word = self.apply_rule(word, 4) if word == prev_word: # verb reduction prev_word = word word = self.apply_rule(word, 5) if word == prev_word: # vowel removal word = self.apply_rule(word, 6) return word def apply_rule(self, word, rule_index): rules = self._model[rule_index] for rule in rules: suffix_length = len(rule[0]) if word[-suffix_length:] == rule[0]: # if suffix matches if len(word) >= suffix_length + rule[1]: # if we have minimum size if word not in rule[3]: # if not an exception word = word[:-suffix_length] + rule[2] break return word def demo(): from nltk import stem stemmer = stem.RSLPStemmer() # white-space tokenizer friendly text; text taken from the first paragraph # of Erico Verissimo's "Música ao Longe" text = u""" Clarissa risca com giz no quadro-negro a paisagem que os alunos devem copiar . Uma casinha de porta e janela , em cima duma coxilha . Um coqueiro do lado ( onde o nosso amor nasceu - pensa ela no momento mesmo em que risca o troco longo e fino ) . Depois , uma estradinha que corre , ondulando como uma cobra , e se perde longe no horizonte . Nuvens de fiz do céu preto , um sol redondo e gordo , chispando raios , árvores , uma lagoa com marrecos nadando ... """ tokens = text.split() for token in tokens: word = token stem = stemmer.stem(token) print "%16s - %16s" % (word, stem) if __name__ == "__main__": demo() nltk-2.0~b9/nltk/stem/regexp.py0000644000175000017500000000315411327451602016346 0ustar bhavanibhavani# Natural Language Toolkit: Stemmers # # Copyright (C) 2001-2010 NLTK Project # Author: Trevor Cohn # Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT import re from api import * class RegexpStemmer(StemmerI): """ A stemmer that uses regular expressions to identify morphological affixes. Any substrings that match the regular expressions will be removed. """ def __init__(self, regexp, min=0): """ Create a new regexp stemmer. @type regexp: C{string} or C{regexp} @param regexp: The regular expression that should be used to identify morphological affixes. @type min: int @param min: The minimum length of string to stem """ if not hasattr(regexp, 'pattern'): regexp = re.compile(regexp) self._regexp = regexp self._min = min def stem(self, word): if len(word) < self._min: return word else: return self._regexp.sub('', word) def __repr__(self): return '' % self._regexp.pattern def demo(): from nltk import tokenize, stem # Create a simple regular expression based stemmer stemmer = stem.RegexpStemmer('ing$|s$|e$', min=4) text = "John was eating icecream" tokens = text.split() # Print the results. print stemmer for word in tokens: print '%20s => %s' % (word, stemmer.stem(word)) print if __name__ == '__main__': demo() nltk-2.0~b9/nltk/stem/porter.py0000644000175000017500000005216411303614106016366 0ustar bhavanibhavani# Copyright (c) 2002 Vivake Gupta (vivakeATomniscia.org). All rights reserved. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 # USA # # This software is maintained by Vivake (vivakeATomniscia.org) and is available at: # http://www.omniscia.org/~vivake/python/PorterStemmer.py # # Additional modifications were made to incorporate this module into # NLTK. All such modifications are marked with "--NLTK--". The NLTK # version of this module is maintained by NLTK developers, # and is available via http://www.nltk.org/ # # GNU Linking Exception: # Using this module statically or dynamically with other modules is # making a combined work based on this module. Thus, the terms and # conditions of the GNU General Public License cover the whole combination. # As a special exception, the copyright holders of this module give # you permission to combine this module with independent modules to # produce an executable program, regardless of the license terms of these # independent modules, and to copy and distribute the resulting # program under terms of your choice, provided that you also meet, # for each linked independent module, the terms and conditions of # the license of that module. An independent module is a module which # is not derived from or based on this module. If you modify this module, # you may extend this exception to your version of the module, but you # are not obliged to do so. If you do not wish to do so, delete this # exception statement from your version. """Porter Stemming Algorithm This is the Porter stemming algorithm, ported to Python from the version coded up in ANSI C by the author. It follows the algorithm presented in Porter, M. "An algorithm for suffix stripping." Program 14.3 (1980): 130-137. only differing from it at the points maked --DEPARTURE-- and --NEW-- below. For a more faithful version of the Porter algorithm, see http://www.tartarus.org/~martin/PorterStemmer/ Later additions: June 2000 The 'l' of the 'logi' -> 'log' rule is put with the stem, so that short stems like 'geo' 'theo' etc work like 'archaeo' 'philo' etc. This follows a suggestion of Barry Wilkins, reasearch student at Birmingham. February 2000 the cvc test for not dropping final -e now looks after vc at the beginning of a word, so are, eve, ice, ore, use keep final -e. In this test c is any consonant, including w, x and y. This extension was suggested by Chris Emerson. -fully -> -ful treated like -fulness -> -ful, and -tionally -> -tion treated like -tional -> -tion both in Step 2. These were suggested by Hiranmay Ghosh, of New Delhi. Invariants proceed, succeed, exceed. Also suggested by Hiranmay Ghosh. Additional modifications were made to incorperate this module into nltk. All such modifications are marked with \"--NLTK--\". The nltk version of this module is maintained by the NLTK developers, and is available from """ ## --NLTK-- ## Declare this module's documentation format. __docformat__ = 'plaintext' import sys import re ## --NLTK-- ## Import the nltk.stemmer module, which defines the stemmer interface from api import * class PorterStemmer(StemmerI): ## --NLTK-- ## Add a module docstring """ A word stemmer based on the Porter stemming algorithm. Porter, M. \"An algorithm for suffix stripping.\" Program 14.3 (1980): 130-137. A few minor modifications have been made to Porter's basic algorithm. See the source code of this module for more information. The Porter Stemmer requires that all tokens have string types. """ def __init__(self): """The main part of the stemming algorithm starts here. b is a buffer holding a word to be stemmed. The letters are in b[k0], b[k0+1] ... ending at b[k]. In fact k0 = 0 in this demo program. k is readjusted downwards as the stemming progresses. Zero termination is not in fact used in the algorithm. Note that only lower case sequences are stemmed. Forcing to lower case should be done before stem(...) is called. """ self.b = "" # buffer for word to be stemmed self.k = 0 self.k0 = 0 self.j = 0 # j is a general offset into the string ## --NEW-- ## This is a table of irregular forms. It is quite short, but still ## reflects the errors actually drawn to Martin Porter's attention over ## a 20 year period! ## ## Extend it as necessary. ## ## The form of the table is: ## { ## "p1" : ["s11","s12","s13", ... ], ## "p2" : ["s21","s22","s23", ... ], ## ... ## "pn" : ["sn1","sn2","sn3", ... ] ## } ## ## String sij is mapped to paradigm form pi, and the main stemming ## process is then bypassed. irregular_forms = { "sky" : ["sky", "skies"], "die" : ["dying"], "lie" : ["lying"], "tie" : ["tying"], "news" : ["news"], "inning" : ["innings", "inning"], "outing" : ["outings", "outing"], "canning" : ["cannings", "canning"], "howe" : ["howe"], # --NEW-- "proceed" : ["proceed"], "exceed" : ["exceed"], "succeed" : ["succeed"], # Hiranmay Ghosh } self.pool = {} for key in irregular_forms.keys(): for val in irregular_forms[key]: self.pool[val] = key def cons(self, i): """cons(i) is TRUE <=> b[i] is a consonant.""" if self.b[i] == 'a' or self.b[i] == 'e' or self.b[i] == 'i' or self.b[i] == 'o' or self.b[i] == 'u': return 0 if self.b[i] == 'y': if i == self.k0: return 1 else: return (not self.cons(i - 1)) return 1 def m(self): """m() measures the number of consonant sequences between k0 and j. if c is a consonant sequence and v a vowel sequence, and <..> indicates arbitrary presence, gives 0 vc gives 1 vcvc gives 2 vcvcvc gives 3 .... """ n = 0 i = self.k0 while 1: if i > self.j: return n if not self.cons(i): break i = i + 1 i = i + 1 while 1: while 1: if i > self.j: return n if self.cons(i): break i = i + 1 i = i + 1 n = n + 1 while 1: if i > self.j: return n if not self.cons(i): break i = i + 1 i = i + 1 def vowelinstem(self): """vowelinstem() is TRUE <=> k0,...j contains a vowel""" for i in range(self.k0, self.j + 1): if not self.cons(i): return 1 return 0 def doublec(self, j): """doublec(j) is TRUE <=> j,(j-1) contain a double consonant.""" if j < (self.k0 + 1): return 0 if (self.b[j] != self.b[j-1]): return 0 return self.cons(j) def cvc(self, i): """cvc(i) is TRUE <=> a) ( --NEW--) i == 1, and p[0] p[1] is vowel consonant, or b) p[i - 2], p[i - 1], p[i] has the form consonant - vowel - consonant and also if the second c is not w, x or y. this is used when trying to restore an e at the end of a short word. e.g. cav(e), lov(e), hop(e), crim(e), but snow, box, tray. """ if i == 0: return 0 # i == 0 never happens perhaps if i == 1: return (not self.cons(0) and self.cons(1)) if not self.cons(i) or self.cons(i-1) or not self.cons(i-2): return 0 ch = self.b[i] if ch == 'w' or ch == 'x' or ch == 'y': return 0 return 1 def ends(self, s): """ends(s) is TRUE <=> k0,...k ends with the string s.""" length = len(s) if s[length - 1] != self.b[self.k]: # tiny speed-up return 0 if length > (self.k - self.k0 + 1): return 0 if self.b[self.k-length+1:self.k+1] != s: return 0 self.j = self.k - length return 1 def setto(self, s): """setto(s) sets (j+1),...k to the characters in the string s, readjusting k.""" length = len(s) self.b = self.b[:self.j+1] + s + self.b[self.j+length+1:] self.k = self.j + length def r(self, s): """r(s) is used further down.""" if self.m() > 0: self.setto(s) def step1ab(self): """step1ab() gets rid of plurals and -ed or -ing. e.g. caresses -> caress ponies -> poni sties -> sti tie -> tie (--NEW--: see below) caress -> caress cats -> cat feed -> feed agreed -> agree disabled -> disable matting -> mat mating -> mate meeting -> meet milling -> mill messing -> mess meetings -> meet """ if self.b[self.k] == 's': if self.ends("sses"): self.k = self.k - 2 elif self.ends("ies"): if self.j == 0: self.k = self.k - 1 # this line extends the original algorithm, so that # 'flies'->'fli' but 'dies'->'die' etc else: self.k = self.k - 2 elif self.b[self.k - 1] != 's': self.k = self.k - 1 if self.ends("ied"): if self.j == 0: self.k = self.k - 1 else: self.k = self.k - 2 # this line extends the original algorithm, so that # 'spied'->'spi' but 'died'->'die' etc elif self.ends("eed"): if self.m() > 0: self.k = self.k - 1 elif (self.ends("ed") or self.ends("ing")) and self.vowelinstem(): self.k = self.j if self.ends("at"): self.setto("ate") elif self.ends("bl"): self.setto("ble") elif self.ends("iz"): self.setto("ize") elif self.doublec(self.k): self.k = self.k - 1 ch = self.b[self.k] if ch == 'l' or ch == 's' or ch == 'z': self.k = self.k + 1 elif (self.m() == 1 and self.cvc(self.k)): self.setto("e") def step1c(self): """step1c() turns terminal y to i when there is another vowel in the stem. --NEW--: This has been modified from the original Porter algorithm so that y->i is only done when y is preceded by a consonant, but not if the stem is only a single consonant, i.e. (*c and not c) Y -> I So 'happy' -> 'happi', but 'enjoy' -> 'enjoy' etc This is a much better rule. Formerly 'enjoy'->'enjoi' and 'enjoyment'-> 'enjoy'. Step 1c is perhaps done too soon; but with this modification that no longer really matters. Also, the removal of the vowelinstem(z) condition means that 'spy', 'fly', 'try' ... stem to 'spi', 'fli', 'tri' and conflate with 'spied', 'tried', 'flies' ... """ if self.ends("y") and self.j > 0 and self.cons(self.k - 1): self.b = self.b[:self.k] + 'i' + self.b[self.k+1:] def step2(self): """step2() maps double suffices to single ones. so -ization ( = -ize plus -ation) maps to -ize etc. note that the string before the suffix must give m() > 0. """ if self.b[self.k - 1] == 'a': if self.ends("ational"): self.r("ate") elif self.ends("tional"): self.r("tion") elif self.b[self.k - 1] == 'c': if self.ends("enci"): self.r("ence") elif self.ends("anci"): self.r("ance") elif self.b[self.k - 1] == 'e': if self.ends("izer"): self.r("ize") elif self.b[self.k - 1] == 'l': if self.ends("bli"): self.r("ble") # --DEPARTURE-- # To match the published algorithm, replace this phrase with # if self.ends("abli"): self.r("able") elif self.ends("alli"): if self.m() > 0: # --NEW-- self.setto("al") self.step2() elif self.ends("fulli"): self.r("ful") # --NEW-- elif self.ends("entli"): self.r("ent") elif self.ends("eli"): self.r("e") elif self.ends("ousli"): self.r("ous") elif self.b[self.k - 1] == 'o': if self.ends("ization"): self.r("ize") elif self.ends("ation"): self.r("ate") elif self.ends("ator"): self.r("ate") elif self.b[self.k - 1] == 's': if self.ends("alism"): self.r("al") elif self.ends("iveness"): self.r("ive") elif self.ends("fulness"): self.r("ful") elif self.ends("ousness"): self.r("ous") elif self.b[self.k - 1] == 't': if self.ends("aliti"): self.r("al") elif self.ends("iviti"): self.r("ive") elif self.ends("biliti"): self.r("ble") elif self.b[self.k - 1] == 'g': # --DEPARTURE-- if self.ends("logi"): self.j = self.j + 1 # --NEW-- (Barry Wilkins) self.r("og") # To match the published algorithm, delete this phrase def step3(self): """step3() dels with -ic-, -full, -ness etc. similar strategy to step2.""" if self.b[self.k] == 'e': if self.ends("icate"): self.r("ic") elif self.ends("ative"): self.r("") elif self.ends("alize"): self.r("al") elif self.b[self.k] == 'i': if self.ends("iciti"): self.r("ic") elif self.b[self.k] == 'l': if self.ends("ical"): self.r("ic") elif self.ends("ful"): self.r("") elif self.b[self.k] == 's': if self.ends("ness"): self.r("") def step4(self): """step4() takes off -ant, -ence etc., in context vcvc.""" if self.b[self.k - 1] == 'a': if self.ends("al"): pass else: return elif self.b[self.k - 1] == 'c': if self.ends("ance"): pass elif self.ends("ence"): pass else: return elif self.b[self.k - 1] == 'e': if self.ends("er"): pass else: return elif self.b[self.k - 1] == 'i': if self.ends("ic"): pass else: return elif self.b[self.k - 1] == 'l': if self.ends("able"): pass elif self.ends("ible"): pass else: return elif self.b[self.k - 1] == 'n': if self.ends("ant"): pass elif self.ends("ement"): pass elif self.ends("ment"): pass elif self.ends("ent"): pass else: return elif self.b[self.k - 1] == 'o': if self.ends("ion") and (self.b[self.j] == 's' or self.b[self.j] == 't'): pass elif self.ends("ou"): pass # takes care of -ous else: return elif self.b[self.k - 1] == 's': if self.ends("ism"): pass else: return elif self.b[self.k - 1] == 't': if self.ends("ate"): pass elif self.ends("iti"): pass else: return elif self.b[self.k - 1] == 'u': if self.ends("ous"): pass else: return elif self.b[self.k - 1] == 'v': if self.ends("ive"): pass else: return elif self.b[self.k - 1] == 'z': if self.ends("ize"): pass else: return else: return if self.m() > 1: self.k = self.j def step5(self): """step5() removes a final -e if m() > 1, and changes -ll to -l if m() > 1. """ self.j = self.k if self.b[self.k] == 'e': a = self.m() if a > 1 or (a == 1 and not self.cvc(self.k-1)): self.k = self.k - 1 if self.b[self.k] == 'l' and self.doublec(self.k) and self.m() > 1: self.k = self.k -1 def stem_word(self, p, i=0, j=None): """In stem(p,i,j), p is a char pointer, and the string to be stemmed is from p[i] to p[j] inclusive. Typically i is zero and j is the offset to the last character of a string, (p[j+1] == '\0'). The stemmer adjusts the characters p[i] ... p[j] and returns the new end-point of the string, k. Stemming never increases word length, so i <= k <= j. To turn the stemmer into a module, declare 'stem' as extern, and delete the remainder of this file. """ ## --NLTK-- ## Don't print results as we go (commented out the next line) #print p[i:j+1] if j == None: j = len(p) - 1 # copy the parameters into statics self.b = p self.k = j self.k0 = i if self.pool.has_key(self.b[self.k0:self.k+1]): return self.pool[self.b[self.k0:self.k+1]] if self.k <= self.k0 + 1: return self.b # --DEPARTURE-- # With this line, strings of length 1 or 2 don't go through the # stemming process, although no mention is made of this in the # published algorithm. Remove the line to match the published # algorithm. self.step1ab() self.step1c() self.step2() self.step3() self.step4() self.step5() return self.b[self.k0:self.k+1] def adjust_case(self, word, stem): lower = word.lower() ret = "" for x in xrange(len(stem)): if lower[x] == stem[x]: ret += word[x] else: ret += stem[x] return ret ## --NLTK-- ## Don't use this procedure; we want to work with individual ## tokens, instead. (commented out the following procedure) #def stem(self, text): # parts = re.split("(\W+)", text) # numWords = (len(parts) + 1)/2 # # ret = "" # for i in xrange(numWords): # word = parts[2 * i] # separator = "" # if ((2 * i) + 1) < len(parts): # separator = parts[(2 * i) + 1] # # stem = self.stem_word(string.lower(word), 0, len(word) - 1) # ret = ret + self.adjust_case(word, stem) # ret = ret + separator # return ret ## --NLTK-- ## Define a stem() method that implements the StemmerI interface. def stem(self, word): stem = self.stem_word(word.lower(), 0, len(word) - 1) return self.adjust_case(word, stem) ## --NLTK-- ## Add a string representation function def __repr__(self): return '' ## --NLTK-- ## This test procedure isn't applicable. #if __name__ == '__main__': # p = PorterStemmer() # if len(sys.argv) > 1: # for f in sys.argv[1:]: # infile = open(f, 'r') # while 1: # w = infile.readline() # if w == '': # break # w = w[:-1] # print p.stem(w) ##--NLTK-- ## Added a demo() function def demo(): """ A demonstration of the porter stemmer on a sample from the Penn Treebank corpus. """ from nltk.corpus import treebank from nltk import stem stemmer = stem.PorterStemmer() orig = [] stemmed = [] for item in treebank.files()[:3]: for (word, tag) in treebank.tagged_words(item): orig.append(word) stemmed.append(stemmer.stem(word)) # Convert the results to a string, and word-wrap them. results = ' '.join(stemmed) results = re.sub(r"(.{,70})\s", r'\1\n', results+' ').rstrip() # Convert the original to a string, and word wrap it. original = ' '.join(orig) original = re.sub(r"(.{,70})\s", r'\1\n', original+' ').rstrip() # Print the results. print '-Original-'.center(70).replace(' ', '*').replace('-', ' ') print original print '-Results-'.center(70).replace(' ', '*').replace('-', ' ') print results print '*'*70 ##--NLTK-- ## Call demo() if we're invoked directly. if __name__ == '__main__': demo() nltk-2.0~b9/nltk/stem/lancaster.py0000644000175000017500000002612111327451602017027 0ustar bhavanibhavani# Natural Language Toolkit: Stemmers # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Tomcavage # URL: # For license information, see LICENSE.TXT """ A word stemmer based on the Lancaster stemming algorithm. Paice, Chris D. "Another Stemmer." ACM SIGIR Forum 24.3 (1990): 56-61. """ import re from api import * class LancasterStemmer(StemmerI): # The rule list is static since it doesn't change between instances rule_tuple = ( "ai*2.", # -ia > - if intact "a*1.", # -a > - if intact "bb1.", # -bb > -b "city3s.", # -ytic > -ys "ci2>", # -ic > - "cn1t>", # -nc > -nt "dd1.", # -dd > -d "dei3y>", # -ied > -y "deec2ss.", # -ceed >", -cess "dee1.", # -eed > -ee "de2>", # -ed > - "dooh4>", # -hood > - "e1>", # -e > - "feil1v.", # -lief > -liev "fi2>", # -if > - "gni3>", # -ing > - "gai3y.", # -iag > -y "ga2>", # -ag > - "gg1.", # -gg > -g "ht*2.", # -th > - if intact "hsiug5ct.", # -guish > -ct "hsi3>", # -ish > - "i*1.", # -i > - if intact "i1y>", # -i > -y "ji1d.", # -ij > -id -- see nois4j> & vis3j> "juf1s.", # -fuj > -fus "ju1d.", # -uj > -ud "jo1d.", # -oj > -od "jeh1r.", # -hej > -her "jrev1t.", # -verj > -vert "jsim2t.", # -misj > -mit "jn1d.", # -nj > -nd "j1s.", # -j > -s "lbaifi6.", # -ifiabl > - "lbai4y.", # -iabl > -y "lba3>", # -abl > - "lbi3.", # -ibl > - "lib2l>", # -bil > -bl "lc1.", # -cl > c "lufi4y.", # -iful > -y "luf3>", # -ful > - "lu2.", # -ul > - "lai3>", # -ial > - "lau3>", # -ual > - "la2>", # -al > - "ll1.", # -ll > -l "mui3.", # -ium > - "mu*2.", # -um > - if intact "msi3>", # -ism > - "mm1.", # -mm > -m "nois4j>", # -sion > -j "noix4ct.", # -xion > -ct "noi3>", # -ion > - "nai3>", # -ian > - "na2>", # -an > - "nee0.", # protect -een "ne2>", # -en > - "nn1.", # -nn > -n "pihs4>", # -ship > - "pp1.", # -pp > -p "re2>", # -er > - "rae0.", # protect -ear "ra2.", # -ar > - "ro2>", # -or > - "ru2>", # -ur > - "rr1.", # -rr > -r "rt1>", # -tr > -t "rei3y>", # -ier > -y "sei3y>", # -ies > -y "sis2.", # -sis > -s "si2>", # -is > - "ssen4>", # -ness > - "ss0.", # protect -ss "suo3>", # -ous > - "su*2.", # -us > - if intact "s*1>", # -s > - if intact "s0.", # -s > -s "tacilp4y.", # -plicat > -ply "ta2>", # -at > - "tnem4>", # -ment > - "tne3>", # -ent > - "tna3>", # -ant > - "tpir2b.", # -ript > -rib "tpro2b.", # -orpt > -orb "tcud1.", # -duct > -duc "tpmus2.", # -sumpt > -sum "tpec2iv.", # -cept > -ceiv "tulo2v.", # -olut > -olv "tsis0.", # protect -sist "tsi3>", # -ist > - "tt1.", # -tt > -t "uqi3.", # -iqu > - "ugo1.", # -ogu > -og "vis3j>", # -siv > -j "vie0.", # protect -eiv "vi2>", # -iv > - "ylb1>", # -bly > -bl "yli3y>", # -ily > -y "ylp0.", # protect -ply "yl2>", # -ly > - "ygo1.", # -ogy > -og "yhp1.", # -phy > -ph "ymo1.", # -omy > -om "ypo1.", # -opy > -op "yti3>", # -ity > - "yte3>", # -ety > - "ytl2.", # -lty > -l "yrtsi5.", # -istry > - "yra3>", # -ary > - "yro3>", # -ory > - "yfi3.", # -ify > - "ycn2t>", # -ncy > -nt "yca3>", # -acy > - "zi2>", # -iz > - "zy1s." # -yz > -ys ) def __init__(self): """Create an instance of the Lancaster stemmer. """ # Setup an empty rule dictionary - this will be filled in later self.rule_dictionary = {} def parseRules(self, rule_tuple): """Validate the set of rules used in this stemmer. """ valid_rule = re.compile("^[a-z]+\*?\d[a-z]*[>\.]?$") # Empty any old rules from the rule set before adding new ones self.rule_dictionary = {} for rule in rule_tuple: if not valid_rule.match(rule): raise ValueError, "The rule %s is invalid" % rule first_letter = rule[0:1] if first_letter in self.rule_dictionary: self.rule_dictionary[first_letter].append(rule) else: self.rule_dictionary[first_letter] = [rule] def stem(self, word): """Stem a word using the Lancaster stemmer. """ # Lower-case the word, since all the rules are lower-cased word = word.lower() # Save a copy of the original word intact_word = word # If the user hasn't supplied any rules, setup the default rules if len(self.rule_dictionary) == 0: self.parseRules(LancasterStemmer.rule_tuple) return self.__doStemming(word, intact_word) def __doStemming(self, word, intact_word): """Perform the actual word stemming """ valid_rule = re.compile("^([a-z]+)(\*?)(\d)([a-z]*)([>\.]?)$") proceed = True while proceed: # Find the position of the last letter of the word to be stemmed last_letter_position = self.__getLastLetter(word) # Only stem the word if it has a last letter and a rule matching that last letter if last_letter_position < 0 or word[last_letter_position] not in self.rule_dictionary: proceed = False else: rule_was_applied = False # Go through each rule that matches the word's final letter for rule in self.rule_dictionary[word[last_letter_position]]: rule_match = valid_rule.match(rule) if rule_match: (ending_string, intact_flag, remove_total, append_string, cont_flag) = rule_match.groups() # Convert the number of chars to remove when stemming # from a string to an integer remove_total = int(remove_total) # Proceed if word's ending matches rule's word ending if word.endswith(ending_string[::-1]): if intact_flag: if (word == intact_word and self.__isAcceptable(word, remove_total)): word = self.__applyRule(word, remove_total, append_string) rule_was_applied = True if cont_flag == '.': proceed = False break elif self.__isAcceptable(word, remove_total): word = self.__applyRule(word, remove_total, append_string) rule_was_applied = True if cont_flag == '.': proceed = False break # If no rules apply, the word doesn't need any more stemming if rule_was_applied == False: proceed = False return word def __getLastLetter(self, word): """Get the zero-based index of the last alphabetic character in this string """ last_letter = -1 for position in range(len(word)): if word[position].isalpha(): last_letter = position else: break return last_letter def __isAcceptable(self, word, remove_total): """Determine if the word is acceptable for stemming. """ word_is_acceptable = False # If the word starts with a vowel, it must be at least 2 # characters long to be stemmed if word[0] in "aeiouy": if (len(word) - remove_total >= 2): word_is_acceptable = True # If the word starts with a consonant, it must be at least 3 # characters long (including one vowel) to be stemmed elif (len(word) - remove_total >= 3): if word[1] in "aeiouy": word_is_acceptable = True elif word[2] in "aeiouy": word_is_acceptable = True return word_is_acceptable def __applyRule(self, word, remove_total, append_string): """Apply the stemming rule to the word """ # Remove letters from the end of the word new_word_length = len(word) - remove_total word = word[0:new_word_length] # And add new letters to the end of the truncated word if append_string: word += append_string return word def __repr__(self): return '' def demo(): """A demonstration of the lancaster stemmer on a samples described in Paice, Chris D. "Another Stemmer." ACM SIGIR Forum 24.3 (1990): 56-61. """ from nltk import stem stemmer = stem.LancasterStemmer() print "%-20s%-20s" % ("Original Word", "Stemmed Word") print "*" * 40 for word in ( 'maximum', # Remove "-um" when word is intact 'presumably', # Don't remove "-um" when word is not intact 'multiply', # No action taken if word ends with "-ply" 'provision', # Replace "-sion" with "-j" to trigger "j" set of rules 'owed', # Word starting with vowel must contain at least 2 letters 'ear', # ditto. 'saying', # Words starting with consonant must contain at least 3 'crying', # letters and one of those letters must be a vowel 'string', # ditto. 'meant', # ditto. 'cement'): # ditto. stemmed_word = stemmer.stem(word) print "%-20s%-20s" % (word, stemmed_word) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/stem/isri.py0000644000175000017500000003610311327451603016023 0ustar bhavanibhavani# -*- coding: utf-8 -*- # # Natural Language Toolkit: The ISRI Arabic Stemmer # # Copyright (C) 2001-2010 NLTK Proejct # Algorithm: Kazem Taghva, Rania Elkhoury, and Jeffrey Coombs (2005) # Author: Hosam Algasaier # URL: # For license information, see LICENSE.TXT """ISRI Arabic Stemmer The algorithm for this stemmer is described in: Taghva, K., Elkoury, R., and Coombs, J. 2005. Arabic Stemming without a root dictionary. Information Science Research Institute. University of Nevada, Las Vegas, USA. The Information Science Research Institute’s (ISRI) Arabic stemmer shares many features with the Khoja stemmer. However, the main difference is that ISRI stemmer does not use root dictionary. Also, if a root is not found, ISRI stemmer returned normalized form, rather than returning the original unmodified word. Additional adjustments were made to improve the algorithm: 1- Adding 60 stop words. 2- Adding the pattern (ØªÙØ§Ø¹ÙŠÙ„) to ISRI pattern set. 3- The step 2 in the original algorithm was normalizing all hamza. This step is discarded because it increases the word ambiguities and changes the original root. """ import re from api import * class ISRIStemmer(StemmerI): ''' ISRI Arabic stemmer based on algorithm: Arabic Stemming without a root dictionary. Information Science Research Institute. University of Nevada, Las Vegas, USA. A few minor modifications have been made to ISRI basic algorithm. See the source code of this module for more information. isri.stem(token) returns Arabic root for the given token. The ISRI Stemmer requires that all tokens have Unicode string types. If you use Python IDLE on Arabic Windows you have to decode text first using Arabic '1256' coding. ''' def __init__(self): self.stm = 'defult none' self.p3 = [u'\u0643\u0627\u0644', u'\u0628\u0627\u0644', u'\u0648\u0644\u0644', u'\u0648\u0627\u0644'] # length three prefixes self.p2 = [u'\u0627\u0644', u'\u0644\u0644'] # length two prefixes self.p1 = [u'\u0644', u'\u0628', u'\u0641', u'\u0633', u'\u0648', u'\u064a', u'\u062a', u'\u0646', u'\u0627'] # length one prefixes self.s3 = [u'\u062a\u0645\u0644', u'\u0647\u0645\u0644', u'\u062a\u0627\u0646', u'\u062a\u064a\u0646', u'\u0643\u0645\u0644'] # length three suffixes self.s2 = [u'\u0648\u0646', u'\u0627\u062a', u'\u0627\u0646', u'\u064a\u0646', u'\u062a\u0646', u'\u0643\u0645', u'\u0647\u0646', u'\u0646\u0627', u'\u064a\u0627', u'\u0647\u0627', u'\u062a\u0645', u'\u0643\u0646', u'\u0646\u064a', u'\u0648\u0627', u'\u0645\u0627', u'\u0647\u0645'] # length two suffixes self.s1 = [u'\u0629', u'\u0647', u'\u064a', u'\u0643', u'\u062a', u'\u0627', u'\u0646'] # length one suffixes self.pr4 = {0:[u'\u0645'], 1:[u'\u0627'], 2:[u'\u0627', u'\u0648', u'\u064A'], 3:[u'\u0629']} # groups of length four patterns self.pr53 = {0:[u'\u0627', u'\u062a'], 1:[u'\u0627', u'\u064a', u'\u0648'], 2:[u'\u0627', u'\u062a', u'\u0645'], 3:[u'\u0645', u'\u064a', u'\u062a'], 4:[u'\u0645', u'\u062a'], 5:[u'\u0627', u'\u0648'], 6:[u'\u0627', u'\u0645']} # Groups of length five patterns and length three roots self.re_short_vowels = re.compile(ur'[\u064B-\u0652]') self.re_hamza = re.compile(ur'[\u0621\u0624\u0626]') self.re_intial_hamza = re.compile(ur'^[\u0622\u0623\u0625]') self.stop_words = [u'\u064a\u0643\u0648\u0646', u'\u0648\u0644\u064a\u0633', u'\u0648\u0643\u0627\u0646', u'\u0643\u0630\u0644\u0643', u'\u0627\u0644\u062a\u064a', u'\u0648\u0628\u064a\u0646', u'\u0639\u0644\u064a\u0647\u0627', u'\u0645\u0633\u0627\u0621', u'\u0627\u0644\u0630\u064a', u'\u0648\u0643\u0627\u0646\u062a', u'\u0648\u0644\u0643\u0646', u'\u0648\u0627\u0644\u062a\u064a', u'\u062a\u0643\u0648\u0646', u'\u0627\u0644\u064a\u0648\u0645', u'\u0627\u0644\u0644\u0630\u064a\u0646', u'\u0639\u0644\u064a\u0647', u'\u0643\u0627\u0646\u062a', u'\u0644\u0630\u0644\u0643', u'\u0623\u0645\u0627\u0645', u'\u0647\u0646\u0627\u0643', u'\u0645\u0646\u0647\u0627', u'\u0645\u0627\u0632\u0627\u0644', u'\u0644\u0627\u0632\u0627\u0644', u'\u0644\u0627\u064a\u0632\u0627\u0644', u'\u0645\u0627\u064a\u0632\u0627\u0644', u'\u0627\u0635\u0628\u062d', u'\u0623\u0635\u0628\u062d', u'\u0623\u0645\u0633\u0649', u'\u0627\u0645\u0633\u0649', u'\u0623\u0636\u062d\u0649', u'\u0627\u0636\u062d\u0649', u'\u0645\u0627\u0628\u0631\u062d', u'\u0645\u0627\u0641\u062a\u0626', u'\u0645\u0627\u0627\u0646\u0641\u0643', u'\u0644\u0627\u0633\u064a\u0645\u0627', u'\u0648\u0644\u0627\u064a\u0632\u0627\u0644', u'\u0627\u0644\u062d\u0627\u0644\u064a', u'\u0627\u0644\u064a\u0647\u0627', u'\u0627\u0644\u0630\u064a\u0646', u'\u0641\u0627\u0646\u0647', u'\u0648\u0627\u0644\u0630\u064a', u'\u0648\u0647\u0630\u0627', u'\u0644\u0647\u0630\u0627', u'\u0641\u0643\u0627\u0646', u'\u0633\u062a\u0643\u0648\u0646', u'\u0627\u0644\u064a\u0647', u'\u064a\u0645\u0643\u0646', u'\u0628\u0647\u0630\u0627', u'\u0627\u0644\u0630\u0649'] def stem(self, token): """ Stemming a word token using the ISRI stemmer. """ self.stm = token self.norm(1) # remove diacritics which representing Arabic short vowels if self.stm in self.stop_words: return self.stm # exclude stop words from being processed self.pre32() # remove length three and length two prefixes in this order self.suf32() # remove length three and length two suffixes in this order self.waw() # remove connective ‘و’ if it precedes a word beginning with ‘و’ self.norm(2) # normalize initial hamza to bare alif if len(self.stm)<=3: return self.stm # return stem if less than or equal to three if len(self.stm)==4: # length 4 word self.pro_w4() return self.stm elif len(self.stm)==5: # length 5 word self.pro_w53() self.end_w5() return self.stm elif len(self.stm)==6: # length 6 word self.pro_w6() self.end_w6() return self.stm elif len(self.stm)==7: # length 7 word self.suf1() if len(self.stm)==7: self.pre1() if len(self.stm)==6: self.pro_w6() self.end_w6() return self.stm return self.stm # if word length >7 , then no stemming def norm(self, num): """ normalization: num=1 normalize diacritics num=2 normalize initial hamza num=3 both 1&2 """ self.k = num if self.k == 1: self.stm = self.re_short_vowels.sub('', self.stm) return self.stm elif self.k == 2: self.stm = self.re_intial_hamza.sub(ur'\u0627',self.stm) return self.stm elif self.k == 3: self.stm = self.re_short_vowels.sub('', self.stm) self.stm = self.re_intial_hamza.sub(ur'\u0627',self.stm) return self.stm def pre32(self): """remove length three and length two prefixes in this order""" if len(self.stm)>=6: for pre3 in self.p3: if self.stm.startswith(pre3): self.stm = self.stm[3:] return self.stm elif len(self.stm)>=5: for pre2 in self.p2: if self.stm.startswith(pre2): self.stm = self.stm[2:] return self.stm def suf32(self): """remove length three and length two suffixes in this order""" if len(self.stm)>=6: for suf3 in self.s3: if self.stm.endswith(suf3): self.stm = self.stm[:-3] return self.stm elif len(self.stm)>=5: for suf2 in self.s2: if self.stm.endswith(suf2): self.stm = self.stm[:-2] return self.stm def waw(self): """remove connective ‘و’ if it precedes a word beginning with ‘و’ """ if (len(self.stm)>=4)&(self.stm[:2] == u'\u0648\u0648'): self.stm = self.stm[1:] return self.stm def pro_w4(self): """process length four patterns and extract length three roots""" if self.stm[0] in self.pr4[0]: # Ù…ÙØ¹Ù„ self.stm = self.stm[1:] return self.stm elif self.stm[1] in self.pr4[1]: # ÙØ§Ø¹Ù„ self.stm = self.stm[0]+self.stm[2:] return self.stm elif self.stm[2] in self.pr4[2]: # ÙØ¹Ø§Ù„ - ÙØ¹ÙˆÙ„ - ÙØ¹ÙŠÙ„ self.stm = self.stm[:2]+self.stm[3] return self.stm elif self.stm[3] in self.pr4[3]: # ÙØ¹Ù„Ø© self.stm = self.stm[:-1] return self.stm else: self.suf1() # do - normalize short sufix if len(self.stm)==4: self.pre1() # do - normalize short prefix return self.stm def pro_w53(self): """process length five patterns and extract length three roots""" if ((self.stm[2] in self.pr53[0]) & (self.stm[0] == u'\u0627')): # Ø§ÙØªØ¹Ù„ - Ø§ÙØ§Ø¹Ù„ self.stm = self.stm[1]+self.stm[3:] return self.stm elif ((self.stm[3] in self.pr53[1]) & (self.stm[0] == u'\u0645')): # Ù…ÙØ¹ÙˆÙ„ - Ù…ÙØ¹Ø§Ù„ - Ù…ÙØ¹ÙŠÙ„ self.stm = self.stm[1:3]+self.stm[4] return self.stm elif ((self.stm[0] in self.pr53[2]) & (self.stm[4] == u'\u0629')): # Ù…ÙØ¹Ù„Ø© - ØªÙØ¹Ù„Ø© - Ø§ÙØ¹Ù„Ø© self.stm = self.stm[1:4] return self.stm elif ((self.stm[0] in self.pr53[3]) & (self.stm[2] == u'\u062a')): # Ù…ÙØªØ¹Ù„ - ÙŠÙØªØ¹Ù„ - ØªÙØªØ¹Ù„ self.stm = self.stm[1]+self.stm[3:] return self.stm elif ((self.stm[0] in self.pr53[4]) & (self.stm[2] == u'\u0627')): #Ù…ÙØ§Ø¹Ù„ - ØªÙØ§Ø¹Ù„ self.stm = self.stm[1]+self.stm[3:] return self.stm elif ((self.stm[2] in self.pr53[5]) & (self.stm[4] == u'\u0629')): # ÙØ¹ÙˆÙ„Ø© - ÙØ¹Ø§Ù„Ø© self.stm = self.stm[:2]+self.stm[3] return self.stm elif ((self.stm[0] in self.pr53[6]) & (self.stm[1] == u'\u0646')): # Ø§Ù†ÙØ¹Ù„ - Ù…Ù†ÙØ¹Ù„ self.stm = self.stm[2:] return self.stm elif ((self.stm[3] == u'\u0627') & (self.stm[0] == u'\u0627')): # Ø§ÙØ¹Ø§Ù„ self.stm = self.stm[1:3]+self.stm[4] return self.stm elif ((self.stm[4] == u'\u0646') & (self.stm[3] == u'\u0627')): # ÙØ¹Ù„ان self.stm = self.stm[:3] return self.stm elif ((self.stm[3] == u'\u064a') & (self.stm[0] == u'\u062a')): # ØªÙØ¹ÙŠÙ„ self.stm = self.stm[1:3]+self.stm[4] return self.stm elif ((self.stm[3] == u'\u0648') & (self.stm[1] == u'\u0627')): # ÙØ§Ø¹ÙˆÙ„ self.stm = self.stm[0]+self.stm[2]+self.stm[4] return self.stm elif ((self.stm[2] == u'\u0627') & (self.stm[1] == u'\u0648')): # Ùواعل self.stm = self.stm[0]+self.stm[3:] return self.stm elif ((self.stm[3] == u'\u0626') & (self.stm[2] == u'\u0627')): # ÙØ¹Ø§Ø¦Ù„ self.stm = self.stm[:2]+self.stm[4] return self.stm elif ((self.stm[4] == u'\u0629') & (self.stm[1] == u'\u0627')): # ÙØ§Ø¹Ù„Ø© self.stm = self.stm[0]+self.stm[2:4] return self.stm elif ((self.stm[4] == u'\u064a') & (self.stm[2] == u'\u0627')): # ÙØ¹Ø§Ù„ÙŠ self.stm = self.stm[:2]+self.stm[3] return self.stm else: self.suf1() # do - normalize short sufix if len(self.stm)==5: self.pre1() # do - normalize short prefix return self.stm def pro_w54(self): """process length five patterns and extract length four roots""" if (self.stm[0] in self.pr53[2]): #ØªÙØ¹Ù„Ù„ - Ø§ÙØ¹Ù„Ù„ - Ù…ÙØ¹Ù„Ù„ self.stm = self.stm[1:] return self.stm elif (self.stm[4] == u'\u0629'): # ÙØ¹Ù„لة self.stm = self.stm[:4] return self.stm elif (self.stm[2] == u'\u0627'): # ÙØ¹Ø§Ù„Ù„ self.stm = self.stm[:2]+self.stm[3:] return self.stm def end_w5(self): """ending step (word of length five)""" if len(self.stm)==3: return self.stm elif len(self.stm)==4: self.pro_w4() return self.stm elif len(self.stm)==5: self.pro_w54() return self.stm def pro_w6(self): """process length six patterns and extract length three roots""" if ((self.stm.startswith(u'\u0627\u0633\u062a')) or (self.stm.startswith(u'\u0645\u0633\u062a'))): # Ù…Ø³ØªÙØ¹Ù„ - Ø§Ø³ØªÙØ¹Ù„ self.stm= self.stm[3:] return self.stm elif (self.stm[0]== u'\u0645' and self.stm[3]== u'\u0627' and self.stm[5]== u'\u0629'): # Ù…ÙØ¹Ø§Ù„Ø© self.stm = self.stm[1:3]+self.stm[4] return self.stm elif (self.stm[0]== u'\u0627' and self.stm[2]== u'\u062a' and self.stm[4]== u'\u0627'): # Ø§ÙØªØ¹Ø§Ù„ self.stm = self.stm[1]+self.stm[3]+self.stm[5] return self.stm elif (self.stm[0]== u'\u0627' and self.stm[3]== u'\u0648' and self.stm[2]==self.stm[4]): # Ø§ÙØ¹ÙˆØ¹Ù„ self.stm = self.stm[1]+self.stm[4:] return self.stm elif (self.stm[0]== u'\u062a' and self.stm[2]== u'\u0627' and self.stm[4]== u'\u064a'): # ØªÙØ§Ø¹ÙŠÙ„ new pattern self.stm = self.stm[1]+self.stm[3]+self.stm[5] return self.stm else: self.suf1() # do - normalize short sufix if len(self.stm)==6: self.pre1() # do - normalize short prefix return self.stm def pro_w64(self): """process length six patterns and extract length four roots""" if (self.stm[0] and self.stm[4])==u'\u0627': # Ø§ÙØ¹Ù„ال self.stm=self.stm[1:4]+self.stm[5] return self.stm elif (self.stm.startswith(u'\u0645\u062a')): # Ù…ØªÙØ¹Ù„Ù„ self.stm = self.stm[2:] return self.stm def end_w6(self): """ending step (word of length six)""" if len(self.stm)==3: return self.stm elif len(self.stm)==5: self.pro_w53() self.end_w5() return self.stm elif len (self.stm)==6: self.pro_w64() return self.stm def suf1(self): """normalize short sufix""" for sf1 in self.s1: if self.stm.endswith(sf1): self.stm = self.stm[:-1] return self.stm def pre1(self): """normalize short prefix""" for sp1 in self.p1: if self.stm.startswith(sp1): self.stm = self.stm[1:] return self.stm nltk-2.0~b9/nltk/stem/api.py0000644000175000017500000000127411327451602015626 0ustar bhavanibhavani# Natural Language Toolkit: Stemmer Interface # # Copyright (C) 2001-2010 NLTK Project # Author: Trevor Cohn # Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT class StemmerI(object): """ A processing interface for removing morphological affixes from words. This process is known as X{stemming}. """ def stem(self, token): """ Strip affixes from the token and return the stem. @param token: The token that should be stemmed. @type token: C{str} """ raise NotImplementedError() nltk-2.0~b9/nltk/stem/__init__.py0000644000175000017500000000344011423114517016607 0ustar bhavanibhavani# Natural Language Toolkit: Stemmers # # Copyright (C) 2001-2010 NLTK Project # Author: Trevor Cohn # Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT """ Interfaces used to remove morphological affixes from words, leaving only the word stem. Stemming algorithms aim to remove those affixes required for eg. grammatical role, tense, derivational morphology leaving only the stem of the word. This is a difficult problem due to irregular words (eg. common verbs in English), complicated morphological rules, and part-of-speech and sense ambiguities (eg. C{ceil-} is not the stem of C{ceiling}). C{StemmerI} defines a standard interface for stemmers. """ from api import * from regexp import * from porter import * from lancaster import * from isri import * from snowball import * from wordnet import * from rslp import * __all__ = [ # Stemmer interface 'StemmerI', # Stemmers 'RegexpStemmer', 'PorterStemmer', 'LancasterStemmer', 'RSLPStemmer', 'WordNetLemmatizer', 'WordnetStemmer', 'ISRIStemmer', 'SnowballStemmer' ] ###################################################################### #{ Deprecated ###################################################################### from nltk.internals import Deprecated class StemI(StemmerI, Deprecated): """Use nltk.StemmerI instead.""" class Regexp(RegexpStemmer, Deprecated): """Use nltk.RegexpStemmer instead.""" class Porter(PorterStemmer, Deprecated): """Use nltk.PorterStemmer instead.""" class Lancaster(LancasterStemmer, Deprecated): """Use nltk.LancasterStemmer instead.""" class Wordnet(WordNetStemmer, Deprecated): """Use nltk.WordNetLemmatizer instead.""" nltk-2.0~b9/nltk/sem/util.py0000644000175000017500000003713411377057401015656 0ustar bhavanibhavani# Natural Language Toolkit: Semantic Interpretation # # Author: Ewan Klein # # Copyright (C) 2001-2010 NLTK Project # URL: # For license information, see LICENSE.TXT """ Utility functions for batch-processing sentences: parsing and extraction of the semantic representation of the root node of the the syntax tree, followed by evaluation of the semantic representation in a first-order model. """ import evaluate import re import nltk from nltk.internals import deprecated from nltk.sem.logic import * ############################################################## ## Utility functions for connecting parse output to semantics ############################################################## def batch_parse(inputs, grammar, trace=0): """ Convert input sentences into syntactic trees. @parameter inputs: sentences to be parsed @type inputs: C{list} of C{str} @parameter grammar: L{FeatureGrammar} or name of feature-based grammar @rtype: C{dict} @return: a mapping from input sentences to a list of L{Tree}s """ if isinstance(grammar, nltk.grammar.FeatureGrammar): cp = nltk.parse.FeatureChartParser(grammar) else: cp = nltk.parse.load_parser(grammar, trace=trace) parses = [] for sent in inputs: tokens = sent.split() # use a tokenizer? syntrees = cp.nbest_parse(tokens) parses.append(syntrees) return parses @deprecated('Use batch_parse() instead.') def text_parse(*args, **kwargs): batch_parse(*args, **kwargs) def root_semrep(syntree, semkey='SEM'): """ Find the semantic representation at the root of a tree. @parameter syntree: a parse L{Tree} @parameter semkey: the feature label to use for the root semantics in the tree @return: the semantic representation at the root of a L{Tree} @rtype: L{logic.Expression} """ node = syntree.node assert isinstance(node, nltk.grammar.FeatStructNonterminal) try: return node[semkey] except KeyError: print node, print "has no specification for the feature %s" % semkey raise def batch_interpret(inputs, grammar, semkey='SEM', trace=0): """ Add the semantic representation to each syntactic parse tree of each input sentence. @parameter inputs: a list of sentences @parameter grammar: L{FeatureGrammar} or name of feature-based grammar @return: a mapping from sentences to lists of pairs (parse-tree, semantic-representations) @rtype: C{dict} """ return [[(syn, root_semrep(syn, semkey)) for syn in syntrees] for syntrees in batch_parse(inputs, grammar, trace=trace)] @deprecated('Use batch_interpret() instead.') def text_interpret(*args, **kwargs): batch_interpret(*args, **kwargs) def batch_evaluate(inputs, grammar, model, assignment, trace=0): """ Add the truth-in-a-model value to each semantic representation for each syntactic parse of each input sentences. @parameter inputs: a list of sentences @parameter grammar: L{FeatureGrammar} or name of feature-based grammar @return: a mapping from sentences to lists of triples (parse-tree, semantic-representations, evaluation-in-model) @rtype: C{dict} """ return [[(syn, sem, model.evaluate(str(sem), assignment, trace=trace)) for (syn, sem) in interpretations] for interpretations in batch_interpret(inputs, grammar)] @deprecated('Use batch_evaluate() instead.') def text_evaluate(*args, **kwargs): batch_evaluate(*args, **kwargs) ########################################## # REs used by the parse_valuation function ########################################## _VAL_SPLIT_RE = re.compile(r'\s*=+>\s*') _ELEMENT_SPLIT_RE = re.compile(r'\s*,\s*') _TUPLES_RE = re.compile(r"""\s* (\([^)]+\)) # tuple-expression \s*""", re.VERBOSE) def parse_valuation_line(s): """ Parse a line in a valuation file. Lines are expected to be of the form:: noosa => n girl => {g1, g2} chase => {(b1, g1), (b2, g1), (g1, d1), (g2, d2)} @parameter s: input line @type s: C{str} @return: a pair (symbol, value) @rtype: C{tuple} """ pieces = _VAL_SPLIT_RE.split(s) symbol = pieces[0] value = pieces[1] # check whether the value is meant to be a set if value.startswith('{'): value = value[1:-1] tuple_strings = _TUPLES_RE.findall(value) # are the set elements tuples? if tuple_strings: set_elements = [] for ts in tuple_strings: ts = ts[1:-1] element = tuple(_ELEMENT_SPLIT_RE.split(ts)) set_elements.append(element) else: set_elements = _ELEMENT_SPLIT_RE.split(value) value = set(set_elements) return symbol, value def parse_valuation(s): """ Convert a valuation file into a valuation. @parameter s: the contents of a valuation file @type s: C{str} @return: a L{nltk.sem} valuation @rtype: L{Valuation} """ statements = [] for linenum, line in enumerate(s.splitlines()): line = line.strip() if line.startswith('#') or line=='': continue try: statements.append(parse_valuation_line(line)) except ValueError: raise ValueError, 'Unable to parse line %s: %s' % (linenum, line) val = evaluate.Valuation(statements) return val def parse_logic(s, logic_parser=None): """ Convert a file of First Order Formulas into a list of {Expression}s. @param s: the contents of the file @type s: C{str} @param logic_parser: The parser to be used to parse the logical expression @type logic_parser: C{LogicParser} @return: a list of parsed formulas. @rtype: C{list} of L{Expression} """ if logic_parser is None: logic_parser = LogicParser() statements = [] for linenum, line in enumerate(s.splitlines()): line = line.strip() if line.startswith('#') or line=='': continue try: statements.append(logic_parser.parse(line)) except ParseException: raise ValueError, 'Unable to parse line %s: %s' % (linenum, line) return statements def skolemize(expression, univ_scope=None, used_variables=None): """ Skolemize the expression and convert to conjunctive normal form (CNF) """ if univ_scope is None: univ_scope = set() if used_variables is None: used_variables = set() if isinstance(expression, AllExpression): term = skolemize(expression.term, univ_scope|set([expression.variable]), used_variables|set([expression.variable])) return term.replace(expression.variable, VariableExpression(unique_variable(ignore=used_variables))) elif isinstance(expression, AndExpression): return skolemize(expression.first, univ_scope, used_variables) &\ skolemize(expression.second, univ_scope, used_variables) elif isinstance(expression, OrExpression): return to_cnf(skolemize(expression.first, univ_scope, used_variables), skolemize(expression.second, univ_scope, used_variables)) elif isinstance(expression, ImpExpression): return to_cnf(skolemize(-expression.first, univ_scope, used_variables), skolemize(expression.second, univ_scope, used_variables)) elif isinstance(expression, IffExpression): return to_cnf(skolemize(-expression.first, univ_scope, used_variables), skolemize(expression.second, univ_scope, used_variables)) &\ to_cnf(skolemize(expression.first, univ_scope, used_variables), skolemize(-expression.second, univ_scope, used_variables)) elif isinstance(expression, EqualityExpression): return expression elif isinstance(expression, NegatedExpression): negated = expression.term if isinstance(negated, AllExpression): term = skolemize(-negated.term, univ_scope, used_variables|set([negated.variable])) if univ_scope: return term.replace(negated.variable, skolem_function(univ_scope)) else: skolem_constant = VariableExpression(unique_variable(ignore=used_variables)) return term.replace(negated.variable, skolem_constant) elif isinstance(negated, AndExpression): return to_cnf(skolemize(-negated.first, univ_scope, used_variables), skolemize(-negated.second, univ_scope, used_variables)) elif isinstance(negated, OrExpression): return skolemize(-negated.first, univ_scope, used_variables) &\ skolemize(-negated.second, univ_scope, used_variables) elif isinstance(negated, ImpExpression): return skolemize(negated.first, univ_scope, used_variables) &\ skolemize(-negated.second, univ_scope, used_variables) elif isinstance(negated, IffExpression): return to_cnf(skolemize(-negated.first, univ_scope, used_variables), skolemize(-negated.second, univ_scope, used_variables)) &\ to_cnf(skolemize(negated.first, univ_scope, used_variables), skolemize(negated.second, univ_scope, used_variables)) elif isinstance(negated, EqualityExpression): return expression elif isinstance(negated, NegatedExpression): return skolemize(negated.term, univ_scope, used_variables) elif isinstance(negated, ExistsExpression): term = skolemize(-negated.term, univ_scope|set([negated.variable]), used_variables|set([negated.variable])) return term.replace(negated.variable, VariableExpression(unique_variable(ignore=used_variables))) elif isinstance(negated, ApplicationExpression): return expression else: raise Exception('\'%s\' cannot be skolemized' % expression) elif isinstance(expression, ExistsExpression): term = skolemize(expression.term, univ_scope, used_variables|set([expression.variable])) if univ_scope: return term.replace(expression.variable, skolem_function(univ_scope)) else: skolem_constant = VariableExpression(unique_variable(ignore=used_variables)) return term.replace(expression.variable, skolem_constant) elif isinstance(expression, ApplicationExpression): return expression else: raise Exception('\'%s\' cannot be skolemized' % expression) def to_cnf(first, second): """ Convert this split disjunction to conjunctive normal form (CNF) """ if isinstance(first, AndExpression): r_first = to_cnf(first.first, second) r_second = to_cnf(first.second, second) return r_first & r_second elif isinstance(second, AndExpression): r_first = to_cnf(first, second.first) r_second = to_cnf(first, second.second) return r_first & r_second else: return first | second def demo_model0(): global m0, g0 #Initialize a valuation of non-logical constants.""" v = [('john', 'b1'), ('mary', 'g1'), ('suzie', 'g2'), ('fido', 'd1'), ('tess', 'd2'), ('noosa', 'n'), ('girl', set(['g1', 'g2'])), ('boy', set(['b1', 'b2'])), ('dog', set(['d1', 'd2'])), ('bark', set(['d1', 'd2'])), ('walk', set(['b1', 'g2', 'd1'])), ('chase', set([('b1', 'g1'), ('b2', 'g1'), ('g1', 'd1'), ('g2', 'd2')])), ('see', set([('b1', 'g1'), ('b2', 'd2'), ('g1', 'b1'),('d2', 'b1'), ('g2', 'n')])), ('in', set([('b1', 'n'), ('b2', 'n'), ('d2', 'n')])), ('with', set([('b1', 'g1'), ('g1', 'b1'), ('d1', 'b1'), ('b1', 'd1')])) ] #Read in the data from C{v} val = evaluate.Valuation(v) #Bind C{dom} to the C{domain} property of C{val} dom = val.domain #Initialize a model with parameters C{dom} and C{val}. m0 = evaluate.Model(dom, val) #Initialize a variable assignment with parameter C{dom} g0 = evaluate.Assignment(dom) def read_sents(file): sents = [l.rstrip() for l in open(file)] # get rid of blank lines sents = [l for l in sents if len(l) > 0] sents = [l for l in sents if not l[0] == '#'] return sents def demo_legacy_grammar(): """ Check that batch_interpret() is compatible with legacy grammars that use a lowercase 'sem' feature. Define 'test.fcfg' to be the following """ g = nltk.parse_fcfg(""" % start S S[sem=] -> 'hello' """) print "Reading grammar: %s" % g print "*" * 20 for reading in batch_interpret(['hello'], g, semkey='sem'): syn, sem = reading[0] print print "output: ", sem def demo(): import sys from optparse import OptionParser description = \ """ Parse and evaluate some sentences. """ opts = OptionParser(description=description) opts.set_defaults(evaluate=True, beta=True, syntrace=0, semtrace=0, demo='default', grammar='', sentences='') opts.add_option("-d", "--demo", dest="demo", help="choose demo D; omit this for the default demo, or specify 'chat80'", metavar="D") opts.add_option("-g", "--gram", dest="grammar", help="read in grammar G", metavar="G") opts.add_option("-m", "--model", dest="model", help="import model M (omit '.py' suffix)", metavar="M") opts.add_option("-s", "--sentences", dest="sentences", help="read in a file of test sentences S", metavar="S") opts.add_option("-e", "--no-eval", action="store_false", dest="evaluate", help="just do a syntactic analysis") opts.add_option("-b", "--no-beta-reduction", action="store_false", dest="beta", help="don't carry out beta-reduction") opts.add_option("-t", "--syntrace", action="count", dest="syntrace", help="set syntactic tracing on; requires '-e' option") opts.add_option("-T", "--semtrace", action="count", dest="semtrace", help="set semantic tracing on") (options, args) = opts.parse_args() SPACER = '-' * 30 demo_model0() sents = [ 'Fido sees a boy with Mary', 'John sees Mary', 'every girl chases a dog', 'every boy chases a girl', 'John walks with a girl in Noosa', 'who walks'] gramfile = 'grammars/sample_grammars/sem2.fcfg' if options.sentences: sentsfile = options.sentences if options.grammar: gramfile = options.grammar if options.model: exec "import %s as model" % options.model if sents is None: sents = read_sents(sentsfile) # Set model and assignment model = m0 g = g0 if options.evaluate: evaluations = \ batch_evaluate(sents, gramfile, model, g, trace=options.semtrace) else: semreps = \ batch_interpret(sents, gramfile, trace=options.syntrace) for i, sent in enumerate(sents): n = 1 print '\nSentence: %s' % sent print SPACER if options.evaluate: for (syntree, semrep, value) in evaluations[i]: if isinstance(value, dict): value = set(value.keys()) print '%d: %s' % (n, semrep) print value n += 1 else: for (syntree, semrep) in semreps[i]: print '%d: %s' % (n, semrep) n += 1 if __name__ == "__main__": #demo() demo_legacy_grammar() nltk-2.0~b9/nltk/sem/relextract.py0000644000175000017500000003520111327451601017042 0ustar bhavanibhavani# Natural Language Toolkit: Relation Extraction # # Copyright (C) 2001-2010 NLTK Project # Author: Ewan Klein # URL: # For license information, see LICENSE.TXT """ Code for extracting relational triples from the ieer and conll2002 corpora. Relations are stored internally as dictionaries ('reldicts'). The two serialization outputs are I{rtuple} and I{clause}. - An I{rtuple} is a tuple of the form C{(subj, filler, obj)}, where C{subj} and C{obj} are pairs of Named Entity mentions, and C{filler} is the string of words occurring between C{sub} and C{obj} (with no intervening NEs). Strings are printed via C{repr()} to circumvent locale variations in rendering utf-8 encoded strings. - A I{clause} is an atom of the form C{relsym(subjsym, objsym)}, where the relation, subject and object have been canonicalized to single strings. """ # todo: get a more general solution to canonicalized symbols for clauses -- maybe use xmlcharrefs? from nltk.compat import defaultdict from string import join import re import htmlentitydefs from itertools import ifilter # Dictionary that associates corpora with NE classes NE_CLASSES = { 'ieer': ['LOCATION', 'ORGANIZATION', 'PERSON', 'DURATION', 'DATE', 'CARDINAL', 'PERCENT', 'MONEY', 'MEASURE'], 'conll2002': ['LOC', 'PER', 'ORG'], 'ace': ['LOCATION', 'ORGANIZATION', 'PERSON', 'DURATION', 'DATE', 'CARDINAL', 'PERCENT', 'MONEY', 'MEASURE', 'FACILITY', 'GPE'], } # Allow abbreviated class labels short2long = dict(LOC = 'LOCATION', ORG = 'ORGANIZATION', PER = 'PERSON') long2short = dict(LOCATION ='LOC', ORGANIZATION = 'ORG', PERSON = 'PER') def _expand(type): """ Expand an NE class name. @type type: C{str} @rtype: C{str} """ try: return short2long[type] except KeyError: return type def class_abbrev(type): """ Abbreviate an NE class name. @type type: C{str} @rtype: C{str} """ try: return long2short[type] except KeyError: return type def _join(lst, sep=' ', untag=False): """ Join a list into a string, turning tags tuples into tag strings or just words. @param untag: if C{True}, omit the tag from tagged input strings. @type lst: C{list} @rtype: C{str} """ try: return join(lst, sep=sep) except TypeError: if untag: return join([tup[0] for tup in lst], sep=sep) from nltk.tag import tuple2str return join([tuple2str(tup) for tup in lst], sep=sep) def descape_entity(m, defs=htmlentitydefs.entitydefs): """ Translate one entity to its ISO Latin value. Inspired by example from effbot.org """ #s = 'mcglashan_&_sarrail' #l = ['mcglashan', '&', 'sarrail'] #pattern = re.compile("&(\w+?);") #new = list2sym(l) #s = pattern.sub(descape_entity, s) #print s, new try: return defs[m.group(1)] except KeyError: return m.group(0) # use as is def list2sym(lst): """ Convert a list of strings into a canonical symbol. @type lst: C{list} @return: a Unicode string without whitespace @rtype: C{unicode} """ sym = _join(lst, '_', untag=True) sym = sym.lower() ENT = re.compile("&(\w+?);") sym = ENT.sub(descape_entity, sym) sym = sym.replace('.', '') return sym def mk_pairs(tree): """ Group a chunk structure into a list of pairs of the form (list(str), L{Tree}) In order to facilitate the construction of (L{Tree}, string, L{Tree}) triples, this identifies pairs whose first member is a list (possibly empty) of terminal strings, and whose second member is a L{Tree} of the form (NE_label, terminals). @param tree: a chunk tree @return: a list of pairs (list(C{str}), L{Tree}) @rtype: C{list} of C{tuple} """ from nltk.tree import Tree pairs = [] pair = [[], None] for dtr in tree: if not isinstance(dtr, Tree): pair[0].append(dtr) else: # dtr is a Tree pair[1] = dtr pairs.append(pair) pair = [[], None] return pairs def mk_reldicts(pairs, window=5, trace=0): """ Converts the pairs generated by L{mk_pairs} into a 'reldict': a dictionary which stores information about the subject and object NEs plus the filler between them. Additionally, a left and right context of length =< window are captured (within a given input sentence). @param pairs: a pair of list(str) and L{Tree}, as generated by @param window: a threshold for the number of items to include in the left and right context @type window: C{int} @return: 'relation' dictionaries whose keys are 'lcon', 'subjclass', 'subjtext', 'subjsym', 'filler', objclass', objtext', 'objsym' and 'rcon' @rtype: C{list} of C{defaultdict} """ result = [] while len(pairs) > 2: reldict = defaultdict(str) reldict['lcon'] = _join(pairs[0][0][-window:]) reldict['subjclass'] = pairs[0][1].node reldict['subjtext'] = _join(pairs[0][1].leaves()) reldict['subjsym'] = list2sym(pairs[0][1].leaves()) reldict['filler'] = _join(pairs[1][0]) reldict['objclass'] = pairs[1][1].node reldict['objtext'] = _join(pairs[1][1].leaves()) reldict['objsym'] = list2sym(pairs[1][1].leaves()) reldict['rcon'] = _join(pairs[2][0][:window]) if trace: print "(rel(%s, %s)" % (reldict['subjclass'], reldict['objclass']) result.append(reldict) pairs = pairs[1:] return result def extract_rels(subjclass, objclass, doc, corpus='ace', pattern=None, window=10): """ Filter the output of L{mk_reldicts} according to specified NE classes and a filler pattern. The parameters C{subjclass} and C{objclass} can be used to restrict the Named Entities to particular types (any of 'LOCATION', 'ORGANIZATION', 'PERSON', 'DURATION', 'DATE', 'CARDINAL', 'PERCENT', 'MONEY', 'MEASURE'). @param subjclass: the class of the subject Named Entity. @type subjclass: C{string} @param objclass: the class of the object Named Entity. @type objclass: C{string} @param doc: input document @type doc: C{ieer} document or a list of chunk trees @param corpus: name of the corpus to take as input; possible values are 'ieer' and 'conll2002' @type corpus: C{string} @param pattern: a regular expression for filtering the fillers of retrieved triples. @type pattern: C{SRE_Pattern} @param window: filters out fillers which exceed this threshold @type window: C{int} @return: see L{mk_reldicts} @rtype: C{list} of C{defaultdict} """ if subjclass and subjclass not in NE_CLASSES[corpus]: if _expand(subjclass) in NE_CLASSES[corpus]: subjclass = _expand(subjclass) else: raise ValueError, "your value for the subject type has not been recognized: %s" % subjclass if objclass and objclass not in NE_CLASSES[corpus]: if _expand(objclass) in NE_CLASSES[corpus]: objclass = _expand(objclass) else: raise ValueError, "your value for the object type has not been recognized: %s" % objclass if corpus == 'ace' or corpus == 'conll2002': pairs = mk_pairs(doc) elif corpus == 'ieer': pairs = mk_pairs(doc.text) + mk_pairs(doc.headline) else: raise ValueError, "corpus type not recognized" reldicts = mk_reldicts(pairs) relfilter = lambda x: (x['subjclass'] == subjclass and len(x['filler'].split()) <= window and pattern.match(x['filler']) and x['objclass'] == objclass) return filter(relfilter, reldicts) def show_raw_rtuple(reldict, lcon=False, rcon=False): """ Pretty print the reldict as an rtuple. @param reldict: a relation dictionary @type reldict: C{defaultdict} """ items = [class_abbrev(reldict['subjclass']), reldict['subjtext'], reldict['filler'], class_abbrev(reldict['objclass']), reldict['objtext']] format = '[%s: %r] %r [%s: %r]' if lcon: items = [reldict['lcon']] + items format = '...%r)' + format if rcon: items.append(reldict['rcon']) format = format + '(%r...' printargs = tuple(items) return format % printargs def show_clause(reldict, relsym): """ Print the relation in clausal form. @param reldict: a relation dictionary @type reldict: C{defaultdict} @param relsym: a label for the relation @type relsym: C{str} """ items = (relsym, reldict['subjsym'], reldict['objsym']) return "%s(%r, %r)" % items ####################################################### # Demos of relation extraction with regular expressions ####################################################### ############################################ # Example of in(ORG, LOC) ############################################ def in_demo(trace=0, sql=True): """ Select pairs of organizations and locations whose mentions occur with an intervening occurrence of the preposition "in". If the sql parameter is set to True, then the entity pairs are loaded into an in-memory database, and subsequently pulled out using an SQL "SELECT" query. """ from nltk.corpus import ieer if sql: try: import sqlite3 connection = sqlite3.connect(":memory:") connection.text_factory = sqlite3.OptimizedUnicode cur = connection.cursor() cur.execute("""create table Locations (OrgName text, LocationName text, DocID text)""") except ImportError: import warnings warnings.warn("Cannot import sqlite; sql flag will be ignored.") IN = re.compile(r'.*\bin\b(?!\b.+ing)') print print "IEER: in(ORG, LOC) -- just the clauses:" print "=" * 45 for file in ieer.fileids(): for doc in ieer.parsed_docs(file): if trace: print doc.docno print "=" * 15 for rel in extract_rels('ORG', 'LOC', doc, corpus='ieer', pattern=IN): print show_clause(rel, relsym='IN') if sql: try: rtuple = (rel['subjtext'], rel['objtext'], doc.docno) cur.execute("""insert into Locations values (?, ?, ?)""", rtuple) connection.commit() except NameError: pass if sql: try: cur.execute("""select OrgName from Locations where LocationName = 'Atlanta'""") print print "Extract data from SQL table: ORGs in Atlanta" print "-" * 15 for row in cur: print row except NameError: pass ############################################ # Example of has_role(PER, LOC) ############################################ def roles_demo(trace=0): from nltk.corpus import ieer roles = """ (.*( # assorted roles analyst| chair(wo)?man| commissioner| counsel| director| economist| editor| executive| foreman| governor| head| lawyer| leader| librarian).*)| manager| partner| president| producer| professor| researcher| spokes(wo)?man| writer| ,\sof\sthe?\s* # "X, of (the) Y" """ ROLES = re.compile(roles, re.VERBOSE) print print "IEER: has_role(PER, ORG) -- raw rtuples:" print "=" * 45 for file in ieer.fileids(): for doc in ieer.parsed_docs(file): lcon = rcon = False if trace: print doc.docno print "=" * 15 lcon = rcon = True for rel in extract_rels('PER', 'ORG', doc, corpus='ieer', pattern=ROLES): print show_raw_rtuple(rel, lcon=lcon, rcon=rcon) ############################################## ### Show what's in the IEER Headlines ############################################## def ieer_headlines(): from nltk.corpus import ieer from nltk.tree import Tree print "IEER: First 20 Headlines" print "=" * 45 trees = [doc.headline for file in ieer.fileids() for doc in ieer.parsed_docs(file)] for tree in trees[:20]: print print "%s:\n%s" % (doc.docno, tree) ############################################# ## Dutch CONLL2002: take_on_role(PER, ORG ############################################# def conllned(trace=1): """ Find the copula+'van' relation ('of') in the Dutch tagged training corpus from CoNLL 2002. """ from nltk.corpus import conll2002 vnv = """ ( is/V| # 3rd sing present and was/V| # past forms of the verb zijn ('be') werd/V| # and also present wordt/V # past of worden ('become) ) .* # followed by anything van/Prep # followed by van ('of') """ VAN = re.compile(vnv, re.VERBOSE) print print "Dutch CoNLL2002: van(PER, ORG) -- raw rtuples with context:" print "=" * 45 for doc in conll2002.chunked_sents('ned.train'): lcon = rcon = False if trace: lcon = rcon = True for rel in extract_rels('PER', 'ORG', doc, corpus='conll2002', pattern=VAN, window=10): print show_raw_rtuple(rel, lcon=True, rcon=True) ############################################# ## Spanish CONLL2002: (PER, ORG) ############################################# def conllesp(): from nltk.corpus import conll2002 de = """ .* ( de/SP| del/SP ) """ DE = re.compile(de, re.VERBOSE) print print "Spanish CoNLL2002: de(ORG, LOC) -- just the first 10 clauses:" print "=" * 45 rels = [rel for doc in conll2002.chunked_sents('esp.train') for rel in extract_rels('ORG', 'LOC', doc, corpus='conll2002', pattern = DE)] for r in rels[:10]: print show_clause(r, relsym='DE') print def ne_chunked(): IN = re.compile(r'.*\bin\b(?!\b.+ing)') rels = [] for sent in nltk.corpus.treebank.tagged_sents()[:100]: sent = nltk.ne_chunk(sent) print extract_rels('ORG', 'LOC', sent, corpus='ace', pattern = IN) if __name__ == '__main__': import nltk from nltk.sem import relextract in_demo(trace=0) roles_demo(trace=0) conllned() conllesp() ieer_headlines() nltk-2.0~b9/nltk/sem/logic.py0000644000175000017500000017502611377057401016001 0ustar bhavanibhavani# Natural Language Toolkit: Logic # # Author: Dan Garrette # # Copyright (C) 2001-2010 NLTK Project # URL: # For license information, see LICENSE.TXT """ A version of first order predicate logic, built on top of the typed lambda calculus. """ import re import operator from nltk.compat import defaultdict from nltk.internals import Counter _counter = Counter() class Tokens(object): # Syntaxes OLD_NLTK = 0 NLTK = 1 PROVER9 = 2 LAMBDA = ['\\', '\\', '\\'] #Quantifiers EXISTS = ['some', 'exists', 'exists', 'exist'] ALL = ['all', 'all', 'all', 'forall'] #Punctuation DOT = ['.', '.', ' '] OPEN = '(' CLOSE = ')' COMMA = ',' #Operations NOT = ['not', '-', '-', '!'] AND = ['and', '&', '&', '^'] OR = ['or', '|', '|'] IMP = ['implies', '->', '->', '=>'] IFF = ['iff', '<->', '<->', '<=>'] EQ = ['=', '=', '=', '=='] NEQ = ['!=', '!=', '!='] #Collection of tokens BINOPS = AND + OR + IMP + IFF QUANTS = EXISTS + ALL PUNCT = [DOT[NLTK], OPEN, CLOSE, COMMA] TOKENS = BINOPS + EQ + NEQ + QUANTS + LAMBDA + PUNCT + NOT #Special SYMBOLS = [x for x in TOKENS if all(c in '\\.(),-!&^|>=<' for c in x)] def boolean_ops(): """ Boolean operators """ names = ["negation", "conjunction", "disjunction", "implication", "equivalence"] for pair in zip(names, [Tokens.NOT[Tokens.NLTK], Tokens.AND[Tokens.NLTK], Tokens.OR[Tokens.NLTK], Tokens.IMP[Tokens.NLTK], Tokens.IFF[Tokens.NLTK]]): print "%-15s\t%s" % pair def equality_preds(): """ Equality predicates """ names = ["equality", "inequality"] for pair in zip(names, [Tokens.EQ[Tokens.NLTK], Tokens.NEQ[Tokens.NLTK]]): print "%-15s\t%s" % pair def binding_ops(): """ Binding operators """ names = ["existential", "universal", "lambda"] for pair in zip(names, [Tokens.EXISTS[Tokens.NLTK], Tokens.ALL[Tokens.NLTK], Tokens.LAMBDA[Tokens.NLTK]]): print "%-15s\t%s" % pair class Variable(object): def __init__(self, name): """ @param name: the name of the variable """ assert isinstance(name, str), "%s is not a string" % name self.name = name def __eq__(self, other): return isinstance(other, Variable) and self.name == other.name def __neq__(self, other): return not (self == other) def __cmp__(self, other): return cmp(type(self), type(other)) or cmp(self.name, other.name) def substitute_bindings(self, bindings): return bindings.get(self, self) def __hash__(self): return hash(self.name) def __str__(self): return self.name def __repr__(self): return 'Variable(\'' + self.name + '\')' def unique_variable(pattern=None, ignore=None): """ Return a new, unique variable. param pattern: C{Variable} that is being replaced. The new variable must be the same type. param term: a C{set} of C{Variable}s that should not be returned from this function. return: C{Variable} """ if pattern is not None: if is_indvar(pattern.name): prefix = 'z' elif is_funcvar(pattern.name): prefix = 'F' elif is_eventvar(pattern.name): prefix = 'e0' else: assert False, "Cannot generate a unique constant" else: prefix = 'z' v = Variable(prefix + str(_counter.get())) while ignore is not None and v in ignore: v = Variable(prefix + str(_counter.get())) return v def skolem_function(univ_scope=None): """ Return a skolem function over the variables in univ_scope param univ_scope """ skolem = VariableExpression(Variable('F%s' % _counter.get())) if univ_scope: for v in list(univ_scope): skolem = skolem(VariableExpression(v)) return skolem class Type(object): def __repr__(self): return str(self) def __hash__(self): return hash(str(self)) class ComplexType(Type): def __init__(self, first, second): assert(isinstance(first, Type)), "%s is not a Type" % first assert(isinstance(second, Type)), "%s is not a Type" % second self.first = first self.second = second def __eq__(self, other): return isinstance(other, ComplexType) and \ self.first == other.first and \ self.second == other.second def matches(self, other): if isinstance(other, ComplexType): return self.first.matches(other.first) and \ self.second.matches(other.second) else: return self == ANY_TYPE def resolve(self, other): if other == ANY_TYPE: return self elif isinstance(other, ComplexType): f = self.first.resolve(other.first) s = self.second.resolve(other.second) if f and s: return ComplexType(f,s) else: return None elif self == ANY_TYPE: return other else: return None def __str__(self): if self == ANY_TYPE: return str(ANY_TYPE) else: return '<%s,%s>' % (self.first, self.second) def str(self): if self == ANY_TYPE: return ANY_TYPE.str() else: return '(%s -> %s)' % (self.first.str(), self.second.str()) class BasicType(Type): def __eq__(self, other): return isinstance(other, BasicType) and str(self) == str(other) def matches(self, other): return other == ANY_TYPE or self == other def resolve(self, other): if self.matches(other): return self else: return None class EntityType(BasicType): def __str__(self): return 'e' def str(self): return 'IND' class TruthValueType(BasicType): def __str__(self): return 't' def str(self): return 'BOOL' class EventType(BasicType): def __str__(self): return 'v' def str(self): return 'EVENT' class AnyType(BasicType, ComplexType): def __init__(self): pass first = property(lambda self: self) second = property(lambda self: self) def __eq__(self, other): return isinstance(other, AnyType) or other.__eq__(self) def matches(self, other): return True def resolve(self, other): return other def __str__(self): return '?' def str(self): return 'ANY' TRUTH_TYPE = TruthValueType() ENTITY_TYPE = EntityType() EVENT_TYPE = EventType() ANY_TYPE = AnyType() def parse_type(type_string): assert isinstance(type_string, str) type_string = type_string.replace(' ', '') #remove spaces if type_string[0] == '<': assert type_string[-1] == '>' paren_count = 0 for i,char in enumerate(type_string): if char == '<': paren_count += 1 elif char == '>': paren_count -= 1 assert paren_count > 0 elif char == ',': if paren_count == 1: break return ComplexType(parse_type(type_string[1 :i ]), parse_type(type_string[i+1:-1])) elif type_string[0] == str(ENTITY_TYPE): return ENTITY_TYPE elif type_string[0] == str(TRUTH_TYPE): return TRUTH_TYPE elif type_string[0] == str(ANY_TYPE): return ANY_TYPE else: raise ParseException("Unexpected character: '%s'." % type_string[0]) class TypeException(Exception): def __init__(self, msg): Exception.__init__(self, msg) class InconsistentTypeHierarchyException(TypeException): def __init__(self, variable, expression=None): if expression: msg = "The variable \'%s\' was found in multiple places with different"\ " types in \'%s\'." % (variable, expression) else: msg = "The variable \'%s\' was found in multiple places with different"\ " types." % (variable) Exception.__init__(self, msg) class TypeResolutionException(TypeException): def __init__(self, expression, other_type): Exception.__init__(self, "The type of '%s', '%s', cannot be " "resolved with type '%s'" % \ (expression, expression.type, other_type)) class IllegalTypeException(TypeException): def __init__(self, expression, other_type, allowed_type): Exception.__init__(self, "Cannot set type of %s '%s' to '%s'; " "must match type '%s'." % (expression.__class__.__name__, expression, other_type, allowed_type)) def typecheck(expressions, signature=None): """ Ensure correct typing across a collection of C{Expression}s. @param expressions: a collection of expressions @param signature: C{dict} that maps variable names to types (or string representations of types) """ #typecheck and create master signature for expression in expressions: signature = expression.typecheck(signature) #apply master signature to all expressions for expression in expressions[:-1]: expression.typecheck(signature) return signature class SubstituteBindingsI(object): """ An interface for classes that can perform substitutions for variables. """ def substitute_bindings(self, bindings): """ @return: The object that is obtained by replacing each variable bound by C{bindings} with its values. Aliases are already resolved. (maybe?) @rtype: (any) """ raise NotImplementedError() def variables(self): """ @return: A list of all variables in this object. """ raise NotImplementedError() class Expression(SubstituteBindingsI): """This is the base abstract object for all logical expressions""" def __call__(self, other, *additional): accum = self.applyto(other) for a in additional: accum = accum(a) return accum def applyto(self, other): assert isinstance(other, Expression), "%s is not an Expression" % other return ApplicationExpression(self, other) def __neg__(self): return NegatedExpression(self) def negate(self): """If this is a negated expression, remove the negation. Otherwise add a negation.""" return -self def __and__(self, other): assert isinstance(other, Expression), "%s is not an Expression" % other return AndExpression(self, other) def __or__(self, other): assert isinstance(other, Expression), "%s is not an Expression" % other return OrExpression(self, other) def __gt__(self, other): assert isinstance(other, Expression), "%s is not an Expression" % other return ImpExpression(self, other) def __lt__(self, other): assert isinstance(other, Expression), "%s is not an Expression" % other return IffExpression(self, other) def __eq__(self, other): raise NotImplementedError() def __neq__(self, other): return not (self == other) def tp_equals(self, other, prover=None): """ Pass the expression (self <-> other) to the theorem prover. If the prover says it is valid, then the self and other are equal. @param other: an C{Expression} to check equality against @param prover: a C{nltk.inference.api.Prover} """ assert isinstance(other, Expression), "%s is not an Expression" % other if prover is None: from nltk.inference import Prover9 prover = Prover9() bicond = IffExpression(self.simplify(), other.simplify()) return prover.prove(bicond) def __hash__(self): return hash(repr(self)) def substitute_bindings(self, bindings): expr = self for var in expr.variables(): if var in bindings: val = bindings[var] if isinstance(val, Variable): val = VariableExpression(val) elif not isinstance(val, Expression): raise ValueError('Can not substitute a non-expression ' 'value into an expression: %r' % (val,)) # Substitute bindings in the target value. val = val.substitute_bindings(bindings) # Replace var w/ the target value. expr = expr.replace(var, val) return expr.simplify() def typecheck(self, signature=None): """ Infer and check types. Raise exceptions if necessary. @param signature: C{dict} that maps variable names to types (or string representations of types) @return: the signature, plus any additional type mappings """ sig = defaultdict(list) if signature: for (key, val) in signature.iteritems(): varEx = VariableExpression(Variable(key)) if isinstance(val, Type): varEx.type = val else: varEx.type = parse_type(val) sig[key].append(varEx) self._set_type(signature=sig) return dict([(key, vars[0].type) for (key, vars) in sig.iteritems()]) def findtype(self, variable): """ Find the type of the given variable as it is used in this expression. For example, finding the type of "P" in "P(x) & Q(x,y)" yields "" @param variable: C{Variable} """ raise NotImplementedError() def _set_type(self, other_type=ANY_TYPE, signature=None): """ Set the type of this expression to be the given type. Raise type exceptions where applicable. @param other_type: C{Type} to set @param signature: C{dict>} store all variable expressions with a given name """ raise NotImplementedError() def replace(self, variable, expression, replace_bound=False): """ Replace every instance of 'variable' with 'expression' @param variable: C{Variable} The variable to replace @param expression: C{Expression} The expression with which to replace it @param replace_bound: C{boolean} Should bound variables be replaced? """ assert isinstance(variable, Variable), "%s is not a Variable" % variable assert isinstance(expression, Expression), "%s is not an Expression" % expression def combinator(a, *additional): if len(additional) == 0: return self.__class__(a) elif len(additional) == 1: return self.__class__(a, additional[0]) return self.visit(lambda e: e.replace(variable, expression, replace_bound), combinator, set()) def normalize(self): """Rename auto-generated unique variables""" def f(e): if isinstance(e,Variable): if re.match(r'^z\d+$', e.name) or re.match(r'^e0\d+$', e.name): return set([e]) else: return set([]) else: combinator = lambda *parts: reduce(operator.or_, parts) return e.visit(f, combinator, set()) result = self for i,v in enumerate(sorted(list(f(self)))): if is_eventvar(v.name): newVar = 'e0%s' % (i+1) else: newVar = 'z%s' % (i+1) result = result.replace(v, self.make_VariableExpression(Variable(newVar)), True) return result def visit(self, function, combinator, default): """ Recursively visit sub expressions @param function: C{Function} to call on each sub expression @param combinator: C{Function} to combine the results of the function calls @return: result of combination """ raise NotImplementedError() def __repr__(self): return '<%s %s>' % (self.__class__.__name__, self) def __str__(self): return self.str() def variables(self): """ Return a set of all the variables that are available to be replaced. This includes free (non-bound) variables as well as predicates. @return: C{set} of C{Variable}s """ return self.visit(lambda e: isinstance(e,Variable) and set([e]) or e.variables(), lambda *parts: reduce(operator.or_, parts), set()) def free(self, indvar_only=True): """ Return a set of all the free (non-bound) variables in self. Variables serving as predicates are not included. @param indvar_only: C{boolean} only return individual variables? @return: C{set} of C{Variable}s """ return self.visit(lambda e: isinstance(e,Variable) and set([e]) or e.free(indvar_only), lambda *parts: reduce(operator.or_, parts), set()) def simplify(self): """ @return: beta-converted version of this expression """ def combinator(a, *additional): if len(additional) == 0: return self.__class__(a) elif len(additional) == 1: return self.__class__(a, additional[0]) return self.visit(lambda e: isinstance(e,Variable) and e or e.simplify(), combinator, set()) def make_VariableExpression(self, variable): return VariableExpression(variable) class ApplicationExpression(Expression): r""" This class is used to represent two related types of logical expressions. The first is a Predicate Expression, such as "P(x,y)". A predicate expression is comprised of a C{FunctionVariableExpression} or C{ConstantExpression} as the predicate and a list of Expressions as the arguments. The second is a an application of one expression to another, such as "(\x.dog(x))(fido)". The reason Predicate Expressions are treated as Application Expressions is that the Variable Expression predicate of the expression may be replaced with another Expression, such as a LambdaExpression, which would mean that the Predicate should be thought of as being applied to the arguments. The LogicParser will always curry arguments in a application expression. So, "\x y.see(x,y)(john,mary)" will be represented internally as "((\x y.(see(x))(y))(john))(mary)". This simplifies the internals since there will always be exactly one argument in an application. The str() method will usually print the curried forms of application expressions. The one exception is when the the application expression is really a predicate expression (ie, underlying function is an C{AbstractVariableExpression}). This means that the example from above will be returned as "(\x y.see(x,y)(john))(mary)". """ def __init__(self, function, argument): """ @param function: C{Expression}, for the function expression @param argument: C{Expression}, for the argument """ assert isinstance(function, Expression), "%s is not an Expression" % function assert isinstance(argument, Expression), "%s is not an Expression" % argument self.function = function self.argument = argument def simplify(self): function = self.function.simplify() argument = self.argument.simplify() if isinstance(function, LambdaExpression): return function.term.replace(function.variable, argument).simplify() else: return self.__class__(function, argument) def free(self, indvar_only=True): """@see: Expression.free()""" if isinstance(self.function, AbstractVariableExpression): return self.argument.free(indvar_only) else: return self.function.free(indvar_only) | \ self.argument.free(indvar_only) def _get_type(self): if isinstance(self.function.type, ComplexType): return self.function.type.second else: return ANY_TYPE type = property(_get_type) def _set_type(self, other_type=ANY_TYPE, signature=None): """@see Expression._set_type()""" assert isinstance(other_type, Type) if signature == None: signature = defaultdict(list) self.argument._set_type(ANY_TYPE, signature) try: self.function._set_type(ComplexType(self.argument.type, other_type), signature) except TypeResolutionException: raise TypeException( "The function '%s' is of type '%s' and cannot be applied " "to '%s' of type '%s'. Its argument must match type '%s'." % (self.function, self.function.type, self.argument, self.argument.type, self.function.type.first)) def findtype(self, variable): """@see Expression.findtype()""" assert isinstance(variable, Variable), "%s is not a Variable" % variable function, args = self.uncurry() if not isinstance(function, AbstractVariableExpression): #It's not a predicate expression ("P(x,y)"), so leave args curried function = self.function args = [self.argument] found = [arg.findtype(variable) for arg in [function]+args] unique = [] for f in found: if f != ANY_TYPE: if unique: for u in unique: if f.matches(u): break else: unique.append(f) if len(unique) == 1: return list(unique)[0] else: return ANY_TYPE def visit(self, function, combinator, default): """@see: Expression.visit()""" return combinator(function(self.function), function(self.argument)) def __eq__(self, other): return isinstance(other, ApplicationExpression) and \ self.function == other.function and \ self.argument == other.argument def str(self, syntax=Tokens.NLTK): # uncurry the arguments and find the base function function, args = self.uncurry() if isinstance(function, AbstractVariableExpression): #It's a predicate expression ("P(x,y)"), so uncurry arguments arg_str = ','.join([arg.str(syntax) for arg in args]) else: #Leave arguments curried function = self.function arg_str = self.argument.str(syntax) function_str = function.str(syntax) parenthesize_function = False if isinstance(function, LambdaExpression): if isinstance(function.term, ApplicationExpression): if not isinstance(function.term.function, AbstractVariableExpression): parenthesize_function = True elif not isinstance(function.term, BooleanExpression): parenthesize_function = True elif isinstance(function, ApplicationExpression): parenthesize_function = True if parenthesize_function: function_str = Tokens.OPEN + function_str + Tokens.CLOSE return function_str + Tokens.OPEN + arg_str + Tokens.CLOSE def uncurry(self): """ Uncurry this application expression return: A tuple (base-function, arg-list) """ function = self.function args = [self.argument] while isinstance(function, ApplicationExpression): #(\x.\y.sees(x,y)(john))(mary) args.insert(0, function.argument) function = function.function return (function, args) args = property(lambda self: self.uncurry()[1], doc="uncurried arg-list") class AbstractVariableExpression(Expression): """This class represents a variable to be used as a predicate or entity""" def __init__(self, variable): """ @param variable: C{Variable}, for the variable """ assert isinstance(variable, Variable), "%s is not a Variable" % variable self.variable = variable def simplify(self): return self def replace(self, variable, expression, replace_bound=False): """@see: Expression.replace()""" assert isinstance(variable, Variable), "%s is not an Variable" % variable assert isinstance(expression, Expression), "%s is not an Expression" % expression if self.variable == variable: return expression else: return self def _set_type(self, other_type=ANY_TYPE, signature=None): """@see Expression._set_type()""" assert isinstance(other_type, Type) if signature == None: signature = defaultdict(list) resolution = other_type for varEx in signature[self.variable.name]: resolution = varEx.type.resolve(resolution) if not resolution: raise InconsistentTypeHierarchyException(self) signature[self.variable.name].append(self) for varEx in signature[self.variable.name]: varEx.type = resolution def findtype(self, variable): """@see Expression.findtype()""" assert isinstance(variable, Variable), "%s is not a Variable" % variable if self.variable == variable: return self.type else: return ANY_TYPE def visit(self, function, combinator, default): """@see: Expression.visit()""" return combinator(function(self.variable)) def __eq__(self, other): """Allow equality between instances of C{AbstractVariableExpression} subtypes.""" return isinstance(other, AbstractVariableExpression) and \ self.variable == other.variable def str(self, syntax=Tokens.NLTK): return str(self.variable) class IndividualVariableExpression(AbstractVariableExpression): """This class represents variables that take the form of a single lowercase character (other than 'e') followed by zero or more digits.""" def _set_type(self, other_type=ANY_TYPE, signature=None): """@see Expression._set_type()""" assert isinstance(other_type, Type) if signature == None: signature = defaultdict(list) if not other_type.matches(ENTITY_TYPE): raise IllegalTypeException(self, other_type, ENTITY_TYPE) signature[self.variable.name].append(self) type = property(lambda self: ENTITY_TYPE, _set_type) class FunctionVariableExpression(AbstractVariableExpression): """This class represents variables that take the form of a single uppercase character followed by zero or more digits.""" type = ANY_TYPE def free(self, indvar_only=True): """@see: Expression.free()""" if not indvar_only: return set([self.variable]) else: return set() class EventVariableExpression(IndividualVariableExpression): """This class represents variables that take the form of a single lowercase 'e' character followed by zero or more digits.""" type = EVENT_TYPE class ConstantExpression(AbstractVariableExpression): """This class represents variables that do not take the form of a single character followed by zero or more digits.""" type = ENTITY_TYPE def _set_type(self, other_type=ANY_TYPE, signature=None): """@see Expression._set_type()""" assert isinstance(other_type, Type) if signature == None: signature = defaultdict(list) if other_type == ANY_TYPE: #entity type by default, for individuals resolution = ENTITY_TYPE else: resolution = other_type if self.type != ENTITY_TYPE: resolution = resolution.resolve(self.type) for varEx in signature[self.variable.name]: resolution = varEx.type.resolve(resolution) if not resolution: raise InconsistentTypeHierarchyException(self) signature[self.variable.name].append(self) for varEx in signature[self.variable.name]: varEx.type = resolution def free(self, indvar_only=True): """@see: Expression.free()""" if not indvar_only: return set([self.variable]) else: return set() def VariableExpression(variable): """ This is a factory method that instantiates and returns a subtype of C{AbstractVariableExpression} appropriate for the given variable. """ assert isinstance(variable, Variable), "%s is not a Variable" % variable if is_indvar(variable.name): return IndividualVariableExpression(variable) elif is_funcvar(variable.name): return FunctionVariableExpression(variable) elif is_eventvar(variable.name): return EventVariableExpression(variable) else: return ConstantExpression(variable) class VariableBinderExpression(Expression): """This an abstract class for any Expression that binds a variable in an Expression. This includes LambdaExpressions and Quantified Expressions""" def __init__(self, variable, term): """ @param variable: C{Variable}, for the variable @param term: C{Expression}, for the term """ assert isinstance(variable, Variable), "%s is not a Variable" % variable assert isinstance(term, Expression), "%s is not an Expression" % term self.variable = variable self.term = term def replace(self, variable, expression, replace_bound=False): """@see: Expression.replace()""" assert isinstance(variable, Variable), "%s is not a Variable" % variable assert isinstance(expression, Expression), "%s is not an Expression" % expression #if the bound variable is the thing being replaced if self.variable == variable: if replace_bound: assert isinstance(expression, AbstractVariableExpression),\ "%s is not a AbstractVariableExpression" % expression return self.__class__(expression.variable, self.term.replace(variable, expression, True)) else: return self else: # if the bound variable appears in the expression, then it must # be alpha converted to avoid a conflict if self.variable in expression.free(): self = self.alpha_convert(unique_variable(pattern=self.variable)) #replace in the term return self.__class__(self.variable, self.term.replace(variable, expression, replace_bound)) def alpha_convert(self, newvar): """Rename all occurrences of the variable introduced by this variable binder in the expression to @C{newvar}. @param newvar: C{Variable}, for the new variable """ assert isinstance(newvar, Variable), "%s is not a Variable" % newvar return self.__class__(newvar, self.term.replace(self.variable, VariableExpression(newvar), True)) def variables(self): """@see: Expression.variables()""" return self.term.variables() - set([self.variable]) def free(self, indvar_only=True): """@see: Expression.free()""" return self.term.free(indvar_only) - set([self.variable]) def findtype(self, variable): """@see Expression.findtype()""" assert isinstance(variable, Variable), "%s is not a Variable" % variable if variable == self.variable: return ANY_TYPE else: return self.term.findtype(variable) def visit(self, function, combinator, default): """@see: Expression.visit()""" return combinator(function(self.variable), function(self.term)) def __eq__(self, other): r"""Defines equality modulo alphabetic variance. If we are comparing \x.M and \y.N, then check equality of M and N[x/y].""" if isinstance(self, other.__class__) or \ isinstance(other, self.__class__): if self.variable == other.variable: return self.term == other.term else: # Comparing \x.M and \y.N. Relabel y in N with x and continue. varex = VariableExpression(self.variable) return self.term == other.term.replace(other.variable, varex) else: return False class LambdaExpression(VariableBinderExpression): type = property(lambda self: ComplexType(self.term.findtype(self.variable), self.term.type)) def _set_type(self, other_type=ANY_TYPE, signature=None): """@see Expression._set_type()""" assert isinstance(other_type, Type) if signature == None: signature = defaultdict(list) self.term._set_type(other_type.second, signature) if not self.type.resolve(other_type): raise TypeResolutionException(self, other_type) def str(self, syntax=Tokens.NLTK): variables = [self.variable] term = self.term if syntax != Tokens.PROVER9: while term.__class__ == self.__class__: variables.append(term.variable) term = term.term return Tokens.LAMBDA[syntax] + ' '.join(str(v) for v in variables) + \ Tokens.DOT[syntax] + term.str(syntax) class QuantifiedExpression(VariableBinderExpression): type = property(lambda self: TRUTH_TYPE) def _set_type(self, other_type=ANY_TYPE, signature=None): """@see Expression._set_type()""" assert isinstance(other_type, Type) if signature == None: signature = defaultdict(list) if not other_type.matches(TRUTH_TYPE): raise IllegalTypeException(self, other_type, TRUTH_TYPE) self.term._set_type(TRUTH_TYPE, signature) def str(self, syntax=Tokens.NLTK): variables = [self.variable] term = self.term if syntax != Tokens.PROVER9: while term.__class__ == self.__class__: variables.append(term.variable) term = term.term return self.getQuantifier(syntax) + ' ' + \ ' '.join(str(v) for v in variables) + \ Tokens.DOT[syntax] + term.str(syntax) class ExistsExpression(QuantifiedExpression): def getQuantifier(self, syntax=Tokens.NLTK): return Tokens.EXISTS[syntax] class AllExpression(QuantifiedExpression): def getQuantifier(self, syntax=Tokens.NLTK): return Tokens.ALL[syntax] class NegatedExpression(Expression): def __init__(self, term): assert isinstance(term, Expression), "%s is not an Expression" % term self.term = term type = property(lambda self: TRUTH_TYPE) def _set_type(self, other_type=ANY_TYPE, signature=None): """@see Expression._set_type()""" assert isinstance(other_type, Type) if signature == None: signature = defaultdict(list) if not other_type.matches(TRUTH_TYPE): raise IllegalTypeException(self, other_type, TRUTH_TYPE) self.term._set_type(TRUTH_TYPE, signature) def findtype(self, variable): assert isinstance(variable, Variable), "%s is not a Variable" % variable return self.term.findtype(variable) def visit(self, function, combinator, default): """@see: Expression.visit()""" return combinator(function(self.term)) def negate(self): """@see: Expression.negate()""" return self.term def __eq__(self, other): return isinstance(other, NegatedExpression) and self.term == other.term def str(self, syntax=Tokens.NLTK): if syntax == Tokens.PROVER9: return Tokens.NOT[syntax] + Tokens.OPEN + self.term.str(syntax) +\ Tokens.CLOSE else: return Tokens.NOT[syntax] + self.term.str(syntax) class BinaryExpression(Expression): def __init__(self, first, second): assert isinstance(first, Expression), "%s is not an Expression" % first assert isinstance(second, Expression), "%s is not an Expression" % second self.first = first self.second = second type = property(lambda self: TRUTH_TYPE) def findtype(self, variable): """@see Expression.findtype()""" assert isinstance(variable, Variable), "%s is not a Variable" % variable f = self.first.findtype(variable) s = self.second.findtype(variable) if f == s or s == ANY_TYPE: return f elif f == ANY_TYPE: return s else: return ANY_TYPE def visit(self, function, combinator, default): """@see: Expression.visit()""" return combinator(function(self.first), function(self.second)) def __eq__(self, other): return (isinstance(self, other.__class__) or \ isinstance(other, self.__class__)) and \ self.first == other.first and self.second == other.second def str(self, syntax=Tokens.NLTK): return Tokens.OPEN + self.first.str(syntax) + ' ' + self.getOp(syntax) \ + ' ' + self.second.str(syntax) + Tokens.CLOSE class BooleanExpression(BinaryExpression): def _set_type(self, other_type=ANY_TYPE, signature=None): """@see Expression._set_type()""" assert isinstance(other_type, Type) if signature == None: signature = defaultdict(list) if not other_type.matches(TRUTH_TYPE): raise IllegalTypeException(self, other_type, TRUTH_TYPE) self.first._set_type(TRUTH_TYPE, signature) self.second._set_type(TRUTH_TYPE, signature) class AndExpression(BooleanExpression): """This class represents conjunctions""" def getOp(self, syntax=Tokens.NLTK): return Tokens.AND[syntax] class OrExpression(BooleanExpression): """This class represents disjunctions""" def getOp(self, syntax=Tokens.NLTK): return Tokens.OR[syntax] class ImpExpression(BooleanExpression): """This class represents implications""" def getOp(self, syntax=Tokens.NLTK): return Tokens.IMP[syntax] class IffExpression(BooleanExpression): """This class represents biconditionals""" def getOp(self, syntax=Tokens.NLTK): return Tokens.IFF[syntax] class EqualityExpression(BinaryExpression): """This class represents equality expressions like "(x = y)".""" def _set_type(self, other_type=ANY_TYPE, signature=None): """@see Expression._set_type()""" assert isinstance(other_type, Type) if signature == None: signature = defaultdict(list) if not other_type.matches(TRUTH_TYPE): raise IllegalTypeException(self, other_type, TRUTH_TYPE) self.first._set_type(ENTITY_TYPE, signature) self.second._set_type(ENTITY_TYPE, signature) def getOp(self, syntax=Tokens.NLTK): return Tokens.EQ[syntax] class LogicParser(object): """A lambda calculus expression parser.""" def __init__(self, type_check=False): """ @param type_check: C{boolean} should type checking be performed? to their types. """ assert isinstance(type_check, bool) self._currentIndex = 0 self._buffer = [] self.type_check = type_check """A list of tuples of quote characters. The 4-tuple is comprised of the start character, the end character, the escape character, and a boolean indicating whether the quotes should be included in the result. Quotes are used to signify that a token should be treated as atomic, ignoring any special characters within the token. The escape character allows the quote end character to be used within the quote. If True, the boolean indicates that the final token should contain the quote and escape characters. This method exists to be overridden""" self.quote_chars = [] self.order_of_operations = dict( [(x,1) for x in Tokens.LAMBDA] + \ [(x,2) for x in Tokens.NOT] + \ [('APP',3)] + \ [(x,4) for x in Tokens.EQ+Tokens.NEQ] + \ [(x,5) for x in Tokens.QUANTS] + \ [(x,6) for x in Tokens.AND] + \ [(x,7) for x in Tokens.OR] + \ [(x,8) for x in Tokens.IMP] + \ [(x,9) for x in Tokens.IFF] + \ [(None,10)]) self.right_associated_operations = [] def parse(self, data, signature=None): """ Parse the expression. @param data: C{str} for the input to be parsed @param signature: C{dict} that maps variable names to type strings @returns: a parsed Expression """ data = data.rstrip() self._currentIndex = 0 self._buffer, mapping = self.process(data) try: result = self.parse_Expression(None) if self.inRange(0): raise UnexpectedTokenException(self._currentIndex+1, self.token(0)) except ParseException, e: msg = str(e) + '\n' + \ data + '\n' + \ ' '*mapping[e.index-1] + '^' raise ParseException(None, msg) if self.type_check: result.typecheck(signature) return result def process(self, data): """Split the data into tokens""" out = [] mapping = {} tokenTrie = StringTrie(self.get_all_symbols()) token = '' data_idx = 0 token_start_idx = data_idx while data_idx < len(data): cur_data_idx = data_idx quoted_token, data_idx = self.process_quoted_token(data_idx, data) if quoted_token: if not token: token_start_idx = cur_data_idx token += quoted_token continue st = tokenTrie c = data[data_idx] symbol = '' while c in st: symbol += c st = st[c] if len(data)-data_idx > len(symbol): c = data[data_idx+len(symbol)] else: break if StringTrie.LEAF in st: #token is a complete symbol if token: mapping[len(out)] = token_start_idx out.append(token) token = '' mapping[len(out)] = data_idx out.append(symbol) data_idx += len(symbol) else: if data[data_idx] in ' \t\n': #any whitespace if token: mapping[len(out)] = token_start_idx out.append(token) token = '' else: if not token: token_start_idx = data_idx token += data[data_idx] data_idx += 1 if token: mapping[len(out)] = token_start_idx out.append(token) mapping[len(out)] = len(data) mapping[len(out)+1] = len(data)+1 return out, mapping def process_quoted_token(self, data_idx, data): token = '' c = data[data_idx] i = data_idx for start, end, escape, incl_quotes in self.quote_chars: if c == start: if incl_quotes: token += c i += 1 while data[i] != end: if data[i] == escape: if incl_quotes: token += data[i] i += 1 if len(data) == i: #if there are no more chars raise ParseException(None, "End of input reached. " "Escape character [%s] found at end." % escape) token += data[i] else: token += data[i] i += 1 if len(data) == i: raise ParseException(None, "End of input reached. " "Expected: [%s]" % end) if incl_quotes: token += data[i] i += 1 if not token: raise ParseException(None, 'Empty quoted token found') break return token, i def get_all_symbols(self): """This method exists to be overridden""" return Tokens.SYMBOLS def inRange(self, location): """Return TRUE if the given location is within the buffer""" return self._currentIndex+location < len(self._buffer) def token(self, location=None): """Get the next waiting token. If a location is given, then return the token at currentIndex+location without advancing currentIndex; setting it gives lookahead/lookback capability.""" try: if location == None: tok = self._buffer[self._currentIndex] self._currentIndex += 1 else: tok = self._buffer[self._currentIndex+location] return tok except IndexError: raise ExpectedMoreTokensException(self._currentIndex+1) def isvariable(self, tok): return tok not in Tokens.TOKENS def parse_Expression(self, context): """Parse the next complete expression from the stream and return it.""" try: tok = self.token() except ExpectedMoreTokensException: raise ExpectedMoreTokensException(self._currentIndex+1, message='Expression expected.') accum = self.handle(tok, context) if not accum: raise UnexpectedTokenException(self._currentIndex, tok, message='Expression expected.') return self.attempt_adjuncts(accum, context) def handle(self, tok, context): """This method is intended to be overridden for logics that use different operators or expressions""" if self.isvariable(tok): return self.handle_variable(tok, context) elif tok in Tokens.NOT: return self.handle_negation(tok, context) elif tok in Tokens.LAMBDA: return self.handle_lambda(tok, context) elif tok in Tokens.QUANTS: return self.handle_quant(tok, context) elif tok == Tokens.OPEN: return self.handle_open(tok, context) def attempt_adjuncts(self, expression, context): cur_idx = None while cur_idx != self._currentIndex: #while adjuncts are added cur_idx = self._currentIndex expression = self.attempt_EqualityExpression(expression, context) expression = self.attempt_ApplicationExpression(expression, context) expression = self.attempt_BooleanExpression(expression, context) return expression def handle_negation(self, tok, context): return self.make_NegatedExpression(self.parse_Expression(Tokens.NOT[Tokens.NLTK])) def make_NegatedExpression(self, expression): return NegatedExpression(expression) def handle_variable(self, tok, context): #It's either: 1) a predicate expression: sees(x,y) # 2) an application expression: P(x) # 3) a solo variable: john OR x accum = self.make_VariableExpression(tok) if self.inRange(0) and self.token(0) == Tokens.OPEN: #The predicate has arguments if isinstance(accum, IndividualVariableExpression): raise ParseException(self._currentIndex, '\'%s\' is an illegal predicate name. ' 'Individual variables may not be used as ' 'predicates.' % tok) self.token() #swallow the Open Paren #curry the arguments accum = self.make_ApplicationExpression(accum, self.parse_Expression('APP')) while self.inRange(0) and self.token(0) == Tokens.COMMA: self.token() #swallow the comma accum = self.make_ApplicationExpression(accum, self.parse_Expression('APP')) self.assertNextToken(Tokens.CLOSE) return accum def get_next_token_variable(self, description): try: tok = self.token() except ExpectedMoreTokensException, e: raise ExpectedMoreTokensException(e.index, 'Variable expected.') if isinstance(self.make_VariableExpression(tok), ConstantExpression): raise ParseException(self._currentIndex, '\'%s\' is an illegal variable name. ' 'Constants may not be %s.' % (tok, description)) return Variable(tok) def handle_lambda(self, tok, context): # Expression is a lambda expression if not self.inRange(0): raise ExpectedMoreTokensException(self._currentIndex+2, message="Variable and Expression expected following lambda operator.") vars = [self.get_next_token_variable('abstracted')] while True: if not self.inRange(0) or (self.token(0) in Tokens.DOT and not self.inRange(1)): raise ExpectedMoreTokensException(self._currentIndex+2, message="Expression expected.") if not self.isvariable(self.token(0)): break # Support expressions like: \x y.M == \x.\y.M vars.append(self.get_next_token_variable('abstracted')) if self.inRange(0) and self.token(0) in Tokens.DOT: self.token() #swallow the dot accum = self.parse_Expression(tok) while vars: accum = self.make_LambdaExpression(vars.pop(), accum) return accum def handle_quant(self, tok, context): # Expression is a quantified expression: some x.M factory = self.get_QuantifiedExpression_factory(tok) if not self.inRange(0): raise ExpectedMoreTokensException(self._currentIndex+2, message="Variable and Expression expected following quantifier '%s'." % tok) vars = [self.get_next_token_variable('quantified')] while True: if not self.inRange(0) or (self.token(0) in Tokens.DOT and not self.inRange(1)): raise ExpectedMoreTokensException(self._currentIndex+2, message="Expression expected.") if not self.isvariable(self.token(0)): break # Support expressions like: some x y.M == some x.some y.M vars.append(self.get_next_token_variable('quantified')) if self.inRange(0) and self.token(0) in Tokens.DOT: self.token() #swallow the dot accum = self.parse_Expression(tok) while vars: accum = self.make_QuanifiedExpression(factory, vars.pop(), accum) return accum def get_QuantifiedExpression_factory(self, tok): """This method serves as a hook for other logic parsers that have different quantifiers""" if tok in Tokens.EXISTS: return ExistsExpression elif tok in Tokens.ALL: return AllExpression else: self.assertToken(tok, Tokens.QUANTS) def make_QuanifiedExpression(self, factory, variable, term): return factory(variable, term) def handle_open(self, tok, context): #Expression is in parens accum = self.parse_Expression(None) self.assertNextToken(Tokens.CLOSE) return accum def attempt_EqualityExpression(self, expression, context): """Attempt to make an equality expression. If the next token is an equality operator, then an EqualityExpression will be returned. Otherwise, the parameter will be returned.""" if self.inRange(0): tok = self.token(0) if tok in Tokens.EQ + Tokens.NEQ and self.has_priority(tok, context): self.token() #swallow the "=" or "!=" expression = self.make_EqualityExpression(expression, self.parse_Expression(tok)) if tok in Tokens.NEQ: expression = self.make_NegatedExpression(expression) return expression def make_EqualityExpression(self, first, second): """This method serves as a hook for other logic parsers that have different equality expression classes""" return EqualityExpression(first, second) def attempt_BooleanExpression(self, expression, context): """Attempt to make a boolean expression. If the next token is a boolean operator, then a BooleanExpression will be returned. Otherwise, the parameter will be returned.""" while self.inRange(0): tok = self.token(0) factory = self.get_BooleanExpression_factory(tok) if factory and self.has_priority(tok, context): self.token() #swallow the operator expression = self.make_BooleanExpression(factory, expression, self.parse_Expression(tok)) else: break return expression def get_BooleanExpression_factory(self, tok): """This method serves as a hook for other logic parsers that have different boolean operators""" if tok in Tokens.AND: return AndExpression elif tok in Tokens.OR: return OrExpression elif tok in Tokens.IMP: return ImpExpression elif tok in Tokens.IFF: return IffExpression else: return None def make_BooleanExpression(self, factory, first, second): return factory(first, second) def attempt_ApplicationExpression(self, expression, context): """Attempt to make an application expression. The next tokens are a list of arguments in parens, then the argument expression is a function being applied to the arguments. Otherwise, return the argument expression.""" if self.has_priority('APP', context): if self.inRange(0) and self.token(0) == Tokens.OPEN: if not isinstance(expression, LambdaExpression) and \ not isinstance(expression, ApplicationExpression): raise ParseException(self._currentIndex, "The function '" + str(expression) + ' is not a Lambda Expression or an ' 'Application Expression, so it may ' 'not take arguments.') self.token() #swallow then open paren #curry the arguments accum = self.make_ApplicationExpression(expression, self.parse_Expression('APP')) while self.inRange(0) and self.token(0) == Tokens.COMMA: self.token() #swallow the comma accum = self.make_ApplicationExpression(accum, self.parse_Expression('APP')) self.assertNextToken(Tokens.CLOSE) return accum return expression def make_ApplicationExpression(self, function, argument): return ApplicationExpression(function, argument) def make_VariableExpression(self, name): return VariableExpression(Variable(name)) def make_LambdaExpression(self, variable, term): return LambdaExpression(variable, term) def has_priority(self, operation, context): return self.order_of_operations[operation] < self.order_of_operations[context] or \ (operation in self.right_associated_operations and \ self.order_of_operations[operation] == self.order_of_operations[context]) def assertNextToken(self, expected): try: tok = self.token() except ExpectedMoreTokensException, e: raise ExpectedMoreTokensException(e.index, message="Expected token '%s'." % expected) if isinstance(expected, list): if tok not in expected: raise UnexpectedTokenException(self._currentIndex, tok, expected) else: if tok != expected: raise UnexpectedTokenException(self._currentIndex, tok, expected) def assertToken(self, tok, expected): if isinstance(expected, list): if tok not in expected: raise UnexpectedTokenException(self._currentIndex, tok, expected) else: if tok != expected: raise UnexpectedTokenException(self._currentIndex, tok, expected) def __repr__(self): if self.inRange(0): msg = 'Next token: ' + self.token(0) else: msg = 'No more tokens' return '<' + self.__class__.__name__ + ': ' + msg + '>' class StringTrie(defaultdict): LEAF = "" def __init__(self, strings=None): defaultdict.__init__(self, StringTrie) if strings: for string in strings: self.insert(string) def insert(self, string): if len(string): self[string[0]].insert(string[1:]) else: #mark the string is complete self[StringTrie.LEAF] = None class ParseException(Exception): def __init__(self, index, message): self.index = index Exception.__init__(self, message) class UnexpectedTokenException(ParseException): def __init__(self, index, unexpected=None, expected=None, message=None): if unexpected and expected: msg = "Unexpected token: '%s'. " \ "Expected token '%s'." % (unexpected, expected) elif unexpected: msg = "Unexpected token: '%s'." % unexpected if message: msg += ' '+message else: msg = "Expected token '%s'." % expected ParseException.__init__(self, index, msg) class ExpectedMoreTokensException(ParseException): def __init__(self, index, message=None): if not message: message = 'More tokens expected.' ParseException.__init__(self, index, 'End of input found. ' + message) def is_indvar(expr): """ An individual variable must be a single lowercase character other than 'e', followed by zero or more digits. @param expr: C{str} @return: C{boolean} True if expr is of the correct form """ assert isinstance(expr, str), "%s is not a string" % expr return re.match(r'^[a-df-z]\d*$', expr) def is_funcvar(expr): """ A function variable must be a single uppercase character followed by zero or more digits. @param expr: C{str} @return: C{boolean} True if expr is of the correct form """ assert isinstance(expr, str), "%s is not a string" % expr return re.match(r'^[A-Z]\d*$', expr) def is_eventvar(expr): """ An event variable must be a single lowercase 'e' character followed by zero or more digits. @param expr: C{str} @return: C{boolean} True if expr is of the correct form """ assert isinstance(expr, str), "%s is not a string" % expr return re.match(r'^e\d*$', expr) def demo(): p = LogicParser().parse print '='*20 + 'Test parser' + '='*20 print p(r'john') print p(r'man(x)') print p(r'-man(x)') print p(r'(man(x) & tall(x) & walks(x))') print p(r'exists x.(man(x) & tall(x) & walks(x))') print p(r'\x.man(x)') print p(r'\x.man(x)(john)') print p(r'\x y.sees(x,y)') print p(r'\x y.sees(x,y)(a,b)') print p(r'(\x.exists y.walks(x,y))(x)') print p(r'exists x.x = y') print p(r'exists x.(x = y)') print p('P(x) & x=y & P(y)') print p(r'\P Q.exists x.(P(x) & Q(x))') print p(r'man(x) <-> tall(x)') print '='*20 + 'Test simplify' + '='*20 print p(r'\x.\y.sees(x,y)(john)(mary)').simplify() print p(r'\x.\y.sees(x,y)(john, mary)').simplify() print p(r'all x.(man(x) & (\x.exists y.walks(x,y))(x))').simplify() print p(r'(\P.\Q.exists x.(P(x) & Q(x)))(\x.dog(x))(\x.bark(x))').simplify() print '='*20 + 'Test alpha conversion and binder expression equality' + '='*20 e1 = p('exists x.P(x)') print e1 e2 = e1.alpha_convert(Variable('z')) print e2 print e1 == e2 def demo_errors(): print '='*20 + 'Test parser errors' + '='*20 demoException('(P(x) & Q(x)') demoException('((P(x) &) & Q(x))') demoException('P(x) -> ') demoException('P(x') demoException('P(x,') demoException('P(x,)') demoException('exists') demoException('exists x.') demoException('\\') demoException('\\ x y.') demoException('P(x)Q(x)') demoException('(P(x)Q(x)') demoException('exists x -> y') def demoException(s): try: LogicParser().parse(s) except ParseException, e: print "%s: %s" % (e.__class__.__name__, e) def printtype(ex): print ex.str() + ' : ' + str(ex.type) if __name__ == '__main__': demo() demo_errors() nltk-2.0~b9/nltk/sem/linearlogic.py0000644000175000017500000003722711377057401017174 0ustar bhavanibhavani# Natural Language Toolkit: Linear Logic # # Author: Dan Garrette # # Copyright (C) 2001-2010 NLTK Project # URL: # For license information, see LICENSE.TXT from nltk.internals import Counter from logic import LogicParser _counter = Counter() class Expression(object): def applyto(self, other, other_indices=None): return ApplicationExpression(self, other, other_indices) def __call__(self, other): return self.applyto(other) def __repr__(self): return '<' + self.__class__.__name__ + ' ' + str(self) + '>' class AtomicExpression(Expression): def __init__(self, name, dependencies=None): """ @param name: C{str} for the constant name @param dependencies: C{list} of C{int} for the indices on which this atom is dependent """ assert isinstance(name, str) self.name = name if not dependencies: dependencies = [] self.dependencies = dependencies def simplify(self, bindings=None): """ If 'self' is bound by 'bindings', return the atomic to which it is bound. Otherwise, return self. @param bindings: C{BindingDict} A dictionary of bindings used to simplify @return: C{AtomicExpression} """ if bindings and self in bindings: return bindings[self] else: return self def compile_pos(self, index_counter, glueFormulaFactory): """ From Iddo Lev's PhD Dissertation p108-109 @param index_counter: C{Counter} for unique indices @param glueFormulaFactory: C{GlueFormula} for creating new glue formulas @return: (C{Expression},C{set}) for the compiled linear logic and any newly created glue formulas """ self.dependencies = [] return (self, []) def compile_neg(self, index_counter, glueFormulaFactory): """ From Iddo Lev's PhD Dissertation p108-109 @param index_counter: C{Counter} for unique indices @param glueFormulaFactory: C{GlueFormula} for creating new glue formulas @return: (C{Expression},C{set}) for the compiled linear logic and any newly created glue formulas """ self.dependencies = [] return (self, []) def initialize_labels(self, fstruct): self.name = fstruct.initialize_label(self.name.lower()) def __eq__(self, other): return self.__class__ == other.__class__ and self.name == other.name def __str__(self): accum = self.name if self.dependencies: accum += str(self.dependencies) return accum def __hash__(self): return hash(self.name) class ConstantExpression(AtomicExpression): def unify(self, other, bindings): """ If 'other' is a constant, then it must be equal to 'self'. If 'other' is a variable, then it must not be bound to anything other than 'self'. @param other: C{Expression} @param bindings: C{BindingDict} A dictionary of all current bindings @return: C{BindingDict} A new combined dictionary of of 'bindings' and any new binding @raise UnificationException: If 'self' and 'other' cannot be unified in the context of 'bindings' """ assert isinstance(other, Expression) if isinstance(other, VariableExpression): try: return bindings + BindingDict([(other, self)]) except VariableBindingException: pass elif self == other: return bindings raise UnificationException(self, other, bindings) class VariableExpression(AtomicExpression): def unify(self, other, bindings): """ 'self' must not be bound to anything other than 'other'. @param other: C{Expression} @param bindings: C{BindingDict} A dictionary of all current bindings @return: C{BindingDict} A new combined dictionary of of 'bindings' and the new binding @raise UnificationException: If 'self' and 'other' cannot be unified in the context of 'bindings' """ assert isinstance(other, Expression) try: if self == other: return bindings else: return bindings + BindingDict([(self, other)]) except VariableBindingException: raise UnificationException(self, other, bindings) class ImpExpression(Expression): def __init__(self, antecedent, consequent): """ @param antecedent: C{Expression} for the antecedent @param consequent: C{Expression} for the consequent """ assert isinstance(antecedent, Expression) assert isinstance(consequent, Expression) self.antecedent = antecedent self.consequent = consequent def simplify(self, bindings=None): return self.__class__(self.antecedent.simplify(bindings), self.consequent.simplify(bindings)) def unify(self, other, bindings): """ Both the antecedent and consequent of 'self' and 'other' must unify. @param other: C{ImpExpression} @param bindings: C{BindingDict} A dictionary of all current bindings @return: C{BindingDict} A new combined dictionary of of 'bindings' and any new bindings @raise UnificationException: If 'self' and 'other' cannot be unified in the context of 'bindings' """ assert isinstance(other, ImpExpression) try: return bindings + self.antecedent.unify(other.antecedent, bindings) + self.consequent.unify(other.consequent, bindings) except VariableBindingException: raise UnificationException(self, other, bindings) def compile_pos(self, index_counter, glueFormulaFactory): """ From Iddo Lev's PhD Dissertation p108-109 @param index_counter: C{Counter} for unique indices @param glueFormulaFactory: C{GlueFormula} for creating new glue formulas @return: (C{Expression},C{set}) for the compiled linear logic and any newly created glue formulas """ (a, a_new) = self.antecedent.compile_neg(index_counter, glueFormulaFactory) (c, c_new) = self.consequent.compile_pos(index_counter, glueFormulaFactory) return (ImpExpression(a,c), a_new + c_new) def compile_neg(self, index_counter, glueFormulaFactory): """ From Iddo Lev's PhD Dissertation p108-109 @param index_counter: C{Counter} for unique indices @param glueFormulaFactory: C{GlueFormula} for creating new glue formulas @return: (C{Expression},C{list} of C{GlueFormula}) for the compiled linear logic and any newly created glue formulas """ (a, a_new) = self.antecedent.compile_pos(index_counter, glueFormulaFactory) (c, c_new) = self.consequent.compile_neg(index_counter, glueFormulaFactory) fresh_index = index_counter.get() c.dependencies.append(fresh_index) new_v = glueFormulaFactory('v%s' % fresh_index, a, set([fresh_index])) return (c, a_new + c_new + [new_v]) def initialize_labels(self, fstruct): self.antecedent.initialize_labels(fstruct) self.consequent.initialize_labels(fstruct) def __eq__(self, other): return self.__class__ == other.__class__ and \ self.antecedent == other.antecedent and self.consequent == other.consequent def __str__(self): return Tokens.OPEN + str(self.antecedent) + ' ' + Tokens.IMP + \ ' ' + str(self.consequent) + Tokens.CLOSE def __hash__(self): return hash('%s%s%s' % (hash(self.antecedent), Tokens.IMP, hash(self.consequent))) class ApplicationExpression(Expression): def __init__(self, function, argument, argument_indices=None): """ @param function: C{Expression} for the function @param argument: C{Expression} for the argument @param argument_indices: C{set} for the indices of the glue formula from which the argument came @raise LinearLogicApplicationException: If 'function' cannot be applied to 'argument' given 'argument_indices'. """ function_simp = function.simplify() argument_simp = argument.simplify() assert isinstance(function_simp, ImpExpression) assert isinstance(argument_simp, Expression) bindings = BindingDict() try: if isinstance(function, ApplicationExpression): bindings += function.bindings if isinstance(argument, ApplicationExpression): bindings += argument.bindings bindings += function_simp.antecedent.unify(argument_simp, bindings) except UnificationException, e: raise LinearLogicApplicationException, 'Cannot apply %s to %s. %s' % (function_simp, argument_simp, e) # If you are running it on complied premises, more conditions apply if argument_indices: # A.dependencies of (A -o (B -o C)) must be a proper subset of argument_indices if not set(function_simp.antecedent.dependencies) < argument_indices: raise LinearLogicApplicationException, 'Dependencies unfulfilled when attempting to apply Linear Logic formula %s to %s' % (function_simp, argument_simp) if set(function_simp.antecedent.dependencies) == argument_indices: raise LinearLogicApplicationException, 'Dependencies not a proper subset of indices when attempting to apply Linear Logic formula %s to %s' % (function_simp, argument_simp) self.function = function self.argument = argument self.bindings = bindings def simplify(self, bindings=None): """ Since function is an implication, return its consequent. There should be no need to check that the application is valid since the checking is done by the constructor. @param bindings: C{BindingDict} A dictionary of bindings used to simplify @return: C{Expression} """ if not bindings: bindings = self.bindings return self.function.simplify(bindings).consequent def __eq__(self, other): return self.__class__ == other.__class__ and \ self.function == other.function and self.argument == other.argument def __str__(self): return str(self.function) + Tokens.OPEN + str(self.argument) + Tokens.CLOSE def __hash__(self): return hash('%s%s%s' % (hash(self.antecedent), Tokens.OPEN, hash(self.consequent))) class BindingDict(object): def __init__(self, binding_list=None): """ @param binding_list: C{list} of (C{VariableExpression}, C{AtomicExpression}) to initialize the dictionary """ self.d = {} if binding_list: for (v, b) in binding_list: self[v] = b def __setitem__(self, variable, binding): """ A binding is consistent with the dict if its variable is not already bound, OR if its variable is already bound to its argument. @param variable: C{VariableExpression} The variable bind @param binding: C{Expression} The expression to which 'variable' should be bound @raise VariableBindingException: If the variable cannot be bound in this dictionary """ assert isinstance(variable, VariableExpression) assert isinstance(binding, Expression) assert variable != binding try: existing = self.d[variable] except KeyError: existing = None if not existing or binding == existing: self.d[variable] = binding else: raise VariableBindingException, 'Variable %s already bound to another value' % (variable) def __getitem__(self, variable): """ Return the expression to which 'variable' is bound """ assert isinstance(variable, VariableExpression) intermediate = self.d[variable] while intermediate: try: intermediate = self.d[intermediate] except KeyError: return intermediate def __contains__(self, item): return item in self.d def __add__(self, other): """ @param other: C{BindingDict} The dict with which to combine self @return: C{BindingDict} A new dict containing all the elements of both parameters @raise VariableBindingException: If the parameter dictionaries are not consistent with each other """ try: combined = BindingDict() for v in self.d: combined[v] = self.d[v] for v in other.d: combined[v] = other.d[v] return combined except VariableBindingException: raise VariableBindingException, 'Attempting to add two contradicting'\ ' VariableBindingsLists: %s, %s' % (self, other) def __str__(self): return '{' + ', '.join(['%s: %s' % (v, self.d[v]) for v in self.d]) + '}' def __repr__(self): return 'BindingDict: ' + str(self) class VariableBindingException(Exception): pass class UnificationException(Exception): def __init__(self, a, b, bindings): Exception.__init__(self, 'Cannot unify %s with %s given %s' % (a, b, bindings)) class LinearLogicApplicationException(Exception): pass class Tokens(object): #Punctuation OPEN = '(' CLOSE = ')' #Operations IMP = '-o' PUNCT = [OPEN, CLOSE] TOKENS = PUNCT + [IMP] class LinearLogicParser(LogicParser): """A linear logic expression parser.""" def __init__(self): LogicParser.__init__(self) self.order_of_operations = {'APP': 1, Tokens.IMP: 2, None: 3} self.right_associated_operations += [Tokens.IMP] def get_all_symbols(self): return Tokens.TOKENS def handle(self, tok, context): if tok not in Tokens.TOKENS: return self.handle_variable(tok, context) elif tok == Tokens.OPEN: return self.handle_open(tok, context) def get_BooleanExpression_factory(self, tok): if tok == Tokens.IMP: return ImpExpression else: return None def make_BooleanExpression(self, factory, first, second): return factory(first, second) def attempt_ApplicationExpression(self, expression, context): """Attempt to make an application expression. If the next tokens are an argument in parens, then the argument expression is a function being applied to the arguments. Otherwise, return the argument expression.""" if self.has_priority('APP', context): if self.inRange(0) and self.token(0) == Tokens.OPEN: self.token() #swallow then open paren argument = self.parse_Expression('APP') self.assertNextToken(Tokens.CLOSE) expression = ApplicationExpression(expression, argument, None) return expression def make_VariableExpression(self, name): if name[0].isupper(): return VariableExpression(name) else: return ConstantExpression(name) def demo(): llp = LinearLogicParser() print llp.parse(r'f') print llp.parse(r'(g -o f)') print llp.parse(r'((g -o G) -o G)') print llp.parse(r'g -o h -o f') print llp.parse(r'(g -o f)(g)').simplify() print llp.parse(r'(H -o f)(g)').simplify() print llp.parse(r'((g -o G) -o G)((g -o f))').simplify() print llp.parse(r'(H -o H)((g -o f))').simplify() if __name__ == '__main__': demo() nltk-2.0~b9/nltk/sem/lfg.py0000644000175000017500000001447111331672543015450 0ustar bhavanibhavani# Natural Language Toolkit: Lexical Functional Grammar # # Author: Dan Garrette # # Copyright (C) 2001-2010 NLTK Project # URL: # For license information, see LICENSE.TXT from nltk.internals import Counter from nltk.compat import defaultdict from nltk.parse.dependencygraph import DependencyGraph class FStructure(dict): def safeappend(self, key, item): """ Append 'item' to the list at 'key'. If no list exists for 'key', then construct one. """ if key not in self: self[key] = [] self[key].append(item) def __setitem__(self, key, value): dict.__setitem__(self, key.lower(), value) def __getitem__(self, key): return dict.__getitem__(self, key.lower()) def __contains__(self, key): return dict.__contains__(self, key.lower()) def to_glueformula_list(self, glue_dict): depgraph = self.to_depgraph() return glue_dict.to_glueformula_list(depgraph) def to_depgraph(self, rel=None): depgraph = DependencyGraph() nodelist = depgraph.nodelist self._to_depgraph(nodelist, 0, 'ROOT') #Add all the dependencies for all the nodes for node_addr, node in enumerate(nodelist): for n2 in nodelist[1:]: if n2['head'] == node_addr: node['deps'].append(n2['address']) depgraph.root = nodelist[1] return depgraph def _to_depgraph(self, nodelist, head, rel): index = len(nodelist) nodelist.append({'address': index, 'word': self.pred[0], 'tag': self.pred[1], 'head': head, 'rel': rel, 'deps': []}) for feature in self: for item in self[feature]: if isinstance(item, FStructure): item._to_depgraph(nodelist, index, feature) elif isinstance(item, tuple): nodelist.append({'address': len(nodelist), 'word': item[0], 'tag': item[1], 'head': index, 'rel': feature, 'deps': []}) elif isinstance(item, list): for n in item: n._to_depgraph(nodelist, index, feature) else: # ERROR raise Exception, 'feature %s is not an FStruct, a list, or a tuple' % feature @staticmethod def read_depgraph(depgraph): return FStructure._read_depgraph(depgraph.root, depgraph) @staticmethod def _read_depgraph(node, depgraph, label_counter=None, parent=None): if not label_counter: label_counter = Counter() if node['rel'].lower() in ['spec', 'punct']: # the value of a 'spec' entry is a word, not an FStructure return (node['word'], node['tag']) else: fstruct = FStructure() fstruct.pred = None fstruct.label = FStructure._make_label(label_counter.get()) fstruct.parent = parent word, tag = node['word'], node['tag'] if tag[:2] == 'VB': if tag[2:3] == 'D': fstruct.safeappend('tense', ('PAST', 'tense')) fstruct.pred = (word, tag[:2]) if not fstruct.pred: fstruct.pred = (word, tag) children = [depgraph.nodelist[idx] for idx in node['deps']] for child in children: fstruct.safeappend(child['rel'], FStructure._read_depgraph(child, depgraph, label_counter, fstruct)) return fstruct @staticmethod def _make_label(value): """ Pick an alphabetic character as identifier for an entity in the model. @parameter value: where to index into the list of characters @type value: C{int} """ letter = ['f','g','h','i','j','k','l','m','n','o','p','q','r','s', 't','u','v','w','x','y','z','a','b','c','d','e'][value-1] num = int(value) / 26 if num > 0: return letter + str(num) else: return letter def __repr__(self): return str(self).replace('\n', '') def __str__(self, indent=3): try: accum = '%s:[' % self.label except NameError: accum = '[' try: accum += 'pred \'%s\'' % (self.pred[0]) except NameError: pass for feature in self: for item in self[feature]: if isinstance(item, FStructure): next_indent = indent+len(feature)+3+len(self.label) accum += '\n%s%s %s' % (' '*(indent), feature, item.__str__(next_indent)) elif isinstance(item, tuple): accum += '\n%s%s \'%s\'' % (' '*(indent), feature, item[0]) elif isinstance(item, list): accum += '\n%s%s {%s}' % (' '*(indent), feature, ('\n%s' % (' '*(indent+len(feature)+2))).join(item)) else: # ERROR raise Exception, 'feature %s is not an FStruct, a list, or a tuple' % feature return accum+']' def demo_read_depgraph(): dg1 = DependencyGraph("""\ Esso NNP 2 SUB said VBD 0 ROOT the DT 5 NMOD Whiting NNP 5 NMOD field NN 6 SUB started VBD 2 VMOD production NN 6 OBJ Tuesday NNP 6 VMOD """) dg2 = DependencyGraph("""\ John NNP 2 SUB sees VBP 0 ROOT Mary NNP 2 OBJ """) dg3 = DependencyGraph("""\ a DT 2 SPEC man NN 3 SUBJ walks VB 0 ROOT """) dg4 = DependencyGraph("""\ every DT 2 SPEC girl NN 3 SUBJ chases VB 0 ROOT a DT 5 SPEC dog NN 3 OBJ """) depgraphs = [dg1,dg2,dg3,dg4] for dg in depgraphs: print FStructure.read_depgraph(dg) if __name__ == '__main__': demo_read_depgraph() nltk-2.0~b9/nltk/sem/hole.py0000644000175000017500000003247711377057401015635 0ustar bhavanibhavani# Natural Language Toolkit: Logic # # Author: Peter Wang # Updated by: Dan Garrette # # Copyright (C) 2001-2010 NLTK Project # URL: # For license information, see LICENSE.TXT from nltk.parse import load_parser from nltk.draw.tree import draw_trees from util import skolemize from nltk.sem import logic """ An implementation of the Hole Semantics model, following Blackburn and Bos, Representation and Inference for Natural Language (CSLI, 2005). The semantic representations are built by the grammar hole.fcfg. This module contains driver code to read in sentences and parse them according to a hole semantics grammar. After parsing, the semantic representation is in the form of an underspecified representation that is not easy to read. We use a "plugging" algorithm to convert that representation into first-order logic formulas. """ # Note that in this code there may be multiple types of trees being referred to: # # 1. parse trees # 2. the underspecified representation # 3. first-order logic formula trees # 4. the search space when plugging (search tree) # class Constants(object): ALL = 'ALL' EXISTS = 'EXISTS' NOT = 'NOT' AND = 'AND' OR = 'OR' IMP = 'IMP' IFF = 'IFF' PRED = 'PRED' LEQ = 'LEQ' HOLE = 'HOLE' LABEL = 'LABEL' MAP = {ALL: lambda v,e: logic.AllExpression(v.variable, e), EXISTS: lambda v,e: logic.ExistsExpression(v.variable, e), NOT: logic.NegatedExpression, AND: logic.AndExpression, OR: logic.OrExpression, IMP: logic.ImpExpression, IFF: logic.IffExpression, PRED: logic.ApplicationExpression} class HoleSemantics(object): """ This class holds the broken-down components of a hole semantics, i.e. it extracts the holes, labels, logic formula fragments and constraints out of a big conjunction of such as produced by the hole semantics grammar. It then provides some operations on the semantics dealing with holes, labels and finding legal ways to plug holes with labels. """ def __init__(self, usr): """ Constructor. `usr' is a C{logic.sem.Expression} representing an Underspecified Representation Structure (USR). A USR has the following special predicates: ALL(l,v,n), EXISTS(l,v,n), AND(l,n,n), OR(l,n,n), IMP(l,n,n), IFF(l,n,n), PRED(l,v,n,v[,v]*) where the brackets and star indicate zero or more repetitions, LEQ(n,n), HOLE(n), LABEL(n) where l is the label of the node described by the predicate, n is either a label or a hole, and v is a variable. """ self.holes = set() self.labels = set() self.fragments = {} # mapping of label -> formula fragment self.constraints = set() # set of Constraints self._break_down(usr) self.top_most_labels = self._find_top_most_labels() self.top_hole = self._find_top_hole() def is_node(self, x): """ Return true if x is a node (label or hole) in this semantic representation. """ return x in (self.labels | self.holes) def _break_down(self, usr): """ Extract holes, labels, formula fragments and constraints from the hole semantics underspecified representation (USR). """ if isinstance(usr, logic.AndExpression): self._break_down(usr.first) self._break_down(usr.second) elif isinstance(usr, logic.ApplicationExpression): func, args = usr.uncurry() if func.variable.name == Constants.LEQ: self.constraints.add(Constraint(args[0], args[1])) elif func.variable.name == Constants.HOLE: self.holes.add(args[0]) elif func.variable.name == Constants.LABEL: self.labels.add(args[0]) else: label = args[0] assert not self.fragments.has_key(label) self.fragments[label] = (func, args[1:]) else: raise ValueError(usr.node) def _find_top_nodes(self, node_list): top_nodes = node_list.copy() for f in self.fragments.itervalues(): #the label is the first argument of the predicate args = f[1] for arg in args: if arg in node_list: top_nodes.discard(arg) return top_nodes def _find_top_most_labels(self): """ Return the set of labels which are not referenced directly as part of another formula fragment. These will be the top-most labels for the subtree that they are part of. """ return self._find_top_nodes(self.labels) def _find_top_hole(self): """ Return the hole that will be the top of the formula tree. """ top_holes = self._find_top_nodes(self.holes) assert len(top_holes) == 1 # it must be unique return top_holes.pop() def pluggings(self): """ Calculate and return all the legal pluggings (mappings of labels to holes) of this semantics given the constraints. """ record = [] self._plug_nodes([(self.top_hole, [])], self.top_most_labels, {}, record) return record def _plug_nodes(self, queue, potential_labels, plug_acc, record): """ Plug the nodes in `queue' with the labels in `potential_labels'. Each element of `queue' is a tuple of the node to plug and the list of ancestor holes from the root of the graph to that node. `potential_labels' is a set of the labels which are still available for plugging. `plug_acc' is the incomplete mapping of holes to labels made on the current branch of the search tree so far. `record' is a list of all the complete pluggings that we have found in total so far. It is the only parameter that is destructively updated. """ if queue != []: (node, ancestors) = queue[0] if node in self.holes: # The node is a hole, try to plug it. self._plug_hole(node, ancestors, queue[1:], potential_labels, plug_acc, record) else: assert node in self.labels # The node is a label. Replace it in the queue by the holes and # labels in the formula fragment named by that label. args = self.fragments[node][1] head = [(a, ancestors) for a in args if self.is_node(a)] self._plug_nodes(head + queue[1:], potential_labels, plug_acc, record) else: raise Exception('queue empty') def _plug_hole(self, hole, ancestors0, queue, potential_labels0, plug_acc0, record): """ Try all possible ways of plugging a single hole. See _plug_nodes for the meanings of the parameters. """ # Add the current hole we're trying to plug into the list of ancestors. assert hole not in ancestors0 ancestors = [hole] + ancestors0 # Try each potential label in this hole in turn. for l in potential_labels0: # Is the label valid in this hole? if self._violates_constraints(l, ancestors): continue plug_acc = plug_acc0.copy() plug_acc[hole] = l potential_labels = potential_labels0.copy() potential_labels.remove(l) if len(potential_labels) == 0: # No more potential labels. That must mean all the holes have # been filled so we have found a legal plugging so remember it. # # Note that the queue might not be empty because there might # be labels on there that point to formula fragments with # no holes in them. _sanity_check_plugging will make sure # all holes are filled. self._sanity_check_plugging(plug_acc, self.top_hole, []) record.append(plug_acc) else: # Recursively try to fill in the rest of the holes in the # queue. The label we just plugged into the hole could have # holes of its own so at the end of the queue. Putting it on # the end of the queue gives us a breadth-first search, so that # all the holes at level i of the formula tree are filled # before filling level i+1. # A depth-first search would work as well since the trees must # be finite but the bookkeeping would be harder. self._plug_nodes(queue + [(l, ancestors)], potential_labels, plug_acc, record) def _violates_constraints(self, label, ancestors): """ Return True if the `label' cannot be placed underneath the holes given by the set `ancestors' because it would violate the constraints imposed on it. """ for c in self.constraints: if c.lhs == label: if c.rhs not in ancestors: return True return False def _sanity_check_plugging(self, plugging, node, ancestors): """ Make sure that a given plugging is legal. We recursively go through each node and make sure that no constraints are violated. We also check that all holes have been filled. """ if node in self.holes: ancestors = [node] + ancestors label = plugging[node] else: label = node assert label in self.labels for c in self.constraints: if c.lhs == label: assert c.rhs in ancestors args = self.fragments[label][1] for arg in args: if self.is_node(arg): self._sanity_check_plugging(plugging, arg, [label] + ancestors) def formula_tree(self, plugging): """ Return the first-order logic formula tree for this underspecified representation using the plugging given. """ return self._formula_tree(plugging, self.top_hole) def _formula_tree(self, plugging, node): if node in plugging: return self._formula_tree(plugging, plugging[node]) elif self.fragments.has_key(node): pred,args = self.fragments[node] children = [self._formula_tree(plugging, arg) for arg in args] return reduce(Constants.MAP[pred.variable.name], children) else: return node class Constraint(object): """ This class represents a constraint of the form (L =< N), where L is a label and N is a node (a label or a hole). """ def __init__(self, lhs, rhs): self.lhs = lhs self.rhs = rhs def __eq__(self, other): if self.__class__ == other.__class__: return self.lhs == other.lhs and self.rhs == other.rhs else: return False def __ne__(self, other): return not (self == other) def __hash__(self): return hash(repr(self)) def __repr__(self): return '(%s < %s)' % (self.lhs, self.rhs) def hole_readings(sentence, grammar_filename=None, verbose=False): if not grammar_filename: grammar_filename = 'grammars/sample_grammars/hole.fcfg' if verbose: print 'Reading grammar file', grammar_filename parser = load_parser(grammar_filename) # Parse the sentence. tokens = sentence.split() trees = parser.nbest_parse(tokens) if verbose: print 'Got %d different parses' % len(trees) all_readings = [] for tree in trees: # Get the semantic feature from the top of the parse tree. sem = tree.node['SEM'].simplify() # Print the raw semantic representation. if verbose: print 'Raw: ', sem # Skolemize away all quantifiers. All variables become unique. while isinstance(sem, logic.LambdaExpression): sem = sem.term skolemized = skolemize(sem) if verbose: print 'Skolemized:', skolemized # Break the hole semantics representation down into its components # i.e. holes, labels, formula fragments and constraints. hole_sem = HoleSemantics(skolemized) # Maybe show the details of the semantic representation. if verbose: print 'Holes: ', hole_sem.holes print 'Labels: ', hole_sem.labels print 'Constraints: ', hole_sem.constraints print 'Top hole: ', hole_sem.top_hole print 'Top labels: ', hole_sem.top_most_labels print 'Fragments:' for (l,f) in hole_sem.fragments.items(): print '\t%s: %s' % (l, f) # Find all the possible ways to plug the formulas together. pluggings = hole_sem.pluggings() # Build FOL formula trees using the pluggings. readings = map(hole_sem.formula_tree, pluggings) # Print out the formulas in a textual format. if verbose: for i,r in enumerate(readings): print print '%d. %s' % (i, r) print all_readings.extend(readings) return all_readings if __name__ == '__main__': for r in hole_readings('a dog barks'): print r print for r in hole_readings('every girl chases a dog'): print r nltk-2.0~b9/nltk/sem/glue.py0000644000175000017500000006467111331672517015644 0ustar bhavanibhavani# Natural Language Toolkit: Glue Semantics # # Author: Dan Garrette # # Copyright (C) 2001-2010 NLTK Project # URL: # For license information, see LICENSE.TXT import os import nltk from nltk.internals import Counter from nltk.parse import * from nltk.parse import MaltParser from nltk.corpus import brown from nltk.tag import * import logic import drt import linearlogic SPEC_SEMTYPES = {'a' : 'ex_quant', 'an' : 'ex_quant', 'every' : 'univ_quant', 'the' : 'def_art', 'no' : 'no_quant', 'default' : 'ex_quant'} OPTIONAL_RELATIONSHIPS = ['nmod', 'vmod', 'punct'] class GlueFormula(object): def __init__(self, meaning, glue, indices=None): if not indices: indices = set() if isinstance(meaning, str): self.meaning = logic.LogicParser().parse(meaning) elif isinstance(meaning, logic.Expression): self.meaning = meaning else: raise RuntimeError, 'Meaning term neither string or expression: %s, %s' % (meaning, meaning.__class__) if isinstance(glue, str): self.glue = linearlogic.LinearLogicParser().parse(glue) elif isinstance(glue, linearlogic.Expression): self.glue = glue else: raise RuntimeError, 'Glue term neither string or expression: %s, %s' % (glue, glue.__class__) self.indices = indices def applyto(self, arg): """ self = (\\x.(walk x), (subj -o f)) arg = (john , subj) returns ((walk john), f) """ if self.indices & arg.indices: # if the sets are NOT disjoint raise linearlogic.LinearLogicApplicationException, "'%s' applied to '%s'. Indices are not disjoint." % (self, arg) else: # if the sets ARE disjoint return_indices = (self.indices | arg.indices) try: return_glue = linearlogic.ApplicationExpression(self.glue, arg.glue, arg.indices) except linearlogic.LinearLogicApplicationException: raise linearlogic.LinearLogicApplicationException, "'%s' applied to '%s'" % (self.simplify(), arg.simplify()) arg_meaning_abstracted = arg.meaning if return_indices: for dep in self.glue.simplify().antecedent.dependencies[::-1]: # if self.glue is (A -o B), dep is in A.dependencies arg_meaning_abstracted = self.make_LambdaExpression(logic.Variable('v%s' % dep), arg_meaning_abstracted) return_meaning = self.meaning.applyto(arg_meaning_abstracted) return self.__class__(return_meaning, return_glue, return_indices) def make_VariableExpression(self, name): return logic.VariableExpression(name) def make_LambdaExpression(self, variable, term): return logic.LambdaExpression(variable, term) def lambda_abstract(self, other): assert isinstance(other, GlueFormula) assert isinstance(other.meaning, logic.AbstractVariableExpression) return self.__class__(self.make_LambdaExpression(other.meaning.variable, self.meaning), linearlogic.ImpExpression(other.glue, self.glue)) def compile(self, counter=None): """From Iddo Lev's PhD Dissertation p108-109""" if not counter: counter = Counter() (compiled_glue, new_forms) = self.glue.simplify().compile_pos(counter, self.__class__) return new_forms + [self.__class__(self.meaning, compiled_glue, set([counter.get()]))] def simplify(self): return self.__class__(self.meaning.simplify(), self.glue.simplify(), self.indices) def __eq__(self, other): return self.__class__ == other.__class__ and self.meaning == other.meaning and self.glue == other.glue def __str__(self): assert isinstance(self.indices, set) accum = '%s : %s' % (self.meaning, self.glue) if self.indices: accum += ' : {' + ', '.join([str(index) for index in self.indices]) + '}' return accum def __repr__(self): return str(self) class GlueDict(dict): def __init__(self, filename): self.filename = filename self.read_file() def read_file(self, empty_first=True): if empty_first: self.clear() try: f = nltk.data.find( os.path.join('grammars', 'sample_grammars', self.filename)) # if f is a ZipFilePathPointer or a FileSystemPathPointer # then we need a little extra massaging if hasattr(f, 'open'): f = f.open() except LookupError, e: try: f = open(self.filename) except LookupError: raise e lines = f.readlines() f.close() for line in lines: # example: 'n : (\\x.( x), (v-or))' # lambdacalc -^ linear logic -^ line = line.strip() # remove trailing newline if not len(line): continue # skip empty lines if line[0] == '#': continue # skip commented out lines parts = line.split(' : ', 2) # ['verb', '(\\x.( x), ( subj -o f ))', '[subj]'] glue_formulas = [] parenCount = 0 tuple_start = 0 tuple_comma = 0 relationships = None if len(parts) > 1: for (i, c) in enumerate(parts[1]): if c == '(': if parenCount == 0: # if it's the first '(' of a tuple tuple_start = i+1 # then save the index parenCount += 1 elif c == ')': parenCount -= 1 if parenCount == 0: # if it's the last ')' of a tuple meaning_term = parts[1][tuple_start:tuple_comma] # '\\x.( x)' glue_term = parts[1][tuple_comma+1:i] # '(v-r)' glue_formulas.append([meaning_term, glue_term]) # add the GlueFormula to the list elif c == ',': if parenCount == 1: # if it's a comma separating the parts of the tuple tuple_comma = i # then save the index elif c == '#': # skip comments at the ends of lines if parenCount != 0: # if the line hasn't parsed correctly so far raise RuntimeError, 'Formula syntax is incorrect for entry ' + line break # break to the next line if len(parts) > 2: #if there is a relationship entry at the end relStart = parts[2].index('[')+1 relEnd = parts[2].index(']') if relStart == relEnd: relationships = frozenset() else: relationships = frozenset([r.strip() for r in parts[2][relStart:relEnd].split(',')]) try: startInheritance = parts[0].index('(') endInheritance = parts[0].index(')') sem = parts[0][:startInheritance].strip() supertype = parts[0][startInheritance+1:endInheritance] except: sem = parts[0].strip() supertype = None if sem not in self: self[sem] = {} if relationships is None: #if not specified for a specific relationship set #add all relationship entries for parents if supertype: for rels, glue in self[supertype].iteritems(): if rels not in self[sem]: self[sem][rels] = [] self[sem][rels].extend(glue) self[sem][rels].extend(glue_formulas) # add the glue formulas to every rel entry else: if None not in self[sem]: self[sem][None] = [] self[sem][None].extend(glue_formulas) # add the glue formulas to every rel entry else: if relationships not in self[sem]: self[sem][relationships] = [] if supertype: self[sem][relationships].extend(self[supertype][relationships]) self[sem][relationships].extend(glue_formulas) # add the glue entry to the dictionary def __str__(self): accum = '' for pos in self: for relset in self[pos]: i = 1 for gf in self[pos][relset]: if i==1: accum += str(pos) + ': ' else: accum += ' '*(len(str(pos))+2) accum += str(gf) if relset and i==len(self[pos][relset]): accum += ' : ' + str(relset) accum += '\n' i += 1 return accum def to_glueformula_list(self, depgraph, node=None, counter=None, verbose=False): if node is None: top = depgraph.nodelist[0] root = depgraph.nodelist[top['deps'][0]] return self.to_glueformula_list(depgraph, root, Counter(), verbose) glueformulas = self.lookup(node, depgraph, counter) for dep_idx in node['deps']: dep = depgraph.nodelist[dep_idx] glueformulas.extend(self.to_glueformula_list(depgraph, dep, counter, verbose)) return glueformulas def lookup(self, node, depgraph, counter): semtype_names = self.get_semtypes(node) semtype = None for name in semtype_names: if name in self: semtype = self[name] break if semtype is None: # raise KeyError, "There is no GlueDict entry for sem type '%s' (for '%s')" % (sem, word) return [] self.add_missing_dependencies(node, depgraph) lookup = self._lookup_semtype_option(semtype, node, depgraph) if not len(lookup): raise KeyError, "There is no GlueDict entry for sem type of '%s'"\ " with tag '%s', and rel '%s'" %\ (node['word'], node['tag'], node['rel']) return self.get_glueformulas_from_semtype_entry(lookup, node['word'], node, depgraph, counter) def add_missing_dependencies(self, node, depgraph): rel = node['rel'].lower() if rel == 'main': headnode = depgraph.nodelist[node['head']] subj = self.lookup_unique('subj', headnode, depgraph) node['deps'].append(subj['address']) def _lookup_semtype_option(self, semtype, node, depgraph): relationships = frozenset([depgraph.nodelist[dep]['rel'].lower() for dep in node['deps'] if depgraph.nodelist[dep]['rel'].lower() not in OPTIONAL_RELATIONSHIPS]) try: lookup = semtype[relationships] except KeyError: # An exact match is not found, so find the best match where # 'best' is defined as the glue entry whose relationship set has the # most relations of any possible relationship set that is a subset # of the actual depgraph best_match = frozenset() for relset_option in set(semtype)-set([None]): if len(relset_option) > len(best_match) and \ relset_option < relationships: best_match = relset_option if not best_match: if None in semtype: best_match = None else: return None lookup = semtype[best_match] return lookup def get_semtypes(self, node): """ Based on the node, return a list of plausible semtypes in order of plausibility. """ semtype_name = None rel = node['rel'].lower() word = node['word'].lower() if rel == 'spec': if word in SPEC_SEMTYPES: return [SPEC_SEMTYPES[word]] else: return [SPEC_SEMTYPES['default']] elif rel in ['nmod', 'vmod']: return [node['tag'], rel] else: return [node['tag']] def get_glueformulas_from_semtype_entry(self, lookup, word, node, depgraph, counter): glueformulas = [] glueFormulaFactory = self.get_GlueFormula_factory() for meaning, glue in lookup: gf = glueFormulaFactory(self.get_meaning_formula(meaning, word), glue) if not len(glueformulas): gf.word = word else: gf.word = '%s%s' % (word, len(glueformulas)+1) gf.glue = self.initialize_labels(gf.glue, node, depgraph, counter.get()) glueformulas.append(gf) return glueformulas def get_meaning_formula(self, generic, word): """ @param generic: A meaning formula string containing the parameter "" @param word: The actual word to be replace "" """ word = word.replace('.', '') return generic.replace('', word) def initialize_labels(self, expr, node, depgraph, unique_index): if isinstance(expr, linearlogic.AtomicExpression): name = self.find_label_name(expr.name.lower(), node, depgraph, unique_index) if name[0].isupper(): return linearlogic.VariableExpression(name) else: return linearlogic.ConstantExpression(name) else: return linearlogic.ImpExpression( self.initialize_labels(expr.antecedent, node, depgraph, unique_index), self.initialize_labels(expr.consequent, node, depgraph, unique_index)) def find_label_name(self, name, node, depgraph, unique_index): try: dot = name.index('.') before_dot = name[:dot] after_dot = name[dot+1:] if before_dot == 'super': return self.find_label_name(after_dot, depgraph.nodelist[node['head']], depgraph, unique_index) else: return self.find_label_name(after_dot, self.lookup_unique(before_dot, node, depgraph), depgraph, unique_index) except ValueError: lbl = self.get_label(node) if name=='f': return lbl elif name=='v': return '%sv' % lbl elif name=='r': return '%sr' % lbl elif name=='super': return self.get_label(depgraph.nodelist[node['head']]) elif name=='var': return '%s%s' % (lbl.upper(), unique_index) elif name=='a': return self.get_label(self.lookup_unique('conja', node, depgraph)) elif name=='b': return self.get_label(self.lookup_unique('conjb', node, depgraph)) else: return self.get_label(self.lookup_unique(name, node, depgraph)) def get_label(self, node): """ Pick an alphabetic character as identifier for an entity in the model. @parameter value: where to index into the list of characters @type value: C{int} """ value = node['address'] letter = ['f','g','h','i','j','k','l','m','n','o','p','q','r','s', 't','u','v','w','x','y','z','a','b','c','d','e'][value-1] num = int(value) / 26 if num > 0: return letter + str(num) else: return letter def lookup_unique(self, rel, node, depgraph): """ Lookup 'key'. There should be exactly one item in the associated relation. """ deps = [depgraph.nodelist[dep] for dep in node['deps'] if depgraph.nodelist[dep]['rel'].lower() == rel.lower()] if len(deps) == 0: raise KeyError, "'%s' doesn't contain a feature '%s'" % (node['word'], rel) elif len(deps) > 1: raise KeyError, "'%s' should only have one feature '%s'" % (node['word'], rel) else: return deps[0] def get_GlueFormula_factory(self): return GlueFormula class Glue(object): def __init__(self, semtype_file=None, remove_duplicates=False, depparser=None, verbose=False): self.verbose = verbose self.remove_duplicates = remove_duplicates self.depparser = depparser from nltk import Prover9 self.prover = Prover9() if semtype_file: self.semtype_file = semtype_file else: self.semtype_file = 'glue.semtype' def train_depparser(self, depgraphs=None): if depgraphs: self.depparser.train(depgraphs) else: self.depparser.train_from_file(nltk.data.find( os.path.join('grammars', 'sample_grammars', 'glue_train.conll'))) def parse_to_meaning(self, sentence): readings = [] for agenda in self.parse_to_compiled(sentence): readings.extend(self.get_readings(agenda)) return readings def get_readings(self, agenda): readings = [] agenda_length = len(agenda) atomics = dict() nonatomics = dict() while agenda: # is not empty cur = agenda.pop() glue_simp = cur.glue.simplify() if isinstance(glue_simp, linearlogic.ImpExpression): # if cur.glue is non-atomic for key in atomics: try: if isinstance(cur.glue, linearlogic.ApplicationExpression): bindings = cur.glue.bindings else: bindings = linearlogic.BindingDict() glue_simp.antecedent.unify(key, bindings) for atomic in atomics[key]: if not (cur.indices & atomic.indices): # if the sets of indices are disjoint try: agenda.append(cur.applyto(atomic)) except linearlogic.LinearLogicApplicationException: pass except linearlogic.UnificationException: pass try: nonatomics[glue_simp.antecedent].append(cur) except KeyError: nonatomics[glue_simp.antecedent] = [cur] else: # else cur.glue is atomic for key in nonatomics: for nonatomic in nonatomics[key]: try: if isinstance(nonatomic.glue, linearlogic.ApplicationExpression): bindings = nonatomic.glue.bindings else: bindings = linearlogic.BindingDict() glue_simp.unify(key, bindings) if not (cur.indices & nonatomic.indices): # if the sets of indices are disjoint try: agenda.append(nonatomic.applyto(cur)) except linearlogic.LinearLogicApplicationException: pass except linearlogic.UnificationException: pass try: atomics[glue_simp].append(cur) except KeyError: atomics[glue_simp] = [cur] for entry in atomics: for gf in atomics[entry]: if len(gf.indices) == agenda_length: self._add_to_reading_list(gf, readings) for entry in nonatomics: for gf in nonatomics[entry]: if len(gf.indices) == agenda_length: self._add_to_reading_list(gf, readings) return readings def _add_to_reading_list(self, glueformula, reading_list): add_reading = True if self.remove_duplicates: for reading in reading_list: try: if reading.tp_equals(glueformula.meaning, self.prover): add_reading = False break; except Exception, e: #if there is an exception, the syntax of the formula #may not be understandable by the prover, so don't #throw out the reading. print 'Error when checking logical equality of statements', e pass if add_reading: reading_list.append(glueformula.meaning) def parse_to_compiled(self, sentence='a man sees Mary'): gfls = [self.depgraph_to_glue(dg) for dg in self.dep_parse(sentence)] return [self.gfl_to_compiled(gfl) for gfl in gfls] def dep_parse(self, sentence='every cat leaves'): #Lazy-initialize the depparser if self.depparser is None: self.depparser = MaltParser(tagger=self.get_pos_tagger()) if not self.depparser._trained: self.train_depparser() return [self.depparser.parse(sentence, verbose=self.verbose)] def depgraph_to_glue(self, depgraph): return self.get_glue_dict().to_glueformula_list(depgraph) def get_glue_dict(self): return GlueDict(self.semtype_file) def gfl_to_compiled(self, gfl): index_counter = Counter() return_list = [] for gf in gfl: return_list.extend(gf.compile(index_counter)) if self.verbose: print 'Compiled Glue Premises:' for cgf in return_list: print cgf return return_list def get_pos_tagger(self): regexp_tagger = RegexpTagger( [(r'^-?[0-9]+(.[0-9]+)?$', 'CD'), # cardinal numbers (r'(The|the|A|a|An|an)$', 'AT'), # articles (r'.*able$', 'JJ'), # adjectives (r'.*ness$', 'NN'), # nouns formed from adjectives (r'.*ly$', 'RB'), # adverbs (r'.*s$', 'NNS'), # plural nouns (r'.*ing$', 'VBG'), # gerunds (r'.*ed$', 'VBD'), # past tense verbs (r'.*', 'NN') # nouns (default) ]) brown_train = brown.tagged_sents(categories='news') unigram_tagger = UnigramTagger(brown_train, backoff=regexp_tagger) bigram_tagger = BigramTagger(brown_train, backoff=unigram_tagger) trigram_tagger = TrigramTagger(brown_train, backoff=bigram_tagger) #Override particular words main_tagger = RegexpTagger( [(r'(A|a|An|an)$', 'ex_quant'), (r'(Every|every|All|all)$', 'univ_quant') ], backoff=trigram_tagger) return main_tagger class DrtGlueFormula(GlueFormula): def __init__(self, meaning, glue, indices=None): if not indices: indices = set() if isinstance(meaning, str): self.meaning = drt.DrtParser().parse(meaning) elif isinstance(meaning, drt.AbstractDrs): self.meaning = meaning else: raise RuntimeError, 'Meaning term neither string or expression: %s, %s' % (meaning, meaning.__class__) if isinstance(glue, str): self.glue = linearlogic.LinearLogicParser().parse(glue) elif isinstance(glue, linearlogic.Expression): self.glue = glue else: raise RuntimeError, 'Glue term neither string or expression: %s, %s' % (glue, glue.__class__) self.indices = indices def make_VariableExpression(self, name): return drt.DrtVariableExpression(name) def make_LambdaExpression(self, variable, term): return drt.DrtLambdaExpression(variable, term) class DrtGlueDict(GlueDict): def get_GlueFormula_factory(self): return DrtGlueFormula class DrtGlue(Glue): def __init__(self, semtype_file=None, remove_duplicates=False, depparser=None, verbose=False): if not semtype_file: semtype_file = 'drt_glue.semtype' Glue.__init__(self, semtype_file, remove_duplicates, depparser, verbose) def get_glue_dict(self): return DrtGlueDict(self.semtype_file) def demo(show_example=-1): examples = ['David sees Mary', 'David eats a sandwich', 'every man chases a dog', 'every man believes a dog sleeps', 'John gives David a sandwich', 'John chases himself'] # 'John persuades David to order a pizza', # 'John tries to go', # 'John tries to find a unicorn', # 'John seems to vanish', # 'a unicorn seems to approach', # 'every big cat leaves', # 'every gray cat leaves', # 'every big gray cat leaves', # 'a former senator leaves', print '============== DEMO ==============' tagger = RegexpTagger( [('^(David|Mary|John)$', 'NNP'), ('^(sees|eats|chases|believes|gives|sleeps|chases|persuades|tries|seems|leaves)$', 'VB'), ('^(go|order|vanish|find|approach)$', 'VB'), ('^(a)$', 'ex_quant'), ('^(every)$', 'univ_quant'), ('^(sandwich|man|dog|pizza|unicorn|cat|senator)$', 'NN'), ('^(big|gray|former)$', 'JJ'), ('^(him|himself)$', 'PRP') ]) depparser = MaltParser(tagger=tagger) glue = Glue(depparser=depparser, verbose=False) for (i, sentence) in enumerate(examples): if i==show_example or show_example==-1: print '[[[Example %s]]] %s' % (i, sentence) for reading in glue.parse_to_meaning(sentence): print reading.simplify() print '' if __name__ == '__main__': demo() nltk-2.0~b9/nltk/sem/evaluate.py0000644000175000017500000005351211327451602016501 0ustar bhavanibhavani# Natural Language Toolkit: Models for first-order languages with lambda # # Copyright (C) 2001-2010 NLTK Project # Author: Ewan Klein , # URL: # For license information, see LICENSE.TXT # # $Id: evaluate.py 8479 2010-01-13 05:40:34Z StevenBird1 $ #TODO: #- fix tracing #- fix iterator-based approach to existentials """ This module provides data structures for representing first-order models. """ from pprint import pformat import inspect import textwrap from nltk.decorators import decorator from nltk.internals import deprecated from logic import * class Error(Exception): pass class Undefined(Error): pass def trace(f, *args, **kw): argspec = inspect.getargspec(f) d = dict(zip(argspec[0], args)) if d.pop('trace', None): print for item in d.items(): print "%s => %s" % item return f(*args, **kw) def is_rel(s): """ Check whether a set represents a relation (of any arity). @param s: a set containing C{tuple}s of C{str} elements @type s: C{set} @rtype: C{bool} """ # we have the empty relation, i.e. set() if len(s) == 0: return True # all the elements are tuples of the same length elif s == set([elem for elem in s if isinstance(elem, tuple)]) and\ len(max(s))==len(min(s)): return True else: raise ValueError, "Set %r contains sequences of different lengths" % s def set2rel(s): """ Convert a set containing individuals (strings or numbers) into a set of unary tuples. Any tuples of strings already in the set are passed through unchanged. For example: - set(['a', 'b']) => set([('a',), ('b',)]) - set([3, 27]) => set([('3',), ('27',)]) @type s: C{set} @rtype: C{set} of C{tuple} of C{str} """ new = set() for elem in s: if isinstance(elem, str): new.add((elem,)) elif isinstance(elem, int): new.add((str(elem,))) else: new.add(elem) return new def arity(rel): """ Check the arity of a relation. @type rel: C{set} of C{tuple}s @rtype: C{int} of C{tuple} of C{str} """ if len(rel) == 0: return 0 return len(list(rel)[0]) @deprecated("Simply use 'args in rel'") def app(rel, args, trace=False): """ Apply a relation (as set of tuples) to an argument. @type rel: C{set} of C{tuple}s @param args: a sequence of appropriate semantic arguments @rtype: C{bool} """ assert is_rel(rel) return args in rel class Valuation(dict): """ A dictionary which represents a model-theoretic Valuation of non-logical constants. Keys are strings representing the constants to be interpreted, and values correspond to individuals (represented as strings) and n-ary relations (represented as sets of tuples of strings). An instance of L{Valuation} will raise a KeyError exception (i.e., just behave like a standard dictionary) if indexed with an expression that is not in its list of symbols. """ def __init__(self, iter): """ @param iter: a C{list} of (symbol, value) pairs. """ dict.__init__(self) for (sym, val) in iter: if isinstance(val, str) or isinstance(val, bool): self[sym] = val elif isinstance(val, set): self[sym] = set2rel(val) else: msg = textwrap.fill("Error in initializing Valuation. " "Unrecognized value for symbol '%s':\n%s" % (sym, val), width=66) raise ValueError(msg) def __getitem__(self, key): if key in self: return dict.__getitem__(self, key) else: raise Undefined, "Unknown expression: '%s'" % key def __str__(self): return pformat(self) def _getDomain(self): dom = [] for val in self.values(): if isinstance(val, str): dom.append(val) elif not isinstance(val, bool): dom.extend([elem for tuple in val for elem in tuple if elem is not None]) return set(dom) domain = property(_getDomain, doc='Set-theoretic domain of the value-space of a Valuation.') def _getSymbols(self): return sorted(self.keys()) symbols = property(_getSymbols, doc='The non-logical constants which the Valuation recognizes.') class Assignment(dict): """ A dictionary which represents an assignment of values to variables. An assigment can only assign values from its domain. If an unknown expression M{a} is passed to a model M{M}'s interpretation function M{i}, M{i} will first check whether M{M}'s valuation assigns an interpretation to M{a} as a constant, and if this fails, M{i} will delegate the interpretation of M{a} to M{g}. M{g} only assigns values to individual variables (i.e., members of the class L{IndividualVariableExpression} in the L{logic} module. If a variable is not assigned a value by M{g}, it will raise an C{Undefined} exception. """ def __init__(self, domain, assign=None): """ @param domain: the domain of discourse @type domain: C{set} @param assign: a list of (varname, value) associations @type assign: C{list} """ dict.__init__(self) self.domain = domain if assign: for (var, val) in assign: assert val in self.domain,\ "'%s' is not in the domain: %s" % (val, self.domain) assert is_indvar(var),\ "Wrong format for an Individual Variable: '%s'" % var self[var] = val self._addvariant() def __getitem__(self, key): if key in self: return dict.__getitem__(self, key) else: raise Undefined, "Not recognized as a variable: '%s'" % key def copy(self): new = Assignment(self.domain) new.update(self) return new def purge(self, var=None): """ Remove one or all keys (i.e. logic variables) from an assignment, and update C{self.variant}. @param var: a Variable acting as a key for the assignment. """ if var: val = self[var] del self[var] else: self.clear() self._addvariant() return None def __str__(self): """ Pretty printing for assignments. {'x', 'u'} appears as 'g[u/x]' """ gstring = "g" for (val, var) in self.variant: gstring += "[%s/%s]" % (val, var) return gstring def _addvariant(self): """ Create a more pretty-printable version of the assignment. """ list = [] for item in self.items(): pair = (item[1], item[0]) list.append(pair) self.variant = list return None def add(self, var, val): """ Add a new variable-value pair to the assignment, and update C{self.variant}. """ assert val in self.domain,\ "%s is not in the domain %s" % (val, self.domain) assert is_indvar(var),\ "Wrong format for an Individual Variable: '%s'" % var self[var] = val self._addvariant() return self class Model(object): """ A first order model is a domain M{D} of discourse and a valuation M{V}. A domain M{D} is a set, and a valuation M{V} is a map that associates expressions with values in the model. The domain of M{V} should be a subset of M{D}. """ def __init__(self, domain, valuation): """ Construct a new L{Model}. @type domain: C{set} @param domain: A set of entities representing the domain of discourse of the model. @type valuation: L{Valuation} @param valuation: the valuation of the model. @param prop: If this is set, then we are building a propositional\ model and don't require the domain of M{V} to be subset of M{D}. """ assert isinstance(domain, set) self.domain = domain self.valuation = valuation if not domain.issuperset(valuation.domain): raise Error,\ "The valuation domain, %s, must be a subset of the model's domain, %s"\ % (valuation.domain, domain) def __repr__(self): return "(%r, %r)" % (self.domain, self.valuation) def __str__(self): return "Domain = %s,\nValuation = \n%s" % (self.domain, self.valuation) def evaluate(self, expr, g, trace=None): """ Call the L{LogicParser} to parse input expressions, and provide a handler for L{satisfy} that blocks further propagation of the C{Undefined} error. @param expr: An C{Expression} of L{logic}. @type g: L{Assignment} @param g: an assignment to individual variables. @rtype: C{bool} or 'Undefined' """ try: lp = LogicParser() parsed = lp.parse(expr) value = self.satisfy(parsed, g, trace=trace) if trace: print print "'%s' evaluates to %s under M, %s" % (expr, value, g) return value except Undefined: if trace: print print "'%s' is undefined under M, %s" % (expr, g) return 'Undefined' def satisfy(self, parsed, g, trace=None): """ Recursive interpretation function for a formula of first-order logic. Raises an C{Undefined} error when C{parsed} is an atomic string but is not a symbol or an individual variable. @return: Returns a truth value or C{Undefined} if C{parsed} is\ complex, and calls the interpretation function C{i} if C{parsed}\ is atomic. @param parsed: An expression of L{logic}. @type g: L{Assignment} @param g: an assignment to individual variables. """ if isinstance(parsed, ApplicationExpression): function, arguments = parsed.uncurry() if isinstance(function, AbstractVariableExpression): #It's a predicate expression ("P(x,y)"), so used uncurried arguments funval = self.satisfy(function, g) argvals = tuple([self.satisfy(arg, g) for arg in arguments]) return argvals in funval else: #It must be a lambda expression, so use curried form funval = self.satisfy(parsed.function, g) argval = self.satisfy(parsed.argument, g) return funval[argval] elif isinstance(parsed, NegatedExpression): return not self.satisfy(parsed.term, g) elif isinstance(parsed, AndExpression): return self.satisfy(parsed.first, g) and \ self.satisfy(parsed.second, g) elif isinstance(parsed, OrExpression): return self.satisfy(parsed.first, g) or \ self.satisfy(parsed.second, g) elif isinstance(parsed, ImpExpression): return (not self.satisfy(parsed.first, g)) or \ self.satisfy(parsed.second, g) elif isinstance(parsed, IffExpression): return self.satisfy(parsed.first, g) == \ self.satisfy(parsed.second, g) elif isinstance(parsed, EqualityExpression): return self.satisfy(parsed.first, g) == \ self.satisfy(parsed.second, g) elif isinstance(parsed, AllExpression): new_g = g.copy() for u in self.domain: new_g.add(parsed.variable.name, u) if not self.satisfy(parsed.term, new_g): return False return True elif isinstance(parsed, ExistsExpression): new_g = g.copy() for u in self.domain: new_g.add(parsed.variable.name, u) if self.satisfy(parsed.term, new_g): return True return False elif isinstance(parsed, LambdaExpression): cf = {} var = parsed.variable.name for u in self.domain: val = self.satisfy(parsed.term, g.add(var, u)) # NB the dict would be a lot smaller if we do this: # if val: cf[u] = val # But then need to deal with cases where f(a) should yield # a function rather than just False. cf[u] = val return cf else: return self.i(parsed, g, trace) #@decorator(trace_eval) def i(self, parsed, g, trace=False): """ An interpretation function. Assuming that C{parsed} is atomic: - if C{parsed} is a non-logical constant, calls the valuation M{V} - else if C{parsed} is an individual variable, calls assignment M{g} - else returns C{Undefined}. @param parsed: an C{Expression} of L{logic}. @type g: L{Assignment} @param g: an assignment to individual variables. @return: a semantic value """ # If parsed is a propositional letter 'p', 'q', etc, it could be in valuation.symbols # and also be an IndividualVariableExpression. We want to catch this first case. # So there is a procedural consequence to the ordering of clauses here: if parsed.variable.name in self.valuation.symbols: return self.valuation[parsed.variable.name] elif isinstance(parsed, IndividualVariableExpression): return g[parsed.variable.name] else: raise Undefined, "Can't find a value for %s" % parsed def satisfiers(self, parsed, varex, g, trace=None, nesting=0): """ Generate the entities from the model's domain that satisfy an open formula. @param parsed: an open formula @type parsed: L{Expression} @param varex: the relevant free individual variable in C{parsed}. @type varex: C{VariableExpression} or C{str} @param g: a variable assignment @type g: L{Assignment} @return: a C{set} of the entities that satisfy C{parsed}. """ spacer = ' ' indent = spacer + (spacer * nesting) candidates = [] if isinstance(varex, str): var = Variable(varex) else: var = varex if var in parsed.free(): if trace: print print (spacer * nesting) + "Open formula is '%s' with assignment %s" % (parsed, g) for u in self.domain: new_g = g.copy() new_g.add(var.name, u) if trace > 1: lowtrace = trace-1 else: lowtrace = 0 value = self.satisfy(parsed, new_g, lowtrace) if trace: print indent + "(trying assignment %s)" % new_g # parsed == False under g[u/var]? if value == False: if trace: print indent + "value of '%s' under %s is False" % (parsed, new_g) # so g[u/var] is a satisfying assignment else: candidates.append(u) if trace: print indent + "value of '%s' under %s is %s" % (parsed, new_g, value) result = set(c for c in candidates) # var isn't free in parsed else: raise Undefined, "%s is not free in %s" % (var.name, parsed) return result #////////////////////////////////////////////////////////////////////// # Demo.. #////////////////////////////////////////////////////////////////////// # number of spacer chars mult = 30 # Demo 1: Propositional Logic ################# def propdemo(trace=None): """Example of a propositional model.""" global val1, dom1, m1, g1 val1 = Valuation([('P', True), ('Q', True), ('R', False)]) dom1 = set([]) m1 = Model(dom1, val1) g1 = Assignment(dom1) print print '*' * mult print "Propositional Formulas Demo" print '*' * mult print '(Propositional constants treated as nullary predicates)' print print "Model m1:\n", m1 print '*' * mult sentences = [ '(P & Q)', '(P & R)', '- P', '- R', '- - P', '- (P & R)', '(P | R)', '(R | P)', '(R | R)', '(- P | R)', '(P | - P)', '(P -> Q)', '(P -> R)', '(R -> P)', '(P <-> P)', '(R <-> R)', '(P <-> R)', ] for sent in sentences: if trace: print m1.evaluate(sent, g1, trace) else: print "The value of '%s' is: %s" % (sent, m1.evaluate(sent, g1)) # Demo 2: FOL Model ############# def folmodel(quiet=False, trace=None): """Example of a first-order model.""" global val2, v2, dom2, m2, g2 v2 = [('adam', 'b1'), ('betty', 'g1'), ('fido', 'd1'),\ ('girl', set(['g1', 'g2'])), ('boy', set(['b1', 'b2'])), ('dog', set(['d1'])), ('love', set([('b1', 'g1'), ('b2', 'g2'), ('g1', 'b1'), ('g2', 'b1')]))] val2 = Valuation(v2) dom2 = val2.domain m2 = Model(dom2, val2) g2 = Assignment(dom2, [('x', 'b1'), ('y', 'g2')]) if not quiet: print print '*' * mult print "Models Demo" print "*" * mult print "Model m2:\n", "-" * 14,"\n", m2 print "Variable assignment = ", g2 exprs = ['adam', 'boy', 'love', 'walks', 'x', 'y', 'z'] lp = LogicParser() parsed_exprs = [lp.parse(e) for e in exprs] print for parsed in parsed_exprs: try: print "The interpretation of '%s' in m2 is %s" % (parsed, m2.i(parsed, g2)) except Undefined: print "The interpretation of '%s' in m2 is Undefined" % parsed applications = [('boy', ('adam')), ('walks', ('adam',)), ('love', ('adam', 'y')), ('love', ('y', 'adam'))] for (fun, args) in applications: try: funval = m2.i(lp.parse(fun), g2) argsval = tuple(m2.i(lp.parse(arg), g2) for arg in args) print "%s(%s) evaluates to %s" % (fun, args, argsval in funval) except Undefined: print "%s(%s) evaluates to Undefined" % (fun, args) # Demo 3: FOL ######### def foldemo(trace=None): """ Interpretation of closed expressions in a first-order model. """ folmodel(quiet=True) print print '*' * mult print "FOL Formulas Demo" print '*' * mult formulas = [ 'love (adam, betty)', '(adam = mia)', '\\x. (boy(x) | girl(x))', '\\x. boy(x)(adam)', '\\x y. love(x, y)', '\\x y. love(x, y)(adam)(betty)', '\\x y. love(x, y)(adam, betty)', '\\x y. (boy(x) & love(x, y))', '\\x. exists y. (boy(x) & love(x, y))', 'exists z1. boy(z1)', 'exists x. (boy(x) & -(x = adam))', 'exists x. (boy(x) & all y. love(y, x))', 'all x. (boy(x) | girl(x))', 'all x. (girl(x) -> exists y. boy(y) & love(x, y))', #Every girl loves exists boy. 'exists x. (boy(x) & all y. (girl(y) -> love(y, x)))', #There is exists boy that every girl loves. 'exists x. (boy(x) & all y. (girl(y) -> love(x, y)))', #exists boy loves every girl. 'all x. (dog(x) -> - girl(x))', 'exists x. exists y. (love(x, y) & love(x, y))' ] for fmla in formulas: g2.purge() if trace: m2.evaluate(fmla, g2, trace) else: print "The value of '%s' is: %s" % (fmla, m2.evaluate(fmla, g2)) # Demo 3: Satisfaction ############# def satdemo(trace=None): """Satisfiers of an open formula in a first order model.""" print print '*' * mult print "Satisfiers Demo" print '*' * mult folmodel(quiet=True) formulas = [ 'boy(x)', '(x = x)', '(boy(x) | girl(x))', '(boy(x) & girl(x))', 'love(adam, x)', 'love(x, adam)', '-(x = adam)', 'exists z22. love(x, z22)', 'exists y. love(y, x)', 'all y. (girl(y) -> love(x, y))', 'all y. (girl(y) -> love(y, x))', 'all y. (girl(y) -> (boy(x) & love(y, x)))', '(boy(x) & all y. (girl(y) -> love(x, y)))', '(boy(x) & all y. (girl(y) -> love(y, x)))', '(boy(x) & exists y. (girl(y) & love(y, x)))', '(girl(x) -> dog(x))', 'all y. (dog(y) -> (x = y))', 'exists y. love(y, x)', 'exists y. (love(adam, y) & love(y, x))' ] if trace: print m2 lp = LogicParser() for fmla in formulas: print fmla lp.parse(fmla) parsed = [lp.parse(fmla) for fmla in formulas] for p in parsed: g2.purge() print "The satisfiers of '%s' are: %s" % (p, m2.satisfiers(p, 'x', g2, trace)) def demo(num=0, trace=None): """ Run exists demos. - num = 1: propositional logic demo - num = 2: first order model demo (only if trace is set) - num = 3: first order sentences demo - num = 4: satisfaction of open formulas demo - any other value: run all the demos @param trace: trace = 1, or trace = 2 for more verbose tracing """ demos = { 1: propdemo, 2: folmodel, 3: foldemo, 4: satdemo} try: demos[num](trace=trace) except KeyError: for num in demos: demos[num](trace=trace) if __name__ == "__main__": demo(2, trace=0) nltk-2.0~b9/nltk/sem/drt_glue_demo.py0000644000175000017500000004360211331672442017505 0ustar bhavanibhavani# Natural Language Toolkit: GUI Demo for Glue Semantics with Discourse # Representation Theory (DRT) as meaning language # # Author: Dan Garrette # # Copyright (C) 2001-2010 NLTK Project # URL: # For license information, see LICENSE.TXT from tkFont import Font from nltk.draw import * from nltk.tag import RegexpTagger from nltk.parse.malt import MaltParser from logic import Variable from drt import DrsDrawer, DrtVariableExpression from glue import DrtGlue class DrtGlueDemo(object): def __init__(self, examples): # Set up the main window. self._top = Tk() self._top.title('DRT Glue Demo') # Set up key bindings. self._init_bindings() # Initialize the fonts.self._error = None self._init_fonts(self._top) self._examples = examples self._readingCache = [None for example in examples] # The user can hide the grammar. self._show_grammar = IntVar(self._top) self._show_grammar.set(1) # Set the data to None self._curExample = -1 self._readings = [] self._drs = None self._drsWidget = None self._error = None self._init_glue() # Create the basic frames. self._init_menubar(self._top) self._init_buttons(self._top) self._init_exampleListbox(self._top) self._init_readingListbox(self._top) self._init_canvas(self._top) # Resize callback self._canvas.bind('', self._configure) ######################################### ## Initialization Helpers ######################################### def _init_glue(self): tagger = RegexpTagger( [('^(David|Mary|John)$', 'NNP'), ('^(walks|sees|eats|chases|believes|gives|sleeps|chases|persuades|tries|seems|leaves)$', 'VB'), ('^(go|order|vanish|find|approach)$', 'VB'), ('^(a)$', 'ex_quant'), ('^(every)$', 'univ_quant'), ('^(sandwich|man|dog|pizza|unicorn|cat|senator)$', 'NN'), ('^(big|gray|former)$', 'JJ'), ('^(him|himself)$', 'PRP') ]) depparser = MaltParser(tagger=tagger) self._glue = DrtGlue(depparser=depparser, remove_duplicates=False) def _init_fonts(self, root): # See: self._sysfont = Font(font=Button()["font"]) root.option_add("*Font", self._sysfont) # TWhat's our font size (default=same as sysfont) self._size = IntVar(root) self._size.set(self._sysfont.cget('size')) self._boldfont = Font(family='helvetica', weight='bold', size=self._size.get()) self._font = Font(family='helvetica', size=self._size.get()) if self._size.get() < 0: big = self._size.get()-2 else: big = self._size.get()+2 self._bigfont = Font(family='helvetica', weight='bold', size=big) def _init_exampleListbox(self, parent): self._exampleFrame = listframe = Frame(parent) self._exampleFrame.pack(fill='both', side='left', padx=2) self._exampleList_label = Label(self._exampleFrame, font=self._boldfont, text='Examples') self._exampleList_label.pack() self._exampleList = Listbox(self._exampleFrame, selectmode='single', relief='groove', background='white', foreground='#909090', font=self._font, selectforeground='#004040', selectbackground='#c0f0c0') self._exampleList.pack(side='right', fill='both', expand=1) for example in self._examples: self._exampleList.insert('end', (' %s' % example)) self._exampleList.config(height=min(len(self._examples), 25), width=40) # Add a scrollbar if there are more than 25 examples. if len(self._examples) > 25: listscroll = Scrollbar(self._exampleFrame, orient='vertical') self._exampleList.config(yscrollcommand = listscroll.set) listscroll.config(command=self._exampleList.yview) listscroll.pack(side='left', fill='y') # If they select a example, apply it. self._exampleList.bind('<>', self._exampleList_select) def _init_readingListbox(self, parent): self._readingFrame = listframe = Frame(parent) self._readingFrame.pack(fill='both', side='left', padx=2) self._readingList_label = Label(self._readingFrame, font=self._boldfont, text='Readings') self._readingList_label.pack() self._readingList = Listbox(self._readingFrame, selectmode='single', relief='groove', background='white', foreground='#909090', font=self._font, selectforeground='#004040', selectbackground='#c0f0c0') self._readingList.pack(side='right', fill='both', expand=1) # Add a scrollbar if there are more than 25 examples. listscroll = Scrollbar(self._readingFrame, orient='vertical') self._readingList.config(yscrollcommand = listscroll.set) listscroll.config(command=self._readingList.yview) listscroll.pack(side='right', fill='y') self._populate_readingListbox() def _populate_readingListbox(self): # Populate the listbox with integers self._readingList.delete(0, 'end') for i in range(len(self._readings)): self._readingList.insert('end', (' %s' % (i+1))) self._readingList.config(height=min(len(self._readings), 25), width=5) # If they select a example, apply it. self._readingList.bind('<>', self._readingList_select) def _init_bindings(self): # Key bindings are a good thing. self._top.bind('', self.destroy) self._top.bind('', self.destroy) self._top.bind('', self.destroy) self._top.bind('n', self.next) self._top.bind('', self.next) self._top.bind('p', self.prev) self._top.bind('', self.prev) def _init_buttons(self, parent): # Set up the frames. self._buttonframe = buttonframe = Frame(parent) buttonframe.pack(fill='none', side='bottom', padx=3, pady=2) Button(buttonframe, text='Prev', background='#90c0d0', foreground='black', command=self.prev,).pack(side='left') Button(buttonframe, text='Next', background='#90c0d0', foreground='black', command=self.next,).pack(side='left') def _configure(self, event): self._autostep = 0 (x1, y1, x2, y2) = self._cframe.scrollregion() y2 = event.height - 6 self._canvas['scrollregion'] = '%d %d %d %d' % (x1,y1,x2,y2) self._redraw() def _init_canvas(self, parent): self._cframe = CanvasFrame(parent, background='white', #width=525, height=250, closeenough=10, border=2, relief='sunken') self._cframe.pack(expand=1, fill='both', side='top', pady=2) canvas = self._canvas = self._cframe.canvas() # Initially, there's no tree or text self._tree = None self._textwidgets = [] self._textline = None def _init_menubar(self, parent): menubar = Menu(parent) filemenu = Menu(menubar, tearoff=0) filemenu.add_command(label='Exit', underline=1, command=self.destroy, accelerator='q') menubar.add_cascade(label='File', underline=0, menu=filemenu) actionmenu = Menu(menubar, tearoff=0) actionmenu.add_command(label='Next', underline=0, command=self.next, accelerator='n, Space') actionmenu.add_command(label='Previous', underline=0, command=self.prev, accelerator='p, Backspace') menubar.add_cascade(label='Action', underline=0, menu=actionmenu) optionmenu = Menu(menubar, tearoff=0) optionmenu.add_checkbutton(label='Remove Duplicates', underline=0, variable=self._glue.remove_duplicates, command=self._toggle_remove_duplicates, accelerator='r') menubar.add_cascade(label='Options', underline=0, menu=optionmenu) viewmenu = Menu(menubar, tearoff=0) viewmenu.add_radiobutton(label='Tiny', variable=self._size, underline=0, value=10, command=self.resize) viewmenu.add_radiobutton(label='Small', variable=self._size, underline=0, value=12, command=self.resize) viewmenu.add_radiobutton(label='Medium', variable=self._size, underline=0, value=14, command=self.resize) viewmenu.add_radiobutton(label='Large', variable=self._size, underline=0, value=18, command=self.resize) viewmenu.add_radiobutton(label='Huge', variable=self._size, underline=0, value=24, command=self.resize) menubar.add_cascade(label='View', underline=0, menu=viewmenu) helpmenu = Menu(menubar, tearoff=0) helpmenu.add_command(label='About', underline=0, command=self.about) menubar.add_cascade(label='Help', underline=0, menu=helpmenu) parent.config(menu=menubar) ######################################### ## Main draw procedure ######################################### def _redraw(self): canvas = self._canvas # Delete the old DRS, widgets, etc. if self._drsWidget is not None: self._drsWidget.clear() if self._drs: self._drsWidget = DrsWidget( self._canvas, self._drs ) self._drsWidget.draw() if self._error: self._drsWidget = DrsWidget( self._canvas, self._error ) self._drsWidget.draw() ######################################### ## Button Callbacks ######################################### def destroy(self, *e): self._autostep = 0 if self._top is None: return self._top.destroy() self._top = None def prev(self, *e): selection = self._readingList.curselection() readingListSize = self._readingList.size() # there are readings if readingListSize > 0: # if one reading is currently selected if len(selection) == 1: index = int(selection[0]) # if it's on (or before) the first item if index <= 0: self._select_previous_example() else: self._readingList_store_selection(index-1) else: #select its first reading self._readingList_store_selection(readingListSize-1) else: self._select_previous_example() def _select_previous_example(self): #if the current example is not the first example if self._curExample > 0: self._exampleList_store_selection(self._curExample-1) else: #go to the last example self._exampleList_store_selection(len(self._examples)-1) def next(self, *e): selection = self._readingList.curselection() readingListSize = self._readingList.size() # if there are readings if readingListSize > 0: # if one reading is currently selected if len(selection) == 1: index = int(selection[0]) # if it's on (or past) the last item if index >= (readingListSize-1): self._select_next_example() else: self._readingList_store_selection(index+1) else: #select its first reading self._readingList_store_selection(0) else: self._select_next_example() def _select_next_example(self): #if the current example is not the last example if self._curExample < len(self._examples)-1: self._exampleList_store_selection(self._curExample+1) else: #go to the first example self._exampleList_store_selection(0) def about(self, *e): ABOUT = ("NLTK Discourse Representation Theory (DRT) Glue Semantics Demo\n"+ "Written by Daniel H. Garrette") TITLE = 'About: NLTK DRT Glue Demo' try: from tkMessageBox import Message Message(message=ABOUT, title=TITLE).show() except: ShowText(self._top, TITLE, ABOUT) def postscript(self, *e): self._autostep = 0 self._cframe.print_to_file() def mainloop(self, *args, **kwargs): """ Enter the Tkinter mainloop. This function must be called if this demo is created from a non-interactive program (e.g. from a secript); otherwise, the demo will close as soon as the script completes. """ if in_idle(): return self._top.mainloop(*args, **kwargs) def resize(self, size=None): if size is not None: self._size.set(size) size = self._size.get() self._font.configure(size=-(abs(size))) self._boldfont.configure(size=-(abs(size))) self._sysfont.configure(size=-(abs(size))) self._bigfont.configure(size=-(abs(size+2))) self._redraw() def _toggle_remove_duplicates(self): self._glue.remove_duplicates = not self._glue.remove_duplicates self._exampleList.selection_clear(0, 'end') self._readings = [] self._populate_readingListbox() self._readingCache = [None for ex in self._examples] self._curExample = -1 self._error = None self._drs = None self._redraw() def _exampleList_select(self, event): selection = self._exampleList.curselection() if len(selection) != 1: return self._exampleList_store_selection(int(selection[0])) def _exampleList_store_selection(self, index): self._curExample = index example = self._examples[index] self._exampleList.selection_clear(0, 'end') if example: cache = self._readingCache[index] if cache: if isinstance(cache, list): self._readings = cache self._error = None else: self._readings = [] self._error = cache else: try: self._readings = self._glue.parse_to_meaning(example) self._error = None self._readingCache[index] = self._readings except Exception, e: self._readings = [] self._error = DrtVariableExpression(Variable('Error: ' + str(e))) self._readingCache[index] = self._error #add a star to the end of the example self._exampleList.delete(index) self._exampleList.insert(index, (' %s *' % example)) self._exampleList.config(height=min(len(self._examples), 25), width=40) self._populate_readingListbox() self._exampleList.selection_set(index) self._drs = None self._redraw() def _readingList_select(self, event): selection = self._readingList.curselection() if len(selection) != 1: return self._readingList_store_selection(int(selection[0])) def _readingList_store_selection(self, index): reading = self._readings[index] self._readingList.selection_clear(0, 'end') if reading: self._readingList.selection_set(index) self._drs = reading.simplify().normalize().resolve_anaphora() self._redraw() class DrsWidget(object): def __init__(self, canvas, drs, **attribs): self._drs = drs self._canvas = canvas canvas.font = Font(font=canvas.itemcget(canvas.create_text(0, 0, text=''), 'font')) canvas._BUFFER = 3 self.bbox = (0, 0, 0, 0) def draw(self): (right, bottom) = DrsDrawer(self._drs, canvas=self._canvas).draw(); self.bbox = (0, 0, right+1, bottom+1) def clear(self): self._canvas.create_rectangle(self.bbox, fill="white", width="0" ) def demo(): examples = ['John walks', 'David sees Mary', 'David eats a sandwich', 'every man chases a dog', # 'every man believes a dog yawns', # 'John gives David a sandwich', 'John chases himself', # 'John persuades David to order a pizza', # 'John tries to go', # 'John tries to find a unicorn', # 'John seems to vanish', # 'a unicorn seems to approach', # 'every big cat leaves', # 'every gray cat leaves', # 'every big gray cat leaves', # 'a former senator leaves', # 'John likes a cat', # 'John likes every cat', # 'he walks', # 'John walks and he leaves' ] DrtGlueDemo(examples).mainloop() if __name__ == '__main__': demo() nltk-2.0~b9/nltk/sem/drt.py0000644000175000017500000010605711377057401015473 0ustar bhavanibhavani# Natural Language Toolkit: Discourse Representation Theory (DRT) # # Author: Dan Garrette # # Copyright (C) 2001-2010 NLTK Project # URL: # For license information, see LICENSE.TXT import operator from logic import * # Import Tkinter-based modules if they are available try: from Tkinter import Canvas from Tkinter import Tk from tkFont import Font from nltk.util import in_idle except ImportError: # No need to print a warning here, nltk.draw has already printed one. pass class DrtTokens(Tokens): DRS = 'DRS' DRS_CONC = '+' PRONOUN = 'PRO' OPEN_BRACKET = '[' CLOSE_BRACKET = ']' PUNCT = [DRS_CONC, OPEN_BRACKET, CLOSE_BRACKET] SYMBOLS = Tokens.SYMBOLS + PUNCT TOKENS = Tokens.TOKENS + [DRS] + PUNCT class AbstractDrs(object): """ This is the base abstract DRT Expression from which every DRT Expression extends. """ def applyto(self, other): return DrtApplicationExpression(self, other) def __neg__(self): return DrtNegatedExpression(self) def __and__(self, other): raise NotImplementedError() def __or__(self, other): assert isinstance(other, AbstractDrs) return DrtOrExpression(self, other) def __gt__(self, other): assert isinstance(other, AbstractDrs) return DrtImpExpression(self, other) def __lt__(self, other): assert isinstance(other, AbstractDrs) return DrtIffExpression(self, other) def tp_equals(self, other, prover=None): """ Pass the expression (self <-> other) to the theorem prover. If the prover says it is valid, then the self and other are equal. @param other: an C{AbstractDrs} to check equality against @param prover: a C{nltk.inference.api.Prover} """ assert isinstance(other, AbstractDrs) f1 = self.simplify().fol(); f2 = other.simplify().fol(); return f1.tp_equals(f2, prover) def _get_type(self): raise AttributeError("'%s' object has no attribute 'type'" % self.__class__.__name__) type = property(_get_type) def typecheck(self, signature=None): raise NotImplementedError() def __add__(self, other): return ConcatenationDRS(self, other) def get_refs(self, recursive=False): """ Return the set of discourse referents in this DRS. @param recursive: C{boolean} Also find discourse referents in subterms? @return: C{list} of C{Variable}s """ raise NotImplementedError() def is_pronoun_function(self): """ Is self of the form "PRO(x)"? """ return isinstance(self, DrtApplicationExpression) and \ isinstance(self.function, DrtAbstractVariableExpression) and \ self.function.variable.name == DrtTokens.PRONOUN and \ isinstance(self.argument, DrtIndividualVariableExpression) def make_EqualityExpression(self, first, second): return DrtEqualityExpression(first, second) def make_VariableExpression(self, variable): return DrtVariableExpression(variable) def resolve_anaphora(self): return resolve_anaphora(self) def draw(self): DrsDrawer(self).draw() class DRS(AbstractDrs, Expression): """A Discourse Representation Structure.""" def __init__(self, refs, conds): """ @param refs: C{list} of C{DrtIndividualVariableExpression} for the discourse referents @param conds: C{list} of C{Expression} for the conditions """ self.refs = refs self.conds = conds def replace(self, variable, expression, replace_bound=False): """Replace all instances of variable v with expression E in self, where v is free in self.""" try: #if a bound variable is the thing being replaced i = self.refs.index(variable) if not replace_bound: return self else: return DRS(self.refs[:i]+[expression.variable]+self.refs[i+1:], [cond.replace(variable, expression, True) for cond in self.conds]) except ValueError: #variable not bound by this DRS # any bound variable that appears in the expression must # be alpha converted to avoid a conflict for ref in (set(self.refs) & expression.free()): newvar = unique_variable(ref) newvarex = DrtVariableExpression(newvar) i = self.refs.index(ref) self = DRS(self.refs[:i]+[newvar]+self.refs[i+1:], [cond.replace(ref, newvarex, True) for cond in self.conds]) #replace in the conditions return DRS(self.refs, [cond.replace(variable, expression, replace_bound) for cond in self.conds]) def variables(self): """@see: Expression.variables()""" conds_vars = reduce(operator.or_, [c.variables() for c in self.conds], set()) return conds_vars - set(self.refs) def free(self, indvar_only=True): """@see: Expression.free()""" conds_free = reduce(operator.or_, [c.free(indvar_only) for c in self.conds], set()) return conds_free - set(self.refs) def get_refs(self, recursive=False): """@see: AbstractExpression.get_refs()""" if recursive: cond_refs = reduce(operator.add, [c.get_refs(True) for c in self.conds], []) return self.refs + cond_refs else: return self.refs def visit(self, function, combinator, default): """@see: Expression.visit()""" return reduce(combinator, [function(e) for e in self.refs + self.conds], default) def simplify(self): return DRS(self.refs, [cond.simplify() for cond in self.conds]) def fol(self): if not self.conds: raise Exception("Cannot convert DRS with no conditions to FOL.") accum = reduce(AndExpression, [c.fol() for c in self.conds]) for ref in self.refs[::-1]: accum = ExistsExpression(ref, accum) return accum def __eq__(self, other): r"""Defines equality modulo alphabetic variance. If we are comparing \x.M and \y.N, then check equality of M and N[x/y].""" if isinstance(other, DRS): if len(self.refs) == len(other.refs): converted_other = other for (r1, r2) in zip(self.refs, converted_other.refs): varex = self.make_VariableExpression(r1) converted_other = converted_other.replace(r2, varex, True) return self.conds == converted_other.conds return False def str(self, syntax=DrtTokens.NLTK): if syntax == DrtTokens.PROVER9: return self.fol().str(syntax) else: return '([%s],[%s])' % (','.join([str(r) for r in self.refs]), ', '.join([c.str(syntax) for c in self.conds])) def DrtVariableExpression(variable): """ This is a factory method that instantiates and returns a subtype of C{DrtAbstractVariableExpression} appropriate for the given variable. """ if is_indvar(variable.name): return DrtIndividualVariableExpression(variable) elif is_funcvar(variable.name): return DrtFunctionVariableExpression(variable) elif is_eventvar(variable.name): return DrtEventVariableExpression(variable) else: return DrtConstantExpression(variable) class DrtAbstractVariableExpression(AbstractDrs, AbstractVariableExpression): def fol(self): return self def get_refs(self, recursive=False): """@see: AbstractExpression.get_refs()""" return [] class DrtIndividualVariableExpression(DrtAbstractVariableExpression, IndividualVariableExpression): pass class DrtFunctionVariableExpression(DrtAbstractVariableExpression, FunctionVariableExpression): pass class DrtEventVariableExpression(DrtIndividualVariableExpression, EventVariableExpression): pass class DrtConstantExpression(DrtAbstractVariableExpression, ConstantExpression): pass class DrtNegatedExpression(AbstractDrs, NegatedExpression): def fol(self): return NegatedExpression(self.term.fol()) def get_refs(self, recursive=False): """@see: AbstractExpression.get_refs()""" return self.term.get_refs(recursive) class DrtLambdaExpression(AbstractDrs, LambdaExpression): def alpha_convert(self, newvar): """Rename all occurrences of the variable introduced by this variable binder in the expression to @C{newvar}. @param newvar: C{Variable}, for the new variable """ return self.__class__(newvar, self.term.replace(self.variable, DrtVariableExpression(newvar), True)) def fol(self): return LambdaExpression(self.variable, self.term.fol()) class DrtBooleanExpression(AbstractDrs, BooleanExpression): def get_refs(self, recursive=False): """@see: AbstractExpression.get_refs()""" if recursive: return self.first.get_refs(True) + self.second.get_refs(True) else: return [] class DrtOrExpression(DrtBooleanExpression, OrExpression): def fol(self): return OrExpression(self.first.fol(), self.second.fol()) class DrtImpExpression(DrtBooleanExpression, ImpExpression): def fol(self): first_drs = self.first second_drs = self.second accum = None if first_drs.conds: accum = reduce(AndExpression, [c.fol() for c in first_drs.conds]) if accum: accum = ImpExpression(accum, second_drs.fol()) else: accum = second_drs.fol() for ref in first_drs.refs[::-1]: accum = AllExpression(ref, accum) return accum class DrtIffExpression(DrtBooleanExpression, IffExpression): def fol(self): return IffExpression(self.first.fol(), self.second.fol()) class DrtEqualityExpression(AbstractDrs, EqualityExpression): def fol(self): return EqualityExpression(self.first.fol(), self.second.fol()) def get_refs(self, recursive=False): """@see: AbstractExpression.get_refs()""" if recursive: return self.first.get_refs(True) + self.second.get_refs(True) else: return [] class ConcatenationDRS(DrtBooleanExpression): """DRS of the form '(DRS + DRS)'""" def replace(self, variable, expression, replace_bound=False): """Replace all instances of variable v with expression E in self, where v is free in self.""" first = self.first second = self.second # If variable is bound by both first and second if isinstance(first, DRS) and isinstance(second, DRS) and \ variable in (set(first.get_refs(True)) & set(second.get_refs(True))): first = first.replace(variable, expression, True) second = second.replace(variable, expression, True) # If variable is bound by first elif isinstance(first, DRS) and variable in first.refs: if replace_bound: first = first.replace(variable, expression, replace_bound) second = second.replace(variable, expression, replace_bound) # If variable is bound by second elif isinstance(second, DRS) and variable in second.refs: if replace_bound: first = first.replace(variable, expression, replace_bound) second = second.replace(variable, expression, replace_bound) else: # alpha convert every ref that is free in 'expression' for ref in (set(self.get_refs(True)) & expression.free()): v = DrtVariableExpression(unique_variable(ref)) first = first.replace(ref, v, True) second = second.replace(ref, v, True) first = first.replace(variable, expression, replace_bound) second = second.replace(variable, expression, replace_bound) return self.__class__(first, second) def simplify(self): first = self.first.simplify() second = self.second.simplify() if isinstance(first, DRS) and isinstance(second, DRS): # For any ref that is in both 'first' and 'second' for ref in (set(first.get_refs(True)) & set(second.get_refs(True))): # alpha convert the ref in 'second' to prevent collision newvar = DrtVariableExpression(unique_variable(ref)) second = second.replace(ref, newvar, True) return DRS(first.refs + second.refs, first.conds + second.conds) else: return self.__class__(first,second) def get_refs(self, recursive=False): """@see: AbstractExpression.get_refs()""" return self.first.get_refs(recursive) + self.second.get_refs(recursive) def getOp(self, syntax=DrtTokens.NLTK): return DrtTokens.DRS_CONC def __eq__(self, other): r"""Defines equality modulo alphabetic variance. If we are comparing \x.M and \y.N, then check equality of M and N[x/y].""" if isinstance(other, ConcatenationDRS): self_refs = self.get_refs() other_refs = other.get_refs() if len(self_refs) == len(other_refs): converted_other = other for (r1,r2) in zip(self_refs, other_refs): varex = self.make_VariableExpression(r1) converted_other = converted_other.replace(r2, varex, True) return self.first == converted_other.first and \ self.second == converted_other.second return False def fol(self): return AndExpression(self.first.fol(), self.second.fol()) class DrtApplicationExpression(AbstractDrs, ApplicationExpression): def fol(self): return ApplicationExpression(self.function.fol(), self.argument.fol()) def get_refs(self, recursive=False): """@see: AbstractExpression.get_refs()""" if recursive: return self.function.get_refs(True) + self.argument.get_refs(True) else: return [] class PossibleAntecedents(list, AbstractDrs, Expression): def free(self, indvar_only=True): """Set of free variables.""" return set(self) def replace(self, variable, expression, replace_bound=False): """Replace all instances of variable v with expression E in self, where v is free in self.""" result = PossibleAntecedents() for item in self: if item == variable: self.append(expression) else: self.append(item) return result def str(self, syntax=DrtTokens.NLTK): return '[' + ','.join(map(str, self)) + ']' class AnaphoraResolutionException(Exception): pass def resolve_anaphora(expression, trail=[]): if isinstance(expression, ApplicationExpression): if expression.is_pronoun_function(): possible_antecedents = PossibleAntecedents() for ancestor in trail: for ref in ancestor.get_refs(): refex = expression.make_VariableExpression(ref) #========================================================== # Don't allow resolution to itself or other types #========================================================== if refex.__class__ == expression.argument.__class__ and \ not (refex == expression.argument): possible_antecedents.append(refex) if len(possible_antecedents) == 1: resolution = possible_antecedents[0] else: resolution = possible_antecedents return expression.make_EqualityExpression(expression.argument, resolution) else: r_function = resolve_anaphora(expression.function, trail + [expression]) r_argument = resolve_anaphora(expression.argument, trail + [expression]) return expression.__class__(r_function, r_argument) elif isinstance(expression, DRS): r_conds = [] for cond in expression.conds: r_cond = resolve_anaphora(cond, trail + [expression]) # if the condition is of the form '(x = [])' then raise exception if isinstance(r_cond, EqualityExpression): if isinstance(r_cond.first, PossibleAntecedents): #Reverse the order so that the variable is on the left temp = r_cond.first r_cond.first = r_cond.second r_cond.second = temp if isinstance(r_cond.second, PossibleAntecedents): if not r_cond.second: raise AnaphoraResolutionException("Variable '%s' does not " "resolve to anything." % r_cond.first) r_conds.append(r_cond) return expression.__class__(expression.refs, r_conds) elif isinstance(expression, AbstractVariableExpression): return expression elif isinstance(expression, NegatedExpression): return expression.__class__(resolve_anaphora(expression.term, trail + [expression])) elif isinstance(expression, ImpExpression): return expression.__class__(resolve_anaphora(expression.first, trail + [expression]), resolve_anaphora(expression.second, trail + [expression, expression.first])) elif isinstance(expression, BinaryExpression): return expression.__class__(resolve_anaphora(expression.first, trail + [expression]), resolve_anaphora(expression.second, trail + [expression])) elif isinstance(expression, LambdaExpression): return expression.__class__(expression.variable, resolve_anaphora(expression.term, trail + [expression])) class DrsDrawer(object): BUFFER = 3 #Space between elements TOPSPACE = 10 #Space above whole DRS OUTERSPACE = 6 #Space to the left, right, and bottom of the whle DRS def __init__(self, drs, size_canvas=True, canvas=None): """ @param drs: C{AbstractDrs}, The DRS to be drawn @param size_canvas: C{boolean}, True if the canvas size should be the exact size of the DRS @param canvas: C{Canvas} The canvas on which to draw the DRS. If none is given, create a new canvas. """ self.syntax = DrtTokens.NLTK master = None if not canvas: master = Tk() master.title("DRT") font = Font(family='helvetica', size=12) if size_canvas: canvas = Canvas(master, width=0, height=0) canvas.font = font self.canvas = canvas (right, bottom) = self._visit(drs, self.OUTERSPACE, self.TOPSPACE) width = max(right+self.OUTERSPACE, 100) height = bottom+self.OUTERSPACE canvas = Canvas(master, width=width, height=height)#, bg='white') else: canvas = Canvas(master, width=300, height=300) canvas.pack() canvas.font = font self.canvas = canvas self.drs = drs self.master = master def _get_text_height(self): """Get the height of a line of text""" return self.canvas.font.metrics("linespace") def draw(self, x=OUTERSPACE, y=TOPSPACE): """Draw the DRS""" self._handle(self.drs, self._draw_command, x, y) if self.master and not in_idle(): self.master.mainloop() else: return self._visit(self.drs, x, y) def _visit(self, expression, x, y): """ Return the bottom-rightmost point without actually drawing the item @param expression: the item to visit @param x: the top of the current drawing area @param y: the left side of the current drawing area @return: the bottom-rightmost point """ return self._handle(expression, self._visit_command, x, y) def _draw_command(self, item, x, y): """ Draw the given item at the given location @param item: the item to draw @param x: the top of the current drawing area @param y: the left side of the current drawing area @return: the bottom-rightmost point """ if isinstance(item, str): self.canvas.create_text(x, y, anchor='nw', font=self.canvas.font, text=item) elif isinstance(item, tuple): # item is the lower-right of a box (right, bottom) = item self.canvas.create_rectangle(x, y, right, bottom) horiz_line_y = y + self._get_text_height() + (self.BUFFER * 2) #the line separating refs from conds self.canvas.create_line(x, horiz_line_y, right, horiz_line_y) return self._visit_command(item, x, y) def _visit_command(self, item, x, y): """ Return the bottom-rightmost point without actually drawing the item @param item: the item to visit @param x: the top of the current drawing area @param y: the left side of the current drawing area @return: the bottom-rightmost point """ if isinstance(item, str): return (x + self.canvas.font.measure(item), y + self._get_text_height()) elif isinstance(item, tuple): return item def _handle(self, expression, command, x=0, y=0): """ @param expression: the expression to handle @param command: the function to apply, either _draw_command or _visit_command @param x: the top of the current drawing area @param y: the left side of the current drawing area @return: the bottom-rightmost point """ if command == self._visit_command: #if we don't need to draw the item, then we can use the cached values try: #attempt to retrieve cached values right = expression._drawing_width + x bottom = expression._drawing_height + y return (right, bottom) except AttributeError: #the values have not been cached yet, so compute them pass if isinstance(expression, DrtAbstractVariableExpression): factory = self._handle_VariableExpression elif isinstance(expression, DRS): factory = self._handle_DRS elif isinstance(expression, DrtNegatedExpression): factory = self._handle_NegatedExpression elif isinstance(expression, DrtLambdaExpression): factory = self._handle_LambdaExpression elif isinstance(expression, BinaryExpression): factory = self._handle_BinaryExpression elif isinstance(expression, DrtApplicationExpression): factory = self._handle_ApplicationExpression elif isinstance(expression, RA.PossibleAntecedents): factory = self._handle_VariableExpression else: raise Exception, expression.__class__.__name__ (right, bottom) = factory(expression, command, x, y) #cache the values expression._drawing_width = right - x expression._drawing_height = bottom - y return (right, bottom) def _handle_VariableExpression(self, expression, command, x, y): return command(expression.str(self.syntax), x, y) def _handle_NegatedExpression(self, expression, command, x, y): # Find the width of the negation symbol right = self._visit_command(DrtTokens.NOT[self.syntax], x, y)[0] # Handle term (right, bottom) = self._handle(expression.term, command, right, y) # Handle variables now that we know the y-coordinate command(DrtTokens.NOT[self.syntax], x, self._get_centered_top(y, bottom - y, self._get_text_height())) return (right, bottom) def _handle_DRS(self, expression, command, x, y): left = x + self.BUFFER #indent the left side bottom = y + self.BUFFER #indent the top # Handle Discourse Referents if expression.refs: refs = ' '.join([str(ref) for ref in expression.refs]) else: refs = ' ' (max_right, bottom) = command(refs, left, bottom) bottom += (self.BUFFER * 2) # Handle Conditions if expression.conds: for cond in expression.conds: (right, bottom) = self._handle(cond, command, left, bottom) max_right = max(max_right, right) bottom += self.BUFFER else: bottom += self._get_text_height() + self.BUFFER # Handle Box max_right += self.BUFFER return command((max_right, bottom), x, y) def _handle_ApplicationExpression(self, expression, command, x, y): function, args = expression.uncurry() if not isinstance(function, DrtAbstractVariableExpression): #It's not a predicate expression ("P(x,y)"), so leave arguments curried function = expression.function args = [expression.argument] # Get the max bottom of any element on the line function_bottom = self._visit(function, x, y)[1] max_bottom = max([function_bottom] + [self._visit(arg, x, y)[1] for arg in args]) line_height = max_bottom - y # Handle 'function' function_drawing_top = self._get_centered_top(y, line_height, function._drawing_height) right = self._handle(function, command, x, function_drawing_top)[0] # Handle open paren centred_string_top = self._get_centered_top(y, line_height, self._get_text_height()) right = command(DrtTokens.OPEN, right, centred_string_top)[0] # Handle each arg for (i,arg) in enumerate(args): arg_drawing_top = self._get_centered_top(y, line_height, arg._drawing_height) right = self._handle(arg, command, right, arg_drawing_top)[0] if i+1 < len(args): #since it's not the last arg, add a comma right = command(DrtTokens.COMMA + ' ', right, centred_string_top)[0] # Handle close paren right = command(DrtTokens.CLOSE, right, centred_string_top)[0] return (right, max_bottom) def _handle_LambdaExpression(self, expression, command, x, y): # Find the width of the lambda symbol and abstracted variables variables = DrtTokens.LAMBDA[self.syntax] + str(expression.variable) + DrtTokens.DOT[self.syntax] right = self._visit_command(variables, x, y)[0] # Handle term (right, bottom) = self._handle(expression.term, command, right, y) # Handle variables now that we know the y-coordinate command(variables, x, self._get_centered_top(y, bottom - y, self._get_text_height())) return (right, bottom) def _handle_BinaryExpression(self, expression, command, x, y): # Get the full height of the line, based on the operands first_height = self._visit(expression.first, 0, 0)[1] second_height = self._visit(expression.second, 0, 0)[1] line_height = max(first_height, second_height) # Handle open paren centred_string_top = self._get_centered_top(y, line_height, self._get_text_height()) right = command(DrtTokens.OPEN, x, centred_string_top)[0] # Handle the first operand first_height = expression.first._drawing_height (right, first_bottom) = self._handle(expression.first, command, right, self._get_centered_top(y, line_height, first_height)) # Handle the operator right = command(' %s ' % expression.getOp(self.syntax), right, centred_string_top)[0] # Handle the second operand second_height = expression.second._drawing_height (right, second_bottom) = self._handle(expression.second, command, right, self._get_centered_top(y, line_height, second_height)) # Handle close paren right = command(DrtTokens.CLOSE, right, centred_string_top)[0] return (right, max(first_bottom, second_bottom)) def _get_centered_top(self, top, full_height, item_height): """Get the y-coordinate of the point that a figure should start at if its height is 'item_height' and it needs to be centered in an area that starts at 'top' and is 'full_height' tall.""" return top + (full_height - item_height) / 2 class DrtParser(LogicParser): """A lambda calculus expression parser.""" def __init__(self): LogicParser.__init__(self) self.order_of_operations = dict( [(x,1) for x in DrtTokens.LAMBDA] + \ [(x,2) for x in DrtTokens.NOT] + \ [('APP',3)] + \ [(x,4) for x in DrtTokens.EQ+Tokens.NEQ] + \ [(DrtTokens.DRS_CONC,5)] + \ [(x,6) for x in DrtTokens.OR] + \ [(x,7) for x in DrtTokens.IMP] + \ [(x,8) for x in DrtTokens.IFF] + \ [(None,9)]) def get_all_symbols(self): """This method exists to be overridden""" return DrtTokens.SYMBOLS def isvariable(self, tok): return tok not in DrtTokens.TOKENS def handle(self, tok, context): """This method is intended to be overridden for logics that use different operators or expressions""" if tok in DrtTokens.NOT: return self.handle_negation(tok, context) elif tok in DrtTokens.LAMBDA: return self.handle_lambda(tok, context) elif tok == DrtTokens.OPEN: if self.inRange(0) and self.token(0) == DrtTokens.OPEN_BRACKET: return self.handle_DRS(tok, context) else: return self.handle_open(tok, context) elif tok.upper() == DrtTokens.DRS: self.assertNextToken(DrtTokens.OPEN) return self.handle_DRS(tok, context) elif self.isvariable(tok): return self.handle_variable(tok, context) def make_NegatedExpression(self, expression): return DrtNegatedExpression(expression) def handle_DRS(self, tok, context): # a DRS self.assertNextToken(DrtTokens.OPEN_BRACKET) refs = [] while self.inRange(0) and self.token(0) != DrtTokens.CLOSE_BRACKET: # Support expressions like: DRS([x y],C) == DRS([x,y],C) if refs and self.token(0) == DrtTokens.COMMA: self.token() # swallow the comma refs.append(self.get_next_token_variable('quantified')) self.assertNextToken(DrtTokens.CLOSE_BRACKET) if self.inRange(0) and self.token(0) == DrtTokens.COMMA: #if there is a comma (it's optional) self.token() # swallow the comma self.assertNextToken(DrtTokens.OPEN_BRACKET) conds = [] while self.inRange(0) and self.token(0) != DrtTokens.CLOSE_BRACKET: # Support expressions like: DRS([x y],C) == DRS([x, y],C) if conds and self.token(0) == DrtTokens.COMMA: self.token() # swallow the comma conds.append(self.parse_Expression(context)) self.assertNextToken(DrtTokens.CLOSE_BRACKET) self.assertNextToken(DrtTokens.CLOSE) return DRS(refs, conds) def make_EqualityExpression(self, first, second): """This method serves as a hook for other logic parsers that have different equality expression classes""" return DrtEqualityExpression(first, second) def get_BooleanExpression_factory(self, tok): """This method serves as a hook for other logic parsers that have different boolean operators""" if tok == DrtTokens.DRS_CONC: return ConcatenationDRS elif tok in DrtTokens.OR: return DrtOrExpression elif tok in DrtTokens.IMP: return DrtImpExpression elif tok in DrtTokens.IFF: return DrtIffExpression else: return None def make_BooleanExpression(self, factory, first, second): return factory(first, second) def make_ApplicationExpression(self, function, argument): return DrtApplicationExpression(function, argument) def make_VariableExpression(self, name): return DrtVariableExpression(Variable(name)) def make_LambdaExpression(self, variables, term): return DrtLambdaExpression(variables, term) def demo(): print '='*20 + 'TEST PARSE' + '='*20 parser = DrtParser() print parser.parse(r'([x,y],[sees(x,y)])') print parser.parse(r'([x],[man(x), walks(x)])') print parser.parse(r'\x.\y.([],[sees(x,y)])') print parser.parse(r'\x.([],[walks(x)])(john)') print parser.parse(r'(([x],[walks(x)]) + ([y],[runs(y)]))') print parser.parse(r'(([],[walks(x)]) -> ([],[runs(x)]))') print parser.parse(r'([x],[PRO(x), sees(John,x)])') print parser.parse(r'([x],[man(x), -([],[walks(x)])])') print parser.parse(r'([],[(([x],[man(x)]) -> ([],[walks(x)]))])') print '='*20 + 'Test fol()' + '='*20 print parser.parse(r'([x,y],[sees(x,y)])').fol() print '='*20 + 'Test alpha conversion and lambda expression equality' + '='*20 e1 = parser.parse(r'\x.([],[P(x)])') print e1 e2 = e1.alpha_convert(Variable('z')) print e2 print e1 == e2 print '='*20 + 'Test resolve_anaphora()' + '='*20 print resolve_anaphora(parser.parse(r'([x,y,z],[dog(x), cat(y), walks(z), PRO(z)])')) print resolve_anaphora(parser.parse(r'([],[(([x],[dog(x)]) -> ([y],[walks(y), PRO(y)]))])')) print resolve_anaphora(parser.parse(r'(([x,y],[]) + ([],[PRO(x)]))')) def test_draw(): expressions = [ r'x', r'([],[])', r'([x],[])', r'([x],[man(x)])', r'([x,y],[sees(x,y)])', r'([x],[man(x), walks(x)])', r'\x.([],[man(x), walks(x)])', r'\x y.([],[sees(x,y)])', r'([],[(([],[walks(x)]) + ([],[runs(x)]))])', r'([x],[man(x), -([],[walks(x)])])', r'([],[(([x],[man(x)]) -> ([],[walks(x)]))])' ] for e in expressions: d = DrtParser().parse(e) d.draw() if __name__ == '__main__': demo() nltk-2.0~b9/nltk/sem/cooper_storage.py0000644000175000017500000000773711327451601017715 0ustar bhavanibhavani# Natural Language Toolkit: Cooper storage for Quantifier Ambiguity # # Copyright (C) 2001-2010 NLTK Project # Author: Ewan Klein # URL: # For license information, see LICENSE.TXT from logic import LambdaExpression, ApplicationExpression, Variable, LogicParser from nltk.parse import load_parser from nltk.parse.featurechart import InstantiateVarsChart class CooperStore(object): """ A container for handling quantifier ambiguity via Cooper storage. """ def __init__(self, featstruct): """ @param featstruct: The value of the C{sem} node in a tree from L{parse_with_bindops()} @type featstruct: A L{FeatStruct} with features C{core} and C{store} """ self.featstruct = featstruct self.readings = [] try: self.core = featstruct['CORE'] self.store = featstruct['STORE'] except KeyError: print "%s is not a Cooper storage structure" % featstruct def _permute(self, lst): """ @return: An iterator over the permutations of the input list @type lst: C{list} @rtype: C{iterator} """ remove = lambda lst0, index: lst0[:index] + lst0[index+1:] if lst: for index, x in enumerate(lst): for y in self._permute(remove(lst, index)): yield (x,)+y else: yield () def s_retrieve(self, trace=False): """ Carry out S-Retrieval of binding operators in store. If hack=True, serialize the bindop and core as strings and reparse. Ugh. Each permutation of the store (i.e. list of binding operators) is taken to be a possible scoping of quantifiers. We iterate through the binding operators in each permutation, and successively apply them to the current term, starting with the core semantic representation, working from the inside out. Binding operators are of the form:: bo(\P.all x.(man(x) -> P(x)),z1) """ for perm, store_perm in enumerate(self._permute(self.store)): if trace: print "Permutation %s" % (perm+1) term = self.core for bindop in store_perm: # we just want the arguments that are wrapped by the 'bo' predicate quant, varex = tuple(bindop.args) # use var to make an abstraction over the current term and then # apply the quantifier to it term = ApplicationExpression(quant, LambdaExpression(varex.variable, term)) if trace: print " ", term term = term.simplify() self.readings.append(term) def parse_with_bindops(sentence, grammar=None, trace=0): """ Use a grammar with Binding Operators to parse a sentence. """ if not grammar: grammar = 'grammars/book_grammars/storage.fcfg' parser = load_parser(grammar, trace=trace, chart_class=InstantiateVarsChart) # Parse the sentence. tokens = sentence.split() return parser.nbest_parse(tokens) def demo(): from nltk.sem import cooper_storage as cs sentence = "every girl chases a dog" #sentence = "a man gives a bone to every dog" print print "Analyis of sentence '%s'" % sentence print "=" * 50 trees = cs.parse_with_bindops(sentence, trace=0) for tree in trees: semrep = cs.CooperStore(tree.node['SEM']) print print "Binding operators:" print "-" * 15 for s in semrep.store: print s print print "Core:" print "-" * 15 print semrep.core print print "S-Retrieval:" print "-" * 15 semrep.s_retrieve(trace=True) print "Readings:" print "-" * 15 for i, reading in enumerate(semrep.readings): print "%s: %s" % (i+1, reading) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/sem/chat80.py0000644000175000017500000006301511331672430015760 0ustar bhavanibhavani# Natural Language Toolkit: Chat-80 KB Reader # See http://www.w3.org/TR/swbp-skos-core-guide/ # # Copyright (C) 2001-2010 NLTK Project # Author: Ewan Klein , # URL: # For license information, see LICENSE.TXT """ Overview ======== Chat-80 was a natural language system which allowed the user to interrogate a Prolog knowledge base in the domain of world geography. It was developed in the early '80s by Warren and Pereira; see U{http://acl.ldc.upenn.edu/J/J82/J82-3002.pdf} for a description and U{http://www.cis.upenn.edu/~pereira/oldies.html} for the source files. This module contains functions to extract data from the Chat-80 relation files ('the world database'), and convert then into a format that can be incorporated in the FOL models of L{nltk.sem.evaluate}. The code assumes that the Prolog input files are available in the NLTK corpora directory. The Chat-80 World Database consists of the following files:: world0.pl rivers.pl cities.pl countries.pl contain.pl borders.pl This module uses a slightly modified version of C{world0.pl}, in which a set of Prolog rules have been omitted. The modified file is named C{world1.pl}. Currently, the file C{rivers.pl} is not read in, since it uses a list rather than a string in the second field. Reading Chat-80 Files ===================== Chat-80 relations are like tables in a relational database. The relation acts as the name of the table; the first argument acts as the 'primary key'; and subsequent arguments are further fields in the table. In general, the name of the table provides a label for a unary predicate whose extension is all the primary keys. For example, relations in C{cities.pl} are of the following form:: 'city(athens,greece,1368).' Here, C{'athens'} is the key, and will be mapped to a member of the unary predicate M{city}. The fields in the table are mapped to binary predicates. The first argument of the predicate is the primary key, while the second argument is the data in the relevant field. Thus, in the above example, the third field is mapped to the binary predicate M{population_of}, whose extension is a set of pairs such as C{'(athens, 1368)'}. An exception to this general framework is required by the relations in the files C{borders.pl} and C{contains.pl}. These contain facts of the following form:: 'borders(albania,greece).' 'contains0(africa,central_africa).' We do not want to form a unary concept out the element in the first field of these records, and we want the label of the binary relation just to be C{'border'}/C{'contain'} respectively. In order to drive the extraction process, we use 'relation metadata bundles' which are Python dictionaries such as the following:: city = {'label': 'city', 'closures': [], 'schema': ['city', 'country', 'population'], 'filename': 'cities.pl'} According to this, the file C{city['filename']} contains a list of relational tuples (or more accurately, the corresponding strings in Prolog form) whose predicate symbol is C{city['label']} and whose relational schema is C{city['schema']}. The notion of a C{closure} is discussed in the next section. Concepts ======== In order to encapsulate the results of the extraction, a class of L{Concept}s is introduced. A L{Concept} object has a number of attributes, in particular a C{prefLabel} and C{extension}, which make it easier to inspect the output of the extraction. In addition, the C{extension} can be further processed: in the case of the C{'border'} relation, we check that the relation is B{symmetric}, and in the case of the C{'contain'} relation, we carry out the B{transitive closure}. The closure properties associated with a concept is indicated in the relation metadata, as indicated earlier. The C{extension} of a L{Concept} object is then incorporated into a L{Valuation} object. Persistence =========== The functions L{val_dump} and L{val_load} are provided to allow a valuation to be stored in a persistent database and re-loaded, rather than having to be re-computed each time. Individuals and Lexical Items ============================= As well as deriving relations from the Chat-80 data, we also create a set of individual constants, one for each entity in the domain. The individual constants are string-identical to the entities. For example, given a data item such as C{'zloty'}, we add to the valuation a pair C{('zloty', 'zloty')}. In order to parse English sentences that refer to these entities, we also create a lexical item such as the following for each individual constant:: PropN[num=sg, sem=<\P.(P zloty)>] -> 'Zloty' The set of rules is written to the file C{chat_pnames.cfg} in the current directory. """ import re import shelve import os import sys import nltk ########################################################################### # Chat-80 relation metadata bundles needed to build the valuation ########################################################################### borders = {'rel_name': 'borders', 'closures': ['symmetric'], 'schema': ['region', 'border'], 'filename': 'borders.pl'} contains = {'rel_name': 'contains0', 'closures': ['transitive'], 'schema': ['region', 'contain'], 'filename': 'contain.pl'} city = {'rel_name': 'city', 'closures': [], 'schema': ['city', 'country', 'population'], 'filename': 'cities.pl'} country = {'rel_name': 'country', 'closures': [], 'schema': ['country', 'region', 'latitude', 'longitude', 'area', 'population', 'capital', 'currency'], 'filename': 'countries.pl'} circle_of_lat = {'rel_name': 'circle_of_latitude', 'closures': [], 'schema': ['circle_of_latitude', 'degrees'], 'filename': 'world1.pl'} circle_of_long = {'rel_name': 'circle_of_longitude', 'closures': [], 'schema': ['circle_of_longitude', 'degrees'], 'filename': 'world1.pl'} continent = {'rel_name': 'continent', 'closures': [], 'schema': ['continent'], 'filename': 'world1.pl'} region = {'rel_name': 'in_continent', 'closures': [], 'schema': ['region', 'continent'], 'filename': 'world1.pl'} ocean = {'rel_name': 'ocean', 'closures': [], 'schema': ['ocean'], 'filename': 'world1.pl'} sea = {'rel_name': 'sea', 'closures': [], 'schema': ['sea'], 'filename': 'world1.pl'} items = ['borders', 'contains', 'city', 'country', 'circle_of_lat', 'circle_of_long', 'continent', 'region', 'ocean', 'sea'] items = tuple(sorted(items)) item_metadata = { 'borders': borders, 'contains': contains, 'city': city, 'country': country, 'circle_of_lat': circle_of_lat, 'circle_of_long': circle_of_long, 'continent': continent, 'region': region, 'ocean': ocean, 'sea': sea } rels = item_metadata.values() not_unary = ['borders.pl', 'contain.pl'] ########################################################################### class Concept(object): """ A Concept class, loosely based on SKOS (U{http://www.w3.org/TR/swbp-skos-core-guide/}). """ def __init__(self, prefLabel, arity, altLabels=[], closures=[], extension=set()): """ @param prefLabel: the preferred label for the concept @type prefLabel: str @param arity: the arity of the concept @type arity: int @keyword altLabels: other (related) labels @type altLabels: list @keyword closures: closure properties of the extension \ (list items can be C{symmetric}, C{reflexive}, C{transitive}) @type closures: list @keyword extension: the extensional value of the concept @type extension: set """ self.prefLabel = prefLabel self.arity = arity self.altLabels = altLabels self.closures = closures #keep _extension internally as a set self._extension = extension #public access is via a list (for slicing) self.extension = list(extension) def __str__(self): #_extension = '' #for element in sorted(self.extension): #if isinstance(element, tuple): #element = '(%s, %s)' % (element) #_extension += element + ', ' #_extension = _extension[:-1] return "Label = '%s'\nArity = %s\nExtension = %s" % \ (self.prefLabel, self.arity, self.extension) def __repr__(self): return "Concept('%s')" % self.prefLabel def augment(self, data): """ Add more data to the C{Concept}'s extension set. @param data: a new semantic value @type data: string or pair of strings @rtype: set """ self._extension.add(data) self.extension = list(self._extension) return self._extension def _make_graph(self, s): """ Convert a set of pairs into an adjacency linked list encoding of a graph. """ g = {} for (x, y) in s: if x in g: g[x].append(y) else: g[x] = [y] return g def _transclose(self, g): """ Compute the transitive closure of a graph represented as a linked list. """ for x in g: for adjacent in g[x]: # check that adjacent is a key if adjacent in g: for y in g[adjacent]: if y not in g[x]: g[x].append(y) return g def _make_pairs(self, g): """ Convert an adjacency linked list back into a set of pairs. """ pairs = [] for node in g: for adjacent in g[node]: pairs.append((node, adjacent)) return set(pairs) def close(self): """ Close a binary relation in the C{Concept}'s extension set. @return: a new extension for the C{Concept} in which the relation is closed under a given property """ from nltk.sem import is_rel assert is_rel(self._extension) if 'symmetric' in self.closures: pairs = [] for (x, y) in self._extension: pairs.append((y, x)) sym = set(pairs) self._extension = self._extension.union(sym) if 'transitive' in self.closures: all = self._make_graph(self._extension) closed = self._transclose(all) trans = self._make_pairs(closed) #print sorted(trans) self._extension = self._extension.union(trans) self.extension = list(self._extension) def clause2concepts(filename, rel_name, schema, closures=[]): """ Convert a file of Prolog clauses into a list of L{Concept} objects. @param filename: filename containing the relations @type filename: C{str} @param rel_name: name of the relation @type rel_name: C{str} @param schema: the schema used in a set of relational tuples @type schema: C{list} @param closures: closure properties for the extension of the concept @type closures: C{list} @return: a list of L{Concept}s @rtype: C{list} """ concepts = [] # position of the subject of a binary relation subj = 0 # label of the 'primary key' pkey = schema[0] # fields other than the primary key fields = schema[1:] # convert a file into a list of lists records = _str2records(filename, rel_name) # add a unary concept corresponding to the set of entities # in the primary key position # relations in 'not_unary' are more like ordinary binary relations if not filename in not_unary: concepts.append(unary_concept(pkey, subj, records)) # add a binary concept for each non-key field for field in fields: obj = schema.index(field) concepts.append(binary_concept(field, closures, subj, obj, records)) return concepts def cities2table(filename, rel_name, dbname, verbose=False, setup=False): """ Convert a file of Prolog clauses into a database table. This is not generic, since it doesn't allow arbitrary schemas to be set as a parameter. Intended usage:: cities2table('cities.pl', 'city', 'city.db', verbose=True, setup=True) @param filename: filename containing the relations @type filename: C{str} @param rel_name: name of the relation @type rel_name: C{str} @param dbname: filename of persistent store @type schema: C{str} """ try: import sqlite3 records = _str2records(filename, rel_name) connection = sqlite3.connect(dbname) cur = connection.cursor() if setup: cur.execute('''CREATE TABLE city_table (City text, Country text, Population int)''') table_name = "city_table" for t in records: cur.execute('insert into %s values (?,?,?)' % table_name, t) if verbose: print "inserting values into %s: " % table_name, t connection.commit() if verbose: print "Commiting update to %s" % dbname cur.close() except ImportError: import warnings warnings.warn("To run this function, first install pysqlite.") def sql_query(dbname, query): """ Execute an SQL query over a database. @param dbname: filename of persistent store @type schema: C{str} @param query: SQL query @type rel_name: C{str} """ try: import sqlite3 path = nltk.data.find(dbname) connection = sqlite3.connect(path) # return ASCII strings if possible connection.text_factory = sqlite3.OptimizedUnicode cur = connection.cursor() return cur.execute(query) except ImportError: import warnings warnings.warn("To run this function, first install pysqlite.") raise def _str2records(filename, rel): """ Read a file into memory and convert each relation clause into a list. """ recs = [] path = nltk.data.find("corpora/chat80/%s" % filename) for line in path.open(): if line.startswith(rel): line = re.sub(rel+r'\(', '', line) line = re.sub(r'\)\.$', '', line) line = line[:-1] record = line.split(',') recs.append(record) return recs def unary_concept(label, subj, records): """ Make a unary concept out of the primary key in a record. A record is a list of entities in some relation, such as C{['france', 'paris']}, where C{'france'} is acting as the primary key. @param label: the preferred label for the concept @type label: string @param subj: position in the record of the subject of the predicate @type subj: int @param records: a list of records @type records: C{list} of C{list}s @return: L{Concept} of arity 1 @rtype: L{Concept} """ c = Concept(label, arity=1, extension=set()) for record in records: c.augment(record[subj]) return c def binary_concept(label, closures, subj, obj, records): """ Make a binary concept out of the primary key and another field in a record. A record is a list of entities in some relation, such as C{['france', 'paris']}, where C{'france'} is acting as the primary key, and C{'paris'} stands in the C{'capital_of'} relation to C{'france'}. More generally, given a record such as C{['a', 'b', 'c']}, where label is bound to C{'B'}, and C{obj} bound to 1, the derived binary concept will have label C{'B_of'}, and its extension will be a set of pairs such as C{('a', 'b')}. @param label: the base part of the preferred label for the concept @type label: C{str} @param closures: closure properties for the extension of the concept @type closures: C{list} @param subj: position in the record of the subject of the predicate @type subj: C{int} @param obj: position in the record of the object of the predicate @type obj: C{int} @param records: a list of records @type records: C{list} of C{list}s @return: L{Concept} of arity 2 @rtype: L{Concept} """ if not label == 'border' and not label == 'contain': label = label + '_of' c = Concept(label, arity=2, closures=closures, extension=set()) for record in records: c.augment((record[subj], record[obj])) # close the concept's extension according to the properties in closures c.close() return c def process_bundle(rels): """ Given a list of relation metadata bundles, make a corresponding dictionary of concepts, indexed by the relation name. @param rels: bundle of metadata needed for constructing a concept @type rels: C{list} of C{dict} @return: a dictionary of concepts, indexed by the relation name. @rtype: C{dict} """ concepts = {} for rel in rels: rel_name = rel['rel_name'] closures = rel['closures'] schema = rel['schema'] filename = rel['filename'] concept_list = clause2concepts(filename, rel_name, schema, closures) for c in concept_list: label = c.prefLabel if(label in concepts.keys()): for data in c.extension: concepts[label].augment(data) concepts[label].close() else: concepts[label] = c return concepts def make_valuation(concepts, read=False, lexicon=False): """ Convert a list of C{Concept}s into a list of (label, extension) pairs; optionally create a C{Valuation} object. @param concepts: concepts @type concepts: list of L{Concept}s @param read: if C{True}, C{(symbol, set)} pairs are read into a C{Valuation} @type read: C{bool} @rtype: C{list} or a L{Valuation} """ vals = [] for c in concepts: vals.append((c.prefLabel, c.extension)) if lexicon: read = True if read: from nltk.sem import Valuation val = Valuation({}) val.update(vals) # add labels for individuals val = label_indivs(val, lexicon=lexicon) return val else: return vals def val_dump(rels, db): """ Make a L{Valuation} from a list of relation metadata bundles and dump to persistent database. @param rels: bundle of metadata needed for constructing a concept @type rels: C{list} of C{dict} @param db: name of file to which data is written. The suffix '.db' will be automatically appended. @type db: string """ concepts = process_bundle(rels).values() valuation = make_valuation(concepts, read=True) db_out = shelve.open(db, 'n') db_out.update(valuation) db_out.close() def val_load(db): """ Load a L{Valuation} from a persistent database. @param db: name of file from which data is read. The suffix '.db' should be omitted from the name. @type db: string """ dbname = db+".db" if not os.access(dbname, os.R_OK): sys.exit("Cannot read file: %s" % dbname) else: db_in = shelve.open(db) from nltk.sem import Valuation val = Valuation(db_in) # val.read(db_in.items()) return val #def alpha(str): #""" #Utility to filter out non-alphabetic constants. #@param str: candidate constant #@type str: string #@rtype: bool #""" #try: #int(str) #return False #except ValueError: ## some unknown values in records are labeled '?' #if not str == '?': #return True def label_indivs(valuation, lexicon=False): """ Assign individual constants to the individuals in the domain of a C{Valuation}. Given a valuation with an entry of the form {'rel': {'a': True}}, add a new entry {'a': 'a'}. @type valuation: L{Valuation} @rtype: L{Valuation} """ # collect all the individuals into a domain domain = valuation.domain # convert the domain into a sorted list of alphabetic terms # use the same string as a label pairs = [(e, e) for e in domain] if lexicon: lex = make_lex(domain) open("chat_pnames.cfg", mode='w').writelines(lex) # read the pairs into the valuation valuation.update(pairs) return valuation def make_lex(symbols): """ Create lexical CFG rules for each individual symbol. Given a valuation with an entry of the form {'zloty': 'zloty'}, create a lexical rule for the proper name 'Zloty'. @param symbols: a list of individual constants in the semantic representation @type symbols: sequence @rtype: list """ lex = [] header = """ ################################################################## # Lexical rules automatically generated by running 'chat80.py -x'. ################################################################## """ lex.append(header) template = "PropN[num=sg, sem=<\P.(P %s)>] -> '%s'\n" for s in symbols: parts = s.split('_') caps = [p.capitalize() for p in parts] pname = ('_').join(caps) rule = template % (s, pname) lex.append(rule) return lex ########################################################################### # Interface function to emulate other corpus readers ########################################################################### def concepts(items = items): """ Build a list of concepts corresponding to the relation names in C{items}. @param items: names of the Chat-80 relations to extract @type items: list of strings @return: the L{Concept}s which are extracted from the relations @rtype: list """ if type(items) is str: items = (items,) rels = [item_metadata[r] for r in items] concept_map = process_bundle(rels) return concept_map.values() ########################################################################### def main(): import sys from optparse import OptionParser description = \ """ Extract data from the Chat-80 Prolog files and convert them into a Valuation object for use in the NLTK semantics package. """ opts = OptionParser(description=description) opts.set_defaults(verbose=True, lex=False, vocab=False) opts.add_option("-s", "--store", dest="outdb", help="store a valuation in DB", metavar="DB") opts.add_option("-l", "--load", dest="indb", help="load a stored valuation from DB", metavar="DB") opts.add_option("-c", "--concepts", action="store_true", help="print concepts instead of a valuation") opts.add_option("-r", "--relation", dest="label", help="print concept with label REL (check possible labels with '-v' option)", metavar="REL") opts.add_option("-q", "--quiet", action="store_false", dest="verbose", help="don't print out progress info") opts.add_option("-x", "--lex", action="store_true", dest="lex", help="write a file of lexical entries for country names, then exit") opts.add_option("-v", "--vocab", action="store_true", dest="vocab", help="print out the vocabulary of concept labels and their arity, then exit") (options, args) = opts.parse_args() if options.outdb and options.indb: opts.error("Options --store and --load are mutually exclusive") if options.outdb: # write the valuation to a persistent database if options.verbose: outdb = options.outdb+".db" print "Dumping a valuation to %s" % outdb val_dump(rels, options.outdb) sys.exit(0) else: # try to read in a valuation from a database if options.indb is not None: dbname = options.indb+".db" if not os.access(dbname, os.R_OK): sys.exit("Cannot read file: %s" % dbname) else: valuation = val_load(options.indb) # we need to create the valuation from scratch else: # build some concepts concept_map = process_bundle(rels) concepts = concept_map.values() # just print out the vocabulary if options.vocab: items = [(c.arity, c.prefLabel) for c in concepts] items.sort() for (arity, label) in items: print label, arity sys.exit(0) # show all the concepts if options.concepts: for c in concepts: print c print if options.label: print concept_map[options.label] sys.exit(0) else: # turn the concepts into a Valuation if options.lex: if options.verbose: print "Writing out lexical rules" make_valuation(concepts, lexicon=True) else: valuation = make_valuation(concepts, read=True) print valuation def sql_demo(): """ Print out every row from the 'city.db' database. """ try: import sqlite3 print print "Using SQL to extract rows from 'city.db' RDB." for row in sql_query('corpora/city_database/city.db', "SELECT * FROM city_table"): print row except ImportError: import warnings warnings.warn("To run the SQL demo, first install pysqlite.") if __name__ == '__main__': main() sql_demo() nltk-2.0~b9/nltk/sem/boxer.py0000644000175000017500000005075211377057401016021 0ustar bhavanibhavani# Natural Language Toolkit: Interface to Boxer # # # Author: Dan Garrette # # Copyright (C) 2001-2010 NLTK Project # URL: # For license information, see LICENSE.TXT from __future__ import with_statement import os import subprocess from optparse import OptionParser import tempfile import nltk from nltk.sem.logic import * from nltk.sem.drt import * """ An interface to Boxer. Usage: Set the environment variable CANDCHOME to the bin directory of your CandC installation. The models directory should be in the CandC root directory. For example: /path/to/candc/ bin/ candc boxer models/ boxer/ """ class Boxer(object): """ This class is used to parse a sentence using Boxer into an NLTK DRS object. The BoxerDrsParser class is used for the actual conversion. """ def __init__(self): self._boxer_bin = None self._candc_bin = None self._candc_models_path = None def interpret(self, input, occur_index=False, sentence_id=None, verbose=False): """ Use Boxer to give a first order representation. @param input: C{str} Input sentences to parse @param occur_index: C{boolean} Should predicates be occurrence indexed? @param sentence_id: C{str} An identifer to be inserted to each occurrence-indexed predicate. @return: C{drt.AbstractDrs} """ return self.batch_interpret([input], occur_index, sentence_id, verbose)[0] def batch_interpret(self, inputs, occur_index=False, sentence_id=None, verbose=False): """ Use Boxer to give a first order representation. @param inputs: C{list} of C{str} Input sentences to parse @param occur_index: C{boolean} Should predicates be occurrence indexed? @param sentence_id: C{str} An identifer to be inserted to each occurrence-indexed predicate. @return: C{list} of C{drt.AbstractDrs} """ _, temp_filename = tempfile.mkstemp(prefix='boxer-', suffix='.in', text=True) candc_out = self._call_candc(inputs, temp_filename, verbose=verbose) boxer_out = self._call_boxer(temp_filename, verbose=verbose) os.remove(temp_filename) # if 'ERROR: input file contains no ccg/2 terms.' in boxer_out: # raise UnparseableInputException('Could not parse with candc: "%s"' % input_str) drs_dict = self._parse_to_drs_dict(boxer_out, occur_index, sentence_id) return [drs_dict.get(i+1, None) for i in range(len(inputs))] def _call_candc(self, inputs, filename, verbose=False): """ Call the C{candc} binary with the given input. @param inputs: C{list} of C{str} Input sentences to parse @param filename: C{str} A filename for the output file @return: stdout """ if self._candc_bin is None: self._candc_bin = self._find_binary('candc', verbose) if self._candc_models_path is None: self._candc_models_path = os.path.normpath(os.path.join(self._candc_bin[:-5], '../models')) args = ['--models', os.path.join(self._candc_models_path, 'boxer'), '--output', filename] return self._call('\n'.join(inputs), self._candc_bin, args, verbose) def _call_boxer(self, filename, verbose=False): """ Call the C{boxer} binary with the given input. @param filename: C{str} A filename for the input file @return: stdout """ if self._boxer_bin is None: self._boxer_bin = self._find_binary('boxer', verbose) args = ['--box', 'false', '--semantics', 'drs', '--format', 'prolog', '--flat', 'false', '--resolve', 'true', '--elimeq', 'true', '--input', filename] return self._call(None, self._boxer_bin, args, verbose) def _find_binary(self, name, verbose=False): return nltk.internals.find_binary(name, env_vars=['CANDCHOME'], url='http://svn.ask.it.usyd.edu.au/trac/candc/', binary_names=[name, name + '.exe'], verbose=verbose) def _call(self, input_str, binary, args=[], verbose=False): """ Call the binary with the given input. @param input_str: A string whose contents are used as stdin. @param binary: The location of the binary to call @param args: A list of command-line arguments. @return: stdout """ if verbose: print 'Calling:', binary print 'Args:', args print 'Input:', input_str print 'Command:', binary + ' ' + ' '.join(args) # Call via a subprocess if input_str is None: cmd = [binary] + args p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) else: cmd = 'echo "%s" | %s %s' % (input_str, binary, ' '.join(args)) p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) stdout, stderr = p.communicate() if verbose: print 'Return code:', p.returncode if stdout: print 'stdout:\n', stdout, '\n' if stderr: print 'stderr:\n', stderr, '\n' if p.returncode != 0: raise Exception('ERROR CALLING: %s %s\nReturncode: %d\n%s' % (binary, ' '.join(args), p.returncode, stderr)) return stdout def _parse_to_drs_dict(self, boxer_out, occur_index, sentence_id): lines = boxer_out.split('\n') drs_dict = {} i = 0 while i < len(lines): line = lines[i] if line.startswith('id('): comma_idx = line.index(',') id = int(line[3:comma_idx]) drs_id = line[comma_idx+1:line.index(')')] line = lines[i+4] assert line.startswith('sem(%s,' % drs_id) line = lines[i+8] assert line.endswith(').') drs_input = line[:-2].strip() drs = BoxerDrsParser(occur_index, sentence_id).parse(drs_input).simplify() drs = self._clean_drs(drs) drs_dict[id] = drs i += 8 i += 1 return drs_dict def _clean_drs(self, drs): #Remove compound nouns drs = self._nn(drs) return drs def _nn(self, drs): if isinstance(drs, DRS): return DRS(drs.refs, map(self._nn, drs.conds)) elif isinstance(drs, DrtNegatedExpression): return DrtNegatedExpression(self._nn(drs.term)) elif isinstance(drs, DrtLambdaExpression): return DrtLambdaExpression(drs.variable, self._nn(drs.term)) elif isinstance(drs, BinaryExpression): return drs.__class__(self._nn(drs.first), self._nn(drs.second)) elif isinstance(drs, DrtApplicationExpression): func, args = drs.uncurry() if func.variable.name == 'r_nn_2': assert len(args) == 2 return DrtEqualityExpression(*args) else: accum = self._nn(func) for arg in args: accum = accum(self._nn(arg)) return accum return drs class BoxerDrsParser(DrtParser): def __init__(self, occur_index=False, sentence_id=None): """ This class is used to parse the Prolog DRS output from Boxer into an NLTK DRS object. Predicates are parsed into the form: _[[_]_]_arity So, the binary predicate representing the word 'see', which is a verb, appearing as the fourth word in sentence with id 't' would be: v_see_t_3_2 Note that sentence id and occurrence indexing are optional and controlled by parameters. @param occur_index: C{boolean} Should predicates be occurrence indexed? @param sentence_id: C{str} An identifer to be inserted to each occurrence-indexed predicate. """ DrtParser.__init__(self) self.occur_index = occur_index self.sentence_id = sentence_id self.quote_chars = [("'", "'", "\\", False)] def get_all_symbols(self): return ['(', ')', ',', '[', ']',':'] def handle(self, tok, context): return self.handle_drs(tok) def attempt_adjuncts(self, expression, context): return expression def parse_condition(self, indices): """ Parse a DRS condition @return: C{list} of C{AbstractDrs} """ tok = self.token() accum = self.handle_condition(tok, indices) if accum is None: raise UnexpectedTokenException(tok) return accum def handle_drs(self, tok): if tok == 'drs': return self.parse_drs() elif tok == 'merge': return self._make_binary_expression(ConcatenationDRS) elif tok == 'smerge': return self._make_binary_expression(ConcatenationDRS) def handle_condition(self, tok, indices): """ Handle a DRS condition @param indices: C{list} of C{int} @return: C{list} of C{AbstractDrs} """ if tok == 'not': self.assertToken(self.token(), '(') e = self.parse_Expression(None) self.assertToken(self.token(), ')') return [DrtNegatedExpression(e)] elif tok == 'or': return [self._make_binary_expression(DrtOrExpression)] elif tok == 'imp': return [self._make_binary_expression(DrtImpExpression)] elif tok == 'eq': return [self._handle_eq(indices)] elif tok == 'prop': return [self._handle_prop(indices)] elif tok == 'pred': return [self._handle_pred(indices)] elif tok == 'named': return [self._handle_named(indices)] elif tok == 'rel': return [self._handle_rel(indices)] elif tok == 'timex': return self._handle_timex(indices) elif tok == 'card': return [self._handle_card(indices)] elif tok == 'whq': return [self._handle_whq(indices)] def _handle_pred(self, indices): #pred(_G3943, dog, n, 0) self.assertToken(self.token(), '(') arg = self.token() self.assertToken(self.token(), ',') f_name = self._format_pred_name(self.token()) self.assertToken(self.token(), ',') f_pos = self.token() self.assertToken(self.token(), ',') f_sense = self.token() self.assertToken(self.token(), ')') f_pred = self._make_pred(f_pos, f_name, indices, 1) return self._make_atom(f_pred, arg) def _format_pred_name(self, name): out = '' for c in name: #if c not in '-\'"()[]/\\:;.,?+!`': if 'A' <= c <= 'Z' or \ 'a' <= c <= 'z' or \ '0' <= c <= '9' or \ c == '_': out += c return out def _handle_named(self, indices): #named(x0, john, per, 0) self.assertToken(self.token(), '(') arg = self.token() self.assertToken(self.token(), ',') f_name = self._format_pred_name(self.token()) self.assertToken(self.token(), ',') f_pos = self.token() self.assertToken(self.token(), ',') f_sense = self.token() self.assertToken(self.token(), ')') f_pred = 'n_%s_%s' % (f_name, 1) return self._make_atom(f_pred, arg) def _handle_rel(self, indices): #rel(_G3993, _G3943, agent, 0) self.assertToken(self.token(), '(') arg1 = self.token() self.assertToken(self.token(), ',') arg2 = self.token() self.assertToken(self.token(), ',') f_name = self._format_pred_name(self.token()) self.assertToken(self.token(), ',') f_sense = self.token() self.assertToken(self.token(), ')') f_pred = 'r_%s_%s' % (f_name, 2) return self._make_atom(f_pred, arg1, arg2) def _handle_timex(self, indices): #timex(_G18322, date([]: +, []:'XXXX', [1004]:'04', []:'XX')) self.assertToken(self.token(), '(') arg = self.token() self.assertToken(self.token(), ',') new_conds = self._handle_time_expression(arg) self.assertToken(self.token(), ')') return new_conds def _handle_time_expression(self, arg): #date([]: +, []:'XXXX', [1004]:'04', []:'XX') tok = self.token() self.assertToken(self.token(), '(') if tok == 'date': conds = self._handle_date(arg) elif tok == 'time': conds = self._handle_time(arg) else: return None self.assertToken(self.token(), ')') pred = 'r_%s_1' % (tok) return [self._make_atom(pred, arg)] + conds def _handle_date(self, arg): #[]: +, []:'XXXX', [1004]:'04', []:'XX' conds = [] self._parse_index_list() pol = self.token()#[1:-1] if pol == '+': conds.append(self._make_atom('r_pol_2',arg,'pos')) elif pol == '-': conds.append(self._make_atom('r_pol_2',arg,'neg')) self.assertToken(self.token(), ',') self._parse_index_list() year = self.token()#[1:-1] if year != 'XXXX': year = year.replace(':', '_') conds.append(self._make_atom('r_year_2',arg,year)) self.assertToken(self.token(), ',') self._parse_index_list() month = self.token()#[1:-1] if month != 'XX': conds.append(self._make_atom('r_month_2',arg,month)) self.assertToken(self.token(), ',') self._parse_index_list() day = self.token()#[1:-1] if day != 'XX': conds.append(self._make_atom('r_day_2',arg,day)) return conds def _handle_time(self, arg): #time([1018]:'18', []:'XX', []:'XX') conds = [] self._parse_index_list() hour = self.token() if hour != 'XX': conds.append(self._make_atom('r_hour_2',arg,hour)) self.assertToken(self.token(), ',') self._parse_index_list() min = self.token() if min != 'XX': conds.append(self._make_atom('r_min_2',arg,min)) self.assertToken(self.token(), ',') self._parse_index_list() sec = self.token() if sec != 'XX': conds.append(self._make_atom('r_sec_2',arg,sec)) return conds def _handle_card(self, indices): #card(_G18535, 28, ge) self.assertToken(self.token(), '(') arg = self.token() self.assertToken(self.token(), ',') value = self.token() self.assertToken(self.token(), ',') rel = self.token() self.assertToken(self.token(), ')') return self._make_atom('r_card_3', arg, value, rel) def _handle_prop(self, indices): #prop(_G15949, drs(...)) self.assertToken(self.token(), '(') arg = self.token() self.assertToken(self.token(), ',') drs = self.parse_Expression(None) self.assertToken(self.token(), ')') return drs def _make_atom(self, pred, *args): if isinstance(pred, str): pred = self._make_Variable(pred) if isinstance(pred, Variable): pred = DrtVariableExpression(pred) else: assert isinstance(pred, DrtAbstractVariableExpression), pred accum = pred for arg in args: if isinstance(arg, str): arg = self._make_Variable(arg) if isinstance(arg, Variable): arg = DrtVariableExpression(arg) else: assert isinstance(arg, DrtAbstractVariableExpression), arg accum = DrtApplicationExpression(accum, arg) return accum def _make_pred(self, pos, name, indices, arity): sent_id_str = '' if self.sentence_id: sent_id_str = '%s_' % self.sentence_id #TODO: removed since multiple indices mean it's not an occurrence word #assert len(indices) < 2, 'indices for %s: %s' % (f_name, indices) index_str = '' if self.occur_index and indices: index_str = '%s_' % indices[0] if not indices: pos = 'r' return '%s_%s_%s%s%s' % (pos, name, sent_id_str, index_str, arity) def _parse_index_list(self): #[1001,1002]: indices = [] self.assertToken(self.token(), '[') while self.token(0) != ']': indices.append(int(self.token())-1001) if self.token(0) == ',': self.token() #swallow ',' self.token() #swallow ']' self.assertToken(self.token(), ':') return indices def parse_drs(self): #drs([[1001]:_G3943], # [[1002]:pred(_G3943, dog, n, 0)] # ) self.assertToken(self.token(), '(') self.assertToken(self.token(), '[') refs = [] while self.token(0) != ']': indices = self._parse_index_list() refs.append(self._make_Variable(self.token())) if self.token(0) == ',': self.token() #swallow ',' self.token() #swallow ']' self.assertToken(self.token(), ',') self.assertToken(self.token(), '[') conds = [] while self.token(0) != ']': indices = self._parse_index_list() conds.extend(self.parse_condition(indices)) if self.token(0) == ',': self.token() #swallow ',' self.token() #swallow ']' self.assertToken(self.token(), ')') return DRS(refs, conds) def _make_binary_expression(self, constructor): self.assertToken(self.token(), '(') e1 = self.parse_Expression(None) self.assertToken(self.token(), ',') e2 = self.parse_Expression(None) self.assertToken(self.token(), ')') return constructor(e1, e2) def _handle_eq(self, indices): self.assertToken(self.token(), '(') e1 = DrtVariableExpression(self._make_Variable(self.token())) self.assertToken(self.token(), ',') e2 = DrtVariableExpression(self._make_Variable(self.token())) self.assertToken(self.token(), ')') return DrtEqualityExpression(e1, e2) def _handle_whq(self, indices): self.assertToken(self.token(), '(') self.assertToken(self.token(), '[') # c = 0 ans_types = [] while self.token(0) != ']': #c > 0 or self.token(0) != ']': # if self.token(0) == '[': # c += 1 # elif self.token(0) == ']': # c -= 1 cat = self.token() self.assertToken(self.token(), ':') if cat == 'des': ans_types.append(self.token()) elif cat == 'num': ans_types.append('number') typ = self.token() if typ == 'cou': ans_types.append('count') else: ans_types.append(typ) else: ans_types.append(self.token()) # self.token() #swallow the token self.token() #swallow the ']' self.assertToken(self.token(), ',') d1 = self.parse_Expression(None) self.assertToken(self.token(), ',') ref = DrtVariableExpression(self._make_Variable(self.token())) self.assertToken(self.token(), ',') d2 = self.parse_Expression(None) self.assertToken(self.token(), ')') d3 = DRS([],[self._make_atom(self._make_pred('n', f_name, [], 1), ref) for f_name in ans_types]) return ConcatenationDRS(d3,ConcatenationDRS(d1,d2)) def _make_Variable(self, name): if name.startswith('_G'): name = 'z' + name[2:] return Variable(name) class UnparseableInputException(Exception): pass if __name__ == '__main__': opts = OptionParser("usage: %prog TEXT [options]") opts.add_option("--verbose", "-v", help="display verbose logs", action="store_true", default=False, dest="verbose") opts.add_option("--fol", "-f", help="output FOL", action="store_true", default=False, dest="fol") (options, args) = opts.parse_args() if len(args) != 1: opts.error("incorrect number of arguments") drs = Boxer().interpret(args[0], verbose=options.verbose) if drs is None: print None else: if options.fol: drs = drs.fol() print drs.normalize() nltk-2.0~b9/nltk/sem/__init__.py0000644000175000017500000000200711377057401016427 0ustar bhavanibhavani# Natural Language Toolkit: Semantic Interpretation # # Copyright (C) 2001-2010 NLTK Project # Author: Ewan Klein # URL: # For license information, see LICENSE.TXT """ This package contains classes for representing semantic structure in formulas of first-order logic and for evaluating such formulas in set-theoretic models. """ from util import * from boxer import * from evaluate import * from logic import * from drt import * from relextract import * from chat80 import * __all__ = [ # Logic parsers 'LogicParser', 'DrtParser', 'Boxer', # Evaluation classes and methods 'Valuation', 'Assignment', 'Model', 'Undefined', 'is_rel', 'set2rel', 'arity', # utility methods 'text_parse', 'text_interpret', 'text_evaluate', 'batch_parse', 'batch_interpret', 'batch_evaluate', 'root_semrep', 'parse_valuation_line', 'parse_valuation', 'parse_logic', 'skolemize', # documentation 'boolean_ops', 'equality_preds', 'binding_ops' ] nltk-2.0~b9/nltk/parse/viterbi.py0000644000175000017500000004173611327451577016705 0ustar bhavanibhavani# Natural Language Toolkit: Viterbi Probabilistic Parser # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT from nltk.tree import Tree, ProbabilisticTree from api import * ##////////////////////////////////////////////////////// ## Viterbi PCFG Parser ##////////////////////////////////////////////////////// class ViterbiParser(ParserI): """ A bottom-up C{PCFG} parser that uses dynamic programming to find the single most likely parse for a text. The C{ViterbiParser} parser parses texts by filling in a X{most likely constituent table}. This table records the most probable tree representation for any given span and node value. In particular, it has an entry for every start index, end index, and node value, recording the most likely subtree that spans from the start index to the end index, and has the given node value. The C{ViterbiParser} parser fills in this table incrementally. It starts by filling in all entries for constituents that span one element of text (i.e., entries where the end index is one greater than the start index). After it has filled in all table entries for constituents that span one element of text, it fills in the entries for constitutants that span two elements of text. It continues filling in the entries for constituents spanning larger and larger portions of the text, until the entire table has been filled. Finally, it returns the table entry for a constituent spanning the entire text, whose node value is the grammar's start symbol. In order to find the most likely constituent with a given span and node value, the C{ViterbiParser} parser considers all productions that could produce that node value. For each production, it finds all children that collectively cover the span and have the node values specified by the production's right hand side. If the probability of the tree formed by applying the production to the children is greater than the probability of the current entry in the table, then the table is updated with this new tree. A pseudo-code description of the algorithm used by C{ViterbiParser} is: - Create an empty most likely constituent table, M{MLC}. - For M{width} in 1...len(M{text}): - For M{start} in 1...len(M{text})-M{width}: - For M{prod} in grammar.productions: - For each sequence of subtrees [M{t[1]}, M{t[2]}, ..., M{t[n]}] in M{MLC}, where M{t[i]}.node==M{prod}.rhs[i], and the sequence covers [M{start}:M{start}+M{width}]: - M{old_p} = M{MLC}[M{start}, M{start+width}, M{prod}.lhs] - M{new_p} = P(M{t[1]})*P(M{t[1]})*...*P(M{t[n]})*P(M{prod}) - if M{new_p} > M{old_p}: - M{new_tree} = Tree(M{prod}.lhs, M{t[1]}, M{t[2]}, ..., M{t[n]}) - M{MLC}[M{start}, M{start+width}, M{prod}.lhs] = M{new_tree} - Return M{MLC}[0, len(M{text}), M{start_symbol}] @type _grammar: C{WeightedGrammar} @ivar _grammar: The grammar used to parse sentences. @type _trace: C{int} @ivar _trace: The level of tracing output that should be generated when parsing a text. """ def __init__(self, grammar, trace=0): """ Create a new C{ViterbiParser} parser, that uses {grammar} to parse texts. @type grammar: C{WeightedGrammar} @param grammar: The grammar used to parse texts. @type trace: C{int} @param trace: The level of tracing that should be used when parsing a text. C{0} will generate no tracing output; and higher numbers will produce more verbose tracing output. """ self._grammar = grammar self._trace = trace def grammar(self): return self._grammar def trace(self, trace=2): """ Set the level of tracing output that should be generated when parsing a text. @type trace: C{int} @param trace: The trace level. A trace level of C{0} will generate no tracing output; and higher trace levels will produce more verbose tracing output. @rtype: C{None} """ self._trace = trace def nbest_parse(self, tokens, n=None): # Inherit docs from ParserI tokens = list(tokens) self._grammar.check_coverage(tokens) # The most likely constituent table. This table specifies the # most likely constituent for a given span and type. # Constituents can be either Trees or tokens. For # Trees, the "type" is the Nonterminal for the tree's # root node value. For Tokens, the "type" is the token's # type. The table is stored as a dictionary, since it is # sparse. constituents = {} # Initialize the constituents dictionary with the words from # the text. if self._trace: print ('Inserting tokens into the most likely'+ ' constituents table...') for index in range(len(tokens)): token = tokens[index] constituents[index,index+1,token] = token if self._trace > 1: self._trace_lexical_insertion(token, index, len(tokens)) # Consider each span of length 1, 2, ..., n; and add any trees # that might cover that span to the constituents dictionary. for length in range(1, len(tokens)+1): if self._trace: print ('Finding the most likely constituents'+ ' spanning %d text elements...' % length) #print constituents for start in range(len(tokens)-length+1): span = (start, start+length) self._add_constituents_spanning(span, constituents, tokens) # Find all trees that span the entire text & have the right cat trees = [constituents.get((0, len(tokens), self._grammar.start()), [])] # Sort the trees, and return the requested number of them. trees.sort(lambda t1,t2: cmp(t2.prob(), t1.prob())) return trees[:n] def _add_constituents_spanning(self, span, constituents, tokens): """ Find any constituents that might cover C{span}, and add them to the most likely constituents table. @rtype: C{None} @type span: C{(int, int)} @param span: The section of the text for which we are trying to find possible constituents. The span is specified as a pair of integers, where the first integer is the index of the first token that should be included in the constituent; and the second integer is the index of the first token that should not be included in the constituent. I.e., the constituent should cover C{M{text}[span[0]:span[1]]}, where C{M{text}} is the text that we are parsing. @type constituents: C{dictionary} from C{(int,int,Nonterminal)} to (C{ProbabilisticToken} or C{ProbabilisticTree}). @param constituents: The most likely constituents table. This table records the most probable tree representation for any given span and node value. In particular, C{constituents(M{s},M{e},M{nv})} is the most likely C{ProbabilisticTree} that covers C{M{text}[M{s}:M{e}]} and has a node value C{M{nv}.symbol()}, where C{M{text}} is the text that we are parsing. When C{_add_constituents_spanning} is called, C{constituents} should contain all possible constituents that are shorter than C{span}. @type tokens: C{list} of tokens @param tokens: The text we are parsing. This is only used for trace output. """ # Since some of the grammar productions may be unary, we need to # repeatedly try all of the productions until none of them add any # new constituents. changed = 1 while changed: changed = 0 # Find all ways instantiations of the grammar productions that # cover the span. instantiations = self._find_instantiations(span, constituents) # For each production instantiation, add a new # ProbabilisticTree whose probability is the product # of the childrens' probabilities and the production's # probability. for (production, children) in instantiations: subtrees = [c for c in children if isinstance(c, Tree)] p = reduce(lambda pr,t:pr*t.prob(), subtrees, production.prob()) node = production.lhs().symbol() tree = ProbabilisticTree(node, children, prob=p) # If it's new a constituent, then add it to the # constituents dictionary. c = constituents.get((span[0], span[1], production.lhs()), None) if self._trace > 1: if c is None or c != tree: if c is None or c.prob() < tree.prob(): print ' Insert:', else: print ' Discard:', self._trace_production(production, p, span, len(tokens)) if c is None or c.prob() < tree.prob(): constituents[span[0], span[1], production.lhs()] = tree changed = 1 def _find_instantiations(self, span, constituents): """ @return: a list of the production instantiations that cover a given span of the text. A X{production instantiation} is a tuple containing a production and a list of children, where the production's right hand side matches the list of children; and the children cover C{span}. @rtype: C{list} of C{pair} of C{Production}, (C{list} of (C{ProbabilisticTree} or token. @type span: C{(int, int)} @param span: The section of the text for which we are trying to find production instantiations. The span is specified as a pair of integers, where the first integer is the index of the first token that should be covered by the production instantiation; and the second integer is the index of the first token that should not be covered by the production instantiation. @type constituents: C{dictionary} from C{(int,int,Nonterminal)} to (C{ProbabilisticToken} or C{ProbabilisticTree}). @param constituents: The most likely constituents table. This table records the most probable tree representation for any given span and node value. See the module documentation for more information. """ rv = [] for production in self._grammar.productions(): childlists = self._match_rhs(production.rhs(), span, constituents) for childlist in childlists: rv.append( (production, childlist) ) return rv def _match_rhs(self, rhs, span, constituents): """ @return: a set of all the lists of children that cover C{span} and that match C{rhs}. @rtype: C{list} of (C{list} of C{ProbabilisticTree} or C{Token}) @type rhs: C{list} of C{Nonterminal} or (any) @param rhs: The list specifying what kinds of children need to cover C{span}. Each nonterminal in C{rhs} specifies that the corresponding child should be a tree whose node value is that nonterminal's symbol. Each terminal in C{rhs} specifies that the corresponding child should be a token whose type is that terminal. @type span: C{(int, int)} @param span: The section of the text for which we are trying to find child lists. The span is specified as a pair of integers, where the first integer is the index of the first token that should be covered by the child list; and the second integer is the index of the first token that should not be covered by the child list. @type constituents: C{dictionary} from C{(int,int,Nonterminal)} to (C{ProbabilisticToken} or C{ProbabilisticTree}). @param constituents: The most likely constituents table. This table records the most probable tree representation for any given span and node value. See the module documentation for more information. """ (start, end) = span # Base case if start >= end and rhs == (): return [[]] if start >= end or rhs == (): return [] # Find everything that matches the 1st symbol of the RHS childlists = [] for split in range(start, end+1): l=constituents.get((start,split,rhs[0])) if l is not None: rights = self._match_rhs(rhs[1:], (split,end), constituents) childlists += [[l]+r for r in rights] return childlists def _trace_production(self, production, p, span, width): """ Print trace output indicating that a given production has been applied at a given location. @param production: The production that has been applied @type production: C{Production} @param p: The probability of the tree produced by the production. @type p: C{float} @param span: The span of the production @type span: C{tuple} @rtype: C{None} """ str = '|' + '.' * span[0] str += '=' * (span[1] - span[0]) str += '.' * (width - span[1]) + '| ' str += '%s' % production if self._trace > 2: str = '%-40s %12.10f ' % (str, p) print str def _trace_lexical_insertion(self, token, index, width): str = ' Insert: |' + '.' * index + '=' + '.' * (width-index-1) + '| ' str += '%s' % (token,) print str def __repr__(self): return '' % self._grammar ##////////////////////////////////////////////////////// ## Test Code ##////////////////////////////////////////////////////// def demo(): """ A demonstration of the probabilistic parsers. The user is prompted to select which demo to run, and how many parses should be found; and then each parser is run on the same demo, and a summary of the results are displayed. """ import sys, time import nltk from nltk import tokenize from nltk.parse import ViterbiParser # Define two demos. Each demo has a sentence and a grammar. demos = [('I saw the man with my telescope', nltk.toy_pcfg1), ('the boy saw Jack with Bob under the table with a telescope', nltk.toy_pcfg2)] # Ask the user which demo they want to use. print for i in range(len(demos)): print '%3s: %s' % (i+1, demos[i][0]) print ' %r' % demos[i][1] print print 'Which demo (%d-%d)? ' % (1, len(demos)), try: snum = int(sys.stdin.readline().strip())-1 sent, grammar = demos[snum] except: print 'Bad sentence number' return # Tokenize the sentence. tokens = sent.split() parser = ViterbiParser(grammar) all_parses = {} print '\nsent: %s\nparser: %s\ngrammar: %s' % (sent,parser,grammar) parser.trace(3) t = time.time() parses = parser.nbest_parse(tokens) time = time.time()-t if parses: average = reduce(lambda a,b:a+b.prob(), parses, 0)/len(parses) else: average = 0 num_parses = len(parses) for p in parses: all_parses[p.freeze()] = 1 # Print some summary statistics print print 'Time (secs) # Parses Average P(parse)' print '-----------------------------------------' print '%11.4f%11d%19.14f' % (time, num_parses, average) parses = all_parses.keys() if parses: p = reduce(lambda a,b:a+b.prob(), parses, 0)/len(parses) else: p = 0 print '------------------------------------------' print '%11s%11d%19.14f' % ('n/a', len(parses), p) # Ask the user if we should draw the parses. print print 'Draw parses (y/n)? ', if sys.stdin.readline().strip().lower().startswith('y'): from nltk.draw.tree import draw_trees print ' please wait...' draw_trees(*parses) # Ask the user if we should print the parses. print print 'Print parses (y/n)? ', if sys.stdin.readline().strip().lower().startswith('y'): for parse in parses: print parse if __name__ == '__main__': demo() nltk-2.0~b9/nltk/parse/util.py0000644000175000017500000001411411331672414016172 0ustar bhavanibhavani# Natural Language Toolkit: Parser Utility Functions # # Author: Ewan Klein # # Copyright (C) 2001-2010 NLTK Project # URL: # For license information, see LICENSE.TXT """ Utility functions for parsers. """ from nltk.grammar import ContextFreeGrammar, FeatureGrammar, WeightedGrammar from chart import Chart, ChartParser from pchart import InsideChartParser from featurechart import FeatureChart, FeatureChartParser import nltk.data def load_parser(grammar_url, trace=0, parser=None, chart_class=None, beam_size=0, **load_args): """ Load a grammar from a file, and build a parser based on that grammar. The parser depends on the grammar format, and might also depend on properties of the grammar itself. The following grammar formats are currently supported: - C{'cfg'} (CFGs: L{ContextFreeGrammar}) - C{'pcfg'} (probabilistic CFGs: L{WeightedGrammar}) - C{'fcfg'} (feature-based CFGs: L{ContextFreeGrammar}) @type grammar_url: C{str} @param grammar_url: A URL specifying where the grammar is located. The default protocol is C{"nltk:"}, which searches for the file in the the NLTK data package. @type trace: C{int} @param trace: The level of tracing that should be used when parsing a text. C{0} will generate no tracing output; and higher numbers will produce more verbose tracing output. @param parser: The class used for parsing; should be L{ChartParser} or a subclass. If C{None}, the class depends on the grammar format. @param chart_class: The class used for storing the chart; should be L{Chart} or a subclass. Only used for CFGs and feature CFGs. If C{None}, the chart class depends on the grammar format. @type beam_size: C{int} @param beam_size: The maximum length for the parser's edge queue. Only used for probabilistic CFGs. @param load_args: Keyword parameters used when loading the grammar. See L{data.load} for more information. """ grammar = nltk.data.load(grammar_url, **load_args) if not isinstance(grammar, ContextFreeGrammar): raise ValueError("The grammar must be a ContextFreeGrammar, " "or a subclass thereof.") if isinstance(grammar, WeightedGrammar): if parser is None: parser = InsideChartParser return parser(grammar, trace=trace, beam_size=beam_size) elif isinstance(grammar, FeatureGrammar): if parser is None: parser = FeatureChartParser if chart_class is None: chart_class = FeatureChart return parser(grammar, trace=trace, chart_class=chart_class) else: # Plain ContextFreeGrammar. if parser is None: parser = ChartParser if chart_class is None: chart_class = Chart return parser(grammar, trace=trace, chart_class=chart_class) ###################################################################### #{ Test Suites ###################################################################### class TestGrammar(object): """ Unit tests for CFG. """ def __init__(self, grammar, suite, accept=None, reject=None): self.test_grammar = grammar self.cp = load_parser(grammar, trace=0) self.suite = suite self._accept = accept self._reject = reject def run(self, show_trees=False): """ Sentences in the test suite are divided into two classes: - grammatical (C{accept}) and - ungrammatical (C{reject}). If a sentence should parse accordng to the grammar, the value of C{trees} will be a non-empty list. If a sentence should be rejected according to the grammar, then the value of C{trees} will be C{None}. """ for test in self.suite: print test['doc'] + ":", for key in ['accept', 'reject']: for sent in test[key]: tokens = sent.split() trees = self.cp.parse(tokens) if show_trees and trees: print print sent for tree in trees: print tree if key=='accept': if trees == []: raise ValueError, "Sentence '%s' failed to parse'" % sent else: accepted = True else: if trees: raise ValueError, "Sentence '%s' received a parse'" % sent else: rejected = True if accepted and rejected: print "All tests passed!" def extract_test_sentences(string, comment_chars="#%;"): """ Parses a string with one test sentence per line. Lines can optionally begin with: - a C{bool}, saying if the sentence is grammatical or not, or - an C{int}, giving the number of parse trees is should have, The result information is followed by a colon, and then the sentence. Empty lines and lines beginning with a comment char are ignored. @return: a C{list} of C{tuple} of sentences and expected results, where a sentence is a C{list} of C{str}, and a result is C{None}, or C{bool}, or C{int} @param comment_chars: L{str} of possible comment characters. """ sentences = [] for sentence in string.split('\n'): if sentence=='' or sentence[0] in comment_chars: continue split_info = sentence.split(':', 1) result = None if len(split_info)==2: if split_info[0] in ['True','true','False','false']: result = split_info[0] in ['True','true'] sentence = split_info[1] else: result = int(split_info[0]) sentence = split_info[1] tokens = sentence.split() if tokens==[]: continue sentences += [(tokens, result)] return sentences nltk-2.0~b9/nltk/parse/sr.py0000644000175000017500000004051611327451577015660 0ustar bhavanibhavani# Natural Language Toolkit: Shift-Reduce Parser # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT import string from nltk.grammar import Nonterminal, parse_cfg from nltk.tree import Tree from api import * ##////////////////////////////////////////////////////// ## Shift/Reduce Parser ##////////////////////////////////////////////////////// class ShiftReduceParser(ParserI): """ A simple bottom-up CFG parser that uses two operations, "shift" and "reduce", to find a single parse for a text. C{ShiftReduceParser} maintains a stack, which records the structure of a portion of the text. This stack is a list of C{String}s and C{Tree}s that collectively cover a portion of the text. For example, while parsing the sentence "the dog saw the man" with a typical grammar, C{ShiftReduceParser} will produce the following stack, which covers "the dog saw":: [(NP: (Det: 'the') (N: 'dog')), (V: 'saw')] C{ShiftReduceParser} attempts to extend the stack to cover the entire text, and to combine the stack elements into a single tree, producing a complete parse for the sentence. Initially, the stack is empty. It is extended to cover the text, from left to right, by repeatedly applying two operations: - X{shift} moves a token from the beginning of the text to the end of the stack. - X{reduce} uses a CFG production to combine the rightmost stack elements into a single C{Tree}. Often, more than one operation can be performed on a given stack. In this case, C{ShiftReduceParser} uses the following heuristics to decide which operation to perform: - Only shift if no reductions are available. - If multiple reductions are available, then apply the reduction whose CFG production is listed earliest in the grammar. Note that these heuristics are not guaranteed to choose an operation that leads to a parse of the text. Also, if multiple parses exists, C{ShiftReduceParser} will return at most one of them. @see: C{nltk.grammar} """ def __init__(self, grammar, trace=0): """ Create a new C{ShiftReduceParser}, that uses C{grammar} to parse texts. @type grammar: C{Grammar} @param grammar: The grammar used to parse texts. @type trace: C{int} @param trace: The level of tracing that should be used when parsing a text. C{0} will generate no tracing output; and higher numbers will produce more verbose tracing output. """ self._grammar = grammar self._trace = trace self._check_grammar() def grammar(self): return self._grammar def parse(self, tokens): tokens = list(tokens) self._grammar.check_coverage(tokens) # initialize the stack. stack = [] remaining_text = tokens # Trace output. if self._trace: print 'Parsing %r' % string.join(tokens) self._trace_stack(stack, remaining_text) # iterate through the text, pushing the token onto # the stack, then reducing the stack. while len(remaining_text) > 0: self._shift(stack, remaining_text) while self._reduce(stack, remaining_text): pass # Did we reduce everything? if len(stack) != 1: return None # Did we end up with the right category? if stack[0].node != self._grammar.start().symbol(): return None # We parsed successfully! return stack[0] def _shift(self, stack, remaining_text): """ Move a token from the beginning of C{remaining_text} to the end of C{stack}. @type stack: C{list} of C{String} and C{Tree} @param stack: A list of C{String}s and C{Tree}s, encoding the structure of the text that has been parsed so far. @type remaining_text: C{list} of C{String} @param remaining_text: The portion of the text that is not yet covered by C{stack}. @rtype: C{None} """ stack.append(remaining_text[0]) remaining_text.remove(remaining_text[0]) if self._trace: self._trace_shift(stack, remaining_text) def _match_rhs(self, rhs, rightmost_stack): """ @rtype: C{boolean} @return: true if the right hand side of a CFG production matches the rightmost elements of the stack. C{rhs} matches C{rightmost_stack} if they are the same length, and each element of C{rhs} matches the corresponding element of C{rightmost_stack}. A nonterminal element of C{rhs} matches any C{Tree} whose node value is equal to the nonterminal's symbol. A terminal element of C{rhs} matches any C{String} whose type is equal to the terminal. @type rhs: C{list} of (terminal and C{Nonterminal}) @param rhs: The right hand side of a CFG production. @type rightmost_stack: C{list} of (C{String} and C{Tree}) @param rightmost_stack: The rightmost elements of the parser's stack. """ if len(rightmost_stack) != len(rhs): return 0 for i in range(len(rightmost_stack)): if isinstance(rightmost_stack[i], Tree): if not isinstance(rhs[i], Nonterminal): return 0 if rightmost_stack[i].node != rhs[i].symbol(): return 0 else: if isinstance(rhs[i], Nonterminal): return 0 if rightmost_stack[i] != rhs[i]: return 0 return 1 def _reduce(self, stack, remaining_text, production=None): """ Find a CFG production whose right hand side matches the rightmost stack elements; and combine those stack elements into a single C{Tree}, with the node specified by the production's left-hand side. If more than one CFG production matches the stack, then use the production that is listed earliest in the grammar. The new C{Tree} replaces the elements in the stack. @rtype: C{Production} or C{None} @return: If a reduction is performed, then return the CFG production that the reduction is based on; otherwise, return false. @type stack: C{list} of C{String} and C{Tree} @param stack: A list of C{String}s and C{Tree}s, encoding the structure of the text that has been parsed so far. @type remaining_text: C{list} of C{String} @param remaining_text: The portion of the text that is not yet covered by C{stack}. """ if production is None: productions = self._grammar.productions() else: productions = [production] # Try each production, in order. for production in productions: rhslen = len(production.rhs()) # check if the RHS of a production matches the top of the stack if self._match_rhs(production.rhs(), stack[-rhslen:]): # combine the tree to reflect the reduction tree = Tree(production.lhs().symbol(), stack[-rhslen:]) stack[-rhslen:] = [tree] # We reduced something if self._trace: self._trace_reduce(stack, production, remaining_text) return production # We didn't reduce anything return None def trace(self, trace=2): """ Set the level of tracing output that should be generated when parsing a text. @type trace: C{int} @param trace: The trace level. A trace level of C{0} will generate no tracing output; and higher trace levels will produce more verbose tracing output. @rtype: C{None} """ # 1: just show shifts. # 2: show shifts & reduces # 3: display which tokens & productions are shifed/reduced self._trace = trace def _trace_stack(self, stack, remaining_text, marker=' '): """ Print trace output displaying the given stack and text. @rtype: C{None} @param marker: A character that is printed to the left of the stack. This is used with trace level 2 to print 'S' before shifted stacks and 'R' before reduced stacks. """ str = ' '+marker+' [ ' for elt in stack: if isinstance(elt, Tree): str += `Nonterminal(elt.node)` + ' ' else: str += `elt` + ' ' str += '* ' + string.join(remaining_text) + ']' print str def _trace_shift(self, stack, remaining_text): """ Print trace output displaying that a token has been shifted. @rtype: C{None} """ if self._trace > 2: print 'Shift %r:' % stack[-1] if self._trace == 2: self._trace_stack(stack, remaining_text, 'S') elif self._trace > 0: self._trace_stack(stack, remaining_text) def _trace_reduce(self, stack, production, remaining_text): """ Print trace output displaying that C{production} was used to reduce C{stack}. @rtype: C{None} """ if self._trace > 2: rhs = string.join(production.rhs()) print 'Reduce %r <- %s' % (production.lhs(), rhs) if self._trace == 2: self._trace_stack(stack, remaining_text, 'R') elif self._trace > 1: self._trace_stack(stack, remaining_text) def _check_grammar(self): """ Check to make sure that all of the CFG productions are potentially useful. If any productions can never be used, then print a warning. @rtype: C{None} """ productions = self._grammar.productions() # Any production whose RHS is an extension of another production's RHS # will never be used. for i in range(len(productions)): for j in range(i+1, len(productions)): rhs1 = productions[i].rhs() rhs2 = productions[j].rhs() if rhs1[:len(rhs2)] == rhs2: print 'Warning: %r will never be used' % productions[i] ##////////////////////////////////////////////////////// ## Stepping Shift/Reduce Parser ##////////////////////////////////////////////////////// class SteppingShiftReduceParser(ShiftReduceParser): """ A C{ShiftReduceParser} that allows you to setp through the parsing process, performing a single operation at a time. It also allows you to change the parser's grammar midway through parsing a text. The C{initialize} method is used to start parsing a text. C{shift} performs a single shift operation, and C{reduce} performs a single reduce operation. C{step} will perform a single reduce operation if possible; otherwise, it will perform a single shift operation. C{parses} returns the set of parses that have been found by the parser. @ivar _history: A list of C{(stack, remaining_text)} pairs, containing all of the previous states of the parser. This history is used to implement the C{undo} operation. @see: C{nltk.grammar} """ def __init__(self, grammar, trace=0): self._grammar = grammar self._trace = trace self._stack = None self._remaining_text = None self._history = [] def nbest_parse(self, tokens, n=None): tokens = list(tokens) self.initialize(tokens) while self.step(): pass return self.parses()[:n] def stack(self): """ @return: The parser's stack. @rtype: C{list} of C{String} and C{Tree} """ return self._stack def remaining_text(self): """ @return: The portion of the text that is not yet covered by the stack. @rtype: C{list} of C{String} """ return self._remaining_text def initialize(self, tokens): """ Start parsing a given text. This sets the parser's stack to C{[]} and sets its remaining text to C{tokens}. """ self._stack = [] self._remaining_text = tokens self._history = [] def step(self): """ Perform a single parsing operation. If a reduction is possible, then perform that reduction, and return the production that it is based on. Otherwise, if a shift is possible, then perform it, and return 1. Otherwise, return 0. @return: 0 if no operation was performed; 1 if a shift was performed; and the CFG production used to reduce if a reduction was performed. @rtype: C{Production} or C{boolean} """ return self.reduce() or self.shift() def shift(self): """ Move a token from the beginning of the remaining text to the end of the stack. If there are no more tokens in the remaining text, then do nothing. @return: True if the shift operation was successful. @rtype: C{boolean} """ if len(self._remaining_text) == 0: return 0 self._history.append( (self._stack[:], self._remaining_text[:]) ) self._shift(self._stack, self._remaining_text) return 1 def reduce(self, production=None): """ Use C{production} to combine the rightmost stack elements into a single C{Tree}. If C{production} does not match the rightmost stack elements, then do nothing. @return: The production used to reduce the stack, if a reduction was performed. If no reduction was performed, return C{None}. @rtype: C{Production} or C{None} """ self._history.append( (self._stack[:], self._remaining_text[:]) ) return_val = self._reduce(self._stack, self._remaining_text, production) if not return_val: self._history.pop() return return_val def undo(self): """ Return the parser to its state before the most recent shift or reduce operation. Calling C{undo} repeatedly return the parser to successively earlier states. If no shift or reduce operations have been performed, C{undo} will make no changes. @return: true if an operation was successfully undone. @rtype: C{boolean} """ if len(self._history) == 0: return 0 (self._stack, self._remaining_text) = self._history.pop() return 1 def reducible_productions(self): """ @return: A list of the productions for which reductions are available for the current parser state. @rtype: C{list} of C{Production} """ productions = [] for production in self._grammar.productions(): rhslen = len(production.rhs()) if self._match_rhs(production.rhs(), self._stack[-rhslen:]): productions.append(production) return productions def parses(self): """ @return: A list of the parses that have been found by this parser so far. @rtype: C{list} of C{Tree} """ if len(self._remaining_text) != 0: return [] if len(self._stack) != 1: return [] if self._stack[0].node != self._grammar.start().symbol(): return [] return self._stack # copied from nltk.parser def set_grammar(self, grammar): """ Change the grammar used to parse texts. @param grammar: The new grammar. @type grammar: C{CFG} """ self._grammar = grammar ##////////////////////////////////////////////////////// ## Demonstration Code ##////////////////////////////////////////////////////// def demo(): """ A demonstration of the shift-reduce parser. """ from nltk import parse, parse_cfg grammar = parse_cfg(""" S -> NP VP NP -> Det N | Det N PP VP -> V NP | V NP PP PP -> P NP NP -> 'I' N -> 'man' | 'park' | 'telescope' | 'dog' Det -> 'the' | 'a' P -> 'in' | 'with' V -> 'saw' """) sent = 'I saw a man in the park'.split() parser = parse.ShiftReduceParser(grammar, trace=2) for p in parser.nbest_parse(sent): print p if __name__ == '__main__': demo() nltk-2.0~b9/nltk/parse/rd.py0000644000175000017500000006157611327451577015652 0ustar bhavanibhavani# Natural Language Toolkit: Recursive Descent Parser # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT import string from nltk.grammar import Nonterminal, parse_cfg from nltk.tree import Tree, ImmutableTree from api import * ##////////////////////////////////////////////////////// ## Recursive Descent Parser ##////////////////////////////////////////////////////// class RecursiveDescentParser(ParserI): """ A simple top-down CFG parser that parses texts by recursively expanding the fringe of a C{Tree}, and matching it against a text. C{RecursiveDescentParser} uses a list of tree locations called a X{frontier} to remember which subtrees have not yet been expanded and which leaves have not yet been matched against the text. Each tree location consists of a list of child indices specifying the path from the root of the tree to a subtree or a leaf; see the reference documentation for C{Tree} for more information about tree locations. When the parser begins parsing a text, it constructs a tree containing only the start symbol, and a frontier containing the location of the tree's root node. It then extends the tree to cover the text, using the following recursive procedure: - If the frontier is empty, and the text is covered by the tree, then return the tree as a possible parse. - If the frontier is empty, and the text is not covered by the tree, then return no parses. - If the first element of the frontier is a subtree, then use CFG productions to X{expand} it. For each applicable production, add the expanded subtree's children to the frontier, and recursively find all parses that can be generated by the new tree and frontier. - If the first element of the frontier is a token, then X{match} it against the next token from the text. Remove the token from the frontier, and recursively find all parses that can be generated by the new tree and frontier. @see: C{nltk.grammar} """ def __init__(self, grammar, trace=0): """ Create a new C{RecursiveDescentParser}, that uses C{grammar} to parse texts. @type grammar: C{ContextFreeGrammar} @param grammar: The grammar used to parse texts. @type trace: C{int} @param trace: The level of tracing that should be used when parsing a text. C{0} will generate no tracing output; and higher numbers will produce more verbose tracing output. """ self._grammar = grammar self._trace = trace def grammar(self): return self._grammar def nbest_parse(self, tokens, n=None): # Inherit docs from ParserI tokens = list(tokens) self._grammar.check_coverage(tokens) # Start a recursive descent parse, with an initial tree # containing just the start symbol. start = self._grammar.start().symbol() initial_tree = Tree(start, []) frontier = [()] if self._trace: self._trace_start(initial_tree, frontier, tokens) parses = self._parse(tokens, initial_tree, frontier) # Return the parses. return parses[:n] def _parse(self, remaining_text, tree, frontier): """ Recursively expand and match each elements of C{tree} specified by C{frontier}, to cover C{remaining_text}. Return a list of all parses found. @return: A list of all parses that can be generated by matching and expanding the elements of C{tree} specified by C{frontier}. @rtype: C{list} of C{Tree} @type tree: C{Tree} @param tree: A partial structure for the text that is currently being parsed. The elements of C{tree} that are specified by C{frontier} have not yet been expanded or matched. @type remaining_text: C{list} of C{String}s @param remaining_text: The portion of the text that is not yet covered by C{tree}. @type frontier: C{list} of C{tuple} of C{int} @param frontier: A list of the locations within C{tree} of all subtrees that have not yet been expanded, and all leaves that have not yet been matched. This list sorted in left-to-right order of location within the tree. """ # If the tree covers the text, and there's nothing left to # expand, then we've found a complete parse; return it. if len(remaining_text) == 0 and len(frontier) == 0: if self._trace: self._trace_succeed(tree, frontier) return [tree] # If there's still text, but nothing left to expand, we failed. elif len(frontier) == 0: if self._trace: self._trace_backtrack(tree, frontier) return [] # If the next element on the frontier is a tree, expand it. elif isinstance(tree[frontier[0]], Tree): return self._expand(remaining_text, tree, frontier) # If the next element on the frontier is a token, match it. else: return self._match(remaining_text, tree, frontier) def _match(self, rtext, tree, frontier): """ @rtype: C{list} of C{Tree} @return: a list of all parses that can be generated by matching the first element of C{frontier} against the first token in C{rtext}. In particular, if the first element of C{frontier} has the same type as the first token in C{rtext}, then substitute the token into C{tree}; and return all parses that can be generated by matching and expanding the remaining elements of C{frontier}. If the first element of C{frontier} does not have the same type as the first token in C{rtext}, then return empty list. @type tree: C{Tree} @param tree: A partial structure for the text that is currently being parsed. The elements of C{tree} that are specified by C{frontier} have not yet been expanded or matched. @type rtext: C{list} of C{String}s @param rtext: The portion of the text that is not yet covered by C{tree}. @type frontier: C{list} of C{tuple} of C{int} @param frontier: A list of the locations within C{tree} of all subtrees that have not yet been expanded, and all leaves that have not yet been matched. """ tree_leaf = tree[frontier[0]] if (len(rtext) > 0 and tree_leaf == rtext[0]): # If it's a terminal that matches rtext[0], then substitute # in the token, and continue parsing. newtree = tree.copy(deep=True) newtree[frontier[0]] = rtext[0] if self._trace: self._trace_match(newtree, frontier[1:], rtext[0]) return self._parse(rtext[1:], newtree, frontier[1:]) else: # If it's a non-matching terminal, fail. if self._trace: self._trace_backtrack(tree, frontier, rtext[:1]) return [] def _expand(self, remaining_text, tree, frontier, production=None): """ @rtype: C{list} of C{Tree} @return: A list of all parses that can be generated by expanding the first element of C{frontier} with C{production}. In particular, if the first element of C{frontier} is a subtree whose node type is equal to C{production}'s left hand side, then add a child to that subtree for each element of C{production}'s right hand side; and return all parses that can be generated by matching and expanding the remaining elements of C{frontier}. If the first element of C{frontier} is not a subtree whose node type is equal to C{production}'s left hand side, then return an empty list. If C{production} is not specified, then return a list of all parses that can be generated by expanding the first element of C{frontier} with I{any} CFG production. @type tree: C{Tree} @param tree: A partial structure for the text that is currently being parsed. The elements of C{tree} that are specified by C{frontier} have not yet been expanded or matched. @type remaining_text: C{list} of C{String}s @param remaining_text: The portion of the text that is not yet covered by C{tree}. @type frontier: C{list} of C{tuple} of C{int} @param frontier: A list of the locations within C{tree} of all subtrees that have not yet been expanded, and all leaves that have not yet been matched. """ if production is None: productions = self._grammar.productions() else: productions = [production] parses = [] for production in productions: lhs = production.lhs().symbol() if lhs == tree[frontier[0]].node: subtree = self._production_to_tree(production) if frontier[0] == (): newtree = subtree else: newtree = tree.copy(deep=True) newtree[frontier[0]] = subtree new_frontier = [frontier[0]+(i,) for i in range(len(production.rhs()))] if self._trace: self._trace_expand(newtree, new_frontier, production) parses += self._parse(remaining_text, newtree, new_frontier + frontier[1:]) return parses def _production_to_tree(self, production): """ @rtype: C{Tree} @return: The C{Tree} that is licensed by C{production}. In particular, given the production:: C{[M{lhs} -> M{elt[1]} ... M{elt[n]}]} Return a tree token that has a node C{M{lhs}.symbol}, and C{M{n}} children. For each nonterminal element C{M{elt[i]}} in the production, the tree token has a childless subtree with node value C{M{elt[i]}.symbol}; and for each terminal element C{M{elt[j]}}, the tree token has a leaf token with type C{M{elt[j]}}. @param production: The CFG production that licenses the tree token that should be returned. @type production: C{Production} """ children = [] for elt in production.rhs(): if isinstance(elt, Nonterminal): children.append(Tree(elt.symbol(), [])) else: # This will be matched. children.append(elt) return Tree(production.lhs().symbol(), children) def trace(self, trace=2): """ Set the level of tracing output that should be generated when parsing a text. @type trace: C{int} @param trace: The trace level. A trace level of C{0} will generate no tracing output; and higher trace levels will produce more verbose tracing output. @rtype: C{None} """ self._trace = trace def _trace_fringe(self, tree, treeloc=None): """ Print trace output displaying the fringe of C{tree}. The fringe of C{tree} consists of all of its leaves and all of its childless subtrees. @rtype: C{None} """ if treeloc == (): print "*", if isinstance(tree, Tree): if len(tree) == 0: print `Nonterminal(tree.node)`, for i in range(len(tree)): if treeloc is not None and i == treeloc[0]: self._trace_fringe(tree[i], treeloc[1:]) else: self._trace_fringe(tree[i]) else: print `tree`, def _trace_tree(self, tree, frontier, operation): """ Print trace output displaying the parser's current state. @param operation: A character identifying the operation that generated the current state. @rtype: C{None} """ if self._trace == 2: print ' %c [' % operation, else: print ' [', if len(frontier) > 0: self._trace_fringe(tree, frontier[0]) else: self._trace_fringe(tree) print ']' def _trace_start(self, tree, frontier, text): print 'Parsing %r' % string.join(text) if self._trace > 2: print 'Start:' if self._trace > 1: self._trace_tree(tree, frontier, ' ') def _trace_expand(self, tree, frontier, production): if self._trace > 2: print 'Expand: %s' % production if self._trace > 1: self._trace_tree(tree, frontier, 'E') def _trace_match(self, tree, frontier, tok): if self._trace > 2: print 'Match: %r' % tok if self._trace > 1: self._trace_tree(tree, frontier, 'M') def _trace_succeed(self, tree, frontier): if self._trace > 2: print 'GOOD PARSE:' if self._trace == 1: print 'Found a parse:\n%s' % tree if self._trace > 1: self._trace_tree(tree, frontier, '+') def _trace_backtrack(self, tree, frontier, toks=None): if self._trace > 2: if toks: print 'Backtrack: %r match failed' % toks[0] else: print 'Backtrack' ##////////////////////////////////////////////////////// ## Stepping Recursive Descent Parser ##////////////////////////////////////////////////////// class SteppingRecursiveDescentParser(RecursiveDescentParser): """ A C{RecursiveDescentParser} that allows you to step through the parsing process, performing a single operation at a time. The C{initialize} method is used to start parsing a text. C{expand} expands the first element on the frontier using a single CFG production, and C{match} matches the first element on the frontier against the next text token. C{backtrack} undoes the most recent expand or match operation. C{step} performs a single expand, match, or backtrack operation. C{parses} returns the set of parses that have been found by the parser. @ivar _history: A list of C{(rtext, tree, frontier)} tripples, containing the previous states of the parser. This history is used to implement the C{backtrack} operation. @ivar _tried_e: A record of all productions that have been tried for a given tree. This record is used by C{expand} to perform the next untried production. @ivar _tried_m: A record of what tokens have been matched for a given tree. This record is used by C{step} to decide whether or not to match a token. @see: C{nltk.grammar} """ def __init__(self, grammar, trace=0): self._grammar = grammar self._trace = trace self._rtext = None self._tree = None self._frontier = [()] self._tried_e = {} self._tried_m = {} self._history = [] self._parses = [] # [XX] TEMPORARY HACK WARNING! This should be replaced with # something nicer when we get the chance. def _freeze(self, tree): c = tree.copy() # for pos in c.treepositions('leaves'): # c[pos] = c[pos].freeze() return ImmutableTree.convert(c) def nbest_parse(self, tokens, n=None): tokens = list(tokens) self.initialize(tokens) while self.step() is not None: pass return self.parses()[:n] def initialize(self, tokens): """ Start parsing a given text. This sets the parser's tree to the start symbol, its frontier to the root node, and its remaining text to C{token['SUBTOKENS']}. """ self._rtext = tokens start = self._grammar.start().symbol() self._tree = Tree(start, []) self._frontier = [()] self._tried_e = {} self._tried_m = {} self._history = [] self._parses = [] if self._trace: self._trace_start(self._tree, self._frontier, self._rtext) def remaining_text(self): """ @return: The portion of the text that is not yet covered by the tree. @rtype: C{list} of C{String} """ return self._rtext def frontier(self): """ @return: A list of the tree locations of all subtrees that have not yet been expanded, and all leaves that have not yet been matched. @rtype: C{list} of C{tuple} of C{int} """ return self._frontier def tree(self): """ @return: A partial structure for the text that is currently being parsed. The elements specified by the frontier have not yet been expanded or matched. @rtype: C{Tree} """ return self._tree def step(self): """ Perform a single parsing operation. If an untried match is possible, then perform the match, and return the matched token. If an untried expansion is possible, then perform the expansion, and return the production that it is based on. If backtracking is possible, then backtrack, and return 1. Otherwise, return 0. @return: 0 if no operation was performed; a token if a match was performed; a production if an expansion was performed; and 1 if a backtrack operation was performed. @rtype: C{Production} or C{String} or C{boolean} """ # Try matching (if we haven't already) if self.untried_match(): token = self.match() if token is not None: return token # Try expanding. production = self.expand() if production is not None: return production # Try backtracking if self.backtrack(): self._trace_backtrack(self._tree, self._frontier) return 1 # Nothing left to do. return None def expand(self, production=None): """ Expand the first element of the frontier. In particular, if the first element of the frontier is a subtree whose node type is equal to C{production}'s left hand side, then add a child to that subtree for each element of C{production}'s right hand side. If C{production} is not specified, then use the first untried expandable production. If all expandable productions have been tried, do nothing. @return: The production used to expand the frontier, if an expansion was performed. If no expansion was performed, return C{None}. @rtype: C{Production} or C{None} """ # Make sure we *can* expand. if len(self._frontier) == 0: return None if not isinstance(self._tree[self._frontier[0]], Tree): return None # If they didn't specify a production, check all untried ones. if production is None: productions = self.untried_expandable_productions() else: productions = [production] parses = [] for prod in productions: # Record that we've tried this production now. self._tried_e.setdefault(self._freeze(self._tree), []).append(prod) # Try expanding. if self._expand(self._rtext, self._tree, self._frontier, prod): return prod # We didn't expand anything. return None def match(self): """ Match the first element of the frontier. In particular, if the first element of the frontier has the same type as the next text token, then substitute the text token into the tree. @return: The token matched, if a match operation was performed. If no match was performed, return C{None} @rtype: C{String} or C{None} """ # Record that we've tried matching this token. tok = self._rtext[0] self._tried_m.setdefault(self._freeze(self._tree), []).append(tok) # Make sure we *can* match. if len(self._frontier) == 0: return None if isinstance(self._tree[self._frontier[0]], Tree): return None if self._match(self._rtext, self._tree, self._frontier): # Return the token we just matched. return self._history[-1][0][0] else: return None def backtrack(self): """ Return the parser to its state before the most recent match or expand operation. Calling C{undo} repeatedly return the parser to successively earlier states. If no match or expand operations have been performed, C{undo} will make no changes. @return: true if an operation was successfully undone. @rtype: C{boolean} """ if len(self._history) == 0: return 0 (self._rtext, self._tree, self._frontier) = self._history.pop() return 1 def expandable_productions(self): """ @return: A list of all the productions for which expansions are available for the current parser state. @rtype: C{list} of C{Production} """ # Make sure we *can* expand. if len(self._frontier) == 0: return [] frontier_child = self._tree[self._frontier[0]] if (len(self._frontier) == 0 or not isinstance(frontier_child, Tree)): return [] return [p for p in self._grammar.productions() if p.lhs().symbol() == frontier_child.node] def untried_expandable_productions(self): """ @return: A list of all the untried productions for which expansions are available for the current parser state. @rtype: C{list} of C{Production} """ tried_expansions = self._tried_e.get(self._freeze(self._tree), []) return [p for p in self.expandable_productions() if p not in tried_expansions] def untried_match(self): """ @return: Whether the first element of the frontier is a token that has not yet been matched. @rtype: C{boolean} """ if len(self._rtext) == 0: return 0 tried_matches = self._tried_m.get(self._freeze(self._tree), []) return (self._rtext[0] not in tried_matches) def currently_complete(self): """ @return: Whether the parser's current state represents a complete parse. @rtype: C{boolean} """ return (len(self._frontier) == 0 and len(self._rtext) == 0) def _parse(self, remaining_text, tree, frontier): """ A stub version of C{_parse} that sets the parsers current state to the given arguments. In C{RecursiveDescentParser}, the C{_parse} method is used to recursively continue parsing a text. C{SteppingRecursiveDescentParser} overrides it to capture these recursive calls. It records the parser's old state in the history (to allow for backtracking), and updates the parser's new state using the given arguments. Finally, it returns C{[1]}, which is used by C{match} and C{expand} to detect whether their operations were successful. @return: C{[1]} @rtype: C{list} of C{int} """ self._history.append( (self._rtext, self._tree, self._frontier) ) self._rtext = remaining_text self._tree = tree self._frontier = frontier # Is it a good parse? If so, record it. if (len(frontier) == 0 and len(remaining_text) == 0): self._parses.append(tree) self._trace_succeed(self._tree, self._frontier) return [1] def parses(self): """ @return: A list of the parses that have been found by this parser so far. @rtype: C{list} of C{Tree} """ return self._parses def set_grammar(self, grammar): """ Change the grammar used to parse texts. @param grammar: The new grammar. @type grammar: C{CFG} """ self._grammar = grammar ##////////////////////////////////////////////////////// ## Demonstration Code ##////////////////////////////////////////////////////// def demo(): """ A demonstration of the recursive descent parser. """ from nltk import parse, parse_cfg grammar = parse_cfg(""" S -> NP VP NP -> Det N | Det N PP VP -> V NP | V NP PP PP -> P NP NP -> 'I' N -> 'man' | 'park' | 'telescope' | 'dog' Det -> 'the' | 'a' P -> 'in' | 'with' V -> 'saw' """) for prod in grammar.productions(): print prod sent = 'I saw a man in the park'.split() parser = parse.RecursiveDescentParser(grammar, trace=2) for p in parser.nbest_parse(sent): print p if __name__ == '__main__': demo() nltk-2.0~b9/nltk/parse/projectivedependencyparser.py0000644000175000017500000006006611327451577022664 0ustar bhavanibhavani# Natural Language Toolkit: Dependency Grammars # # Copyright (C) 2001-2010 NLTK Project # Author: Jason Narad # # URL: # For license information, see LICENSE.TXT # import math from nltk.grammar import DependencyProduction, DependencyGrammar,\ StatisticalDependencyGrammar, parse_dependency_grammar from dependencygraph import * from pprint import pformat ################################################################# # Dependency Span ################################################################# class DependencySpan(object): """ A contiguous span over some part of the input string representing dependency (head -> modifier) relationships amongst words. An atomic span corresponds to only one word so it isn't a 'span' in the conventional sense, as its _start_index = _end_index = _head_index for concatenation purposes. All other spans are assumed to have arcs between all nodes within the start and end indexes of the span, and one head index corresponding to the head word for the entire span. This is the same as the root node if the dependency structure were depicted as a graph. """ def __init__(self, start_index, end_index, head_index, arcs, tags): self._start_index = start_index self._end_index = end_index self._head_index = head_index self._arcs = arcs self._hash = hash((start_index, end_index, head_index, tuple(arcs))) self._tags = tags def head_index(self): """ @return: An value indexing the head of the entire C{DependencySpan}. @rtype: C{int}. """ return self._head_index def __repr__(self): """ @return: A concise string representatino of the C{DependencySpan}. @rtype: C{string}. """ return 'Span %d-%d; Head Index: %d' % (self._start_index, self._end_index, self._head_index) def __str__(self): """ @return: A verbose string representation of the C{DependencySpan}. @rtype: C{string}. """ str = 'Span %d-%d; Head Index: %d' % (self._start_index, self._end_index, self._head_index) for i in range(len(self._arcs)): str += '\n%d <- %d, %s' % (i, self._arcs[i], self._tags[i]) return str def __eq__(self, other): """ @return: true if this C{DependencySpan} is equal to C{other}. @rtype: C{boolean}. """ return (isinstance(other, self.__class__) and self._start_index == other._start_index and self._end_index == other._end_index and self._head_index == other._head_index and self._arcs == other._arcs) def __ne__(self, other): """ @return: false if this C{DependencySpan} is equal to C{other} @rtype: C{boolean} """ return not (self == other) def __cmp__(self, other): """ @return: -1 if args are of different class. Otherwise returns the cmp() of the two sets of spans. @rtype: C{int} """ if not isinstance(other, self.__class__): return -1 return cmp((self._start_index, self._start_index, self._head_index), (other._end_index, other._end_index, other._head_index)) def __hash__(self): """ @return: The hash value of this C{DependencySpan}. """ return self._hash ################################################################# # Chart Cell ################################################################# class ChartCell(object): """ A cell from the parse chart formed when performing the CYK algorithm. Each cell keeps track of its x and y coordinates (though this will probably be discarded), and a list of spans serving as the cell's entries. """ def __init__(self, x, y): """ @param x: This cell's x coordinate. @type x: C{int}. @param y: This cell's y coordinate. @type y: C{int}. """ self._x = x self._y = y self._entries = set([]) def add(self, span): """ Appends the given span to the list of spans representing the chart cell's entries. @param span: The span to add. @type span: C{DependencySpan}. """ self._entries.add(span); def __str__(self): """ @return: A verbose string representation of this C{ChartCell}. @rtype: C{string}. """ return 'CC[%d,%d]: %s' % (self._x, self._y, self._entries) def __repr__(self): """ @return: A concise string representation of this C{ChartCell}. @rtype: C{string}. """ return '%s' % self ################################################################# # Parsing with Dependency Grammars ################################################################# class ProjectiveDependencyParser(object): """ A projective, rule-based, dependency parser. A ProjectiveDependencyParser is created with a DependencyGrammar, a set of productions specifying word-to-word dependency relations. The parse() method will then return the set of all parses, in tree representation, for a given input sequence of tokens. Each parse must meet the requirements of the both the grammar and the projectivity constraint which specifies that the branches of the dependency tree are not allowed to cross. Alternatively, this can be understood as stating that each parent node and its children in the parse tree form a continuous substring of the input sequence. """ def __init__(self, dependency_grammar): """ Create a new ProjectiveDependencyParser, from a word-to-word dependency grammar C{DependencyGrammar}. @param dependency_grammar: A word-to-word relation dependencygrammar. @type dependency_grammar: A C{DependencyGrammar}. """ self._grammar = dependency_grammar def parse(self, tokens): """ Performs a projective dependency parse on the list of tokens using a chart-based, span-concatenation algorithm similar to Eisner (1996). @param tokens: The list of input tokens. @type tokens:a C{list} of L{String} @return: A list of parse trees. @rtype: a C{list} of L{tree} """ self._tokens = list(tokens) chart = [] for i in range(0, len(self._tokens) + 1): chart.append([]) for j in range(0, len(self._tokens) + 1): chart[i].append(ChartCell(i,j)) if i==j+1: chart[i][j].add(DependencySpan(i-1,i,i-1,[-1], ['null'])) for i in range(1,len(self._tokens)+1): for j in range(i-2,-1,-1): for k in range(i-1,j,-1): for span1 in chart[k][j]._entries: for span2 in chart[i][k]._entries: for newspan in self.concatenate(span1, span2): chart[i][j].add(newspan) graphs = [] trees = [] for parse in chart[len(self._tokens)][0]._entries: conll_format = "" # malt_format = "" for i in range(len(tokens)): # malt_format += '%s\t%s\t%d\t%s\n' % (tokens[i], 'null', parse._arcs[i] + 1, 'null') conll_format += '\t%d\t%s\t%s\t%s\t%s\t%s\t%d\t%s\t%s\t%s\n' % (i+1, tokens[i], tokens[i], 'null', 'null', 'null', parse._arcs[i] + 1, 'null', '-', '-') dg = DependencyGraph(conll_format) # if self.meets_arity(dg): graphs.append(dg) trees.append(dg.tree()) return trees def concatenate(self, span1, span2): """ Concatenates the two spans in whichever way possible. This includes rightward concatenation (from the leftmost word of the leftmost span to the rightmost word of the rightmost span) and leftward concatenation (vice-versa) between adjacent spans. Unlike Eisner's presentation of span concatenation, these spans do not share or pivot on a particular word/word-index. return: A list of new spans formed through concatenation. rtype: A C{list} of L{DependencySpan} """ spans = [] if span1._start_index == span2._start_index: print 'Error: Mismatched spans - replace this with thrown error' if span1._start_index > span2._start_index: temp_span = span1 span1 = span2 span2 = temp_span # adjacent rightward covered concatenation new_arcs = span1._arcs + span2._arcs new_tags = span1._tags + span2._tags if self._grammar.contains(self._tokens[span1._head_index], self._tokens[span2._head_index]): # print 'Performing rightward cover %d to %d' % (span1._head_index, span2._head_index) new_arcs[span2._head_index - span1._start_index] = span1._head_index spans.append(DependencySpan(span1._start_index, span2._end_index, span1._head_index, new_arcs, new_tags)) # adjacent leftward covered concatenation new_arcs = span1._arcs + span2._arcs if self._grammar.contains(self._tokens[span2._head_index], self._tokens[span1._head_index]): # print 'performing leftward cover %d to %d' % (span2._head_index, span1._head_index) new_arcs[span1._head_index - span1._start_index] = span2._head_index spans.append(DependencySpan(span1._start_index, span2._end_index, span2._head_index, new_arcs, new_tags)) return spans ################################################################# # Parsing with Probabilistic Dependency Grammars ################################################################# class ProbabilisticProjectiveDependencyParser(object): """ A probabilistic, projective dependency parser. This parser returns the most probable projective parse derived from the probabilistic dependency grammar derived from the train() method. The probabilistic model is an implementation of Eisner's (1996) Model C, which conditions on head-word, head-tag, child-word, and child-tag. The decoding uses a bottom-up chart-based span concatenation algorithm that's identical to the one utilized by the rule-based projective parser. """ def __init__(self): """ Create a new probabilistic dependency parser. No additional operations are necessary. """ print '' def parse(self, tokens): """ Parses the list of tokens subject to the projectivity constraint and the productions in the parser's grammar. This uses a method similar to the span-concatenation algorithm defined in Eisner (1996). It returns the most probable parse derived from the parser's probabilistic dependency grammar. """ self._tokens = list(tokens) chart = [] for i in range(0, len(self._tokens) + 1): chart.append([]) for j in range(0, len(self._tokens) + 1): chart[i].append(ChartCell(i,j)) if i==j+1: if self._grammar._tags.has_key(tokens[i-1]): for tag in self._grammar._tags[tokens[i-1]]: chart[i][j].add(DependencySpan(i-1,i,i-1,[-1], [tag])) else: print 'No tag found for input token \'%s\', parse is impossible.' % tokens[i-1] return [] for i in range(1,len(self._tokens)+1): for j in range(i-2,-1,-1): for k in range(i-1,j,-1): for span1 in chart[k][j]._entries: for span2 in chart[i][k]._entries: for newspan in self.concatenate(span1, span2): chart[i][j].add(newspan) graphs = [] trees = [] max_parse = None max_score = 0 for parse in chart[len(self._tokens)][0]._entries: conll_format = "" malt_format = "" for i in range(len(tokens)): malt_format += '%s\t%s\t%d\t%s\n' % (tokens[i], 'null', parse._arcs[i] + 1, 'null') conll_format += '\t%d\t%s\t%s\t%s\t%s\t%s\t%d\t%s\t%s\t%s\n' % (i+1, tokens[i], tokens[i], parse._tags[i], parse._tags[i], 'null', parse._arcs[i] + 1, 'null', '-', '-') dg = DependencyGraph(conll_format) score = self.compute_prob(dg) if score > max_score: max_parse = dg.tree() max_score = score return [max_parse, max_score] def concatenate(self, span1, span2): """ Concatenates the two spans in whichever way possible. This includes rightward concatenation (from the leftmost word of the leftmost span to the rightmost word of the rightmost span) and leftward concatenation (vice-versa) between adjacent spans. Unlike Eisner's presentation of span concatenation, these spans do not share or pivot on a particular word/word-index. return: A list of new spans formed through concatenation. rtype: A C{list} of L{DependencySpan} """ spans = [] if span1._start_index == span2._start_index: print 'Error: Mismatched spans - replace this with thrown error' if span1._start_index > span2._start_index: temp_span = span1 span1 = span2 span2 = temp_span # adjacent rightward covered concatenation new_arcs = span1._arcs + span2._arcs new_tags = span1._tags + span2._tags if self._grammar.contains(self._tokens[span1._head_index], self._tokens[span2._head_index]): new_arcs[span2._head_index - span1._start_index] = span1._head_index spans.append(DependencySpan(span1._start_index, span2._end_index, span1._head_index, new_arcs, new_tags)) # adjacent leftward covered concatenation new_arcs = span1._arcs + span2._arcs new_tags = span1._tags + span2._tags if self._grammar.contains(self._tokens[span2._head_index], self._tokens[span1._head_index]): new_arcs[span1._head_index - span1._start_index] = span2._head_index spans.append(DependencySpan(span1._start_index, span2._end_index, span2._head_index, new_arcs, new_tags)) return spans def train(self, graphs): """ Trains a StatisticalDependencyGrammar based on the list of input DependencyGraphs. This model is an implementation of Eisner's (1996) Model C, which derives its statistics from head-word, head-tag, child-word, and child-tag relationships. param graphs: A list of dependency graphs to train from. type: A list of C{DependencyGraph} """ productions = [] events = {} tags = {} for dg in graphs: for node_index in range(1,len(dg.nodelist)): children = dg.nodelist[node_index]['deps'] nr_left_children = dg.left_children(node_index) nr_right_children = dg.right_children(node_index) nr_children = nr_left_children + nr_right_children for child_index in range(0 - (nr_left_children + 1), nr_right_children + 2): head_word = dg.nodelist[node_index]['word'] head_tag = dg.nodelist[node_index]['tag'] if tags.has_key(head_word): tags[head_word].add(head_tag) else: tags[head_word] = set([head_tag]) child = 'STOP' child_tag = 'STOP' prev_word = 'START' prev_tag = 'START' if child_index < 0: array_index = child_index + nr_left_children if array_index >= 0: child = dg.nodelist[children[array_index]]['word'] child_tag = dg.nodelist[children[array_index]]['tag'] if child_index != -1: prev_word = dg.nodelist[children[array_index + 1]]['word'] prev_tag = dg.nodelist[children[array_index + 1]]['tag'] if child != 'STOP': productions.append(DependencyProduction(head_word, [child])) head_event = '(head (%s %s) (mods (%s, %s, %s) left))' % (child, child_tag, prev_tag, head_word, head_tag) mod_event = '(mods (%s, %s, %s) left))' % (prev_tag, head_word, head_tag) if events.has_key(head_event): events[head_event] += 1 else: events[head_event] = 1 if events.has_key(mod_event): events[mod_event] += 1 else: events[mod_event] = 1 elif child_index > 0: array_index = child_index + nr_left_children - 1 if array_index < nr_children: child = dg.nodelist[children[array_index]]['word'] child_tag = dg.nodelist[children[array_index]]['tag'] if child_index != 1: prev_word = dg.nodelist[children[array_index - 1]]['word'] prev_tag = dg.nodelist[children[array_index - 1]]['tag'] if child != 'STOP': productions.append(DependencyProduction(head_word, [child])) head_event = '(head (%s %s) (mods (%s, %s, %s) right))' % (child, child_tag, prev_tag, head_word, head_tag) mod_event = '(mods (%s, %s, %s) right))' % (prev_tag, head_word, head_tag) if events.has_key(head_event): events[head_event] += 1 else: events[head_event] = 1 if events.has_key(mod_event): events[mod_event] += 1 else: events[mod_event] = 1 self._grammar = StatisticalDependencyGrammar(productions, events, tags) # print self._grammar def compute_prob(self, dg): """ Computes the probability of a dependency graph based on the parser's probability model (defined by the parser's statistical dependency grammar). param dg: A dependency graph to score. type dg: a C{DependencyGraph} return: The probability of the dependency graph. rtype: A number/double. """ prob = 1.0 for node_index in range(1,len(dg.nodelist)): children = dg.nodelist[node_index]['deps'] nr_left_children = dg.left_children(node_index) nr_right_children = dg.right_children(node_index) nr_children = nr_left_children + nr_right_children for child_index in range(0 - (nr_left_children + 1), nr_right_children + 2): head_word = dg.nodelist[node_index]['word'] head_tag = dg.nodelist[node_index]['tag'] child = 'STOP' child_tag = 'STOP' prev_word = 'START' prev_tag = 'START' if child_index < 0: array_index = child_index + nr_left_children if array_index >= 0: child = dg.nodelist[children[array_index]]['word'] child_tag = dg.nodelist[children[array_index]]['tag'] if child_index != -1: prev_word = dg.nodelist[children[array_index + 1]]['word'] prev_tag = dg.nodelist[children[array_index + 1]]['tag'] head_event = '(head (%s %s) (mods (%s, %s, %s) left))' % (child, child_tag, prev_tag, head_word, head_tag) mod_event = '(mods (%s, %s, %s) left))' % (prev_tag, head_word, head_tag) h_count = self._grammar._events[head_event] m_count = self._grammar._events[mod_event] prob *= (h_count / m_count) elif child_index > 0: array_index = child_index + nr_left_children - 1 if array_index < nr_children: child = dg.nodelist[children[array_index]]['word'] child_tag = dg.nodelist[children[array_index]]['tag'] if child_index != 1: prev_word = dg.nodelist[children[array_index - 1]]['word'] prev_tag = dg.nodelist[children[array_index - 1]]['tag'] head_event = '(head (%s %s) (mods (%s, %s, %s) right))' % (child, child_tag, prev_tag, head_word, head_tag) mod_event = '(mods (%s, %s, %s) right))' % (prev_tag, head_word, head_tag) h_count = self._grammar._events[head_event] m_count = self._grammar._events[mod_event] prob *= (h_count / m_count) return prob ################################################################# # Demos ################################################################# def demo(): projective_rule_parse_demo() # arity_parse_demo() projective_prob_parse_demo() def projective_rule_parse_demo(): """ A demonstration showing the creation and use of a C{DependencyGrammar} to perform a projective dependency parse. """ grammar = parse_dependency_grammar(""" 'scratch' -> 'cats' | 'walls' 'walls' -> 'the' 'cats' -> 'the' """) print grammar pdp = ProjectiveDependencyParser(grammar) trees = pdp.parse(['the', 'cats', 'scratch', 'the', 'walls']) for tree in trees: print tree def arity_parse_demo(): """ A demonstration showing the creation of a C{DependencyGrammar} in which a specific number of modifiers is listed for a given head. This can further constrain the number of possible parses created by a C{ProjectiveDependencyParser}. """ print print 'A grammar with no arity constraints. Each DependencyProduction' print 'specifies a relationship between one head word and only one' print 'modifier word.' grammar = parse_dependency_grammar(""" 'fell' -> 'price' | 'stock' 'price' -> 'of' | 'the' 'of' -> 'stock' 'stock' -> 'the' """) print grammar print print 'For the sentence \'The price of the stock fell\', this grammar' print 'will produce the following three parses:' pdp = ProjectiveDependencyParser(grammar) trees = pdp.parse(['the', 'price', 'of', 'the', 'stock', 'fell']) for tree in trees: print tree print print 'By contrast, the following grammar contains a ' print 'DependencyProduction that specifies a relationship' print 'between a single head word, \'price\', and two modifier' print 'words, \'of\' and \'the\'.' grammar = parse_dependency_grammar(""" 'fell' -> 'price' | 'stock' 'price' -> 'of' 'the' 'of' -> 'stock' 'stock' -> 'the' """) print grammar print print 'This constrains the number of possible parses to just one:' # unimplemented, soon to replace pdp = ProjectiveDependencyParser(grammar) trees = pdp.parse(['the', 'price', 'of', 'the', 'stock', 'fell']) for tree in trees: print tree def projective_prob_parse_demo(): """ A demo showing the training and use of a projective dependency parser. """ graphs = [DependencyGraph(entry) for entry in conll_data2.split('\n\n') if entry] ppdp = ProbabilisticProjectiveDependencyParser() print 'Training Probabilistic Projective Dependency Parser...' ppdp.train(graphs) sent = ['Cathy', 'zag', 'hen', 'wild', 'zwaaien', '.'] print 'Parsing \'', " ".join(sent), '\'...' parse = ppdp.parse(sent) print 'Parse:' print parse[0] if __name__ == '__main__': demo() nltk-2.0~b9/nltk/parse/pchart.py0000644000175000017500000004451311327451577016516 0ustar bhavanibhavani# Natural Language Toolkit: Probabilistic Chart Parsers # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT """ Classes and interfaces for associating probabilities with tree structures that represent the internal organization of a text. The probabilistic parser module defines C{BottomUpProbabilisticChartParser}. C{BottomUpProbabilisticChartParser} is an abstract class that implements a bottom-up chart parser for C{PCFG}s. It maintains a queue of edges, and adds them to the chart one at a time. The ordering of this queue is based on the probabilities associated with the edges, allowing the parser to expand more likely edges before less likely ones. Each subclass implements a different queue ordering, producing different search strategies. Currently the following subclasses are defined: - C{InsideChartParser} searches edges in decreasing order of their trees' inside probabilities. - C{RandomChartParser} searches edges in random order. - C{LongestChartParser} searches edges in decreasing order of their location's length. The C{BottomUpProbabilisticChartParser} constructor has an optional argument beam_size. If non-zero, this controls the size of the beam (aka the edge queue). This option is most useful with InsideChartParser. """ ##////////////////////////////////////////////////////// ## Bottom-Up PCFG Chart Parser ##////////////////////////////////////////////////////// # [XX] This might not be implemented quite right -- it would be better # to associate probabilities with child pointer lists. from nltk.tree import Tree, ProbabilisticTree from nltk.grammar import Nonterminal, WeightedGrammar from api import * from chart import Chart, LeafEdge, TreeEdge, AbstractChartRule # Probabilistic edges class ProbabilisticLeafEdge(LeafEdge): def prob(self): return 1.0 class ProbabilisticTreeEdge(TreeEdge): def __init__(self, prob, *args, **kwargs): self._prob = prob TreeEdge.__init__(self, *args, **kwargs) def prob(self): return self._prob def __cmp__(self, other): if self._prob != other.prob(): return -1 return TreeEdge.__cmp__(self, other) def from_production(production, index, p): return ProbabilisticTreeEdge(p, (index, index), production.lhs(), production.rhs(), 0) from_production = staticmethod(from_production) # Rules using probabilistic edges class ProbabilisticBottomUpInitRule(AbstractChartRule): NUM_EDGES=0 def apply_iter(self, chart, grammar): for index in range(chart.num_leaves()): new_edge = ProbabilisticLeafEdge(chart.leaf(index), index) if chart.insert(new_edge, ()): yield new_edge class ProbabilisticBottomUpPredictRule(AbstractChartRule): NUM_EDGES=1 def apply_iter(self, chart, grammar, edge): if edge.is_incomplete(): return for prod in grammar.productions(): if edge.lhs() == prod.rhs()[0]: new_edge = ProbabilisticTreeEdge.from_production(prod, edge.start(), prod.prob()) if chart.insert(new_edge, ()): yield new_edge class ProbabilisticFundamentalRule(AbstractChartRule): NUM_EDGES=2 def apply_iter(self, chart, grammar, left_edge, right_edge): # Make sure the rule is applicable. if not (left_edge.end() == right_edge.start() and left_edge.next() == right_edge.lhs() and left_edge.is_incomplete() and right_edge.is_complete()): return # Construct the new edge. p = left_edge.prob() * right_edge.prob() new_edge = ProbabilisticTreeEdge(p, span=(left_edge.start(), right_edge.end()), lhs=left_edge.lhs(), rhs=left_edge.rhs(), dot=left_edge.dot()+1) # Add it to the chart, with appropriate child pointers. changed_chart = False for cpl1 in chart.child_pointer_lists(left_edge): if chart.insert(new_edge, cpl1+(right_edge,)): changed_chart = True # If we changed the chart, then generate the edge. if changed_chart: yield new_edge class SingleEdgeProbabilisticFundamentalRule(AbstractChartRule): NUM_EDGES=1 _fundamental_rule = ProbabilisticFundamentalRule() def apply_iter(self, chart, grammar, edge1): fr = self._fundamental_rule if edge1.is_incomplete(): # edge1 = left_edge; edge2 = right_edge for edge2 in chart.select(start=edge1.end(), is_complete=True, lhs=edge1.next()): for new_edge in fr.apply_iter(chart, grammar, edge1, edge2): yield new_edge else: # edge2 = left_edge; edge1 = right_edge for edge2 in chart.select(end=edge1.start(), is_complete=False, next=edge1.lhs()): for new_edge in fr.apply_iter(chart, grammar, edge2, edge1): yield new_edge def __str__(self): return 'Fundamental Rule' class BottomUpProbabilisticChartParser(ParserI): """ An abstract bottom-up parser for C{PCFG}s that uses a C{Chart} to record partial results. C{BottomUpProbabilisticChartParser} maintains a queue of edges that can be added to the chart. This queue is initialized with edges for each token in the text that is being parsed. C{BottomUpProbabilisticChartParser} inserts these edges into the chart one at a time, starting with the most likely edges, and proceeding to less likely edges. For each edge that is added to the chart, it may become possible to insert additional edges into the chart; these are added to the queue. This process continues until enough complete parses have been generated, or until the queue is empty. The sorting order for the queue is not specified by C{BottomUpProbabilisticChartParser}. Different sorting orders will result in different search strategies. The sorting order for the queue is defined by the method C{sort_queue}; subclasses are required to provide a definition for this method. @type _grammar: C{PCFG} @ivar _grammar: The grammar used to parse sentences. @type _trace: C{int} @ivar _trace: The level of tracing output that should be generated when parsing a text. """ def __init__(self, grammar, beam_size=0, trace=0): """ Create a new C{BottomUpProbabilisticChartParser}, that uses C{grammar} to parse texts. @type grammar: C{PCFG} @param grammar: The grammar used to parse texts. @type beam_size: C{int} @param beam_size: The maximum length for the parser's edge queue. @type trace: C{int} @param trace: The level of tracing that should be used when parsing a text. C{0} will generate no tracing output; and higher numbers will produce more verbose tracing output. """ if not isinstance(grammar, WeightedGrammar): raise ValueError("The grammar must be probabilistic WeightedGrammar") self._grammar = grammar self.beam_size = beam_size self._trace = trace def grammar(self): return self._grammar def trace(self, trace=2): """ Set the level of tracing output that should be generated when parsing a text. @type trace: C{int} @param trace: The trace level. A trace level of C{0} will generate no tracing output; and higher trace levels will produce more verbose tracing output. @rtype: C{None} """ self._trace = trace # TODO: change this to conform more with the standard ChartParser def nbest_parse(self, tokens, n=None): self._grammar.check_coverage(tokens) chart = Chart(list(tokens)) grammar = self._grammar # Chart parser rules. bu_init = ProbabilisticBottomUpInitRule() bu = ProbabilisticBottomUpPredictRule() fr = SingleEdgeProbabilisticFundamentalRule() # Our queue! queue = [] # Initialize the chart. for edge in bu_init.apply_iter(chart, grammar): if self._trace > 1: print ' %-50s [%s]' % (chart.pp_edge(edge,width=2), edge.prob()) queue.append(edge) while len(queue) > 0: # Re-sort the queue. self.sort_queue(queue, chart) # Prune the queue to the correct size if a beam was defined if self.beam_size: self._prune(queue, chart) # Get the best edge. edge = queue.pop() if self._trace > 0: print ' %-50s [%s]' % (chart.pp_edge(edge,width=2), edge.prob()) # Apply BU & FR to it. queue.extend(bu.apply(chart, grammar, edge)) queue.extend(fr.apply(chart, grammar, edge)) # Get a list of complete parses. parses = chart.parses(grammar.start(), ProbabilisticTree) # Assign probabilities to the trees. prod_probs = {} for prod in grammar.productions(): prod_probs[prod.lhs(), prod.rhs()] = prod.prob() for parse in parses: self._setprob(parse, prod_probs) # Sort by probability parses.sort(lambda a,b: cmp(b.prob(), a.prob())) return parses[:n] def _setprob(self, tree, prod_probs): if tree.prob() is not None: return # Get the prob of the CFG production. lhs = Nonterminal(tree.node) rhs = [] for child in tree: if isinstance(child, Tree): rhs.append(Nonterminal(child.node)) else: rhs.append(child) prob = prod_probs[lhs, tuple(rhs)] # Get the probs of children. for child in tree: if isinstance(child, Tree): self._setprob(child, prod_probs) prob *= child.prob() tree.set_prob(prob) def sort_queue(self, queue, chart): """ Sort the given queue of C{Edge}s, placing the edge that should be tried first at the beginning of the queue. This method will be called after each C{Edge} is added to the queue. @param queue: The queue of C{Edge}s to sort. Each edge in this queue is an edge that could be added to the chart by the fundamental rule; but that has not yet been added. @type queue: C{list} of C{Edge} @param chart: The chart being used to parse the text. This chart can be used to provide extra information for sorting the queue. @type chart: C{Chart} @rtype: C{None} """ raise AssertionError, "BottomUpProbabilisticChartParser is an abstract class" def _prune(self, queue, chart): """ Discard items in the queue if the queue is longer than the beam.""" if len(queue) > self.beam_size: split = len(queue)-self.beam_size if self._trace > 2: for edge in queue[:split]: print ' %-50s [DISCARDED]' % chart.pp_edge(edge,2) del queue[:split] class InsideChartParser(BottomUpProbabilisticChartParser): """ A bottom-up parser for C{PCFG}s that tries edges in descending order of the inside probabilities of their trees. The X{inside probability} of a tree is simply the probability of the entire tree, ignoring its context. In particular, the inside probability of a tree generated by production M{p} with children M{c[1]}, M{c[2]}, ..., M{c[n]} is P(M{p})*P(M{c[1]})*P(M{c[2]})*M{...}*P(M{c[n]}); and the inside probability of a token is 1 if it is present in the text, and 0 if it is absent. This sorting order results in a type of lowest-cost-first search strategy. """ # Inherit constructor. def sort_queue(self, queue, chart): """ Sort the given queue of edges, in descending order of the inside probabilities of the edges' trees. @param queue: The queue of C{Edge}s to sort. Each edge in this queue is an edge that could be added to the chart by the fundamental rule; but that has not yet been added. @type queue: C{list} of C{Edge} @param chart: The chart being used to parse the text. This chart can be used to provide extra information for sorting the queue. @type chart: C{Chart} @rtype: C{None} """ queue.sort(lambda e1,e2:cmp(e1.prob(), e2.prob())) # Eventually, this will become some sort of inside-outside parser: # class InsideOutsideParser(BottomUpProbabilisticChartParser): # def __init__(self, grammar, trace=0): # # Inherit docs. # BottomUpProbabilisticChartParser.__init__(self, grammar, trace) # # # Find the best path from S to each nonterminal # bestp = {} # for production in grammar.productions(): bestp[production.lhs()]=0 # bestp[grammar.start()] = 1.0 # # for i in range(len(grammar.productions())): # for production in grammar.productions(): # lhs = production.lhs() # for elt in production.rhs(): # bestp[elt] = max(bestp[lhs]*production.prob(), # bestp.get(elt,0)) # # self._bestp = bestp # for (k,v) in self._bestp.items(): print k,v # # def _cmp(self, e1, e2): # return cmp(e1.structure()[PROB]*self._bestp[e1.lhs()], # e2.structure()[PROB]*self._bestp[e2.lhs()]) # # def sort_queue(self, queue, chart): # queue.sort(self._cmp) import random class RandomChartParser(BottomUpProbabilisticChartParser): """ A bottom-up parser for C{PCFG}s that tries edges in random order. This sorting order results in a random search strategy. """ # Inherit constructor def sort_queue(self, queue, chart): i = random.randint(0, len(queue)-1) (queue[-1], queue[i]) = (queue[i], queue[-1]) class UnsortedChartParser(BottomUpProbabilisticChartParser): """ A bottom-up parser for C{PCFG}s that tries edges in whatever order. """ # Inherit constructor def sort_queue(self, queue, chart): return class LongestChartParser(BottomUpProbabilisticChartParser): """ A bottom-up parser for C{PCFG}s that tries longer edges before shorter ones. This sorting order results in a type of best-first search strategy. """ # Inherit constructor def sort_queue(self, queue, chart): queue.sort(lambda e1,e2: cmp(e1.length(), e2.length())) ##////////////////////////////////////////////////////// ## Test Code ##////////////////////////////////////////////////////// def demo(choice=None, draw_parses=None, print_parses=None): """ A demonstration of the probabilistic parsers. The user is prompted to select which demo to run, and how many parses should be found; and then each parser is run on the same demo, and a summary of the results are displayed. """ import sys, time from nltk import tokenize, toy_pcfg1, toy_pcfg2 from nltk.parse import pchart # Define two demos. Each demo has a sentence and a grammar. demos = [('I saw John with my telescope', toy_pcfg1), ('the boy saw Jack with Bob under the table with a telescope', toy_pcfg2)] if choice is None: # Ask the user which demo they want to use. print for i in range(len(demos)): print '%3s: %s' % (i+1, demos[i][0]) print ' %r' % demos[i][1] print print 'Which demo (%d-%d)? ' % (1, len(demos)), choice = int(sys.stdin.readline().strip())-1 try: sent, grammar = demos[choice] except: print 'Bad sentence number' return # Tokenize the sentence. tokens = sent.split() # Define a list of parsers. We'll use all parsers. parsers = [ pchart.InsideChartParser(grammar), pchart.RandomChartParser(grammar), pchart.UnsortedChartParser(grammar), pchart.LongestChartParser(grammar), pchart.InsideChartParser(grammar, beam_size = len(tokens)+1) # was BeamParser ] # Run the parsers on the tokenized sentence. times = [] average_p = [] num_parses = [] all_parses = {} for parser in parsers: print '\ns: %s\nparser: %s\ngrammar: %s' % (sent,parser,grammar) parser.trace(3) t = time.time() parses = parser.nbest_parse(tokens) times.append(time.time()-t) if parses: p = reduce(lambda a,b:a+b.prob(), parses, 0)/len(parses) else: p = 0 average_p.append(p) num_parses.append(len(parses)) for p in parses: all_parses[p.freeze()] = 1 # Print some summary statistics print print ' Parser Beam | Time (secs) # Parses Average P(parse)' print '------------------------+------------------------------------------' for i in range(len(parsers)): print '%18s %4d |%11.4f%11d%19.14f' % (parsers[i].__class__.__name__, parsers[i].beam_size, times[i],num_parses[i],average_p[i]) parses = all_parses.keys() if parses: p = reduce(lambda a,b:a+b.prob(), parses, 0)/len(parses) else: p = 0 print '------------------------+------------------------------------------' print '%18s |%11s%11d%19.14f' % ('(All Parses)', 'n/a', len(parses), p) if draw_parses is None: # Ask the user if we should draw the parses. print print 'Draw parses (y/n)? ', draw_parses = sys.stdin.readline().strip().lower().startswith('y') if draw_parses: from nltk.draw.tree import draw_trees print ' please wait...' draw_trees(*parses) if print_parses is None: # Ask the user if we should print the parses. print print 'Print parses (y/n)? ', print_parses = sys.stdin.readline().strip().lower().startswith('y') if print_parses: for parse in parses: print parse if __name__ == '__main__': demo() nltk-2.0~b9/nltk/parse/nonprojectivedependencyparser.py0000644000175000017500000006461211327451577023400 0ustar bhavanibhavani# Natural Language Toolkit: Dependency Grammars # # Copyright (C) 2001-2010 NLTK Project # Author: Jason Narad # # URL: # For license information, see LICENSE.TXT # import math from nltk.grammar import parse_dependency_grammar from dependencygraph import * ################################################################# # DependencyScorerI - Interface for Graph-Edge Weight Calculation ################################################################# class DependencyScorerI(object): """ A scorer for calculated the weights on the edges of a weighted dependency graph. This is used by a C{ProbabilisticNonprojectiveParser} to initialize the edge weights of a C{DependencyGraph}. While typically this would be done by training a binary classifier, any class that can return a multidimensional list representation of the edge weights can implement this interface. As such, it has no necessary fields. """ def __init__(self): if self.__class__ == DependencyScorerI: raise TypeError('DependencyScorerI is an abstract interface') def train(self, graphs): """ @type graphs: A list of C{DependencyGraph} @param graphs: A list of dependency graphs to train the scorer. Typically the edges present in the graphs can be used as positive training examples, and the edges not present as negative examples. """ raise AssertionError('DependencyScorerI is an abstract interface') def score(self, graph): """ @type graph: A C{DependencyGraph} @param graph: A dependency graph whose set of edges need to be scored. @rtype: A three-dimensional list of numbers. @return: The score is returned in a multidimensional(3) list, such that the outer-dimension refers to the head, and the inner-dimension refers to the dependencies. For instance, scores[0][1] would reference the list of scores corresponding to arcs from node 0 to node 1. The node's 'address' field can be used to determine its number identification. For further illustration, a score list corresponding to Fig.2 of Keith Hall's 'K-best Spanning Tree Parsing' paper: scores = [[[], [5], [1], [1]], [[], [], [11], [4]], [[], [10], [], [5]], [[], [8], [8], []]] When used in conjunction with a MaxEntClassifier, each score would correspond to the confidence of a particular edge being classified with the positive training examples. """ raise AssertionError('DependencyScorerI is an abstract interface') #//////////////////////////////////////////////////////////// # Comparisons #//////////////////////////////////////////////////////////// def __cmp__(self, other): raise AssertionError('DependencyScorerI is an abstract interface') def __hash__(self, other): raise AssertionError('DependencyScorerI is an abstract interface') ################################################################# # NaiveBayesDependencyScorer ################################################################# class NaiveBayesDependencyScorer(DependencyScorerI): """ A dependency scorer built around a MaxEnt classifier. In this particular class that classifier is a C{NaiveBayesClassifier}. It uses head-word, head-tag, child-word, and child-tag features for classification. """ def __init__(self): print # Do nothing without throwing error? def train(self, graphs): """ Trains a C{NaiveBayesClassifier} using the edges present in graphs list as positive examples, the edges not present as negative examples. Uses a feature vector of head-word, head-tag, child-word, and child-tag. @type graphs: A list of C{DependencyGraph} @param graphs: A list of dependency graphs to train the scorer. """ # Create training labeled training examples labeled_examples = [] for graph in graphs: for head_node in graph.nodelist: for child_index in range(len(graph.nodelist)): child_node = graph.get_by_address(child_index) if child_index in head_node['deps']: label = "T" else: label = "F" features = [head_node['word'], head_node['tag'], child_node['word'], child_node['tag']] labeled_examples.append((dict(a=head_node['word'],b=head_node['tag'],c=child_node['word'],d=child_node['tag']), label)) # Train the classifier import nltk nltk.usage(nltk.ClassifierI) self.classifier = nltk.classify.NaiveBayesClassifier.train(labeled_examples) def score(self, graph): """ Converts the graph into a feature-based representation of each edge, and then assigns a score to each based on the confidence of the classifier in assigning it to the positive label. Scores are returned in a multidimensional list. @type graph: C{DependencyGraph} @param graph: A dependency graph to score. @rtype: 3 dimensional list @return: Edge scores for the graph parameter. """ # Convert graph to feature representation edges = [] for i in range(len(graph.nodelist)): for j in range(len(graph.nodelist)): head_node = graph.get_by_address(i) child_node = graph.get_by_address(j) print head_node print child_node edges.append((dict(a=head_node['word'],b=head_node['tag'],c=child_node['word'],d=child_node['tag']))) # Score edges edge_scores = [] row = [] count = 0 for pdist in self.classifier.batch_prob_classify(edges): print '%.4f %.4f' % (pdist.prob('T'), pdist.prob('F')) row.append([math.log(pdist.prob("T"))]) count += 1 if count == len(graph.nodelist): edge_scores.append(row) row = [] count = 0 return edge_scores ################################################################# # A Scorer for Demo Purposes ################################################################# # A short class necessary to show parsing example from paper class DemoScorer: def train(self, graphs): print 'Training...' def score(self, graph): # scores for Keith Hall 'K-best Spanning Tree Parsing' paper return [[[], [5], [1], [1]], [[], [], [11], [4]], [[], [10], [], [5]], [[], [8], [8], []]] ################################################################# # Non-Projective Probabilistic Parsing ################################################################# class ProbabilisticNonprojectiveParser(object): """ A probabilistic non-projective dependency parser. Nonprojective dependencies allows for "crossing branches" in the parse tree which is necessary for representing particular linguistic phenomena, or even typical parses in some languages. This parser follows the MST parsing algorithm, outlined in McDonald(2005), which likens the search for the best non-projective parse to finding the maximum spanning tree in a weighted directed graph. """ def __init__(self): """ Creates a new non-projective parser. """ print 'initializing prob. nonprojective...' def train(self, graphs, dependency_scorer): """ Trains a C{DependencyScorerI} from a set of C{DependencyGraph} objects, and establishes this as the parser's scorer. This is used to initialize the scores on a C{DependencyGraph} during the parsing procedure. @type graphs: A list of C{DependencyGraph} @param graphs: A list of dependency graphs to train the scorer. @type dependency_scorer: C{DependencyScorerI} @param dependency_scorer: A scorer which implements the C{DependencyScorerI} interface. """ self._scorer = dependency_scorer self._scorer.train(graphs) def initialize_edge_scores(self, graph): """ Assigns a score to every edge in the C{DependencyGraph} graph. These scores are generated via the parser's scorer which was assigned during the training process. @type graph: C{DependencyGraph} @param graph: A dependency graph to assign scores to. """ self.scores = self._scorer.score(graph) def collapse_nodes(self, new_node, cycle_path, g_graph, b_graph, c_graph): """ Takes a list of nodes that have been identified to belong to a cycle, and collapses them into on larger node. The arcs of all nodes in the graph must be updated to account for this. @type new_node: Node. @param new_node: A Node (Dictionary) to collapse the cycle nodes into. @type cycle_path: A list of integers. @param cycle_path: A list of node addresses, each of which is in the cycle. @type g_graph, b_graph, c_graph: C{DependencyGraph} @param g_graph, b_graph, c_graph: Graphs which need to be updated. """ print 'Collapsing nodes...' # Collapse all cycle nodes into v_n+1 in G_Graph for cycle_node_index in cycle_path: g_graph.remove_by_address(cycle_node_index) g_graph.nodelist.append(new_node) g_graph.redirect_arcs(cycle_path, new_node['address']) def update_edge_scores(self, new_node, cycle_path): """ Updates the edge scores to reflect a collapse operation into new_node. @type new_node: A Node. @param new_node: The node which cycle nodes are collapsed into. @type cycle_path: A list of integers. @param cycle_path: A list of node addresses that belong to the cycle. """ print 'cycle', cycle_path cycle_path = self.compute_original_indexes(cycle_path) print 'old cycle ', cycle_path print 'Prior to update:\n', self.scores for i, row in enumerate(self.scores): for j, column in enumerate(self.scores[i]): print self.scores[i][j] if j in cycle_path and not i in cycle_path and len(self.scores[i][j]) > 0: new_vals = [] subtract_val = self.compute_max_subtract_score(j, cycle_path) print self.scores[i][j], ' - ', subtract_val for cur_val in self.scores[i][j]: new_vals.append(cur_val - subtract_val) self.scores[i][j] = new_vals for i, row in enumerate(self.scores): for j, cell in enumerate(self.scores[i]): if i in cycle_path and j in cycle_path: self.scores[i][j] = [] print 'After update:\n', self.scores def compute_original_indexes(self, new_indexes): """ As nodes are collapsed into others, they are replaced by the new node in the graph, but it's still necessary to keep track of what these original nodes were. This takes a list of node addresses and replaces any collapsed node addresses with their original addresses. @type new_address: A list of integers. @param new_addresses: A list of node addresses to check for subsumed nodes. """ swapped = True while(swapped): originals = [] swapped = False for new_index in new_indexes: if self.inner_nodes.has_key(new_index): for old_val in self.inner_nodes[new_index]: if not old_val in originals: originals.append(old_val) swapped = True else: originals.append(new_index) new_indexes = originals return new_indexes def compute_max_subtract_score(self, column_index, cycle_indexes): """ When updating scores the score of the highest-weighted incoming arc is subtracted upon collapse. This returns the correct amount to subtract from that edge. @type column_index: integer. @param column_index: A index representing the column of incoming arcs to a particular node being updated @type cycle_indexes: A list of integers. @param cycle_indexes: Only arcs from cycle nodes are considered. This is a list of such nodes addresses. """ max_score = -100000 for row_index in cycle_indexes: for subtract_val in self.scores[row_index][column_index]: if subtract_val > max_score: max_score = subtract_val return max_score def best_incoming_arc(self, node_index): """ Returns the source of the best incoming arc to the node with address: node_index @type node_index: integer. @param node_index: The address of the 'destination' node, the node that is arced to. """ originals = self.compute_original_indexes([node_index]) print 'originals:', originals max_arc = None max_score = None for row_index in range(len(self.scores)): for col_index in range(len(self.scores[row_index])): # print self.scores[row_index][col_index] if col_index in originals and self.scores[row_index][col_index] > max_score: max_score = self.scores[row_index][col_index] max_arc = row_index print row_index, ',', col_index print max_score for key in self.inner_nodes: replaced_nodes = self.inner_nodes[key] if max_arc in replaced_nodes: return key return max_arc def original_best_arc(self, node_index): """ ??? """ originals = self.compute_original_indexes([node_index]) max_arc = None max_score = None max_orig = None for row_index in range(len(self.scores)): for col_index in range(len(self.scores[row_index])): if col_index in originals and self.scores[row_index][col_index] > max_score: max_score = self.scores[row_index][col_index] max_arc = row_index max_orig = col_index return [max_arc, max_orig] def parse(self, tokens, tags): """ Parses a list of tokens in accordance to the MST parsing algorithm for non-projective dependency parses. Assumes that the tokens to be parsed have already been tagged and those tags are provided. Various scoring methods can be used by implementing the C{DependencyScorerI} interface and passing it to the training algorithm. @type tokens: A list of C{String}. @param tokens: A list of words or punctuation to be parsed. @type tags: A List of C{String}. @param tags: A list of tags corresponding by index to the words in the tokens list. """ self.inner_nodes = {} # Initialize g_graph g_graph = DependencyGraph() for index, token in enumerate(tokens): g_graph.nodelist.append({'word':token, 'tag':tags[index], 'deps':[], 'rel':'NTOP', 'address':index+1}) # Fully connect non-root nodes in g_graph g_graph.connect_graph() original_graph = DependencyGraph() for index, token in enumerate(tokens): original_graph.nodelist.append({'word':token, 'tag':tags[index], 'deps':[], 'rel':'NTOP', 'address':index+1}) # Initialize b_graph b_graph = DependencyGraph() b_graph.nodelist = [] # Initialize c_graph c_graph = DependencyGraph() c_graph.nodelist = [{'word':token, 'tag':tags[index], 'deps':[], 'rel':'NTOP', 'address':index+1} for index, token in enumerate(tokens)] # Assign initial scores to g_graph edges self.initialize_edge_scores(g_graph) print self.scores # Initialize a list of unvisited vertices (by node address) unvisited_vertices = [vertex['address'] for vertex in c_graph.nodelist] # Iterate over unvisited vertices nr_vertices = len(tokens) betas = {} while(len(unvisited_vertices) > 0): # Mark current node as visited current_vertex = unvisited_vertices.pop(0) print 'current_vertex:', current_vertex # Get corresponding node n_i to vertex v_i current_node = g_graph.get_by_address(current_vertex) print 'current_node:', current_node # Get best in-edge node b for current node best_in_edge = self.best_incoming_arc(current_vertex) betas[current_vertex] = self.original_best_arc(current_vertex) print 'best in arc: ', best_in_edge, ' --> ', current_vertex # b_graph = Union(b_graph, b) for new_vertex in [current_vertex, best_in_edge]: b_graph.add_node({'word':'TEMP', 'deps':[], 'rel': 'NTOP', 'address': new_vertex}) b_graph.add_arc(best_in_edge, current_vertex) # Beta(current node) = b - stored for parse recovery # If b_graph contains a cycle, collapse it cycle_path = b_graph.contains_cycle() if cycle_path: # Create a new node v_n+1 with address = len(nodes) + 1 new_node = {'word': 'NONE', 'deps':[], 'rel': 'NTOP', 'address': nr_vertices + 1} # c_graph = Union(c_graph, v_n+1) c_graph.add_node(new_node) # Collapse all nodes in cycle C into v_n+1 self.update_edge_scores(new_node, cycle_path) self.collapse_nodes(new_node, cycle_path, g_graph, b_graph, c_graph) for cycle_index in cycle_path: c_graph.add_arc(new_node['address'], cycle_index) # self.replaced_by[cycle_index] = new_node['address'] self.inner_nodes[new_node['address']] = cycle_path # Add v_n+1 to list of unvisited vertices unvisited_vertices.insert(0, nr_vertices + 1) # increment # of nodes counter nr_vertices += 1 # Remove cycle nodes from b_graph; B = B - cycle c for cycle_node_address in cycle_path: b_graph.remove_by_address(cycle_node_address) print 'g_graph:\n', g_graph print print 'b_graph:\n', b_graph print print 'c_graph:\n', c_graph print print 'Betas:\n', betas print 'replaced nodes', self.inner_nodes print #Recover parse tree print 'Final scores:\n', self.scores print 'Recovering parse...' for i in range(len(tokens) + 1, nr_vertices + 1): betas[betas[i][1]] = betas[i] print 'Betas: ', betas new_graph = DependencyGraph() for node in original_graph.nodelist: node['deps'] = [] for i in range(1, len(tokens) + 1): # print i, betas[i] original_graph.add_arc(betas[i][0], betas[i][1]) # print original_graph return original_graph print 'Done.' ################################################################# # Rule-based Non-Projective Parser ################################################################# class NonprojectiveDependencyParser(object): """ A non-projective, rule-based, dependency parser. This parser will return the set of all possible non-projective parses based on the word-to-word relations defined in the parser's dependency grammar, and will allow the branches of the parse tree to cross in order to capture a variety of linguistic phenomena that a projective parser will not. """ def __init__(self, dependency_grammar): """ Creates a new C{NonprojectiveDependencyParser}. @param dependency_grammar: a grammar of word-to-word relations. @type depenedncy_grammar: C{DependencyGrammar} """ self._grammar = dependency_grammar def parse(self, tokens): """ Parses the input tokens with respect to the parser's grammar. Parsing is accomplished by representing the search-space of possible parses as a fully-connected directed graph. Arcs that would lead to ungrammatical parses are removed and a lattice is constructed of length n, where n is the number of input tokens, to represent all possible grammatical traversals. All possible paths through the lattice are then enumerated to produce the set of non-projective parses. param tokens: A list of tokens to parse. type tokens: A C{list} of L{String}. return: A set of non-projective parses. rtype: A C{list} of L{DependencyGraph} """ # Create graph representation of tokens self._graph = DependencyGraph() self._graph.nodelist = [] # Remove the default root for index, token in enumerate(tokens): self._graph.nodelist.append({'word':token, 'deps':[], 'rel':'NTOP', 'address':index}) for head_node in self._graph.nodelist: deps = [] for dep_node in self._graph.nodelist: if self._grammar.contains(head_node['word'], dep_node['word']) and not head_node['word'] == dep_node['word']: deps.append(dep_node['address']) head_node['deps'] = deps # Create lattice of possible heads roots = [] possible_heads = [] for i, word in enumerate(tokens): heads = [] for j, head in enumerate(tokens): if (i != j) and self._grammar.contains(head, word): heads.append(j) if len(heads) == 0: roots.append(i) possible_heads.append(heads) # Set roots to attempt if len(roots) > 1: print "No parses found." return False elif len(roots) == 0: for i in range(len(tokens)): roots.append(i) # Traverse lattice analyses = [] for root in roots: stack = [] analysis = [[] for i in range(len(possible_heads))] i = 0 forward = True while(i >= 0): if forward: if len(possible_heads[i]) == 1: analysis[i] = possible_heads[i][0] elif len(possible_heads[i]) == 0: analysis[i] = -1 else: head = possible_heads[i].pop() analysis[i] = head stack.append([i, head]) if not forward: index_on_stack = False for stack_item in stack: # print stack_item if stack_item[0] == i: index_on_stack = True orig_length = len(possible_heads[i]) # print len(possible_heads[i]) if index_on_stack and orig_length == 0: for j in xrange(len(stack) -1, -1, -1): stack_item = stack[j] if stack_item[0] == i: possible_heads[i].append(stack.pop(j)[1]) # print stack elif index_on_stack and orig_length > 0: head = possible_heads[i].pop() analysis[i] = head stack.append([i, head]) forward = True # print 'Index on stack:', i, index_on_stack if i + 1 == len(possible_heads): analyses.append(analysis[:]) forward = False if forward: i += 1 else: i -= 1 # Filter parses graphs = [] #ensure 1 root, every thing has 1 head for analysis in analyses: root_count = 0 root = [] for i, cell in enumerate(analysis): if cell == -1: root_count += 1 root = i if root_count == 1: graph = DependencyGraph() graph.nodelist[0]['deps'] = root + 1 for i in range(len(tokens)): node = {'word':tokens[i], 'address':i+1} node['deps'] = [j+1 for j in range(len(tokens)) if analysis[j] == i] graph.nodelist.append(node) # cycle = graph.contains_cycle() # if not cycle: graphs.append(graph) return graphs ################################################################# # Demos ################################################################# def demo(): # hall_demo() nonprojective_conll_parse_demo() rule_based_demo() def hall_demo(): npp = ProbabilisticNonprojectiveParser() npp.train([], DemoScorer()) parse_graph = npp.parse(['v1', 'v2', 'v3'], [None, None, None]) print parse_graph def nonprojective_conll_parse_demo(): graphs = [DependencyGraph(entry) for entry in conll_data2.split('\n\n') if entry] npp = ProbabilisticNonprojectiveParser() npp.train(graphs, NaiveBayesDependencyScorer()) parse_graph = npp.parse(['Cathy', 'zag', 'hen', 'zwaaien', '.'], ['N', 'V', 'Pron', 'Adj', 'N', 'Punc']) print parse_graph def rule_based_demo(): grammar = parse_dependency_grammar(""" 'taught' -> 'play' | 'man' 'man' -> 'the' | 'in' 'in' -> 'corner' 'corner' -> 'the' 'play' -> 'golf' | 'dachshund' | 'to' 'dachshund' -> 'his' """) print grammar ndp = NonprojectiveDependencyParser(grammar) graphs = ndp.parse(['the', 'man', 'in', 'the', 'corner', 'taught', 'his', 'dachshund', 'to', 'play', 'golf']) print 'Graphs:' for graph in graphs: print graph if __name__ == '__main__': demo() nltk-2.0~b9/nltk/parse/malt.py0000644000175000017500000001603011331672403016147 0ustar bhavanibhavani# Natural Language Toolkit: Interface to MaltParser # # Author: Dan Garrette # # Copyright (C) 2001-2010 NLTK Project # URL: # For license information, see LICENSE.TXT import os import tempfile import subprocess import glob from operator import add import nltk from api import ParserI from dependencygraph import DependencyGraph from nltk.internals import find_binary class MaltParser(ParserI): def __init__(self, tagger=None): self.config_malt() self.mco = 'malt_temp' self._trained = False if tagger is not None: self.tagger = tagger else: self.tagger = nltk.tag.RegexpTagger( [(r'^-?[0-9]+(.[0-9]+)?$', 'CD'), # cardinal numbers (r'(The|the|A|a|An|an)$', 'AT'), # articles (r'.*able$', 'JJ'), # adjectives (r'.*ness$', 'NN'), # nouns formed from adjectives (r'.*ly$', 'RB'), # adverbs (r'.*s$', 'NNS'), # plural nouns (r'.*ing$', 'VBG'), # gerunds (r'.*ed$', 'VBD'), # past tense verbs (r'.*', 'NN') # nouns (default) ]) def config_malt(self, bin=None, verbose=False): """ Configure NLTK's interface to the C{malt} package. This searches for a directory containing the malt jar @param bin: The full path to the C{malt} binary. If not specified, then nltk will search the system for a C{malt} binary; and if one is not found, it will raise a C{LookupError} exception. @type bin: C{string} """ #: A list of directories that should be searched for the malt #: executables. This list is used by L{config_malt} when searching #: for the malt executables. _malt_path = ['.', '/usr/lib/malt-1*', '/usr/share/malt-1*', '/usr/local/bin', '/usr/local/malt-1*', '/usr/local/bin/malt-1*', '/usr/local/malt-1*', '/usr/local/share/malt-1*'] # Expand wildcards in _malt_path: malt_path = reduce(add, map(glob.glob, _malt_path)) # Find the malt binary. self._malt_bin = find_binary('malt.jar', bin, searchpath=malt_path, env_vars=['MALTPARSERHOME'], url='http://w3.msi.vxu.se/~jha/maltparser/index.html', verbose=verbose) def parse(self, sentence, verbose=False): """ Use MaltParser to parse a sentence @param sentence: Input sentence to parse @type sentence: L{str} @return: C{DependencyGraph} the dependency graph representation of the sentence """ if not self._malt_bin: raise Exception("MaltParser location is not configured. Call config_malt() first.") if not self._trained: raise Exception("Parser has not been trained. Call train() first.") input_file = os.path.join(tempfile.gettempdir(), 'malt_input.conll') output_file = os.path.join(tempfile.gettempdir(), 'malt_output.conll') execute_string = 'java -jar %s -w %s -c %s -i %s -o %s -m parse' if not verbose: execute_string += ' > ' + os.path.join(tempfile.gettempdir(), "malt.out") f = None try: f = open(input_file, 'w') for (i, (word,tag)) in enumerate(self.tagger.tag(sentence.split())): f.write('%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' % (i+1, word, '_', tag, tag, '_', '0', 'a', '_', '_')) f.write('\n') f.close() cmd = ['java', '-jar %s' % self._malt_bin, '-w %s' % tempfile.gettempdir(), '-c %s' % self.mco, '-i %s' % input_file, '-o %s' % output_file, '-m parse'] self._execute(cmd, 'parse', verbose) return DependencyGraph.load(output_file) finally: if f: f.close() def train(self, depgraphs, verbose=False): """ Train MaltParser from a list of C{DependencyGraph}s @param depgraphs: C{list} of C{DependencyGraph}s for training input data """ input_file = os.path.join(tempfile.gettempdir(),'malt_train.conll') f = None try: f = open(input_file, 'w') f.write('\n'.join([dg.to_conll(10) for dg in depgraphs])) finally: if f: f.close() self.train_from_file(input_file, verbose=verbose) def train_from_file(self, conll_file, verbose=False): """ Train MaltParser from a file @param conll_file: C{str} for the filename of the training input data """ if not self._malt_bin: raise Exception("MaltParser location is not configured. Call config_malt() first.") # If conll_file is a ZipFilePathPointer, then we need to do some extra massaging f = None if hasattr(conll_file, 'zipfile'): zip_conll_file = conll_file conll_file = os.path.join(tempfile.gettempdir(),'malt_train.conll') conll_str = zip_conll_file.open().read() f = open(conll_file,'w') f.write(conll_str) f.close() cmd = ['java', '-jar %s' % self._malt_bin, '-w %s' % tempfile.gettempdir(), '-c %s' % self.mco, '-i %s' % conll_file, '-m learn'] # p = subprocess.Popen(cmd, stdout=subprocess.PIPE, # stderr=subprocess.STDOUT, # stdin=subprocess.PIPE) # (stdout, stderr) = p.communicate() self._execute(cmd, 'train', verbose) self._trained = True def _execute(self, cmd, type, verbose=False): if not verbose: temp_dir = os.path.join(tempfile.gettempdir(), '') cmd.append(' > %smalt_%s.out 2> %smalt_%s.err' % ((temp_dir, type)*2)) malt_exit = os.system(' '.join(cmd)) def demo(): dg1 = DependencyGraph("""1 John _ NNP _ _ 2 SUBJ _ _ 2 sees _ VB _ _ 0 ROOT _ _ 3 a _ DT _ _ 4 SPEC _ _ 4 dog _ NN _ _ 2 OBJ _ _ """) dg2 = DependencyGraph("""1 John _ NNP _ _ 2 SUBJ _ _ 2 walks _ VB _ _ 0 ROOT _ _ """) verbose = False maltParser = MaltParser() maltParser.train([dg1,dg2], verbose=verbose) print maltParser.parse('John sees Mary', verbose=verbose).tree().pprint() print maltParser.parse('a man runs', verbose=verbose).tree().pprint() if __name__ == '__main__': demo() nltk-2.0~b9/nltk/parse/generate.py0000644000175000017500000000261611327451576017024 0ustar bhavanibhavani# Natural Language Toolkit: Generating from a CFG # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT # from nltk.grammar import Nonterminal, parse_cfg def generate(grammar, start=None): if not start: start = grammar.start() return _generate_all(grammar, [start])[0] def _generate_all(grammar, items): frags = [] if len(items) == 1: if isinstance(items[0], Nonterminal): for prod in grammar.productions(lhs=items[0]): frags.append(_generate_all(grammar, prod.rhs())) else: frags.append(items[0]) else: for frag1 in _generate_all(grammar, [items[0]]): for frag2 in _generate_all(grammar, items[1:]): for frag in _multiply(frag1, frag2): frags.append(frag) return frags def _multiply(frag1, frag2): frags = [] if len(frag1) == 1: frag1 = [frag1] if len(frag2) == 1: frag2 = [frag2] for f1 in frag1: for f2 in frag2: frags.append(f1+f2) return frags grammar = parse_cfg(""" S -> NP VP NP -> Det N VP -> V NP Det -> 'the' Det -> 'a' N -> 'man' | 'park' | 'dog' | 'telescope' V -> 'saw' | 'walked' P -> 'in' | 'with' """) for sent in generate(grammar): print ' '.join(sent) nltk-2.0~b9/nltk/parse/featurechart.py0000644000175000017500000005225211327451577017711 0ustar bhavanibhavani# -*- coding: utf-8 -*- # Natural Language Toolkit: Chart Parser for Feature-Based Grammars # # Copyright (C) 2001-2010 NLTK Project # Author: Rob Speer # Peter Ljunglöf # URL: # For license information, see LICENSE.TXT # # $Id: featurechart.py 8479 2010-01-13 05:40:34Z StevenBird1 $ """ Extension of chart parsing implementation to handle grammars with feature structures as nodes. """ import yaml, sys from nltk.featstruct import FeatStruct, unify, FeatStructParser, TYPE from nltk.sem import logic from nltk.grammar import Nonterminal, Production, ContextFreeGrammar from nltk.compat import defaultdict from nltk.grammar import FeatStructNonterminal import nltk.data from api import * from chart import * #//////////////////////////////////////////////////////////// # Tree Edge #//////////////////////////////////////////////////////////// class FeatureTreeEdge(TreeEdge): """ A specialized tree edge that allows shared variable bindings between nonterminals on the left-hand side and right-hand side. Each C{FeatureTreeEdge} contains a set of C{bindings}, i.e., a dictionary mapping from variables to values. If the edge is not complete, then these bindings are simply stored. However, if the edge is complete, then the constructor applies these bindings to every nonterminal in the edge whose symbol implements the interface L{SubstituteBindingsI}. """ def __init__(self, span, lhs, rhs, dot=0, bindings=None): """ Construct a new edge. If the edge is incomplete (i.e., if C{dot} S{alpha} * B1 S{beta}][i:j] - [B2 S{->} S{gamma} *][j:k] licenses the edge: - [A S{->} S{alpha} B3 * S{beta}][i:j] assuming that B1 and B2 can be unified to generate B3. """ def apply_iter(self, chart, grammar, left_edge, right_edge): # Make sure the rule is applicable. if not (left_edge.end() == right_edge.start() and left_edge.is_incomplete() and right_edge.is_complete() and isinstance(left_edge, FeatureTreeEdge)): return if isinstance(right_edge, FeatureTreeEdge): if not (isinstance(left_edge.next(), FeatStructNonterminal) and left_edge.next()[TYPE] == right_edge.lhs()[TYPE]): return # Unify B1 (left_edge.next) with B2 (right_edge.lhs) to # generate B3 (result). bindings = left_edge.bindings() # creates a copy. result = unify(left_edge.next(), right_edge.lhs(), bindings, rename_vars=False) if result is None: return else: if not (left_edge.next() == right_edge.lhs()): return bindings = left_edge.bindings() # creates a copy. # Construct the new edge. new_edge = FeatureTreeEdge(span=(left_edge.start(), right_edge.end()), lhs=left_edge.lhs(), rhs=left_edge.rhs(), dot=left_edge.dot()+1, bindings=bindings) # Add it to the chart, with appropriate child pointers. changed_chart = False for cpl1 in chart.child_pointer_lists(left_edge): if chart.insert(new_edge, cpl1+(right_edge,)): changed_chart = True # If we changed the chart, then generate the edge. if changed_chart: yield new_edge class FeatureSingleEdgeFundamentalRule(SingleEdgeFundamentalRule): """ A specialized version of the completer / single edge fundamental rule that operates on nonterminals whose symbols are C{FeatStructNonterminal}s. Rather than simply comparing the nonterminals for equality, they are unified. """ _fundamental_rule = FeatureFundamentalRule() def apply_iter(self, chart, grammar, edge): fr = self._fundamental_rule if edge.is_complete(): for left_edge in chart.select(end=edge.start(), is_complete=False, next=edge.lhs()): for new_edge in fr.apply_iter(chart, grammar, left_edge, edge): yield new_edge else: for right_edge in chart.select(start=edge.end(), is_complete=True, lhs=edge.next()): for new_edge in fr.apply_iter(chart, grammar, edge, right_edge): yield new_edge #//////////////////////////////////////////////////////////// # Top-Down Prediction #//////////////////////////////////////////////////////////// class FeatureTopDownInitRule(TopDownInitRule): def apply_iter(self, chart, grammar): root = FeatStructNonterminal('[*type*="[INIT]"]') edge = FeatureTreeEdge((0,0), root, (grammar.start(),), 0) if chart.insert(edge, ()): yield edge # TODO: Add a cached version of this rule: class FeatureTopDownPredictRule(TopDownPredictRule): """ A specialized version of the top down predict rule that operates on nonterminals whose symbols are C{FeatStructNonterminal}s. Rather tha simply comparing the nonterminals for equality, they are unified. The top down expand rule states that: - [A S{->} S{alpha} * B1 S{beta}][i:j] licenses the edge: - [B2 S{->} * S{gamma}][j:j] for each grammar production C{B2 S{->} S{gamma}}, assuming that B1 and B2 can be unified. """ def apply_iter(self, chart, grammar, edge): if edge.is_complete(): return #if not isinstance(edge.next(), FeatStructNonterminal): return for prod in grammar.productions(lhs=edge.next()): # Note: we rename vars here, because we don't want variables # from the two different productions to match. if (unify(prod.lhs(), edge.next_with_bindings(), rename_vars=True)): new_edge = FeatureTreeEdge(span=(edge.end(), edge.end()), lhs=prod.lhs(), rhs=prod.rhs(), dot=0) if chart.insert(new_edge, ()): yield new_edge #//////////////////////////////////////////////////////////// # Bottom-Up Prediction #//////////////////////////////////////////////////////////// class FeatureBottomUpPredictRule(BottomUpPredictRule): def apply_iter(self, chart, grammar, edge): if edge.is_incomplete(): return new_span = (edge.start(), edge.start()) if isinstance(edge, FeatureTreeEdge): for prod in grammar.productions(rhs=edge.lhs()): next = prod.rhs()[0] if not isinstance(next, FeatStructNonterminal): continue new_edge = FeatureTreeEdge(new_span, prod.lhs(), prod.rhs()) if chart.insert(new_edge, ()): yield new_edge else: # The edge is a LeafEdge: for prod in grammar.productions(rhs=edge.lhs()): new_edge = FeatureTreeEdge(new_span, prod.lhs(), prod.rhs()) if chart.insert(new_edge, ()): yield new_edge class FeatureBottomUpPredictCombineRule(BottomUpPredictCombineRule): def apply_iter(self, chart, grammar, edge): if edge.is_incomplete(): return if isinstance(edge, FeatureTreeEdge): for prod in grammar.productions(rhs=edge.lhs()): next = prod.rhs()[0] if not isinstance(next, FeatStructNonterminal): continue bindings = {} if unify(next, edge.lhs(), bindings): new_edge = FeatureTreeEdge(edge.span(), prod.lhs(), prod.rhs(), 1, bindings) if chart.insert(new_edge, (edge,)): yield new_edge else: # The edge is a LeafEdge: for prod in grammar.productions(rhs=edge.lhs()): new_edge = FeatureTreeEdge(edge.span(), prod.lhs(), prod.rhs(), 1) if chart.insert(new_edge, (edge,)): yield new_edge class FeatureEmptyPredictRule(EmptyPredictRule): def apply_iter(self, chart, grammar): for prod in grammar.productions(empty=True): for index in xrange(chart.num_leaves() + 1): new_edge = FeatureTreeEdge(span=(index, index), lhs=prod.lhs(), rhs=prod.rhs()) if chart.insert(new_edge, ()): yield new_edge #//////////////////////////////////////////////////////////// # Feature Chart Parser #//////////////////////////////////////////////////////////// TD_FEATURE_STRATEGY = [LeafInitRule(), FeatureTopDownInitRule(), FeatureTopDownPredictRule(), FeatureSingleEdgeFundamentalRule()] BU_FEATURE_STRATEGY = [LeafInitRule(), FeatureEmptyPredictRule(), FeatureBottomUpPredictRule(), FeatureSingleEdgeFundamentalRule()] BU_LC_FEATURE_STRATEGY = [LeafInitRule(), FeatureEmptyPredictRule(), FeatureBottomUpPredictCombineRule(), FeatureSingleEdgeFundamentalRule()] class FeatureChartParser(ChartParser): def __init__(self, grammar, strategy=BU_LC_FEATURE_STRATEGY, trace_chart_width=20, chart_class=FeatureChart, **parser_args): ChartParser.__init__(self, grammar, strategy=strategy, trace_chart_width=trace_chart_width, chart_class=chart_class, **parser_args) class FeatureTopDownChartParser(FeatureChartParser): def __init__(self, grammar, **parser_args): FeatureChartParser.__init__(self, grammar, TD_FEATURE_STRATEGY, **parser_args) class FeatureBottomUpChartParser(FeatureChartParser): def __init__(self, grammar, **parser_args): FeatureChartParser.__init__(self, grammar, BU_FEATURE_STRATEGY, **parser_args) class FeatureBottomUpLeftCornerChartParser(FeatureChartParser): def __init__(self, grammar, **parser_args): FeatureChartParser.__init__(self, grammar, BU_LC_FEATURE_STRATEGY, **parser_args) #//////////////////////////////////////////////////////////// # Instantiate Variable Chart #//////////////////////////////////////////////////////////// class InstantiateVarsChart(FeatureChart): """ A specialized chart that 'instantiates' variables whose names start with '@', by replacing them with unique new variables. In particular, whenever a complete edge is added to the chart, any variables in the edge's C{lhs} whose names start with '@' will be replaced by unique new L{Variable}s. """ def __init__(self, tokens): FeatureChart.__init__(self, tokens) def initialize(self): self._instantiated = set() FeatureChart.initialize(self) def insert(self, edge, child_pointer_list): if edge in self._instantiated: return False self.instantiate_edge(edge) return FeatureChart.insert(self, edge, child_pointer_list) def instantiate_edge(self, edge): """ If the edge is a L{FeatureTreeEdge}, and it is complete, then instantiate all variables whose names start with '@', by replacing them with unique new variables. Note that instantiation is done in-place, since the parsing algorithms might already hold a reference to the edge for future use. """ # If the edge is a leaf, or is not complete, or is # already in the chart, then just return it as-is. if not isinstance(edge, FeatureTreeEdge): return if not edge.is_complete(): return if edge in self._edge_to_cpls: return # Get a list of variables that need to be instantiated. # If there are none, then return as-is. inst_vars = self.inst_vars(edge) if not inst_vars: return # Instantiate the edge! self._instantiated.add(edge) edge._lhs = edge.lhs().substitute_bindings(inst_vars) def inst_vars(self, edge): return dict((var, logic.unique_variable()) for var in edge.lhs().variables() if var.name.startswith('@')) #//////////////////////////////////////////////////////////// # Deprecated parser loading #//////////////////////////////////////////////////////////// @deprecated("Use nltk.load_parser() instead.") def load_earley(filename, trace=0, cache=False, verbose=False, chart_class=FeatureChart, logic_parser=None, fstruct_parser=None): """ Load a grammar from a file, and build an Earley feature parser based on that grammar. You can optionally specify a tracing level, for how much output you want to see: 0: No output. 1: Show edges from scanner and completer rules (not predictor). 2 (default): Show all edges as they are added to the chart. 3: Show all edges, plus the results of successful unifications. 4: Show all edges, plus the results of all attempted unifications. 5: Show all edges, plus the results of all attempted unifications, including those with cached results. If C{verbose} is set to C{True}, then more diagnostic information about grammar-loading is displayed. """ grammar = nltk.data.load(filename, cache=cache, verbose=verbose, logic_parser=logic_parser, fstruct_parser=fstruct_parser) return FeatureChartParser(grammar, trace=trace, chart_class=chart_class) #//////////////////////////////////////////////////////////// # Demo #//////////////////////////////////////////////////////////// def demo_grammar(): return nltk.grammar.parse_fcfg(""" S -> NP VP PP -> Prep NP NP -> NP PP VP -> VP PP VP -> Verb NP VP -> Verb NP -> Det[pl=?x] Noun[pl=?x] NP -> "John" NP -> "I" Det -> "the" Det -> "my" Det[-pl] -> "a" Noun[-pl] -> "dog" Noun[-pl] -> "cookie" Verb -> "ate" Verb -> "saw" Prep -> "with" Prep -> "under" """) def demo(should_print_times=True, should_print_grammar=True, should_print_trees=True, should_print_sentence=True, trace=1, parser=FeatureChartParser, sent='I saw John with a dog with my cookie'): import sys, time print grammar = demo_grammar() if should_print_grammar: print grammar print print "*", parser.__name__ if should_print_sentence: print "Sentence:", sent tokens = sent.split() t = time.clock() cp = parser(grammar, trace=trace) chart = cp.chart_parse(tokens) trees = chart.parses(grammar.start()) if should_print_times: print "Time: %s" % (time.clock() - t) if should_print_trees: for tree in trees: print tree else: print "Nr trees:", len(trees) def run_profile(): import profile profile.run('for i in range(1): demo()', '/tmp/profile.out') import pstats p = pstats.Stats('/tmp/profile.out') p.strip_dirs().sort_stats('time', 'cum').print_stats(60) p.strip_dirs().sort_stats('cum', 'time').print_stats(60) if __name__ == '__main__': demo() print grammar = nltk.data.load('grammars/book_grammars/feat0.fcfg') cp = FeatureChartParser(grammar, trace=2) sent = 'Kim likes children' tokens = sent.split() trees = cp.nbest_parse(tokens) for tree in trees: print tree nltk-2.0~b9/nltk/parse/earleychart.py0000644000175000017500000004336411354233125017526 0ustar bhavanibhavani# -*- coding: utf-8 -*- # Natural Language Toolkit: An Incremental Earley Chart Parser # # Copyright (C) 2001-2010 NLTK Project # Author: Peter Ljunglöf # Rob Speer # Edward Loper # Steven Bird # Jean Mark Gawron # URL: # For license information, see LICENSE.TXT # # $Id: chart.py 8144 2009-06-01 22:27:39Z edloper $ """ Data classes and parser implementations for I{incremental} chart parsers, which use dynamic programming to efficiently parse a text. A X{chart parser} derives parse trees for a text by iteratively adding \"edges\" to a \"chart\". Each X{edge} represents a hypothesis about the tree structure for a subsequence of the text. The X{chart} is a \"blackboard\" for composing and combining these hypotheses. A parser is X{incremental}, if it guarantees that for all i, j where i < j, all edges ending at i are built before any edges ending at j. This is appealing for, say, speech recognizer hypothesis filtering. The main parser class is L{EarleyChartParser}, which is a top-down algorithm, originally formulated by Jay Earley (1970). """ from nltk.grammar import * from api import * from chart import * from featurechart import * #//////////////////////////////////////////////////////////// # Incremental Chart #//////////////////////////////////////////////////////////// class IncrementalChart(Chart): def initialize(self): # A sequence of edge lists contained in this chart. self._edgelists = tuple([] for x in self._positions()) # The set of child pointer lists associated with each edge. self._edge_to_cpls = {} # Indexes mapping attribute values to lists of edges # (used by select()). self._indexes = {} def edges(self): return list(self.iteredges()) def iteredges(self): return (edge for edgelist in self._edgelists for edge in edgelist) def select(self, end, **restrictions): edgelist = self._edgelists[end] # If there are no restrictions, then return all edges. if restrictions=={}: return iter(edgelist) # Find the index corresponding to the given restrictions. restr_keys = restrictions.keys() restr_keys.sort() restr_keys = tuple(restr_keys) # If it doesn't exist, then create it. if restr_keys not in self._indexes: self._add_index(restr_keys) vals = tuple(restrictions[key] for key in restr_keys) return iter(self._indexes[restr_keys][end].get(vals, [])) def _add_index(self, restr_keys): # Make sure it's a valid index. for key in restr_keys: if not hasattr(EdgeI, key): raise ValueError, 'Bad restriction: %s' % key # Create the index. index = self._indexes[restr_keys] = tuple({} for x in self._positions()) # Add all existing edges to the index. for end, edgelist in enumerate(self._edgelists): this_index = index[end] for edge in edgelist: vals = tuple(getattr(edge, key)() for key in restr_keys) this_index.setdefault(vals, []).append(edge) def _register_with_indexes(self, edge): end = edge.end() for (restr_keys, index) in self._indexes.items(): vals = tuple(getattr(edge, key)() for key in restr_keys) index[end].setdefault(vals, []).append(edge) def _append_edge(self, edge): self._edgelists[edge.end()].append(edge) def _positions(self): return xrange(self.num_leaves() + 1) class FeatureIncrementalChart(IncrementalChart, FeatureChart): def select(self, end, **restrictions): edgelist = self._edgelists[end] # If there are no restrictions, then return all edges. if restrictions=={}: return iter(edgelist) # Find the index corresponding to the given restrictions. restr_keys = restrictions.keys() restr_keys.sort() restr_keys = tuple(restr_keys) # If it doesn't exist, then create it. if restr_keys not in self._indexes: self._add_index(restr_keys) vals = tuple(self._get_type_if_possible(restrictions[key]) for key in restr_keys) return iter(self._indexes[restr_keys][end].get(vals, [])) def _add_index(self, restr_keys): # Make sure it's a valid index. for key in restr_keys: if not hasattr(EdgeI, key): raise ValueError, 'Bad restriction: %s' % key # Create the index. index = self._indexes[restr_keys] = tuple({} for x in self._positions()) # Add all existing edges to the index. for end, edgelist in enumerate(self._edgelists): this_index = index[end] for edge in edgelist: vals = tuple(self._get_type_if_possible(getattr(edge, key)()) for key in restr_keys) this_index.setdefault(vals, []).append(edge) def _register_with_indexes(self, edge): end = edge.end() for (restr_keys, index) in self._indexes.items(): vals = tuple(self._get_type_if_possible(getattr(edge, key)()) for key in restr_keys) index[end].setdefault(vals, []).append(edge) #//////////////////////////////////////////////////////////// # Incremental CFG Rules #//////////////////////////////////////////////////////////// class CompleteFundamentalRule(SingleEdgeFundamentalRule): def apply_iter(self, chart, grammar, edge): if edge.is_complete(): for new_edge in self._apply_complete(chart, edge): yield new_edge class CompleterRule(CompleteFundamentalRule): def apply_iter(self, chart, grammar, edge): if not isinstance(edge, LeafEdge): if edge.is_complete(): for new_edge in self._apply_complete(chart, edge): yield new_edge class ScannerRule(CompleteFundamentalRule): def apply_iter(self, chart, grammar, edge): if isinstance(edge, LeafEdge): if edge.is_complete(): for new_edge in self._apply_complete(chart, edge): yield new_edge class PredictorRule(CachedTopDownPredictRule): pass class FilteredCompleteFundamentalRule(FilteredSingleEdgeFundamentalRule): def apply_iter(self, chart, grammar, edge): if edge.is_complete(): for new_edge in self._apply_complete(chart, grammar, edge): yield new_edge #//////////////////////////////////////////////////////////// # Incremental FCFG Rules #//////////////////////////////////////////////////////////// class FeatureCompleteFundamentalRule(FeatureSingleEdgeFundamentalRule): _fundamental_rule = FeatureFundamentalRule() def apply_iter(self, chart, grammar, edge): if edge.is_complete(): fr = self._fundamental_rule for left_edge in chart.select(end=edge.start(), is_complete=False, next=edge.lhs()): for new_edge in fr.apply_iter(chart, grammar, left_edge, edge): yield new_edge class FeatureCompleterRule(FeatureCompleteFundamentalRule): _fundamental_rule = FeatureCompleteFundamentalRule() def apply_iter(self, chart, grammar, edge): if not isinstance(edge, LeafEdge): for new_edge in self._fundamental_rule.apply_iter(chart, grammar, edge): yield new_edge class FeatureScannerRule(FeatureCompleteFundamentalRule): _fundamental_rule = FeatureCompleteFundamentalRule() def apply_iter(self, chart, grammar, edge): if isinstance(edge, LeafEdge): for new_edge in self._fundamental_rule.apply_iter(chart, grammar, edge): yield new_edge class FeaturePredictorRule(FeatureTopDownPredictRule): pass #//////////////////////////////////////////////////////////// # Incremental CFG Chart Parsers #//////////////////////////////////////////////////////////// EARLEY_STRATEGY = [LeafInitRule(), TopDownInitRule(), CompleterRule(), ScannerRule(), PredictorRule()] TD_INCREMENTAL_STRATEGY = [LeafInitRule(), TopDownInitRule(), CachedTopDownPredictRule(), CompleteFundamentalRule()] BU_INCREMENTAL_STRATEGY = [LeafInitRule(), EmptyPredictRule(), BottomUpPredictRule(), CompleteFundamentalRule()] BU_LC_INCREMENTAL_STRATEGY = [LeafInitRule(), EmptyPredictRule(), BottomUpPredictCombineRule(), CompleteFundamentalRule()] LC_INCREMENTAL_STRATEGY = [LeafInitRule(), EmptyPredictRule(), FilteredBottomUpPredictCombineRule(), FilteredCompleteFundamentalRule()] class IncrementalChartParser(ChartParser): """ An I{incremental} chart parser implementing Jay Earley's parsing algorithm: - For each index I{end} in [0, 1, ..., N]: - For each I{edge} s.t. I{edge}.end = I{end}: - If I{edge} is incomplete, and I{edge}.next is not a part of speech: - Apply PredictorRule to I{edge} - If I{edge} is incomplete, and I{edge}.next is a part of speech: - Apply ScannerRule to I{edge} - If I{edge} is complete: - Apply CompleterRule to I{edge} - Return any complete parses in the chart """ def __init__(self, grammar, strategy=BU_LC_INCREMENTAL_STRATEGY, trace=0, trace_chart_width=50, chart_class=IncrementalChart): """ Create a new Earley chart parser, that uses C{grammar} to parse texts. @type grammar: C{ContextFreeGrammar} @param grammar: The grammar used to parse texts. @type trace: C{int} @param trace: The level of tracing that should be used when parsing a text. C{0} will generate no tracing output; and higher numbers will produce more verbose tracing output. @type trace_chart_width: C{int} @param trace_chart_width: The default total width reserved for the chart in trace output. The remainder of each line will be used to display edges. @param chart_class: The class that should be used to create the charts used by this parser. """ self._grammar = grammar self._trace = trace self._trace_chart_width = trace_chart_width self._chart_class = chart_class self._axioms = [] self._inference_rules = [] for rule in strategy: if rule.NUM_EDGES == 0: self._axioms.append(rule) elif rule.NUM_EDGES == 1: self._inference_rules.append(rule) else: raise ValueError("Incremental inference rules must have " "NUM_EDGES == 0 or 1") def chart_parse(self, tokens, trace=None): if trace is None: trace = self._trace trace_new_edges = self._trace_new_edges tokens = list(tokens) self._grammar.check_coverage(tokens) chart = self._chart_class(tokens) grammar = self._grammar # Width, for printing trace edges. trace_edge_width = self._trace_chart_width / (chart.num_leaves() + 1) if trace: print chart.pp_leaves(trace_edge_width) for axiom in self._axioms: new_edges = axiom.apply(chart, grammar) trace_new_edges(chart, axiom, new_edges, trace, trace_edge_width) inference_rules = self._inference_rules for end in range(chart.num_leaves()+1): if trace > 1: print "\n* Processing queue:", end, "\n" agenda = list(chart.select(end=end)) while agenda: edge = agenda.pop() for rule in inference_rules: new_edges = rule.apply_iter(chart, grammar, edge) if trace: new_edges = list(new_edges) trace_new_edges(chart, rule, new_edges, trace, trace_edge_width) for new_edge in new_edges: if new_edge.end()==end: agenda.append(new_edge) return chart class EarleyChartParser(IncrementalChartParser): def __init__(self, grammar, **parser_args): IncrementalChartParser.__init__(self, grammar, EARLEY_STRATEGY, **parser_args) pass class IncrementalTopDownChartParser(IncrementalChartParser): def __init__(self, grammar, **parser_args): IncrementalChartParser.__init__(self, grammar, TD_INCREMENTAL_STRATEGY, **parser_args) class IncrementalBottomUpChartParser(IncrementalChartParser): def __init__(self, grammar, **parser_args): IncrementalChartParser.__init__(self, grammar, BU_INCREMENTAL_STRATEGY, **parser_args) class IncrementalBottomUpLeftCornerChartParser(IncrementalChartParser): def __init__(self, grammar, **parser_args): IncrementalChartParser.__init__(self, grammar, BU_LC_INCREMENTAL_STRATEGY, **parser_args) class IncrementalLeftCornerChartParser(IncrementalChartParser): def __init__(self, grammar, **parser_args): if not grammar.is_nonempty(): raise ValueError("IncrementalLeftCornerParser only works for grammars " "without empty productions.") IncrementalChartParser.__init__(self, grammar, LC_INCREMENTAL_STRATEGY, **parser_args) #//////////////////////////////////////////////////////////// # Incremental FCFG Chart Parsers #//////////////////////////////////////////////////////////// EARLEY_FEATURE_STRATEGY = [LeafInitRule(), FeatureTopDownInitRule(), FeatureCompleterRule(), FeatureScannerRule(), FeaturePredictorRule()] TD_INCREMENTAL_FEATURE_STRATEGY = [LeafInitRule(), FeatureTopDownInitRule(), FeatureTopDownPredictRule(), FeatureCompleteFundamentalRule()] BU_INCREMENTAL_FEATURE_STRATEGY = [LeafInitRule(), FeatureEmptyPredictRule(), FeatureBottomUpPredictRule(), FeatureCompleteFundamentalRule()] BU_LC_INCREMENTAL_FEATURE_STRATEGY = [LeafInitRule(), FeatureEmptyPredictRule(), FeatureBottomUpPredictCombineRule(), FeatureCompleteFundamentalRule()] class FeatureIncrementalChartParser(IncrementalChartParser, FeatureChartParser): def __init__(self, grammar, strategy=BU_LC_INCREMENTAL_FEATURE_STRATEGY, trace_chart_width=20, chart_class=FeatureIncrementalChart, **parser_args): IncrementalChartParser.__init__(self, grammar, strategy=strategy, trace_chart_width=trace_chart_width, chart_class=chart_class, **parser_args) class FeatureEarleyChartParser(FeatureIncrementalChartParser): def __init__(self, grammar, **parser_args): FeatureIncrementalChartParser.__init__(self, grammar, EARLEY_FEATURE_STRATEGY, **parser_args) class FeatureIncrementalTopDownChartParser(FeatureIncrementalChartParser): def __init__(self, grammar, **parser_args): FeatureIncrementalChartParser.__init__(self, grammar, TD_INCREMENTAL_FEATURE_STRATEGY, **parser_args) class FeatureIncrementalBottomUpChartParser(FeatureIncrementalChartParser): def __init__(self, grammar, **parser_args): FeatureIncrementalChartParser.__init__(self, grammar, BU_INCREMENTAL_FEATURE_STRATEGY, **parser_args) class FeatureIncrementalBottomUpLeftCornerChartParser(FeatureIncrementalChartParser): def __init__(self, grammar, **parser_args): FeatureIncrementalChartParser.__init__(self, grammar, BU_LC_INCREMENTAL_FEATURE_STRATEGY, **parser_args) #//////////////////////////////////////////////////////////// # Demonstration #//////////////////////////////////////////////////////////// def demo(should_print_times=True, should_print_grammar=False, should_print_trees=True, trace=2, sent='I saw John with a dog with my cookie', numparses=5): """ A demonstration of the Earley parsers. """ import sys, time # The grammar for ChartParser and SteppingChartParser: grammar = nltk.parse.chart.demo_grammar() if should_print_grammar: print "* Grammar" print grammar # Tokenize the sample sentence. print "* Sentence:" print sent tokens = sent.split() print tokens print # Do the parsing. earley = EarleyChartParser(grammar, trace=trace) t = time.clock() chart = earley.chart_parse(tokens) parses = chart.parses(grammar.start()) t = time.clock()-t # Print results. if numparses: assert len(parses)==numparses, 'Not all parses found' if should_print_trees: for tree in parses: print tree else: print "Nr trees:", len(parses) if should_print_times: print "Time:", t if __name__ == '__main__': demo() nltk-2.0~b9/nltk/parse/dependencygraph.py0000644000175000017500000005456211327451577020402 0ustar bhavanibhavani# Natural Language Toolkit: Dependency Grammars # # Copyright (C) 2001-2010 NLTK Project # Author: Jason Narad # Steven Bird (modifications) # # URL: # For license information, see LICENSE.TXT # """ Tools for reading and writing dependency trees. The input is assumed to be in U{Malt-TAB} format. Currently only reads the first tree in a file. """ from nltk.tree import Tree from pprint import pformat import re ################################################################# # DependencyGraph Class ################################################################# class DependencyGraph(object): """ A container for the nodes and labelled edges of a dependency structure. """ def __init__(self, tree_str=None): """ We place a dummy 'top' node in the first position in the nodelist, since the root node is often assigned '0' as its head. This also means that the indexing of the nodelist corresponds directly to the Malt-TAB format, which starts at 1. """ top = {'word':None, 'deps':[], 'rel': 'TOP', 'tag': 'TOP', 'address': 0} self.nodelist = [top] self.root = None self.stream = None if tree_str: self._parse(tree_str) def remove_by_address(self, address): """ Removes the node with the given address. References to this node in others will still exist. """ node_index = len(self.nodelist) - 1 while(node_index >= 0): node = self.nodelist[node_index] if node['address'] == address: self.nodelist.pop(node_index) node_index -= 1 def redirect_arcs(self, originals, redirect): """ Redirects arcs to any of the nodes in the originals list to the redirect node address. """ for node in self.nodelist: new_deps = [] for dep in node['deps']: if dep in originals: new_deps.append(redirect) else: new_deps.append(dep) node['deps'] = new_deps def add_arc(self, head_address, mod_address): """ Adds an arc from the node specified by head_address to the node specified by the mod address. """ for node in self.nodelist: if node['address'] == head_address and (mod_address not in node['deps']): node['deps'].append(mod_address) def connect_graph(self): """ Fully connects all non-root nodes. All nodes are set to be dependents of the root node. """ for node1 in self.nodelist: for node2 in self.nodelist: if node1['address'] != node2['address'] and node2['rel'] != 'TOP': node1['deps'].append(node2['address']) # fix error and return def get_by_address(self, node_address): """ Returns the node with the given address. """ for node in self.nodelist: if node['address'] == node_address: return node print 'THROW ERROR: address not found in -get_by_address-' return -1 def contains_address(self, node_address): """ Returns true if the graph contains a node with the given node address, false otherwise. """ for node in self.nodelist: if node['address'] == node_address: return True return False def __str__(self): return pformat(self.nodelist) def __repr__(self): return "" % len(self.nodelist) @staticmethod def load(file): """ @param file: a file in Malt-TAB format """ return DependencyGraph(open(file).read()) @staticmethod def _normalize(line): """ Deal with lines in which spaces are used rather than tabs. """ SPC = re.compile(' +') return re.sub(SPC, '\t', line).strip() def left_children(self, node_index): """ Returns the number of left children under the node specified by the given address. """ children = self.nodelist[node_index]['deps'] index = self.nodelist[node_index]['address'] return sum(1 for c in children if c < index) def right_children(self, node_index): """ Returns the number of right children under the node specified by the given address. """ children = self.nodelist[node_index]['deps'] index = self.nodelist[node_index]['address'] return sum(1 for c in children if c > index) def add_node(self, node): if not self.contains_address(node['address']): self.nodelist.append(node) def _parse(self, input): lines = [DependencyGraph._normalize(line) for line in input.split('\n') if line.strip()] temp = [] for index, line in enumerate(lines): # print line try: cells = line.split('\t') nrCells = len(cells) if nrCells == 3: word, tag, head = cells rel = '' elif nrCells == 4: word, tag, head, rel = cells elif nrCells == 10: _, word, _, _, tag, _, head, rel, _, _ = cells else: raise ValueError('Number of tab-delimited fields (%d) not supported by CoNLL(10) or Malt-Tab(4) format' % (nrCells)) head = int(head) self.nodelist.append({'address': index+1, 'word': word, 'tag': tag, 'head': head, 'rel': rel, 'deps': [d for (d,h) in temp if h == index+1]}) try: self.nodelist[head]['deps'].append(index+1) except IndexError: temp.append((index+1, head)) except ValueError: break root_address = self.nodelist[0]['deps'][0] self.root = self.nodelist[root_address] def _word(self, node, filter=True): w = node['word'] if filter: if w != ',': return w return w def _tree(self, i): """ Recursive function for turning dependency graphs into NLTK trees. @type i: C{int} @param i: index of a node in C{nodelist} @return: either a word (if the indexed node is a leaf) or a L{Tree}. """ node = self.nodelist[i] word = node['word'] deps = node['deps'] if len(deps) == 0: return word else: return Tree(word, [self._tree(j) for j in deps]) def tree(self): """ Starting with the C{root} node, build a dependency tree using the NLTK L{Tree} constructor. Dependency labels are omitted. """ node = self.root word = node['word'] deps = node['deps'] return Tree(word, [self._tree(i) for i in deps]) def _hd(self, i): try: return self.nodelist[i]['head'] except IndexError: return None def _rel(self, i): try: return self.nodelist[i]['rel'] except IndexError: return None # what's the return type? Boolean or list? def contains_cycle(self): distances = {} for node in self.nodelist: for dep in node['deps']: key = tuple([node['address'], dep]) #'%d -> %d' % (node['address'], dep) distances[key] = 1 window = 0 for n in range(len(self.nodelist)): new_entries = {} for pair1 in distances: for pair2 in distances: if pair1[1] == pair2[0]: key = tuple([pair1[0], pair2[1]]) new_entries[key] = distances[pair1] + distances[pair2] for pair in new_entries: distances[pair] = new_entries[pair] if pair[0] == pair[1]: print pair[0] path = self.get_cycle_path(self.get_by_address(pair[0]), pair[0]) #self.nodelist[pair[0]], pair[0]) return path return False # return []? def get_cycle_path(self, curr_node, goal_node_index): for dep in curr_node['deps']: if dep == goal_node_index: return [curr_node['address']] for dep in curr_node['deps']: path = self.get_cycle_path(self.get_by_address(dep), goal_node_index)#self.nodelist[dep], goal_node_index) if len(path) > 0: path.insert(0, curr_node['address']) return path return [] def to_conll(self, style): """ The dependency graph in CoNLL format. @param style: the style to use for the format (3, 4, 10 columns) @type style: C{int} @rtype: C{str} """ lines = [] for i, node in enumerate(self.nodelist[1:]): word, tag, head, rel = node['word'], node['tag'], node['head'], node['rel'] if style == 3: lines.append('%s\t%s\t%s\n' % (word, tag, head)) elif style == 4: lines.append('%s\t%s\t%s\t%s\n' % (word, tag, head, rel)) elif style == 10: lines.append('%s\t%s\t_\t%s\t%s\t_\t%s\t%s\t_\t_\n' % (i+1, word, tag, tag, head, rel)) else: raise ValueError('Number of tab-delimited fields (%d) not supported by CoNLL(10) or Malt-Tab(4) format' % (style)) return ''.join(lines) def nx_graph(self): """ Convert the data in a C{nodelist} into a networkx labeled directed graph. @rtype: C{XDigraph} """ nx_nodelist = range(1, len(self.nodelist)) nx_edgelist = [(n, self._hd(n), self._rel(n)) for n in nx_nodelist if self._hd(n)] self.nx_labels = {} for n in nx_nodelist: self.nx_labels[n] = self.nodelist[n]['word'] g = NX.XDiGraph() g.add_nodes_from(nx_nodelist) g.add_edges_from(nx_edgelist) return g def demo(): malt_demo() conll_demo() conll_file_demo() cycle_finding_demo() def malt_demo(nx=False): """ A demonstration of the result of reading a dependency version of the first sentence of the Penn Treebank. """ dg = DependencyGraph("""Pierre NNP 2 NMOD Vinken NNP 8 SUB , , 2 P 61 CD 5 NMOD years NNS 6 AMOD old JJ 2 NMOD , , 2 P will MD 0 ROOT join VB 8 VC the DT 11 NMOD board NN 9 OBJ as IN 9 VMOD a DT 15 NMOD nonexecutive JJ 15 NMOD director NN 12 PMOD Nov. NNP 9 VMOD 29 CD 16 NMOD . . 9 VMOD """) tree = dg.tree() print tree.pprint() if nx: #currently doesn't work try: import networkx as NX import pylab as P except ImportError: raise g = dg.nx_graph() g.info() pos = NX.spring_layout(g, dim=1) NX.draw_networkx_nodes(g, pos, node_size=50) #NX.draw_networkx_edges(g, pos, edge_color='k', width=8) NX.draw_networkx_labels(g, pos, dg.nx_labels) P.xticks([]) P.yticks([]) P.savefig('tree.png') P.show() def conll_demo(): """ A demonstration of how to read a string representation of a CoNLL format dependency tree. """ dg = DependencyGraph(conll_data1) tree = dg.tree() print tree.pprint() print dg print dg.to_conll(4) def conll_file_demo(): print 'Mass conll_read demo...' graphs = [DependencyGraph(entry) for entry in conll_data2.split('\n\n') if entry] for graph in graphs: tree = graph.tree() print '\n' + tree.pprint() def cycle_finding_demo(): dg = DependencyGraph(treebank_data) print dg.contains_cycle() cyclic_dg = DependencyGraph() top = {'word':None, 'deps':[1], 'rel': 'TOP', 'address': 0} child1 = {'word':None, 'deps':[2], 'rel': 'NTOP', 'address': 1} child2 = {'word':None, 'deps':[4], 'rel': 'NTOP', 'address': 2} child3 = {'word':None, 'deps':[1], 'rel': 'NTOP', 'address': 3} child4 = {'word':None, 'deps':[3], 'rel': 'NTOP', 'address': 4} cyclic_dg.nodelist = [top, child1, child2, child3, child4] cyclic_dg.root = top print cyclic_dg.contains_cycle() treebank_data = """Pierre NNP 2 NMOD Vinken NNP 8 SUB , , 2 P 61 CD 5 NMOD years NNS 6 AMOD old JJ 2 NMOD , , 2 P will MD 0 ROOT join VB 8 VC the DT 11 NMOD board NN 9 OBJ as IN 9 VMOD a DT 15 NMOD nonexecutive JJ 15 NMOD director NN 12 PMOD Nov. NNP 9 VMOD 29 CD 16 NMOD . . 9 VMOD """ conll_data1 = """ 1 Ze ze Pron Pron per|3|evofmv|nom 2 su _ _ 2 had heb V V trans|ovt|1of2of3|ev 0 ROOT _ _ 3 met met Prep Prep voor 8 mod _ _ 4 haar haar Pron Pron bez|3|ev|neut|attr 5 det _ _ 5 moeder moeder N N soort|ev|neut 3 obj1 _ _ 6 kunnen kan V V hulp|ott|1of2of3|mv 2 vc _ _ 7 gaan ga V V hulp|inf 6 vc _ _ 8 winkelen winkel V V intrans|inf 11 cnj _ _ 9 , , Punc Punc komma 8 punct _ _ 10 zwemmen zwem V V intrans|inf 11 cnj _ _ 11 of of Conj Conj neven 7 vc _ _ 12 terrassen terras N N soort|mv|neut 11 cnj _ _ 13 . . Punc Punc punt 12 punct _ _ """ conll_data2 = """1 Cathy Cathy N N eigen|ev|neut 2 su _ _ 2 zag zie V V trans|ovt|1of2of3|ev 0 ROOT _ _ 3 hen hen Pron Pron per|3|mv|datofacc 2 obj1 _ _ 4 wild wild Adj Adj attr|stell|onverv 5 mod _ _ 5 zwaaien zwaai N N soort|mv|neut 2 vc _ _ 6 . . Punc Punc punt 5 punct _ _ 1 Ze ze Pron Pron per|3|evofmv|nom 2 su _ _ 2 had heb V V trans|ovt|1of2of3|ev 0 ROOT _ _ 3 met met Prep Prep voor 8 mod _ _ 4 haar haar Pron Pron bez|3|ev|neut|attr 5 det _ _ 5 moeder moeder N N soort|ev|neut 3 obj1 _ _ 6 kunnen kan V V hulp|ott|1of2of3|mv 2 vc _ _ 7 gaan ga V V hulp|inf 6 vc _ _ 8 winkelen winkel V V intrans|inf 11 cnj _ _ 9 , , Punc Punc komma 8 punct _ _ 10 zwemmen zwem V V intrans|inf 11 cnj _ _ 11 of of Conj Conj neven 7 vc _ _ 12 terrassen terras N N soort|mv|neut 11 cnj _ _ 13 . . Punc Punc punt 12 punct _ _ 1 Dat dat Pron Pron aanw|neut|attr 2 det _ _ 2 werkwoord werkwoord N N soort|ev|neut 6 obj1 _ _ 3 had heb V V hulp|ovt|1of2of3|ev 0 ROOT _ _ 4 ze ze Pron Pron per|3|evofmv|nom 6 su _ _ 5 zelf zelf Pron Pron aanw|neut|attr|wzelf 3 predm _ _ 6 uitgevonden vind V V trans|verldw|onverv 3 vc _ _ 7 . . Punc Punc punt 6 punct _ _ 1 Het het Pron Pron onbep|neut|zelfst 2 su _ _ 2 hoorde hoor V V trans|ovt|1of2of3|ev 0 ROOT _ _ 3 bij bij Prep Prep voor 2 ld _ _ 4 de de Art Art bep|zijdofmv|neut 6 det _ _ 5 warme warm Adj Adj attr|stell|vervneut 6 mod _ _ 6 zomerdag zomerdag N N soort|ev|neut 3 obj1 _ _ 7 die die Pron Pron betr|neut|zelfst 6 mod _ _ 8 ze ze Pron Pron per|3|evofmv|nom 12 su _ _ 9 ginds ginds Adv Adv gew|aanw 12 mod _ _ 10 achter achter Adv Adv gew|geenfunc|stell|onverv 12 svp _ _ 11 had heb V V hulp|ovt|1of2of3|ev 7 body _ _ 12 gelaten laat V V trans|verldw|onverv 11 vc _ _ 13 . . Punc Punc punt 12 punct _ _ 1 Ze ze Pron Pron per|3|evofmv|nom 2 su _ _ 2 hadden heb V V trans|ovt|1of2of3|mv 0 ROOT _ _ 3 languit languit Adv Adv gew|geenfunc|stell|onverv 11 mod _ _ 4 naast naast Prep Prep voor 11 mod _ _ 5 elkaar elkaar Pron Pron rec|neut 4 obj1 _ _ 6 op op Prep Prep voor 11 ld _ _ 7 de de Art Art bep|zijdofmv|neut 8 det _ _ 8 strandstoelen strandstoel N N soort|mv|neut 6 obj1 _ _ 9 kunnen kan V V hulp|inf 2 vc _ _ 10 gaan ga V V hulp|inf 9 vc _ _ 11 liggen lig V V intrans|inf 10 vc _ _ 12 . . Punc Punc punt 11 punct _ _ 1 Zij zij Pron Pron per|3|evofmv|nom 2 su _ _ 2 zou zal V V hulp|ovt|1of2of3|ev 7 cnj _ _ 3 mams mams N N soort|ev|neut 4 det _ _ 4 rug rug N N soort|ev|neut 5 obj1 _ _ 5 ingewreven wrijf V V trans|verldw|onverv 6 vc _ _ 6 hebben heb V V hulp|inf 2 vc _ _ 7 en en Conj Conj neven 0 ROOT _ _ 8 mam mam V V trans|ovt|1of2of3|ev 7 cnj _ _ 9 de de Art Art bep|zijdofmv|neut 10 det _ _ 10 hare hare Pron Pron bez|3|ev|neut|attr 8 obj1 _ _ 11 . . Punc Punc punt 10 punct _ _ 1 Of of Conj Conj onder|metfin 0 ROOT _ _ 2 ze ze Pron Pron per|3|evofmv|nom 3 su _ _ 3 had heb V V hulp|ovt|1of2of3|ev 0 ROOT _ _ 4 gewoon gewoon Adj Adj adv|stell|onverv 10 mod _ _ 5 met met Prep Prep voor 10 mod _ _ 6 haar haar Pron Pron bez|3|ev|neut|attr 7 det _ _ 7 vriendinnen vriendin N N soort|mv|neut 5 obj1 _ _ 8 rond rond Adv Adv deelv 10 svp _ _ 9 kunnen kan V V hulp|inf 3 vc _ _ 10 slenteren slenter V V intrans|inf 9 vc _ _ 11 in in Prep Prep voor 10 mod _ _ 12 de de Art Art bep|zijdofmv|neut 13 det _ _ 13 buurt buurt N N soort|ev|neut 11 obj1 _ _ 14 van van Prep Prep voor 13 mod _ _ 15 Trafalgar_Square Trafalgar_Square MWU N_N eigen|ev|neut_eigen|ev|neut 14 obj1 _ _ 16 . . Punc Punc punt 15 punct _ _ """ if __name__ == '__main__': demo() nltk-2.0~b9/nltk/parse/chart.py0000644000175000017500000020105611354233125016316 0ustar bhavanibhavani# -*- coding: utf-8 -*- # Natural Language Toolkit: A Chart Parser # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # Jean Mark Gawron # Peter Ljunglöf # URL: # For license information, see LICENSE.TXT # # $Id: chart.py 8506 2010-03-08 22:16:33Z peter.ljunglof@heatherleaf.se $ """ Data classes and parser implementations for \"chart parsers\", which use dynamic programming to efficiently parse a text. A X{chart parser} derives parse trees for a text by iteratively adding \"edges\" to a \"chart.\" Each X{edge} represents a hypothesis about the tree structure for a subsequence of the text. The X{chart} is a \"blackboard\" for composing and combining these hypotheses. When a chart parser begins parsing a text, it creates a new (empty) chart, spanning the text. It then incrementally adds new edges to the chart. A set of X{chart rules} specifies the conditions under which new edges should be added to the chart. Once the chart reaches a stage where none of the chart rules adds any new edges, parsing is complete. Charts are encoded with the L{Chart} class, and edges are encoded with the L{TreeEdge} and L{LeafEdge} classes. The chart parser module defines three chart parsers: - C{ChartParser} is a simple and flexible chart parser. Given a set of chart rules, it will apply those rules to the chart until no more edges are added. - C{SteppingChartParser} is a subclass of C{ChartParser} that can be used to step through the parsing process. """ from nltk.tree import Tree from nltk.grammar import WeightedGrammar, is_nonterminal, is_terminal from nltk.compat import defaultdict from api import * import re import warnings ######################################################################## ## Edges ######################################################################## class EdgeI(object): """ A hypothesis about the structure of part of a sentence. Each edge records the fact that a structure is (partially) consistent with the sentence. An edge contains: - A X{span}, indicating what part of the sentence is consistent with the hypothesized structure. - A X{left-hand side}, specifying what kind of structure is hypothesized. - A X{right-hand side}, specifying the contents of the hypothesized structure. - A X{dot position}, indicating how much of the hypothesized structure is consistent with the sentence. Every edge is either X{complete} or X{incomplete}: - An edge is X{complete} if its structure is fully consistent with the sentence. - An edge is X{incomplete} if its structure is partially consistent with the sentence. For every incomplete edge, the span specifies a possible prefix for the edge's structure. There are two kinds of edge: - C{TreeEdges} record which trees have been found to be (partially) consistent with the text. - C{LeafEdges} record the tokens occur in the text. The C{EdgeI} interface provides a common interface to both types of edge, allowing chart parsers to treat them in a uniform manner. """ def __init__(self): if self.__class__ == EdgeI: raise TypeError('Edge is an abstract interface') #//////////////////////////////////////////////////////////// # Span #//////////////////////////////////////////////////////////// def span(self): """ @return: A tuple C{(s,e)}, where C{subtokens[s:e]} is the portion of the sentence that is consistent with this edge's structure. @rtype: C{(int, int)} """ raise AssertionError('EdgeI is an abstract interface') def start(self): """ @return: The start index of this edge's span. @rtype: C{int} """ raise AssertionError('EdgeI is an abstract interface') def end(self): """ @return: The end index of this edge's span. @rtype: C{int} """ raise AssertionError('EdgeI is an abstract interface') def length(self): """ @return: The length of this edge's span. @rtype: C{int} """ raise AssertionError('EdgeI is an abstract interface') #//////////////////////////////////////////////////////////// # Left Hand Side #//////////////////////////////////////////////////////////// def lhs(self): """ @return: This edge's left-hand side, which specifies what kind of structure is hypothesized by this edge. @see: L{TreeEdge} and L{LeafEdge} for a description of the left-hand side values for each edge type. """ raise AssertionError('EdgeI is an abstract interface') #//////////////////////////////////////////////////////////// # Right Hand Side #//////////////////////////////////////////////////////////// def rhs(self): """ @return: This edge's right-hand side, which specifies the content of the structure hypothesized by this edge. @see: L{TreeEdge} and L{LeafEdge} for a description of the right-hand side values for each edge type. """ raise AssertionError('EdgeI is an abstract interface') def dot(self): """ @return: This edge's dot position, which indicates how much of the hypothesized structure is consistent with the sentence. In particular, C{self.rhs[:dot]} is consistent with C{subtoks[self.start():self.end()]}. @rtype: C{int} """ raise AssertionError('EdgeI is an abstract interface') def next(self): """ @return: The element of this edge's right-hand side that immediately follows its dot. @rtype: C{Nonterminal} or X{terminal} or C{None} """ raise AssertionError('EdgeI is an abstract interface') def is_complete(self): """ @return: True if this edge's structure is fully consistent with the text. @rtype: C{boolean} """ raise AssertionError('EdgeI is an abstract interface') def is_incomplete(self): """ @return: True if this edge's structure is partially consistent with the text. @rtype: C{boolean} """ raise AssertionError('EdgeI is an abstract interface') #//////////////////////////////////////////////////////////// # Comparisons #//////////////////////////////////////////////////////////// def __cmp__(self, other): raise AssertionError('EdgeI is an abstract interface') def __hash__(self, other): raise AssertionError('EdgeI is an abstract interface') class TreeEdge(EdgeI): """ An edge that records the fact that a tree is (partially) consistent with the sentence. A tree edge consists of: - A X{span}, indicating what part of the sentence is consistent with the hypothesized tree. - A X{left-hand side}, specifying the hypothesized tree's node value. - A X{right-hand side}, specifying the hypothesized tree's children. Each element of the right-hand side is either a terminal, specifying a token with that terminal as its leaf value; or a nonterminal, specifying a subtree with that nonterminal's symbol as its node value. - A X{dot position}, indicating which children are consistent with part of the sentence. In particular, if C{dot} is the dot position, C{rhs} is the right-hand size, C{(start,end)} is the span, and C{sentence} is the list of subtokens in the sentence, then C{subtokens[start:end]} can be spanned by the children specified by C{rhs[:dot]}. For more information about edges, see the L{EdgeI} interface. """ def __init__(self, span, lhs, rhs, dot=0): """ Construct a new C{TreeEdge}. @type span: C{(int, int)} @param span: A tuple C{(s,e)}, where C{subtokens[s:e]} is the portion of the sentence that is consistent with the new edge's structure. @type lhs: L{Nonterminal} @param lhs: The new edge's left-hand side, specifying the hypothesized tree's node value. @type rhs: C{list} of (L{Nonterminal} and C{string}) @param rhs: The new edge's right-hand side, specifying the hypothesized tree's children. @type dot: C{int} @param dot: The position of the new edge's dot. This position specifies what prefix of the production's right hand side is consistent with the text. In particular, if C{sentence} is the list of subtokens in the sentence, then C{subtokens[span[0]:span[1]]} can be spanned by the children specified by C{rhs[:dot]}. """ self._lhs = lhs self._rhs = tuple(rhs) self._span = span self._dot = dot # [staticmethod] def from_production(production, index): """ @return: A new C{TreeEdge} formed from the given production. The new edge's left-hand side and right-hand side will be taken from C{production}; its span will be C{(index,index)}; and its dot position will be C{0}. @rtype: L{TreeEdge} """ return TreeEdge(span=(index, index), lhs=production.lhs(), rhs=production.rhs(), dot=0) from_production = staticmethod(from_production) def move_dot_forward(self, new_end): """ @return: A new C{TreeEdge} formed from this edge. The new edge's dot position is increased by C{1}, and its end index will be replaced by C{new_end}. @rtype: L{TreeEdge} @param new_end: The new end index. @type new_end: C{int} """ return TreeEdge(span=(self._span[0], new_end), lhs=self._lhs, rhs=self._rhs, dot=self._dot+1) # Accessors def lhs(self): return self._lhs def span(self): return self._span def start(self): return self._span[0] def end(self): return self._span[1] def length(self): return self._span[1] - self._span[0] def rhs(self): return self._rhs def dot(self): return self._dot def is_complete(self): return self._dot == len(self._rhs) def is_incomplete(self): return self._dot != len(self._rhs) def next(self): if self._dot >= len(self._rhs): return None else: return self._rhs[self._dot] # Comparisons & hashing def __cmp__(self, other): if self.__class__ != other.__class__: return -1 return cmp((self._span, self.lhs(), self.rhs(), self._dot), (other._span, other.lhs(), other.rhs(), other._dot)) def __hash__(self): return hash((self.lhs(), self.rhs(), self._span, self._dot)) # String representation def __str__(self): str = '[%s:%s] ' % (self._span[0], self._span[1]) str += '%-2r ->' % (self._lhs,) for i in range(len(self._rhs)): if i == self._dot: str += ' *' str += ' %r' % (self._rhs[i],) if len(self._rhs) == self._dot: str += ' *' return str def __repr__(self): return '[Edge: %s]' % self class LeafEdge(EdgeI): """ An edge that records the fact that a leaf value is consistent with a word in the sentence. A leaf edge consists of: - An X{index}, indicating the position of the word. - A X{leaf}, specifying the word's content. A leaf edge's left-hand side is its leaf value, and its right hand side is C{()}. Its span is C{[index, index+1]}, and its dot position is C{0}. """ def __init__(self, leaf, index): """ Construct a new C{LeafEdge}. @param leaf: The new edge's leaf value, specifying the word that is recorded by this edge. @param index: The new edge's index, specifying the position of the word that is recorded by this edge. """ self._leaf = leaf self._index = index # Accessors def lhs(self): return self._leaf def span(self): return (self._index, self._index+1) def start(self): return self._index def end(self): return self._index+1 def length(self): return 1 def rhs(self): return () def dot(self): return 0 def is_complete(self): return True def is_incomplete(self): return False def next(self): return None # Comparisons & hashing def __cmp__(self, other): if not isinstance(other, LeafEdge): return -1 return cmp((self._index, self._leaf), (other._index, other._leaf)) def __hash__(self): return hash((self._index, self._leaf)) # String representations def __str__(self): return '[%s:%s] %r' % (self._index, self._index+1, self._leaf) def __repr__(self): return '[Edge: %s]' % (self) ######################################################################## ## Chart ######################################################################## class Chart(object): """ A blackboard for hypotheses about the syntactic constituents of a sentence. A chart contains a set of edges, and each edge encodes a single hypothesis about the structure of some portion of the sentence. The L{select} method can be used to select a specific collection of edges. For example C{chart.select(is_complete=True, start=0)} yields all complete edges whose start indices are 0. To ensure the efficiency of these selection operations, C{Chart} dynamically creates and maintains an index for each set of attributes that have been selected on. In order to reconstruct the trees that are represented by an edge, the chart associates each edge with a set of child pointer lists. A X{child pointer list} is a list of the edges that license an edge's right-hand side. @ivar _tokens: The sentence that the chart covers. @ivar _num_leaves: The number of tokens. @ivar _edges: A list of the edges in the chart @ivar _edge_to_cpls: A dictionary mapping each edge to a set of child pointer lists that are associated with that edge. @ivar _indexes: A dictionary mapping tuples of edge attributes to indices, where each index maps the corresponding edge attribute values to lists of edges. """ def __init__(self, tokens): """ Construct a new chart. The chart is initialized with the leaf edges corresponding to the terminal leaves. @type tokens: L{list} @param tokens: The sentence that this chart will be used to parse. """ # Record the sentence token and the sentence length. self._tokens = tuple(tokens) self._num_leaves = len(self._tokens) # Initialise the chart. self.initialize() def initialize(self): """ Clear the chart. """ # A list of edges contained in this chart. self._edges = [] # The set of child pointer lists associated with each edge. self._edge_to_cpls = {} # Indexes mapping attribute values to lists of edges # (used by select()). self._indexes = {} #//////////////////////////////////////////////////////////// # Sentence Access #//////////////////////////////////////////////////////////// def num_leaves(self): """ @return: The number of words in this chart's sentence. @rtype: C{int} """ return self._num_leaves def leaf(self, index): """ @return: The leaf value of the word at the given index. @rtype: C{string} """ return self._tokens[index] def leaves(self): """ @return: A list of the leaf values of each word in the chart's sentence. @rtype: C{list} of C{string} """ return self._tokens #//////////////////////////////////////////////////////////// # Edge access #//////////////////////////////////////////////////////////// def edges(self): """ @return: A list of all edges in this chart. New edges that are added to the chart after the call to edges() will I{not} be contained in this list. @rtype: C{list} of L{EdgeI} @see: L{iteredges}, L{select} """ return self._edges[:] def iteredges(self): """ @return: An iterator over the edges in this chart. It is I{not} guaranteed that new edges which are added to the chart before the iterator is exhausted will also be generated. @rtype: C{iter} of L{EdgeI} @see: L{edges}, L{select} """ return iter(self._edges) # Iterating over the chart yields its edges. __iter__ = iteredges def num_edges(self): """ @return: The number of edges contained in this chart. @rtype: C{int} """ return len(self._edge_to_cpls) def select(self, **restrictions): """ @return: An iterator over the edges in this chart. Any new edges that are added to the chart before the iterator is exahusted will also be generated. C{restrictions} can be used to restrict the set of edges that will be generated. @rtype: C{iter} of L{EdgeI} @kwarg span: Only generate edges C{e} where C{e.span()==span} @kwarg start: Only generate edges C{e} where C{e.start()==start} @kwarg end: Only generate edges C{e} where C{e.end()==end} @kwarg length: Only generate edges C{e} where C{e.length()==length} @kwarg lhs: Only generate edges C{e} where C{e.lhs()==lhs} @kwarg rhs: Only generate edges C{e} where C{e.rhs()==rhs} @kwarg next: Only generate edges C{e} where C{e.next()==next} @kwarg dot: Only generate edges C{e} where C{e.dot()==dot} @kwarg is_complete: Only generate edges C{e} where C{e.is_complete()==is_complete} @kwarg is_incomplete: Only generate edges C{e} where C{e.is_incomplete()==is_incomplete} """ # If there are no restrictions, then return all edges. if restrictions=={}: return iter(self._edges) # Find the index corresponding to the given restrictions. restr_keys = restrictions.keys() restr_keys.sort() restr_keys = tuple(restr_keys) # If it doesn't exist, then create it. if restr_keys not in self._indexes: self._add_index(restr_keys) vals = tuple(restrictions[key] for key in restr_keys) return iter(self._indexes[restr_keys].get(vals, [])) def _add_index(self, restr_keys): """ A helper function for L{select}, which creates a new index for a given set of attributes (aka restriction keys). """ # Make sure it's a valid index. for key in restr_keys: if not hasattr(EdgeI, key): raise ValueError, 'Bad restriction: %s' % key # Create the index. index = self._indexes[restr_keys] = {} # Add all existing edges to the index. for edge in self._edges: vals = tuple(getattr(edge, key)() for key in restr_keys) index.setdefault(vals, []).append(edge) def _register_with_indexes(self, edge): """ A helper function for L{insert}, which registers the new edge with all existing indexes. """ for (restr_keys, index) in self._indexes.items(): vals = tuple(getattr(edge, key)() for key in restr_keys) index.setdefault(vals, []).append(edge) #//////////////////////////////////////////////////////////// # Edge Insertion #//////////////////////////////////////////////////////////// def insert_with_backpointer(self, new_edge, previous_edge, child_edge): """ Add a new edge to the chart, using a pointer to the previous edge. """ cpls = self.child_pointer_lists(previous_edge) new_cpls = [cpl+(child_edge,) for cpl in cpls] return self.insert(new_edge, *new_cpls) def insert(self, edge, *child_pointer_lists): """ Add a new edge to the chart. @type edge: L{EdgeI} @param edge: The new edge @type child_pointer_lists: C(sequence} of C{tuple} of L{EdgeI} @param child_pointer_lists: A sequence of lists of the edges that were used to form this edge. This list is used to reconstruct the trees (or partial trees) that are associated with C{edge}. @rtype: C{bool} @return: True if this operation modified the chart. In particular, return true iff the chart did not already contain C{edge}, or if it did not already associate C{child_pointer_lists} with C{edge}. """ # Is it a new edge? if edge not in self._edge_to_cpls: # Add it to the list of edges. self._append_edge(edge) # Register with indexes. self._register_with_indexes(edge) # Get the set of child pointer lists for this edge. cpls = self._edge_to_cpls.setdefault(edge,{}) chart_was_modified = False for child_pointer_list in child_pointer_lists: child_pointer_list = tuple(child_pointer_list) if child_pointer_list not in cpls: # It's a new CPL; register it, and return true. cpls[child_pointer_list] = True chart_was_modified = True return chart_was_modified def _append_edge(self, edge): self._edges.append(edge) #//////////////////////////////////////////////////////////// # Tree extraction & child pointer lists #//////////////////////////////////////////////////////////// def parses(self, root, tree_class=Tree): """ @return: A list of the complete tree structures that span the entire chart, and whose root node is C{root}. """ trees = [] for edge in self.select(start=0, end=self._num_leaves, lhs=root): trees += self.trees(edge, tree_class=tree_class, complete=True) return trees def trees(self, edge, tree_class=Tree, complete=False): """ @return: A list of the tree structures that are associated with C{edge}. If C{edge} is incomplete, then the unexpanded children will be encoded as childless subtrees, whose node value is the corresponding terminal or nonterminal. @rtype: C{list} of L{Tree} @note: If two trees share a common subtree, then the same C{Tree} may be used to encode that subtree in both trees. If you need to eliminate this subtree sharing, then create a deep copy of each tree. """ return self._trees(edge, complete, memo={}, tree_class=tree_class) def _trees(self, edge, complete, memo, tree_class): """ A helper function for L{trees}. @param memo: A dictionary used to record the trees that we've generated for each edge, so that when we see an edge more than once, we can reuse the same trees. """ # If we've seen this edge before, then reuse our old answer. if edge in memo: return memo[edge] trees = [] # when we're reading trees off the chart, don't use incomplete edges if complete and edge.is_incomplete(): return trees # Until we're done computing the trees for edge, set # memo[edge] to be empty. This has the effect of filtering # out any cyclic trees (i.e., trees that contain themselves as # descendants), because if we reach this edge via a cycle, # then it will appear that the edge doesn't generate any # trees. memo[edge] = [] # Leaf edges. if isinstance(edge, LeafEdge): leaf = self._tokens[edge.start()] memo[edge] = leaf return [leaf] # Each child pointer list can be used to form trees. for cpl in self.child_pointer_lists(edge): # Get the set of child choices for each child pointer. # child_choices[i] is the set of choices for the tree's # ith child. child_choices = [self._trees(cp, complete, memo, tree_class) for cp in cpl] # For each combination of children, add a tree. for children in self._choose_children(child_choices): lhs = edge.lhs().symbol() trees.append(tree_class(lhs, children)) # If the edge is incomplete, then extend it with "partial trees": if edge.is_incomplete(): unexpanded = [tree_class(elt,[]) for elt in edge.rhs()[edge.dot():]] for tree in trees: tree.extend(unexpanded) # Update the memoization dictionary. memo[edge] = trees # Return the list of trees. return trees def _choose_children(self, child_choices): """ A helper function for L{_trees} that finds the possible sets of subtrees for a new tree. @param child_choices: A list that specifies the options for each child. In particular, C{child_choices[i]} is a list of tokens and subtrees that can be used as the C{i}th child. """ children_lists = [[]] for child_choice in child_choices: if hasattr(child_choice, '__iter__') and \ not isinstance(child_choice, basestring): # Only iterate over the child trees # if child_choice is iterable and NOT a string children_lists = [child_list+[child] for child in child_choice for child_list in children_lists] else: # If child_choice is a string (or non-iterable) # then it is a leaf children_lists = [child_list+[child_choice] for child_list in children_lists] return children_lists def child_pointer_lists(self, edge): """ @rtype: C{list} of C{list} of C{EdgeI} @return: The set of child pointer lists for the given edge. Each child pointer list is a list of edges that have been used to form this edge. """ # Make a copy, in case they modify it. return self._edge_to_cpls.get(edge, {}).keys() #//////////////////////////////////////////////////////////// # Display #//////////////////////////////////////////////////////////// def pp_edge(self, edge, width=None): """ @return: A pretty-printed string representation of a given edge in this chart. @rtype: C{string} @param width: The number of characters allotted to each index in the sentence. """ if width is None: width = 50/(self.num_leaves()+1) (start, end) = (edge.start(), edge.end()) str = '|' + ('.'+' '*(width-1))*start # Zero-width edges are "#" if complete, ">" if incomplete if start == end: if edge.is_complete(): str += '#' else: str += '>' # Spanning complete edges are "[===]"; Other edges are # "[---]" if complete, "[--->" if incomplete elif edge.is_complete() and edge.span() == (0,self._num_leaves): str += '['+('='*width)*(end-start-1) + '='*(width-1)+']' elif edge.is_complete(): str += '['+('-'*width)*(end-start-1) + '-'*(width-1)+']' else: str += '['+('-'*width)*(end-start-1) + '-'*(width-1)+'>' str += (' '*(width-1)+'.')*(self._num_leaves-end) return str + '| %s' % edge def pp_leaves(self, width=None): """ @return: A pretty-printed string representation of this chart's leaves. This string can be used as a header for calls to L{pp_edge}. """ if width is None: width = 50/(self.num_leaves()+1) if self._tokens is not None and width>1: header = '|.' for tok in self._tokens: header += tok[:width-1].center(width-1)+'.' header += '|' else: header = '' return header def pp(self, width=None): """ @return: A pretty-printed string representation of this chart. @rtype: C{string} @param width: The number of characters allotted to each index in the sentence. """ if width is None: width = 50/(self.num_leaves()+1) # sort edges: primary key=length, secondary key=start index. # (and filter out the token edges) edges = [(e.length(), e.start(), e) for e in self] edges.sort() edges = [e for (_,_,e) in edges] return (self.pp_leaves(width) + '\n' + '\n'.join(self.pp_edge(edge, width) for edge in edges)) #//////////////////////////////////////////////////////////// # Display: Dot (AT&T Graphviz) #//////////////////////////////////////////////////////////// def dot_digraph(self): # Header s = 'digraph nltk_chart {\n' #s += ' size="5,5";\n' s += ' rankdir=LR;\n' s += ' node [height=0.1,width=0.1];\n' s += ' node [style=filled, color="lightgray"];\n' # Set up the nodes for y in range(self.num_edges(), -1, -1): if y == 0: s += ' node [style=filled, color="black"];\n' for x in range(self.num_leaves()+1): if y == 0 or (x <= self._edges[y-1].start() or x >= self._edges[y-1].end()): s += ' %04d.%04d [label=""];\n' % (x,y) # Add a spacer s += ' x [style=invis]; x->0000.0000 [style=invis];\n' # Declare ranks. for x in range(self.num_leaves()+1): s += ' {rank=same;' for y in range(self.num_edges()+1): if y == 0 or (x <= self._edges[y-1].start() or x >= self._edges[y-1].end()): s += ' %04d.%04d' % (x,y) s += '}\n' # Add the leaves s += ' edge [style=invis, weight=100];\n' s += ' node [shape=plaintext]\n' s += ' 0000.0000' for x in range(self.num_leaves()): s += '->%s->%04d.0000' % (self.leaf(x), x+1) s += ';\n\n' # Add the edges s += ' edge [style=solid, weight=1];\n' for y, edge in enumerate(self): for x in range(edge.start()): s += (' %04d.%04d -> %04d.%04d [style="invis"];\n' % (x, y+1, x+1, y+1)) s += (' %04d.%04d -> %04d.%04d [label="%s"];\n' % (edge.start(), y+1, edge.end(), y+1, edge)) for x in range(edge.end(), self.num_leaves()): s += (' %04d.%04d -> %04d.%04d [style="invis"];\n' % (x, y+1, x+1, y+1)) s += '}\n' return s ######################################################################## ## Chart Rules ######################################################################## class ChartRuleI(object): """ A rule that specifies what new edges are licensed by any given set of existing edges. Each chart rule expects a fixed number of edges, as indicated by the class variable L{NUM_EDGES}. In particular: - A chart rule with C{NUM_EDGES=0} specifies what new edges are licensed, regardless of existing edges. - A chart rule with C{NUM_EDGES=1} specifies what new edges are licensed by a single existing edge. - A chart rule with C{NUM_EDGES=2} specifies what new edges are licensed by a pair of existing edges. @type NUM_EDGES: C{int} @cvar NUM_EDGES: The number of existing edges that this rule uses to license new edges. Typically, this number ranges from zero to two. """ def apply(self, chart, grammar, *edges): """ Add the edges licensed by this rule and the given edges to the chart. @type edges: C{list} of L{EdgeI} @param edges: A set of existing edges. The number of edges that should be passed to C{apply} is specified by the L{NUM_EDGES} class variable. @rtype: C{list} of L{EdgeI} @return: A list of the edges that were added. """ raise AssertionError, 'ChartRuleI is an abstract interface' def apply_iter(self, chart, grammar, *edges): """ @return: A generator that will add edges licensed by this rule and the given edges to the chart, one at a time. Each time the generator is resumed, it will either add a new edge and yield that edge; or return. @rtype: C{iter} of L{EdgeI} @type edges: C{list} of L{EdgeI} @param edges: A set of existing edges. The number of edges that should be passed to C{apply} is specified by the L{NUM_EDGES} class variable. """ raise AssertionError, 'ChartRuleI is an abstract interface' def apply_everywhere(self, chart, grammar): """ Add all the edges licensed by this rule and the edges in the chart to the chart. @rtype: C{list} of L{EdgeI} @return: A list of the edges that were added. """ raise AssertionError, 'ChartRuleI is an abstract interface' def apply_everywhere_iter(self, chart, grammar): """ @return: A generator that will add all edges licensed by this rule, given the edges that are currently in the chart, one at a time. Each time the generator is resumed, it will either add a new edge and yield that edge; or return. @rtype: C{iter} of L{EdgeI} """ raise AssertionError, 'ChartRuleI is an abstract interface' class AbstractChartRule(ChartRuleI): """ An abstract base class for chart rules. C{AbstractChartRule} provides: - A default implementation for C{apply}, based on C{apply_iter}. - A default implementation for C{apply_everywhere_iter}, based on C{apply_iter}. - A default implementation for C{apply_everywhere}, based on C{apply_everywhere_iter}. Currently, this implementation assumes that C{NUM_EDGES}<=3. - A default implementation for C{__str__}, which returns a name basd on the rule's class name. """ # Subclasses must define apply_iter. def apply_iter(self, chart, grammar, *edges): raise AssertionError, 'AbstractChartRule is an abstract class' # Default: loop through the given number of edges, and call # self.apply() for each set of edges. def apply_everywhere_iter(self, chart, grammar): if self.NUM_EDGES == 0: for new_edge in self.apply_iter(chart, grammar): yield new_edge elif self.NUM_EDGES == 1: for e1 in chart: for new_edge in self.apply_iter(chart, grammar, e1): yield new_edge elif self.NUM_EDGES == 2: for e1 in chart: for e2 in chart: for new_edge in self.apply_iter(chart, grammar, e1, e2): yield new_edge elif self.NUM_EDGES == 3: for e1 in chart: for e2 in chart: for e3 in chart: for new_edge in self.apply_iter(chart,grammar,e1,e2,e3): yield new_edge else: raise AssertionError, 'NUM_EDGES>3 is not currently supported' # Default: delegate to apply_iter. def apply(self, chart, grammar, *edges): return list(self.apply_iter(chart, grammar, *edges)) # Default: delegate to apply_everywhere_iter. def apply_everywhere(self, chart, grammar): return list(self.apply_everywhere_iter(chart, grammar)) # Default: return a name based on the class name. def __str__(self): # Add spaces between InitialCapsWords. return re.sub('([a-z])([A-Z])', r'\1 \2', self.__class__.__name__) #//////////////////////////////////////////////////////////// # Fundamental Rule #//////////////////////////////////////////////////////////// class FundamentalRule(AbstractChartRule): """ A rule that joins two adjacent edges to form a single combined edge. In particular, this rule specifies that any pair of edges: - [A S{->} S{alpha} * B S{beta}][i:j] - [B S{->} S{gamma} *][j:k] licenses the edge: - [A S{->} S{alpha} B * S{beta}][i:j] """ NUM_EDGES = 2 def apply_iter(self, chart, grammar, left_edge, right_edge): # Make sure the rule is applicable. if not (left_edge.is_incomplete() and right_edge.is_complete() and left_edge.end() == right_edge.start() and left_edge.next() == right_edge.lhs()): return # Construct the new edge. new_edge = left_edge.move_dot_forward(right_edge.end()) # Insert it into the chart. if chart.insert_with_backpointer(new_edge, left_edge, right_edge): yield new_edge class SingleEdgeFundamentalRule(FundamentalRule): """ A rule that joins a given edge with adjacent edges in the chart, to form combined edges. In particular, this rule specifies that either of the edges: - [A S{->} S{alpha} * B S{beta}][i:j] - [B S{->} S{gamma} *][j:k] licenses the edge: - [A S{->} S{alpha} B * S{beta}][i:j] if the other edge is already in the chart. @note: This is basically L{FundamentalRule}, with one edge left unspecified. """ NUM_EDGES = 1 def apply_iter(self, chart, grammar, edge): if edge.is_incomplete(): for new_edge in self._apply_incomplete(chart, edge): yield new_edge else: for new_edge in self._apply_complete(chart, edge): yield new_edge def _apply_complete(self, chart, right_edge): for left_edge in chart.select(end=right_edge.start(), is_complete=False, next=right_edge.lhs()): new_edge = left_edge.move_dot_forward(right_edge.end()) if chart.insert_with_backpointer(new_edge, left_edge, right_edge): yield new_edge def _apply_incomplete(self, chart, left_edge): for right_edge in chart.select(start=left_edge.end(), is_complete=True, lhs=left_edge.next()): new_edge = left_edge.move_dot_forward(right_edge.end()) if chart.insert_with_backpointer(new_edge, left_edge, right_edge): yield new_edge #//////////////////////////////////////////////////////////// # Inserting Terminal Leafs #//////////////////////////////////////////////////////////// class LeafInitRule(AbstractChartRule): NUM_EDGES=0 def apply_iter(self, chart, grammar): for index in range(chart.num_leaves()): new_edge = LeafEdge(chart.leaf(index), index) if chart.insert(new_edge, ()): yield new_edge #//////////////////////////////////////////////////////////// # Top-Down Prediction #//////////////////////////////////////////////////////////// class TopDownInitRule(AbstractChartRule): """ A rule licensing edges corresponding to the grammar productions for the grammar's start symbol. In particular, this rule specifies that: - [S S{->} * S{alpha}][0:i] is licensed for each grammar production C{S S{->} S{alpha}}, where C{S} is the grammar's start symbol. """ NUM_EDGES = 0 def apply_iter(self, chart, grammar): for prod in grammar.productions(lhs=grammar.start()): new_edge = TreeEdge.from_production(prod, 0) if chart.insert(new_edge, ()): yield new_edge class CachedTopDownInitRule(TopDownInitRule, Deprecated): """Use L{TopDownInitRule} instead.""" class TopDownPredictRule(AbstractChartRule): """ A rule licensing edges corresponding to the grammar productions for the nonterminal following an incomplete edge's dot. In particular, this rule specifies that: - [A S{->} S{alpha} * B S{beta}][i:j] licenses the edge: - [B S{->} * S{gamma}][j:j] for each grammar production C{B S{->} S{gamma}}. @note: This rule corresponds to the Predictor Rule in Earley parsing. """ NUM_EDGES = 1 def apply_iter(self, chart, grammar, edge): if edge.is_complete(): return for prod in grammar.productions(lhs=edge.next()): new_edge = TreeEdge.from_production(prod, edge.end()) if chart.insert(new_edge, ()): yield new_edge class TopDownExpandRule(TopDownPredictRule, Deprecated): """Use TopDownPredictRule instead""" class CachedTopDownPredictRule(TopDownPredictRule): """ A cached version of L{TopDownPredictRule}. After the first time this rule is applied to an edge with a given C{end} and C{next}, it will not generate any more edges for edges with that C{end} and C{next}. If C{chart} or C{grammar} are changed, then the cache is flushed. """ def __init__(self): TopDownPredictRule.__init__(self) self._done = {} def apply_iter(self, chart, grammar, edge): if edge.is_complete(): return next, index = edge.next(), edge.end() if not is_nonterminal(edge.next()): return # If we've already applied this rule to an edge with the same # next & end, and the chart & grammar have not changed, then # just return (no new edges to add). done = self._done.get((next, index), (None,None)) if done[0] is chart and done[1] is grammar: return # Add all the edges indicated by the top down expand rule. for prod in grammar.productions(lhs=next): first = prod.rhs() and prod.rhs()[0] # If the left corner in the predicted production is # leaf, it must match with the input. if is_terminal(first): if index >= chart.num_leaves() or first != chart.leaf(index): continue new_edge = TreeEdge.from_production(prod, index) if chart.insert(new_edge, ()): yield new_edge # Record the fact that we've applied this rule. self._done[next, index] = (chart, grammar) class CachedTopDownExpandRule(CachedTopDownPredictRule, Deprecated): """Use L{CachedTopDownPredictRule} instead.""" #//////////////////////////////////////////////////////////// # Bottom-Up Prediction #//////////////////////////////////////////////////////////// class BottomUpPredictRule(AbstractChartRule): """ A rule licensing any edge corresponding to a production whose right-hand side begins with a complete edge's left-hand side. In particular, this rule specifies that: - [A S{->} S{alpha} *] licenses the edge: - [B S{->} * A S{beta}] for each grammar production C{B S{->} A S{beta}}. """ NUM_EDGES = 1 def apply_iter(self, chart, grammar, edge): if edge.is_incomplete(): return for prod in grammar.productions(rhs=edge.lhs()): new_edge = TreeEdge.from_production(prod, edge.start()) if chart.insert(new_edge, ()): yield new_edge class BottomUpPredictCombineRule(BottomUpPredictRule): """ A rule licensing any edge corresponding to a production whose right-hand side begins with a complete edge's left-hand side. In particular, this rule specifies that: - [A S{->} S{alpha} *] licenses the edge: - [B S{->} A * S{beta}] for each grammar production C{B S{->} A S{beta}}. @note: This is like L{BottomUpPredictRule}, but it also applies the L{FundamentalRule} to the resulting edge. """ NUM_EDGES = 1 def apply_iter(self, chart, grammar, edge): if edge.is_incomplete(): return for prod in grammar.productions(rhs=edge.lhs()): new_edge = TreeEdge(edge.span(), prod.lhs(), prod.rhs(), 1) if chart.insert(new_edge, (edge,)): yield new_edge class EmptyPredictRule(AbstractChartRule): """ A rule that inserts all empty productions as passive edges, in every position in the chart. """ NUM_EDGES = 0 def apply_iter(self, chart, grammar): for prod in grammar.productions(empty=True): for index in xrange(chart.num_leaves() + 1): new_edge = TreeEdge.from_production(prod, index) if chart.insert(new_edge, ()): yield new_edge ######################################################################## ## Filtered Bottom Up ######################################################################## class FilteredSingleEdgeFundamentalRule(SingleEdgeFundamentalRule): def apply_iter(self, chart, grammar, edge): if edge.is_incomplete(): for new_edge in self._apply_incomplete(chart, grammar, edge): yield new_edge else: for new_edge in self._apply_complete(chart, grammar, edge): yield new_edge def _apply_complete(self, chart, grammar, right_edge): end = right_edge.end() nexttoken = end < chart.num_leaves() and chart.leaf(end) for left_edge in chart.select(end=right_edge.start(), is_complete=False, next=right_edge.lhs()): if _bottomup_filter(grammar, nexttoken, left_edge.rhs(), left_edge.dot()): new_edge = left_edge.move_dot_forward(right_edge.end()) if chart.insert_with_backpointer(new_edge, left_edge, right_edge): yield new_edge def _apply_incomplete(self, chart, grammar, left_edge): for right_edge in chart.select(start=left_edge.end(), is_complete=True, lhs=left_edge.next()): end = right_edge.end() nexttoken = end < chart.num_leaves() and chart.leaf(end) if _bottomup_filter(grammar, nexttoken, left_edge.rhs(), left_edge.dot()): new_edge = left_edge.move_dot_forward(right_edge.end()) if chart.insert_with_backpointer(new_edge, left_edge, right_edge): yield new_edge class FilteredBottomUpPredictCombineRule(BottomUpPredictCombineRule): def apply_iter(self, chart, grammar, edge): if edge.is_incomplete(): return leftcorners = grammar.leftcorners end = edge.end() nexttoken = end < chart.num_leaves() and chart.leaf(end) for prod in grammar.productions(rhs=edge.lhs()): if _bottomup_filter(grammar, nexttoken, prod.rhs()): new_edge = TreeEdge(edge.span(), prod.lhs(), prod.rhs(), 1) if chart.insert(new_edge, (edge,)): yield new_edge def _bottomup_filter(grammar, nexttoken, rhs, dot=0): if len(rhs) <= dot + 1: return True next = rhs[dot + 1] if is_terminal(next): return nexttoken == next else: return grammar.is_leftcorner(next, nexttoken) ######################################################################## ## Generic Chart Parser ######################################################################## TD_STRATEGY = [LeafInitRule(), TopDownInitRule(), CachedTopDownPredictRule(), SingleEdgeFundamentalRule()] BU_STRATEGY = [LeafInitRule(), EmptyPredictRule(), BottomUpPredictRule(), SingleEdgeFundamentalRule()] BU_LC_STRATEGY = [LeafInitRule(), EmptyPredictRule(), BottomUpPredictCombineRule(), SingleEdgeFundamentalRule()] LC_STRATEGY = [LeafInitRule(), FilteredBottomUpPredictCombineRule(), FilteredSingleEdgeFundamentalRule()] class ChartParser(ParserI): """ A generic chart parser. A X{strategy}, or list of L{ChartRules}, is used to decide what edges to add to the chart. In particular, C{ChartParser} uses the following algorithm to parse texts: - Until no new edges are added: - For each I{rule} in I{strategy}: - Apply I{rule} to any applicable edges in the chart. - Return any complete parses in the chart """ def __init__(self, grammar, strategy=BU_LC_STRATEGY, trace=0, trace_chart_width=50, use_agenda=True, chart_class=Chart): """ Create a new chart parser, that uses C{grammar} to parse texts. @type grammar: L{ContextFreeGrammar} @param grammar: The grammar used to parse texts. @type strategy: C{list} of L{ChartRuleI} @param strategy: A list of rules that should be used to decide what edges to add to the chart (top-down strategy by default). @type trace: C{int} @param trace: The level of tracing that should be used when parsing a text. C{0} will generate no tracing output; and higher numbers will produce more verbose tracing output. @type trace_chart_width: C{int} @param trace_chart_width: The default total width reserved for the chart in trace output. The remainder of each line will be used to display edges. @type use_agenda: C{bool} @param use_agenda: Use an optimized agenda-based algorithm, if possible. @param chart_class: The class that should be used to create the parse charts. """ self._grammar = grammar self._strategy = strategy self._trace = trace self._trace_chart_width = trace_chart_width # If the strategy only consists of axioms (NUM_EDGES==0) and # inference rules (NUM_EDGES==1), we can use an agenda-based algorithm: self._use_agenda = use_agenda self._chart_class = chart_class self._axioms = [] self._inference_rules = [] for rule in strategy: if rule.NUM_EDGES == 0: self._axioms.append(rule) elif rule.NUM_EDGES == 1: self._inference_rules.append(rule) else: self._use_agenda = False def grammar(self): return self._grammar def _trace_new_edges(self, chart, rule, new_edges, trace, edge_width): if not trace: return should_print_rule_header = trace > 1 for edge in new_edges: if should_print_rule_header: print '%s:' % rule should_print_rule_header = False print chart.pp_edge(edge, edge_width) def chart_parse(self, tokens, trace=None): """ @return: The final parse L{Chart}, from which all possible parse trees can be extracted. @param tokens: The sentence to be parsed @type tokens: L{list} of L{string} @rtype: L{Chart} """ if trace is None: trace = self._trace trace_new_edges = self._trace_new_edges tokens = list(tokens) self._grammar.check_coverage(tokens) chart = self._chart_class(tokens) grammar = self._grammar # Width, for printing trace edges. trace_edge_width = self._trace_chart_width / (chart.num_leaves() + 1) if trace: print chart.pp_leaves(trace_edge_width) if self._use_agenda: # Use an agenda-based algorithm. for axiom in self._axioms: new_edges = axiom.apply(chart, grammar) trace_new_edges(chart, axiom, new_edges, trace, trace_edge_width) inference_rules = self._inference_rules agenda = chart.edges() # We reverse the initial agenda, since it is a stack # but chart.edges() functions as a queue. agenda.reverse() while agenda: edge = agenda.pop() for rule in inference_rules: new_edges = rule.apply_iter(chart, grammar, edge) if trace: new_edges = list(new_edges) trace_new_edges(chart, rule, new_edges, trace, trace_edge_width) agenda += new_edges else: # Do not use an agenda-based algorithm. edges_added = True while edges_added: edges_added = False for rule in self._strategy: new_edges = rule.apply_everywhere(chart, grammar) edges_added = len(new_edges) trace_new_edges(chart, rule, new_edges, trace, trace_edge_width) # Return the final chart. return chart def nbest_parse(self, tokens, n=None, tree_class=Tree): chart = self.chart_parse(tokens) # Return a list of complete parses. return chart.parses(self._grammar.start(), tree_class=tree_class)[:n] class TopDownChartParser(ChartParser): """ A L{ChartParser} using a top-down parsing strategy. See L{ChartParser} for more information. """ def __init__(self, grammar, **parser_args): ChartParser.__init__(self, grammar, TD_STRATEGY, **parser_args) class BottomUpChartParser(ChartParser): """ A L{ChartParser} using a bottom-up parsing strategy. See L{ChartParser} for more information. """ def __init__(self, grammar, **parser_args): if isinstance(grammar, WeightedGrammar): warnings.warn("BottomUpChartParser only works for ContextFreeGrammar, " "use BottomUpProbabilisticChartParser instead", category=DeprecationWarning) ChartParser.__init__(self, grammar, BU_STRATEGY, **parser_args) class BottomUpLeftCornerChartParser(ChartParser): """ A L{ChartParser} using a bottom-up left-corner parsing strategy. This strategy is often more efficient than standard bottom-up. See L{ChartParser} for more information. """ def __init__(self, grammar, **parser_args): ChartParser.__init__(self, grammar, BU_LC_STRATEGY, **parser_args) class LeftCornerChartParser(ChartParser): def __init__(self, grammar, **parser_args): if not grammar.is_nonempty(): raise ValueError("LeftCornerParser only works for grammars " "without empty productions.") ChartParser.__init__(self, grammar, LC_STRATEGY, **parser_args) ######################################################################## ## Stepping Chart Parser ######################################################################## class SteppingChartParser(ChartParser): """ A C{ChartParser} that allows you to step through the parsing process, adding a single edge at a time. It also allows you to change the parser's strategy or grammar midway through parsing a text. The C{initialize} method is used to start parsing a text. C{step} adds a single edge to the chart. C{set_strategy} changes the strategy used by the chart parser. C{parses} returns the set of parses that has been found by the chart parser. @ivar _restart: Records whether the parser's strategy, grammar, or chart has been changed. If so, then L{step} must restart the parsing algorithm. """ def __init__(self, grammar, strategy=[], trace=0): self._chart = None self._current_chartrule = None self._restart = False ChartParser.__init__(self, grammar, strategy, trace) #//////////////////////////////////////////////////////////// # Initialization #//////////////////////////////////////////////////////////// def initialize(self, tokens): "Begin parsing the given tokens." self._chart = Chart(list(tokens)) self._restart = True #//////////////////////////////////////////////////////////// # Stepping #//////////////////////////////////////////////////////////// def step(self): """ @return: A generator that adds edges to the chart, one at a time. Each time the generator is resumed, it adds a single edge and yields that edge. If no more edges can be added, then it yields C{None}. If the parser's strategy, grammar, or chart is changed, then the generator will continue adding edges using the new strategy, grammar, or chart. Note that this generator never terminates, since the grammar or strategy might be changed to values that would add new edges. Instead, it yields C{None} when no more edges can be added with the current strategy and grammar. """ if self._chart is None: raise ValueError, 'Parser must be initialized first' while 1: self._restart = False w = 50/(self._chart.num_leaves()+1) for e in self._parse(): if self._trace > 1: print self._current_chartrule if self._trace > 0: print self._chart.pp_edge(e,w) yield e if self._restart: break else: yield None # No more edges. def _parse(self): """ A generator that implements the actual parsing algorithm. L{step} iterates through this generator, and restarts it whenever the parser's strategy, grammar, or chart is modified. """ chart = self._chart grammar = self._grammar edges_added = 1 while edges_added > 0: edges_added = 0 for rule in self._strategy: self._current_chartrule = rule for e in rule.apply_everywhere_iter(chart, grammar): edges_added += 1 yield e #//////////////////////////////////////////////////////////// # Accessors #//////////////////////////////////////////////////////////// def strategy(self): "@return: The strategy used by this parser." return self._strategy def grammar(self): "@return: The grammar used by this parser." return self._grammar def chart(self): "@return: The chart that is used by this parser." return self._chart def current_chartrule(self): "@return: The chart rule used to generate the most recent edge." return self._current_chartrule def parses(self, tree_class=Tree): "@return: The parse trees currently contained in the chart." return self._chart.parses(self._grammar.start(), tree_class) #//////////////////////////////////////////////////////////// # Parser modification #//////////////////////////////////////////////////////////// def set_strategy(self, strategy): """ Change the startegy that the parser uses to decide which edges to add to the chart. @type strategy: C{list} of L{ChartRuleI} @param strategy: A list of rules that should be used to decide what edges to add to the chart. """ if strategy == self._strategy: return self._strategy = strategy[:] # Make a copy. self._restart = True def set_grammar(self, grammar): "Change the grammar used by the parser." if grammar is self._grammar: return self._grammar = grammar self._restart = True def set_chart(self, chart): "Load a given chart into the chart parser." if chart is self._chart: return self._chart = chart self._restart = True #//////////////////////////////////////////////////////////// # Standard parser methods #//////////////////////////////////////////////////////////// def nbest_parse(self, tokens, n=None, tree_class=Tree): tokens = list(tokens) self._grammar.check_coverage(tokens) # Initialize ourselves. self.initialize(tokens) # Step until no more edges are generated. for e in self.step(): if e is None: break # Return a list of complete parses. return self.parses(tree_class=tree_class)[:n] ######################################################################## ## Demo Code ######################################################################## def demo_grammar(): from nltk.grammar import parse_cfg return parse_cfg(""" S -> NP VP PP -> "with" NP NP -> NP PP VP -> VP PP VP -> Verb NP VP -> Verb NP -> Det Noun NP -> "John" NP -> "I" Det -> "the" Det -> "my" Det -> "a" Noun -> "dog" Noun -> "cookie" Verb -> "ate" Verb -> "saw" Prep -> "with" Prep -> "under" """) def demo(choice=None, should_print_times=True, should_print_grammar=False, should_print_trees=True, trace=2, sent='I saw John with a dog with my cookie', numparses=5): """ A demonstration of the chart parsers. """ import sys, time from nltk import nonterminals, Production, ContextFreeGrammar # The grammar for ChartParser and SteppingChartParser: grammar = demo_grammar() if should_print_grammar: print "* Grammar" print grammar # Tokenize the sample sentence. print "* Sentence:" print sent tokens = sent.split() print tokens print # Ask the user which parser to test, # if the parser wasn't provided as an argument if choice is None: print ' 1: Top-down chart parser' print ' 2: Bottom-up chart parser' print ' 3: Bottom-up left-corner chart parser' print ' 4: Left-corner chart parser with bottom-up filter' print ' 5: Stepping chart parser (alternating top-down & bottom-up)' print ' 6: All parsers' print '\nWhich parser (1-6)? ', choice = sys.stdin.readline().strip() print choice = str(choice) if choice not in "123456": print 'Bad parser number' return # Keep track of how long each parser takes. times = {} strategies = {'1': ('Top-down', TD_STRATEGY), '2': ('Bottom-up', BU_STRATEGY), '3': ('Bottom-up left-corner', BU_LC_STRATEGY), '4': ('Filtered left-corner', LC_STRATEGY)} choices = [] if strategies.has_key(choice): choices = [choice] if choice=='6': choices = "1234" # Run the requested chart parser(s), except the stepping parser. for strategy in choices: print "* Strategy: " + strategies[strategy][0] print cp = ChartParser(grammar, strategies[strategy][1], trace=trace) t = time.time() chart = cp.chart_parse(tokens) parses = chart.parses(grammar.start()) times[strategies[strategy][0]] = time.time()-t print "Nr edges in chart:", len(chart.edges()) if numparses: assert len(parses)==numparses, 'Not all parses found' if should_print_trees: for tree in parses: print tree else: print "Nr trees:", len(parses) print # Run the stepping parser, if requested. if choice in "56": print "* Strategy: Stepping (top-down vs bottom-up)" print t = time.time() cp = SteppingChartParser(grammar, trace=trace) cp.initialize(tokens) for i in range(5): print '*** SWITCH TO TOP DOWN' cp.set_strategy(TD_STRATEGY) for j, e in enumerate(cp.step()): if j>20 or e is None: break print '*** SWITCH TO BOTTOM UP' cp.set_strategy(BU_STRATEGY) for j, e in enumerate(cp.step()): if j>20 or e is None: break times['Stepping'] = time.time()-t print "Nr edges in chart:", len(cp.chart().edges()) if numparses: assert len(cp.parses())==numparses, 'Not all parses found' if should_print_trees: for tree in cp.parses(): print tree else: print "Nr trees:", len(cp.parses()) print # Print the times of all parsers: if not (should_print_times and times): return print "* Parsing times" print maxlen = max(len(key) for key in times.keys()) format = '%' + `maxlen` + 's parser: %6.3fsec' times_items = times.items() times_items.sort(lambda a,b:cmp(a[1], b[1])) for (parser, t) in times_items: print format % (parser, t) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/parse/api.py0000644000175000017500000001533011327451577016001 0ustar bhavanibhavani# Natural Language Toolkit: Parser API # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT # import itertools from nltk.internals import deprecated, Deprecated, overridden class ParserI(object): """ A processing class for deriving trees that represent possible structures for a sequence of tokens. These tree structures are known as X{parses}. Typically, parsers are used to derive syntax trees for sentences. But parsers can also be used to derive other kinds of tree structure, such as morphological trees and discourse structures. Subclasses must define: - at least one of: L{parse()}, L{nbest_parse()}, L{iter_parse()}, L{batch_parse()}, L{batch_nbest_parse()}, L{batch_iter_parse()}. Subclasses may define: - L{grammar()} - either L{prob_parse()} or L{batch_prob_parse()} (or both) """ def grammar(self): """ @return: The grammar used by this parser. """ raise NotImplementedError() def parse(self, sent): """ @return: A parse tree that represents the structure of the given sentence, or C{None} if no parse tree is found. If multiple parses are found, then return the best parse. @param sent: The sentence to be parsed @type sent: L{list} of L{string} @rtype: L{Tree} """ if overridden(self.batch_parse): return self.batch_parse([sent])[0] else: trees = self.nbest_parse(sent, 1) if trees: return trees[0] else: return None def nbest_parse(self, sent, n=None): """ @return: A list of parse trees that represent possible structures for the given sentence. When possible, this list is sorted from most likely to least likely. If C{n} is specified, then the returned list will contain at most C{n} parse trees. @param sent: The sentence to be parsed @type sent: L{list} of L{string} @param n: The maximum number of trees to return. @type n: C{int} @rtype: C{list} of L{Tree} """ if overridden(self.batch_nbest_parse): return self.batch_nbest_parse([sent],n)[0] elif overridden(self.parse) or overridden(self.batch_parse): tree = self.parse(sent) if tree: return [tree] else: return [] else: return list(itertools.islice(self.iter_parse(sent), n)) def iter_parse(self, sent): """ @return: An iterator that generates parse trees that represent possible structures for the given sentence. When possible, this list is sorted from most likely to least likely. @param sent: The sentence to be parsed @type sent: L{list} of L{string} @rtype: C{iterator} of L{Tree} """ if overridden(self.batch_iter_parse): return self.batch_iter_parse([sent])[0] elif overridden(self.nbest_parse) or overridden(self.batch_nbest_parse): return iter(self.nbest_parse(sent)) elif overridden(self.parse) or overridden(self.batch_parse): tree = self.parse(sent) if tree: return iter([tree]) else: return iter([]) else: raise NotImplementedError() def prob_parse(self, sent): """ @return: A probability distribution over the possible parse trees for the given sentence. If there are no possible parse trees for the given sentence, return a probability distribution that assigns a probability of 1.0 to C{None}. @param sent: The sentence to be parsed @type sent: L{list} of L{string} @rtype: L{ProbDistI} of L{Tree} """ if overridden(self.batch_prob_parse): return self.batch_prob_parse([sent])[0] else: raise NotImplementedError def batch_parse(self, sents): """ Apply L{self.parse()} to each element of C{sents}. I.e.: >>> return [self.parse(sent) for sent in sents] @rtype: C{list} of L{Tree} """ return [self.parse(sent) for sent in sents] def batch_nbest_parse(self, sents, n=None): """ Apply L{self.nbest_parse()} to each element of C{sents}. I.e.: >>> return [self.nbest_parse(sent, n) for sent in sents] @rtype: C{list} of C{list} of L{Tree} """ return [self.nbest_parse(sent,n ) for sent in sents] def batch_iter_parse(self, sents): """ Apply L{self.iter_parse()} to each element of C{sents}. I.e.: >>> return [self.iter_parse(sent) for sent in sents] @rtype: C{list} of C{iterator} of L{Tree} """ return [self.iter_parse(sent) for sent in sents] def batch_prob_parse(self, sents): """ Apply L{self.prob_parse()} to each element of C{sents}. I.e.: >>> return [self.prob_parse(sent) for sent in sents] @rtype: C{list} of L{ProbDistI} of L{Tree} """ return [self.prob_parse(sent) for sent in sents] #//////////////////////////////////////////////////////////// #{ Deprecated @deprecated("Use parse() instead.") def get_parse(self, sent): return self.parse(sent) @deprecated("Use nbest_parse() instead.") def get_parse_list(self, sent): return self.nbest_parse(sent) @deprecated("Use prob_parse() instead.") def get_parse_prob(self, sent): return self.prob_parse(sent) @deprecated("Use prob_parse() instead.") def get_parse_dict(self, sent): return self.prob_parse(sent) @deprecated("No longer supported.") def batch_test(self, filename): f = open(filename) for line in f: line = line.strip() if not line: continue if line.startswith('#'): print line continue print "Sentence:", line parses = self.nbest_parse(line) print "%d parses." % len(parses) for tree in parses: print tree #} #//////////////////////////////////////////////////////////// ###################################################################### #{ Deprecated class ParseI(ParserI, Deprecated): "Use ParserI instead." class AbstractParser(Deprecated, ParserI): """Use ParserI instead.""" @deprecated("Use nltk.ContextFreeGrammar.check_coverage() instead.") def _check_coverage(self, tokens): self._grammar.check_coverage(tokens) #} ###################################################################### nltk-2.0~b9/nltk/parse/__init__.py0000644000175000017500000001327711327451577016777 0ustar bhavanibhavani# Natural Language Toolkit: Parsers # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT # """ Classes and interfaces for producing tree structures that represent the internal organization of a text. This task is known as X{parsing} the text, and the resulting tree structures are called the text's X{parses}. Typically, the text is a single sentence, and the tree structure represents the syntactic structure of the sentence. However, parsers can also be used in other domains. For example, parsers can be used to derive the morphological structure of the morphemes that make up a word, or to derive the discourse structure for a set of utterances. Sometimes, a single piece of text can be represented by more than one tree structure. Texts represented by more than one tree structure are called X{ambiguous} texts. Note that there are actually two ways in which a text can be ambiguous: - The text has multiple correct parses. - There is not enough information to decide which of several candidate parses is correct. However, the parser module does I{not} distinguish these two types of ambiguity. The parser module defines C{ParserI}, a standard interface for parsing texts; and two simple implementations of that interface, C{ShiftReduceParser} and C{RecursiveDescentParser}. It also contains three sub-modules for specialized kinds of parsing: - C{nltk.parser.chart} defines chart parsing, which uses dynamic programming to efficiently parse texts. - C{nltk.parser.probabilistic} defines probabilistic parsing, which associates a probability with each parse. """ from api import * from chart import * from featurechart import * from earleychart import * from pchart import * from rd import * from sr import * from util import * from viterbi import * from dependencygraph import * from projectivedependencyparser import * from nonprojectivedependencyparser import * from malt import * __all__ = [ # Parser interface 'ParserI', # Generic parser loading function 'load_parser', # Parsers # from rd.py: 'RecursiveDescentParser', 'SteppingRecursiveDescentParser', # from sr.py: 'ShiftReduceParser', 'SteppingShiftReduceParser', # from chart.py: 'ChartParser', 'SteppingChartParser', 'TopDownChartParser', 'BottomUpChartParser', 'BottomUpLeftCornerChartParser', 'LeftCornerChartParser', # from pchart.py: 'BottomUpProbabilisticChartParser', 'InsideChartParser', 'RandomChartParser', 'UnsortedChartParser', 'LongestChartParser', 'ViterbiParser', # from featurechart.py: 'FeatureChartParser', 'FeatureTopDownChartParser', 'FeatureBottomUpChartParser', 'FeatureBottomUpLeftCornerChartParser', # from earleychart.py: 'IncrementalChartParser', 'EarleyChartParser', 'IncrementalTopDownChartParser', 'IncrementalBottomUpChartParser', 'IncrementalBottomUpLeftCornerChartParser', 'IncrementalLeftCornerChartParser', 'FeatureIncrementalChartParser', 'FeatureEarleyChartParser', 'FeatureIncrementalTopDownChartParser', 'FeatureIncrementalBottomUpChartParser', 'FeatureIncrementalBottomUpLeftCornerChartParser', # from dependencygraph.py, projectivedependencyparser.py, # projectivedependencyparser.py, malt.py: 'DependencyGraph', 'nx_graph', 'ProjectiveDependencyParser', 'ProbabilisticProjectiveDependencyParser', 'NaiveBayesDependencyScorer', 'ProbabilisticNonprojectiveParser', 'NonprojectiveDependencyParser', 'MaltParser', ] ###################################################################### #{ Deprecated ###################################################################### from nltk.internals import Deprecated class ParseI(ParserI, Deprecated): """Use nltk.ParserI instead.""" class AbstractParse(AbstractParser, Deprecated): """Use nltk.ParserI instead.""" class RecursiveDescent(RecursiveDescentParser, Deprecated): """Use nltk.RecursiveDescentParser instead.""" class SteppingRecursiveDescent(SteppingRecursiveDescentParser, Deprecated): """Use nltk.SteppingRecursiveDescentParser instead.""" class ShiftReduce(ShiftReduceParser, Deprecated): """Use nltk.ShiftReduceParser instead.""" class SteppingShiftReduce(SteppingShiftReduceParser, Deprecated): """Use nltk.SteppingShiftReduceParser instead.""" class EarleyChartParse(EarleyChartParser, Deprecated): """Use nltk.EarleyChartParser instead.""" class FeatureEarleyChartParse(FeatureEarleyChartParser, Deprecated): """Use nltk.FeatureEarleyChartParser instead.""" class ChartParse(ChartParser, Deprecated): """Use nltk.ChartParser instead.""" class SteppingChartParse(SteppingChartParser, Deprecated): """Use nltk.SteppingChartParser instead.""" class BottomUpChartParse(BottomUpProbabilisticChartParser, Deprecated): """Use nltk.BottomUpProbabilisticChartParser instead.""" class InsideParse(InsideChartParser, Deprecated): """Use nltk.InsideChartParser instead.""" class RandomParse(RandomChartParser, Deprecated): """Use nltk.RandomChartParser instead.""" class UnsortedParse(UnsortedChartParser, Deprecated): """Use nltk.UnsortedChartParser instead.""" class LongestParse(LongestChartParser, Deprecated): """Use nltk.LongestChartParser instead.""" class ViterbiParse(ViterbiParser, Deprecated): """Use nltk.ViterbiParser instead.""" class GrammarFile(Deprecated): """Use nltk.data.load() instead.""" # [xx] had directives: %start, %kimmo, %tagger_file? def __init__(self, filename=None, verbose=False): raise ValueError("GrammarFile is no longer supported -- " "use nltk.data.load() instead.") nltk-2.0~b9/nltk/model/ngram.py0000644000175000017500000001175611414551264016321 0ustar bhavanibhavani# Natural Language Toolkit: Language Models # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT import random from itertools import chain from math import log from nltk.probability import (ConditionalProbDist, ConditionalFreqDist, MLEProbDist) from nltk.util import ingrams from api import * def _estimator(fdist, bins): """ Default estimator function using an MLEProbDist. """ # can't be an instance method of NgramModel as they # can't be pickled either. return MLEProbDist(fdist) class NgramModel(ModelI): """ A processing interface for assigning a probability to the next word. """ # add cutoff def __init__(self, n, train, estimator=None): """ Creates an ngram language model to capture patterns in n consecutive words of training text. An estimator smooths the probabilities derived from the text and may allow generation of ngrams not seen during training. @param n: the order of the language model (ngram size) @type n: C{int} @param train: the training text @type train: C{list} of C{string} @param estimator: a function for generating a probability distribution @type estimator: a function that takes a C{ConditionalFreqDist} and returns a C{ConditionalProbDist} """ self._n = n if estimator is None: estimator = _estimator cfd = ConditionalFreqDist() self._ngrams = set() self._prefix = ('',) * (n - 1) for ngram in ingrams(chain(self._prefix, train), n): self._ngrams.add(ngram) context = tuple(ngram[:-1]) token = ngram[-1] cfd[context].inc(token) self._model = ConditionalProbDist(cfd, estimator, len(cfd)) # recursively construct the lower-order models if n > 1: self._backoff = NgramModel(n-1, train, estimator) # Katz Backoff probability def prob(self, word, context): """ Evaluate the probability of this word in this context. """ context = tuple(context) if context + (word,) in self._ngrams: return self[context].prob(word) elif self._n > 1: return self._alpha(context) * self._backoff.prob(word, context[1:]) else: raise RuntimeError("No probability mass assigned to word %s in " + "context %s" % (word, ' '.join(context))) def _alpha(self, tokens): return self._beta(tokens) / self._backoff._beta(tokens[1:]) def _beta(self, tokens): if tokens in self: return self[tokens].discount() else: return 1 def logprob(self, word, context): """ Evaluate the (negative) log probability of this word in this context. """ return -log(self.prob(word, context), 2) def choose_random_word(self, context): '''Randomly select a word that is likely to appear in this context.''' return self.generate(1, context)[-1] # NB, this will always start with same word since model # is trained on a single text def generate(self, num_words, context=()): '''Generate random text based on the language model.''' text = list(context) for i in range(num_words): text.append(self._generate_one(text)) return text def _generate_one(self, context): context = (self._prefix + tuple(context))[-self._n+1:] # print "Context (%d): <%s>" % (self._n, ','.join(context)) if context in self: return self[context].generate() elif self._n > 1: return self._backoff._generate_one(context[1:]) else: return '.' def entropy(self, text): """ Evaluate the total entropy of a text with respect to the model. This is the sum of the log probability of each word in the message. """ e = 0.0 for i in range(self._n - 1, len(text)): context = tuple(text[i - self._n + 1 : i - 1]) token = text[i] e += self.logprob(token, context) return e def __contains__(self, item): return tuple(item) in self._model def __getitem__(self, item): return self._model[tuple(item)] def __repr__(self): return '' % (len(self._ngrams), self._n) def demo(): from nltk.corpus import brown from nltk.probability import LidstoneProbDist, WittenBellProbDist estimator = lambda fdist, bins: LidstoneProbDist(fdist, 0.2) # estimator = lambda fdist, bins: WittenBellProbDist(fdist, 0.2) lm = NgramModel(3, brown.words(categories='news'), estimator) print lm # print lm.entropy(sent) text = lm.generate(100) import textwrap print '\n'.join(textwrap.wrap(' '.join(text))) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/model/api.py0000644000175000017500000000241711331672172015760 0ustar bhavanibhavani# Natural Language Toolkit: API for Language Models # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT # should this be a subclass of ConditionalProbDistI? class ModelI(object): """ A processing interface for assigning a probability to the next word. """ def __init__(self): '''Create a new language model.''' raise NotImplementedError() def prob(self, word, context): '''Evaluate the probability of this word in this context.''' raise NotImplementedError() def logprob(self, word, context): '''Evaluate the (negative) log probability of this word in this context.''' raise NotImplementedError() def choose_random_word(self, context): '''Randomly select a word that is likely to appear in this context.''' raise NotImplementedError() def generate(self, n): '''Generate n words of text from the language model.''' raise NotImplementedError() def entropy(self, text): '''Evaluate the total entropy of a message with respect to the model. This is the sum of the log probability of each word in the message.''' raise NotImplementedError() nltk-2.0~b9/nltk/model/__init__.py0000644000175000017500000000040711327451576016753 0ustar bhavanibhavani# Natural Language Toolkit: Language Models # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT from ngram import * __all__ = [ 'NgramModel', ] nltk-2.0~b9/nltk/misc/wordfinder.py0000644000175000017500000000777111327451602017213 0ustar bhavanibhavani# Natural Language Toolkit: Word Finder # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT # Simplified from PHP version by Robert Klein # http://fswordfinder.sourceforge.net/ import random from string import strip # reverse a word with probability 0.5 def revword(word): if random.randint(1,2) == 1: return word[::-1] return word # try to insert word at position x,y; direction encoded in xf,yf def step(word, x, xf, y, yf, grid): for i in range(len(word)): if grid[xf(i)][yf(i)] != "" and grid[xf(i)][yf(i)] != word[i]: return False for i in range(len(word)): grid[xf(i)][yf(i)] = word[i] return True # try to insert word at position x,y, in direction dir def check(word, dir, x, y, grid, rows, cols): if dir==1: if x-len(word)<0 or y-len(word)<0: return False return step(word, x, lambda i:x-i, y, lambda i:y-i, grid) elif dir==2: if x-len(word)<0: return False return step(word, x, lambda i:x-i, y, lambda i:y, grid) elif dir==3: if x-len(word)<0 or y+(len(word)-1)>=cols: return False return step(word, x, lambda i:x-i, y, lambda i:y+i, grid) elif dir==4: if y-len(word)<0: return False return step(word, x, lambda i:x, y, lambda i:y-i, grid) def wordfinder(words, rows=20, cols=20, attempts=50, alph='ABCDEFGHIJKLMNOPQRSTUVWXYZ'): """ Attempt to arrange words into a letter-grid with the specified number of rows and columns. Try each word in several positions and directions, until it can be fitted into the grid, or the maximum number of allowable attempts is exceeded. Returns a tuple consisting of the grid and the words that were successfully placed. @param words: the list of words to be put into the grid @type words: C(list) @param rows: the number of rows in the grid @type rows: C(int) @param cols: the number of columns in the grid @type cols: C(int) @param attempts: the number of times to attempt placing a word @type attempts: C(int) @param alph: the alpabet, to be used for filling blank cells @type alph: C(list) @rtype: C(tuple) """ # place longer words first words.sort(cmp=lambda x,y:cmp(len(x),len(y)), reverse=True) grid = [] # the letter grid used = [] # the words we used # initialize the grid for i in range(rows): grid.append([""] * cols) # try to place each word for word in words: word = strip(word).upper() # normalize save = word # keep a record of the word word = revword(word) for attempt in range(attempts): r = random.randint(0, len(word)) dir = random.choice([1,2,3,4]) x = random.randint(0,rows) y = random.randint(0,cols) if dir==1: x+=r; y+=r elif dir==2: x+=r elif dir==3: x+=r; y-=r elif dir==4: y+=r if 0<=x # URL: # For license information, see LICENSE.TXT """ This module provides a variety of list sorting algorithms, to illustrate the many different algorithms (recipes) for solving a problem, and how to analyze algorithms experimentally. """ # These algorithms are taken from: # Levitin (2004) The Design and Analysis of Algorithms ################################################################## # Selection Sort ################################################################## def selection(a): """ Selection Sort: scan the list to find its smallest element, then swap it with the first element. The remainder of the list is one element smaller; apply the same method to this list, and so on. """ count = 0 for i in range(len(a) - 1): min = i for j in range(i+1, len(a)): if a[j] < a[min]: min = j count += 1 a[min],a[i] = a[i],a[min] return count ################################################################## # Bubble Sort ################################################################## def bubble(a): """ Bubble Sort: compare adjacent elements of the list left-to-right, and swap them if they are out of order. After one pass through the list swapping adjacent items, the largest item will be in the rightmost position. The remainder is one element smaller; apply the same method to this list, and so on. """ count = 0 for i in range(len(a)-1): for j in range(len(a)-i-1): if a[j+1] < a[j]: a[j],a[j+1] = a[j+1],a[j] count += 1 return count ################################################################## # Merge Sort ################################################################## def _merge_lists(b, c): count = 0 i = j = 0 a = [] while (i < len(b) and j < len(c)): count += 1 if b[i] <= c[j]: a.append(b[i]) i += 1 else: a.append(c[j]) j += 1 if i == len(b): a += c[j:] else: a += b[i:] return a, count def merge(a): """ Merge Sort: split the list in half, and sort each half, then combine the sorted halves. """ count = 0 if len(a) > 1: midpoint = len(a)/2 b = a[:midpoint] c = a[midpoint:] count_b = merge(b) count_c = merge(c) result, count_a = _merge_lists(b, c) a[:] = result # copy the result back into a. count = count_a + count_b + count_c return count ################################################################## # Quick Sort ################################################################## def _partition(a, l, r): p = a[l]; i = l; j = r+1 count = 0 while True: while i < r: i += 1 if a[i] >= p: break while j > l: j -= 1 if j < l or a[j] <= p: break a[i],a[j] = a[j],a[i] # swap count += 1 if i >= j: break a[i],a[j] = a[j],a[i] # undo last swap a[l],a[j] = a[j],a[l] return j, count def _quick(a, l, r): count = 0 if l # URL: # For license information, see LICENSE.TXT class MinimalSet(object): """ Find contexts where more than one possible target value can appear. E.g. if targets are word-initial letters, and contexts are the remainders of words, then we would like to find cases like "fat" vs "cat", and "training" vs "draining". If targets are parts-of-speech and contexts are words, then we would like to find cases like wind (noun) 'air in rapid motion', vs wind (verb) 'coil, wrap'. """ def __init__(self, parameters=None): """ Create a new minimal set. @param parameters: The (context, target, display) tuples for the item @type parameters: C{list} of C{tuple} of C{string} """ self._targets = set() # the contrastive information self._contexts = set() # what we are controlling for self._seen = defaultdict(set) # to record what we have seen self._displays = {} # what we will display if parameters: for context, target, display in parameters: self.add(context, target, display) def add(self, context, target, display): """ Add a new item to the minimal set, having the specified context, target, and display form. @param context: The context in which the item of interest appears @type context: C{string} @param target: The item of interest @type target: C{string} @param display: The information to be reported for each item @type display: C{string} """ # Store the set of targets that occurred in this context self._seen[context].add(target) # Keep track of which contexts and targets we have seen self._contexts.add(context) self._targets.add(target) # For a given context and target, store the display form self._displays[(context, target)] = display def contexts(self, minimum=2): """ Determine which contexts occurred with enough distinct targets. @param minimum: the minimum number of distinct target forms @type minimum: C{int} @rtype C{list} """ return [c for c in self._contexts if len(self._seen[c]) >= minimum] def display(self, context, target, default=""): if (context, target) in self._displays: return self._displays[(context, target)] else: return default def display_all(self, context): result = [] for target in self._targets: x = self.display(context, target) if x: result.append(x) return result def targets(self): return self._targets nltk-2.0~b9/nltk/misc/chomsky.py0000644000175000017500000001212711140171432016505 0ustar bhavanibhavani# Chomsky random text generator, version 1.1, Raymond Hettinger, 2005/09/13 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440546 import string """CHOMSKY is an aid to writing linguistic papers in the style of the great master. It is based on selected phrases taken from actual books and articles written by Noam Chomsky. Upon request, it assembles the phrases in the elegant stylistic patterns that Chomsky is noted for. To generate n sentences of linguistic wisdom, type (CHOMSKY n) -- for example (CHOMSKY 5) generates half a screen of linguistic truth.""" leadins = """To characterize a linguistic level L, On the other hand, This suggests that It appears that Furthermore, We will bring evidence in favor of the following thesis: To provide a constituent structure for T(Z,K), From C1, it follows that For any transformation which is sufficiently diversified in \ application to be of any interest, Analogously, Clearly, Note that Of course, Suppose, for instance, that Thus With this clarification, Conversely, We have already seen that By combining adjunctions and certain deformations, I suggested that these results would follow from the assumption that If the position of the trace in (99c) were only relatively \ inaccessible to movement, However, this assumption is not correct, since Comparing these examples with their parasitic gap counterparts in \ (96) and (97), we see that In the discussion of resumptive pronouns following (81), So far, Nevertheless, For one thing, Summarizing, then, we assume that A consequence of the approach just outlined is that Presumably, On our assumptions, It may be, then, that It must be emphasized, once again, that Let us continue to suppose that Notice, incidentally, that """ # List of LEADINs to buy time. subjects = """ the notion of level of grammaticalness a case of semigrammaticalness of a different sort most of the methodological work in modern linguistics a subset of English sentences interesting on quite independent grounds the natural general principle that will subsume this case an important property of these three types of EC any associated supporting element the appearance of parasitic gaps in domains relatively inaccessible \ to ordinary extraction the speaker-hearer's linguistic intuition the descriptive power of the base component the earlier discussion of deviance this analysis of a formative as a pair of sets of features this selectionally introduced contextual feature a descriptively adequate grammar the fundamental error of regarding functional notions as categorial relational information the systematic use of complex symbols the theory of syntactic features developed earlier""" # List of SUBJECTs chosen for maximum professorial macho. verbs = """can be defined in such a way as to impose delimits suffices to account for cannot be arbitrary in is not subject to does not readily tolerate raises serious doubts about is not quite equivalent to does not affect the structure of may remedy and, at the same time, eliminate is not to be considered in determining is to be regarded as is unspecified with respect to is, apparently, determined by is necessary to impose an interpretation on appears to correlate rather closely with is rather different from""" #List of VERBs chosen for autorecursive obfuscation. objects = """ problems of phonemic and morphological analysis. a corpus of utterance tokens upon which conformity has been defined \ by the paired utterance test. the traditional practice of grammarians. the levels of acceptability from fairly high (e.g. (99a)) to virtual \ gibberish (e.g. (98d)). a stipulation to place the constructions into these various categories. a descriptive fact. a parasitic gap construction. the extended c-command discussed in connection with (34). the ultimate standard that determines the accuracy of any proposed grammar. the system of base rules exclusive of the lexicon. irrelevant intervening contexts in selectional rules. nondistinctness in the sense of distinctive feature theory. a general convention regarding the forms of the grammar. an abstract underlying order. an important distinction in language use. the requirement that branching is not tolerated within the dominance \ scope of a complex symbol. the strong generative capacity of the theory.""" # List of OBJECTs selected for profound sententiousness. import textwrap, random from itertools import chain, islice, izip def generate_chomsky(times=5, line_length=72): parts = [] for part in (leadins, subjects, verbs, objects): phraselist = map(str.strip, part.splitlines()) random.shuffle(phraselist) parts.append(phraselist) output = chain(*islice(izip(*parts), 0, times)) print textwrap.fill(string.join(output), line_length) if __name__ == '__main__': generate_chomsky() nltk-2.0~b9/nltk/misc/babelfish.py0000644000175000017500000001535711150146676016774 0ustar bhavanibhavani# babelizer.py - API for simple access to babelfish.altavista.com. # Requires python 2.0 or better. # From: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/64937 # Modified by Steven Bird to work with current babelfish # # See it in use at http://babel.MrFeinberg.com/ """API for simple access to babelfish.altavista.com. Summary: >>> import babelizer >>> print ' '.join(babelizer.available_languages) >>> print babelizer.translate( 'How much is that doggie in the window?', 'English', 'French' ) >>> def babel_callback(phrase): print phrase sys.stdout.flush() >>> babelizer.babelize( 'I love a reigning knight.', 'English', 'German', callback = babel_callback ) @group Helper Functions: clean @sort: BabelizerError, BabelfishChangedError, BabelizerIOError @var available_languages: A list of languages available for use with babelfish. @version: $Id: babelfish.py 1361 2003-10-24 14:41:44Z edloper $ @author: Jonathan Feinberg """ import re import string import urllib import sys """ Various patterns I have encountered in looking for the babelfish result. We try each of them in turn, based on the relative number of times I've seen each of these patterns. $1.00 to anyone who can provide a heuristic for knowing which one to use. This includes AltaVista employees. """ __where = [ re.compile(r'

([^<]*)'), re.compile(r'name=\"q\">([^<]*)'), re.compile(r'td bgcolor=white>([^<]*)'), re.compile(r'<\/strong>
([^<]*)'), re.compile(r'padding:10px[^>]+>([^<]*)') ] __languages = { 'english' : 'en', 'french' : 'fr', 'spanish' : 'es', 'german' : 'de', 'greek' : 'el', 'italian' : 'it', 'portuguese': 'pt', 'chinese' : 'zh', 'japanese' : 'ja', 'korean' : 'ko', 'russian' : 'ru' } """ All of the available language names. """ available_languages = [ x.title() for x in __languages.keys() ] class BabelizerError(Exception): """ Calling translate() or babelize() can raise a BabelizerError """ class BabelfishChangedError(BabelizerError): """ Thrown when babelfish.yahoo.com changes some detail of their HTML layout, and babelizer no longer submits data in the correct form, or can no longer parse the results. """ class BabelizerIOError(BabelizerError): """ Thrown for various networking and IO errors. """ def clean(text): return re.sub(r'\s+', ' ', text.strip()) def translate(phrase, source, target): """ Use babelfish to translate phrase from source language to target language. It's only guaranteed to work if 'english' is one of the two languages. @raise BabelizeError: If an error is encountered. """ phrase = clean(phrase) try: source_code = __languages[source] target_code = __languages[target] except KeyError, lang: raise ValueError, "Language %s not available" % lang params = urllib.urlencode({'doit': 'done', 'tt': 'urltext', 'urltext': phrase, 'lp': source_code + '_' + target_code}) try: response = urllib.urlopen('http://babelfish.yahoo.com/translate_txt', params) except IOError, what: raise BabelizerIOError("Couldn't talk to server: %s" % what) html = response.read() for regex in __where: match = regex.search(html) if match: break if not match: raise BabelfishChangedError("Can't recognize translated string.") return clean(match.group(1)) def babelize(phrase, source, target, limit = 12): """ Uses babelfish to translate back and forth between source and target until either no more changes occur in translation or limit iterations have been reached, whichever comes first. It's only guaranteed to work if 'english' is one of the two languages. @raise BabelizeError: If an error is encountered. """ phrase = clean(phrase) seen = set([phrase]) yield phrase flip = {source: target, target: source} next = source for i in range(limit): phrase = translate(phrase, next, flip[next]) if phrase in seen: break seen.add(phrase) yield phrase next = flip[next] HELP = """NLTK Babelizer Commands: All single-word inputs are commands: help: this help message languages: print the list of languages language: the name of a language to use""" def babelize_shell(): """ An interactive shell that uses babelfish to translate back and forth between source and target until either no more changes occur in translation or limit iterations have been reached, whichever comes first. It's only guaranteed to work if 'english' is one of the two languages. @raise BabelizeError: If an error is encountered. """ print "NLTK Babelizer: type 'help' for a list of commands." language = '' phrase = '' try: while True: command = raw_input('Babel> ') command = clean(command) if ' ' not in command: command = command.lower() if command == 'help': print HELP elif command == 'languages': print ' '.join(sorted(__languages)) elif command in __languages: language = command elif command in ['quit', 'bye', 'end']: break elif command == 'run': if not language: print "Please specify a language first (type 'languages' for a list)." elif not phrase: print "Please enter a phrase first (just type it in at the prompt)." else: for count, new_phrase in enumerate(babelize(phrase, 'english', language)): print "%s>" % count, new_phrase sys.stdout.flush() else: print "Command not recognized (type 'help' for help)." # if the command contains a space, it must have multiple words, and be a new phrase else: phrase = command except EOFError: print pass # I won't take that from you, or from your doggie (Korean) # the pig I found looked happy (chinese) # absence makes the heart grow fonder (italian) # more idioms: http://www.idiomsite.com/ if __name__ == '__main__': babelize_shell() nltk-2.0~b9/nltk/misc/__init__.py0000644000175000017500000000055611327451602016601 0ustar bhavanibhavani# Natural Language Toolkit: Miscellaneous modules # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT from chomsky import generate_chomsky from wordfinder import word_finder from minimalset import MinimalSet from babelfish import babelize, babelize_shell nltk-2.0~b9/nltk/metrics/windowdiff.py0000644000175000017500000000372011327451602017711 0ustar bhavanibhavani# Natural Language Toolkit: Windowdiff # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT ########################################################################## # Windowdiff # Pevzner, L., and Hearst, M., A Critique and Improvement of # an Evaluation Metric for Text Segmentation, # Computational Linguistics,, 28 (1), March 2002, pp. 19-36 ########################################################################## def windowdiff(seg1, seg2, k, boundary="1"): """ Compute the windowdiff score for a pair of segmentations. A segmentation is any sequence over a vocabulary of two items (e.g. "0", "1"), where the specified boundary value is used to mark the edge of a segmentation. >>> s1 = "00000010000000001000000" >>> s2 = "00000001000000010000000" >>> s3 = "00010000000000000001000" >>> windowdiff(s1, s1, 3) 0 >>> windowdiff(s1, s2, 3) 4 >>> windowdiff(s2, s3, 3) 16 @param seg1: a segmentation @type seg1: C{string} or C{list} @param seg2: a segmentation @type seg2: C{string} or C{list} @param k: window width @type k: C{int} @param boundary: boundary value @type boundary: C{string} or C{int} or C{bool} @rtype: C{int} """ if len(seg1) != len(seg2): raise ValueError, "Segmentations have unequal length" wd = 0 for i in range(len(seg1) - k): wd += abs(seg1[i:i+k+1].count(boundary) - seg2[i:i+k+1].count(boundary)) return wd def demo(): s1 = "00000010000000001000000" s2 = "00000001000000010000000" s3 = "00010000000000000001000" print "s1:", s1 print "s2:", s2 print "s3:", s3 print "windowdiff(s1, s1, 3) = ", windowdiff(s1, s1, 3) print "windowdiff(s1, s2, 3) = ", windowdiff(s1, s2, 3) print "windowdiff(s2, s3, 3) = ", windowdiff(s2, s3, 3) nltk-2.0~b9/nltk/metrics/spearman.py0000644000175000017500000000413611327451602017361 0ustar bhavanibhavani# Natural Language Toolkit: Spearman Rank Correlation # # Copyright (C) 2001-2010 NLTK Project # Author: Joel Nothman # URL: # For license information, see LICENSE.TXT """ Tools for comparing ranked lists. """ def _rank_dists(ranks1, ranks2): """Finds the difference between the values in ranks1 and ranks2 for keys present in both dicts. If the arguments are not dicts, they are converted from (key, rank) sequences. """ ranks1 = dict(ranks1) ranks2 = dict(ranks2) for k, v1 in ranks1.iteritems(): try: yield k, v1 - ranks2[k] except KeyError: pass def spearman_correlation(ranks1, ranks2): """Returns the Spearman correlation coefficient for two rankings, which should be dicts or sequences of (key, rank). The coefficient ranges from -1.0 (ranks are opposite) to 1.0 (ranks are identical), and is only calculated for keys in both rankings (for meaningful results, remove keys present in only one list before ranking).""" n = 0 res = 0 for k, d in _rank_dists(ranks1, ranks2): res += d * d n += 1 try: return 1 - (6 * float(res) / (n * (n*n - 1))) except ZeroDivisionError: # Result is undefined if only one item is ranked return 0.0 def ranks_from_sequence(seq): """Given a sequence, yields each element with an increasing rank, suitable for use as an argument to L{spearman_correlation}. """ return ((k, i) for i, k in enumerate(seq)) def ranks_from_scores(scores, rank_gap=1e-15): """Given a sequence of (key, score) tuples, yields each key with an increasing rank, tying with previous key's rank if the difference between their scores is less than rank_gap. Suitable for use as an argument to L{spearman_correlation}. """ prev_score = None rank = 0 for i, (key, score) in enumerate(scores): try: if abs(score - prev_score) > rank_gap: rank = i except TypeError: pass yield key, rank prev_score = score nltk-2.0~b9/nltk/metrics/scores.py0000644000175000017500000001745011327451602017054 0ustar bhavanibhavani# Natural Language Toolkit: Evaluation # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT import sys import math import random try: from scipy.stats.stats import betai except ImportError: betai = None from nltk.util import LazyConcatenation, LazyMap from itertools import izip from nltk.probability import FreqDist def accuracy(reference, test): """ Given a list of reference values and a corresponding list of test values, return the fraction of corresponding values that are equal. In particular, return the fraction of indices C{0= actual_stat: c += 1 if verbose and i % 10 == 0: print 'pseudo-statistic: %f' % pseudo_stat print 'significance: %f' % (float(c + 1) / (i + 1)) print '-' * 60 significance = float(c + 1) / (shuffles + 1) if verbose: print 'significance: %f' % significance if betai: for phi in [0.01, 0.05, 0.10, 0.15, 0.25, 0.50]: print "prob(phi<=%f): %f" % (phi, betai(c, shuffles, phi)) return (significance, c, shuffles) def demo(): print '-'*75 reference = 'DET NN VB DET JJ NN NN IN DET NN'.split() test = 'DET VB VB DET NN NN NN IN DET NN'.split() print 'Reference =', reference print 'Test =', test print 'Accuracy:', accuracy(reference, test) print '-'*75 reference_set = set(reference) test_set = set(test) print 'Reference =', reference_set print 'Test = ', test_set print 'Precision:', precision(reference_set, test_set) print ' Recall:', recall(reference_set, test_set) print 'F-Measure:', f_measure(reference_set, test_set) print '-'*75 if __name__ == '__main__': demo() nltk-2.0~b9/nltk/metrics/distance.py0000644000175000017500000001077011374105242017344 0ustar bhavanibhavani# Natural Language Toolkit: Distance Metrics # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # Tom Lippincott # URL: # For license information, see LICENSE.TXT # """ Distance Metrics. Compute the distance between two items (usually strings). As metrics, they must satisfy the following three requirements: 1. d(a, a) = 0 2. d(a, b) >= 0 3. d(a, c) <= d(a, b) + d(b, c) """ from nltk.internals import deprecated def _edit_dist_init(len1, len2): lev = [] for i in range(len1): lev.append([0] * len2) # initialize 2-D array to zero for i in range(len1): lev[i][0] = i # column 0: 0,1,2,3,4,... for j in range(len2): lev[0][j] = j # row 0: 0,1,2,3,4,... return lev def _edit_dist_step(lev, i, j, c1, c2): a = lev[i-1][j ] + 1 # skipping s1[i] b = lev[i-1][j-1] + (c1 != c2) # matching s1[i] with s2[j] c = lev[i ][j-1] + 1 # skipping s2[j] lev[i][j] = min(a,b,c) # pick the cheapest @deprecated('Use edit_distance() instead.') def edit_dist(s1, s2): return edit_distance(s1, s2) def edit_distance(s1, s2): """ Calculate the Levenshtein edit-distance between two strings. The edit distance is the number of characters that need to be substituted, inserted, or deleted, to transform s1 into s2. For example, transforming "rain" to "shine" requires three steps, consisting of two substitutions and one insertion: "rain" -> "sain" -> "shin" -> "shine". These operations could have been done in other orders, but at least three steps are needed. @param s1, s2: The strings to be analysed @type s1: C{string} @type s2: C{string} @rtype C{int} """ # set up a 2-D array len1 = len(s1); len2 = len(s2) lev = _edit_dist_init(len1+1, len2+1) # iterate over the array for i in range(len1): for j in range (len2): _edit_dist_step(lev, i+1, j+1, s1[i], s2[j]) return lev[len1][len2] def binary_distance(label1, label2): """Simple equality test. 0.0 if the labels are identical, 1.0 if they are different. >>> binary_distance(1,1) 0.0 >>> binary_distance(1,3) 1.0 """ if label1 == label2: return 0.0 else: return 1.0 def jaccard_distance(label1, label2): """Distance metric comparing set-similarity. """ return (len(label1.union(label2)) - len(label1.intersection(label2)))/float(len(label1.union(label2))) def masi_distance(label1, label2): """Distance metric that takes into account partial agreement when multiple labels are assigned. >>> masi_distance(set([1,2]),set([1,2,3,4])) 0.5 Passonneau 2005, Measuring Agreement on Set-Valued Items (MASI) for Semantic and Pragmatic Annotation. """ return 1 - float(len(label1.intersection(label2)))/float(max(len(label1),len(label2))) def interval_distance(label1,label2): """Krippendorff'1 interval distance metric >>> interval_distance(1,10) 81 Krippendorff 1980, Content Analysis: An Introduction to its Methodology """ try: return pow(label1-label2,2) # return pow(list(label1)[0]-list(label2)[0],2) except: print "non-numeric labels not supported with interval distance" def presence(label): """Higher-order function to test presence of a given label """ return lambda x,y: 1.0*((label in x) == (label in y)) def fractional_presence(label): return lambda x,y:abs((float(1.0/len(x)) - float(1.0/len(y))))*(label in x and label in y) or 0.0*(label not in x and label not in y) or abs((float(1.0/len(x))))*(label in x and label not in y) or ((float(1.0/len(y))))*(label not in x and label in y) def custom_distance(file): data = {} for l in open(file): labelA, labelB, dist = l.strip().split("\t") labelA = frozenset([labelA]) labelB = frozenset([labelB]) data[frozenset([labelA,labelB])] = float(dist) return lambda x,y:data[frozenset([x,y])] def demo(): s1 = "rain" s2 = "shine" print "Edit distance between '%s' and '%s':" % (s1,s2), edit_distance(s1, s2) s1 = set([1,2,3,4]) s2 = set([3,4,5]) print "s1:", s1 print "s2:", s2 print "Binary distance:", binary_distance(s1, s2) print "Jaccard distance:", jaccard_distance(s1, s2) print "MASI distance:", masi_distance(s1, s2) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/metrics/confusionmatrix.py0000644000175000017500000001634111327451602021004 0ustar bhavanibhavani# Natural Language Toolkit: Confusion Matrices # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT class ConfusionMatrix(object): """ The confusion matrix between a list of reference values and a corresponding list of test values. Entry [M{r},M{t}] of this matrix is a count of the number of times that the reference value M{r} corresponds to the test value M{t}. E.g.: >>> ref = 'DET NN VB DET JJ NN NN IN DET NN'.split() >>> test = 'DET VB VB DET NN NN NN IN DET NN'.split() >>> cm = ConfusionMatrix(ref, test) >>> print cm['NN', 'NN'] 3 Note that the diagonal entries (M{Ri}=M{Tj}) of this matrix corresponds to correct values; and the off-diagonal entries correspond to incorrect values. """ def __init__(self, reference, test, sort_by_count=False): """ Construct a new confusion matrix from a list of reference values and a corresponding list of test values. @type reference: C{list} @param reference: An ordered list of reference values. @type test: C{list} @param test: A list of values to compare against the corresponding reference values. @raise ValueError: If C{reference} and C{length} do not have the same length. """ if len(reference) != len(test): raise ValueError('Lists must have the same length.') # Get a list of all values. if sort_by_count: ref_fdist = FreqDist(reference) test_fdist = FreqDist(test) def key(v): return -(ref_fdist[v]+test_fdist[v]) values = sorted(set(reference+test), key=key) else: values = sorted(set(reference+test)) # Construct a value->index dictionary indices = dict((val,i) for (i,val) in enumerate(values)) # Make a confusion matrix table. confusion = [[0 for val in values] for val in values] max_conf = 0 # Maximum confusion for w,g in zip(reference, test): confusion[indices[w]][indices[g]] += 1 max_conf = max(max_conf, confusion[indices[w]][indices[g]]) #: A list of all values in C{reference} or C{test}. self._values = values #: A dictionary mapping values in L{self._values} to their indices. self._indices = indices #: The confusion matrix itself (as a list of lists of counts). self._confusion = confusion #: The greatest count in L{self._confusion} (used for printing). self._max_conf = max_conf #: The total number of values in the confusion matrix. self._total = len(reference) #: The number of correct (on-diagonal) values in the matrix. self._correct = sum(confusion[i][i] for i in range(len(values))) def __getitem__(self, (li,lj)): """ @return: The number of times that value C{li} was expected and value C{lj} was given. @rtype: C{int} """ i = self._indices[li] j = self._indices[lj] return self._confusion[i][j] def __repr__(self): return '' % (self._correct, self._total) def __str__(self): return self.pp() def pp(self, show_percents=False, values_in_chart=True, truncate=None, sort_by_count=False): """ @return: A multi-line string representation of this confusion matrix. @type truncate: int @param truncate: If specified, then only show the specified number of values. Any sorting (e.g., sort_by_count) will be performed before truncation. @param sort_by_count: If true, then sort by the count of each label in the reference data. I.e., labels that occur more frequently in the reference label will be towards the left edge of the matrix, and labels that occur less frequently will be towards the right edge. @todo: add marginals? """ confusion = self._confusion values = self._values if sort_by_count: values = sorted(values, key=lambda v: -sum(self._confusion[self._indices[v]])) if truncate: values = values[:truncate] if values_in_chart: value_strings = [str(val) for val in values] else: value_strings = [str(n+1) for n in range(len(values))] # Construct a format string for row values valuelen = max(len(val) for val in value_strings) value_format = '%' + `valuelen` + 's | ' # Construct a format string for matrix entries if show_percents: entrylen = 6 entry_format = '%5.1f%%' zerostr = ' .' else: entrylen = len(`self._max_conf`) entry_format = '%' + `entrylen` + 'd' zerostr = ' '*(entrylen-1) + '.' # Write the column values. s = '' for i in range(valuelen): s += (' '*valuelen)+' |' for val in value_strings: if i >= valuelen-len(val): s += val[i-valuelen+len(val)].rjust(entrylen+1) else: s += ' '*(entrylen+1) s += ' |\n' # Write a dividing line s += '%s-+-%s+\n' % ('-'*valuelen, '-'*((entrylen+1)*len(values))) # Write the entries. for val, li in zip(value_strings, values): i = self._indices[li] s += value_format % val for lj in values: j = self._indices[lj] if confusion[i][j] == 0: s += zerostr elif show_percents: s += entry_format % (100.0*confusion[i][j]/self._total) else: s += entry_format % confusion[i][j] if i == j: prevspace = s.rfind(' ') s = s[:prevspace] + '<' + s[prevspace+1:] + '>' else: s += ' ' s += '|\n' # Write a dividing line s += '%s-+-%s+\n' % ('-'*valuelen, '-'*((entrylen+1)*len(values))) # Write a key s += '(row = reference; col = test)\n' if not values_in_chart: s += 'Value key:\n' for i, value in enumerate(values): s += '%6d: %s\n' % (i+1, value) return s def key(self): values = self._values str = 'Value key:\n' indexlen = len(`len(values)-1`) key_format = ' %'+`indexlen`+'d: %s\n' for i in range(len(values)): str += key_format % (i, values[i]) return str def demo(): reference = 'DET NN VB DET JJ NN NN IN DET NN'.split() test = 'DET VB VB DET NN NN NN IN DET NN'.split() print 'Reference =', reference print 'Test =', test print 'Confusion matrix:' print ConfusionMatrix(reference, test) print ConfusionMatrix(reference, test).pp(sort_by_count=True) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/metrics/association.py0000644000175000017500000002437611327451602020077 0ustar bhavanibhavani# Natural Language Toolkit: Ngram Association Measures # # Copyright (C) 2001-2010 NLTK Project # Author: Joel Nothman # URL: # For license information, see LICENSE.TXT """ Provides scoring functions for a number of association measures through a generic, abstract implementation in L{NgramAssocMeasures}, and n-specific L{BigramAssocMeasures} and L{TrigramAssocMeasures}. """ import math as _math _log2 = lambda x: _math.log(x, 2.0) _ln = _math.log _product = lambda s: reduce(lambda x, y: x * y, s) _SMALL = 1e-20 ### Indices to marginals arguments: NGRAM = 0 """Marginals index for the ngram count""" UNIGRAMS = -2 """Marginals index for a tuple of each unigram count""" TOTAL = -1 """Marginals index for the number of words in the data""" class NgramAssocMeasures(object): """ An abstract class defining a collection of generic association measures. Each public method returns a score, taking the following arguments: score_fn(count_of_ngram, (count_of_n-1gram_1, ..., count_of_n-1gram_j), (count_of_n-2gram_1, ..., count_of_n-2gram_k), ..., (count_of_1gram_1, ..., count_of_1gram_n), count_of_total_words) See L{BigramAssocMeasures} and L{TrigramAssocMeasures} Inheriting classes should define a property _n, and a method _contingency which calculates contingency values from marginals in order for all association measures defined here to be usable. """ @staticmethod def _contingency(*marginals): """Calculates values of a contingency table from marginal values.""" raise NotImplementedError, ("The contingency table is not available" "in the general ngram case") @staticmethod def _marginals(*contingency): """Calculates values of contingency table marginals from its values.""" raise NotImplementedError, ("The contingency table is not available" "in the general ngram case") @classmethod def _expected_values(cls, cont): """Calculates expected values for a contingency table.""" n_all = sum(cont) bits = [1 << i for i in range(cls._n)] # For each contingency table cell for i in range(len(cont)): # Yield the expected value yield (_product(cont[i] + cont[i ^ j] for j in bits) / float(n_all ** 2)) @staticmethod def raw_freq(*marginals): """Scores ngrams by their frequency""" return float(marginals[NGRAM]) / marginals[TOTAL] @classmethod def student_t(cls, *marginals): """Scores ngrams using Student's t test with independence hypothesis for unigrams, as in Manning and Schutze 5.3.2. """ return ((marginals[NGRAM] * marginals[TOTAL] - _product(marginals[UNIGRAMS])) / (marginals[TOTAL] ** (cls._n - 1) * (marginals[NGRAM] + _SMALL) ** .5)) @classmethod def chi_sq(cls, *marginals): """Scores ngrams using Pearson's chi-square as in Manning and Schutze 5.3.3. """ cont = cls._contingency(*marginals) exps = cls._expected_values(cont) return sum((obs - exp) ** 2 / (exp + _SMALL) for obs, exp in zip(cont, exps)) @staticmethod def mi_like(*marginals, **kwargs): """Scores ngrams using a variant of mutual information. The keyword argument power sets an exponent (default 3) for the numerator. No logarithm of the result is calculated. """ return (marginals[NGRAM] ** kwargs.get('power', 3) / float(_product(marginals[UNIGRAMS]))) @classmethod def pmi(cls, *marginals): """Scores ngrams by pointwise mutual information, as in Manning and Schutze 5.4. """ return (_log2(marginals[NGRAM] * marginals[TOTAL] ** (cls._n - 1)) - _log2(_product(marginals[UNIGRAMS]))) @classmethod def likelihood_ratio(cls, *marginals): """Scores ngrams using likelihood ratios as in Manning and Schutze 5.3.4. """ cont = cls._contingency(*marginals) # I don't understand why this negation is needed return ((-1) ** cls._n * 2 * sum(obs * _ln(float(obs) / (exp + _SMALL) + _SMALL) for obs, exp in zip(cont, cls._expected_values(cont)))) @classmethod def poisson_stirling(cls, *marginals): """Scores ngrams using the Poisson-Stirling measure.""" exp = (_product(marginals[UNIGRAMS]) / float(marginals[TOTAL] ** (cls._n - 1))) return marginals[NGRAM] * (_log2(marginals[NGRAM] / exp) - 1) @classmethod def jaccard(cls, *marginals): """Scores ngrams using the Jaccard index.""" cont = cls._contingency(*marginals) return float(cont[0]) / sum(cont[:-1]) class BigramAssocMeasures(NgramAssocMeasures): """ A collection of trigram association measures. Each association measure is provided as a function with three arguments: bigram_score_fn(n_ii, (n_ix, n_xi), n_xx) The arguments constitute the marginals of a contingency table, counting the occurrences of particular events in a corpus. The letter i in the suffix refers to the appearance of the word in question, while x indicates the appearance of any word. Thus, for example: n_ii counts (w1, w2), i.e. the bigram being scored n_ix counts (w1, *) n_xi counts (*, w2) n_xx counts (*, *), i.e. any bigram This may be shown with respect to a contingency table: w1 ~w1 ------ ------ w2 | n_ii | n_oi | = n_xi ------ ------ ~w2 | n_io | n_oo | ------ ------ = n_ix TOTAL = n_xx """ _n = 2 @staticmethod def _contingency(n_ii, (n_ix, n_xi), n_xx): """Calculates values of a bigram contingency table from marginal values.""" n_oi = n_xi - n_ii n_io = n_ix - n_ii return (n_ii, n_oi, n_io, n_xx - n_ii - n_oi - n_io) @staticmethod def _marginals(n_ii, n_oi, n_io, n_oo): """Calculates values of contingency table marginals from its values.""" return (n_ii, (n_oi + n_ii, n_io + n_ii), n_oo + n_oi + n_io + n_ii) @staticmethod def _expected_values(cont): """Calculates expected values for a contingency table.""" n_xx = sum(cont) # For each contingency table cell for i in range(4): yield (cont[i] + cont[i ^ 1]) * (cont[i] + cont[i ^ 2]) / float(n_xx) @classmethod def phi_sq(cls, *marginals): """Scores bigrams using phi-square, the square of the Pearson correlation coefficient. """ n_ii, n_io, n_oi, n_oo = cls._contingency(*marginals) return (float((n_ii*n_oo - n_io*n_oi)**2) / ((n_ii + n_io) * (n_ii + n_oi) * (n_io + n_oo) * (n_oi + n_oo))) @classmethod def chi_sq(cls, n_ii, (n_ix, n_xi), n_xx): """Scores bigrams using chi-square, i.e. phi-sq multiplied by the number of bigrams, as in Manning and Schutze 5.3.3. """ return n_xx * cls.phi_sq(n_ii, (n_ix, n_xi), n_xx) @staticmethod def dice(n_ii, (n_ix, n_xi), n_xx): """Scores bigrams using Dice's coefficient.""" return 2 * float(n_ii) / (n_ix + n_xi) class TrigramAssocMeasures(NgramAssocMeasures): """ A collection of trigram association measures. Each association measure is provided as a function with four arguments: trigram_score_fn(n_iii, (n_iix, n_ixi, n_xii), (n_ixx, n_xix, n_xxi), n_xxx) The arguments constitute the marginals of a contingency table, counting the occurrences of particular events in a corpus. The letter i in the suffix refers to the appearance of the word in question, while x indicates the appearance of any word. Thus, for example: n_iii counts (w1, w2, w3), i.e. the trigram being scored n_ixx counts (w1, *, *) n_xxx counts (*, *, *), i.e. any trigram """ _n = 3 @staticmethod def _contingency(n_iii, (n_iix, n_ixi, n_xii), (n_ixx, n_xix, n_xxi), n_xxx): """Calculates values of a trigram contingency table (or cube) from marginal values. """ n_oii = n_xii - n_iii n_ioi = n_ixi - n_iii n_iio = n_iix - n_iii n_ooi = n_xxi - n_iii - n_oii - n_ioi n_oio = n_xix - n_iii - n_oii - n_iio n_ioo = n_ixx - n_iii - n_ioi - n_iio n_ooo = n_xxx - n_iii - n_oii - n_ioi - n_iio - n_ooi - n_oio - n_ioo return (n_iii, n_oii, n_ioi, n_ooi, n_iio, n_oio, n_ioo, n_ooo) @staticmethod def _marginals(*contingency): """Calculates values of contingency table marginals from its values.""" n_iii, n_oii, n_ioi, n_ooi, n_iio, n_oio, n_ioo, n_ooo = contingency return (n_iii, (n_iii + n_iio, n_iii + n_ioi, n_iii + n_oii), (n_iii + n_ioi + n_iio + n_ioo, n_iii + n_oii + n_iio + n_oio, n_iii + n_oii + n_ioi + n_ooi), sum(contingency)) class ContingencyMeasures(object): """Wraps NgramAssocMeasures classes such that the arguments of association measures are contingency table values rather than marginals. """ def __init__(self, measures): """Constructs a ContingencyMeasures given a NgramAssocMeasures class""" self.__class__.__name__ = 'Contingency' + measures.__class__.__name__ for k in dir(measures): if k.startswith('__'): continue v = getattr(measures, k) if not k.startswith('_'): v = self._make_contingency_fn(measures, v) setattr(self, k, v) @staticmethod def _make_contingency_fn(measures, old_fn): """From an association measure function, produces a new function which accepts contingency table values as its arguments. """ def res(*contingency): return old_fn(*measures._marginals(*contingency)) res.__doc__ = old_fn.__doc__ res.__name__ = old_fn.__name__ return res nltk-2.0~b9/nltk/metrics/agreement.py0000644000175000017500000003037511327451602017526 0ustar bhavanibhavani# Natural Language Toolkit: Agreement Metrics # # Copyright (C) 2001-2010 NLTK Project # Author: Tom Lippincott # URL: # For license information, see LICENSE.TXT # """ Implementations of inter-annotator agreement coefficients surveyed by Artstein and Poesio (2007), Inter-Coder Agreement for Computational Linguistics. An agreement coefficient calculates the amount that annotators agreed on label assignments beyond what is expected by chance. In defining the AnnotationTask class, we use naming conventions similar to the paper's terminology. There are three types of objects in an annotation task: the coders (variables "c" and "C") the items to be annotated (variables "i" and "I") the potential categories to be assigned (variables "k" and "K") Additionally, it is often the case that we don't want to treat two different labels as complete disagreement, and so the AnnotationTask constructor can also take a distance metric as a final argument. Distance metrics are simply functions that take two arguments, and return a value between 0.0 and 1.0 indicating the distance between them. If not supplied, the default is binary comparison between the arguments. The simplest way to initialize an AnnotationTask is with a list of equal-length lists, each containing a coder's assignments for all objects in the task: task = AnnotationTask([],[],[]) Alpha (Krippendorff 1980) Kappa (Cohen 1960) S (Bennet, Albert and Goldstein 1954) Pi (Scott 1955) TODO: Describe handling of multiple coders and missing data Expected results from the Artstein and Poesio survey paper: >>> t = AnnotationTask(data=[x.split() for x in open("%sartstein_poesio_example.txt" % (__file__.replace("__init__.py", "")))]) >>> t.avg_Ao() 0.88 >>> t.pi() 0.7995322418977614 >>> t.S() 0.81999999999999984 """ import logging from distance import * class AnnotationTask: """Represents an annotation task, i.e. people assign labels to items. Notation tries to match notation in Artstein and Poesio (2007). In general, coders and items can be represented as any hashable object. Integers, for example, are fine, though strings are more readable. Labels must support the distance functions applied to them, so e.g. a string-edit-distance makes no sense if your labels are integers, whereas interval distance needs numeric values. A notable case of this is the MASI metric, which requires Python sets. """ def __init__(self, data=None, distance=binary_distance): """Initialize an empty annotation task. """ self.distance = distance self.I = set() self.K = set() self.C = set() self.data = [] if data != None: self.load_array(data) def __str__(self): return "\r\n".join(map(lambda x:"%s\t%s\t%s" % (x['coder'], x['item'].replace('_', "\t"), ",".join(x['labels'])), self.data)) def load_array(self, array): """Load the results of annotation. The argument is a list of 3-tuples, each representing a coder's labeling of an item: (coder,item,label) """ for coder, item, labels in array: self.C.add(coder) self.K.add(labels) self.I.add(item) self.data.append({'coder':coder, 'labels':labels, 'item':item}) def agr(self, cA, cB, i): """Agreement between two coders on a given item """ kA = filter(lambda x:x['coder']==cA and x['item']==i, self.data)[0] kB = filter(lambda x:x['coder']==cB and x['item']==i, self.data)[0] ret = 1.0 - float(self.distance(kA['labels'], kB['labels'])) logging.debug("Observed agreement between %s and %s on %s: %f", cA, cB, i, ret) logging.debug("Distance between \"%s\" and \"%s\": %f", ",".join(kA['labels']), ",".join(kB['labels']), 1.0 - ret) return ret def N(self, k=None, i=None, c=None): """Implements the "n-notation" used in Artstein and Poesio (2007) """ if k != None and i == None and c == None: ret = len(filter(lambda x:k == x['labels'], self.data)) elif k != None and i != None and c == None: ret = len(filter(lambda x:k == x['labels'] and i == x['item'], self.data)) elif k != None and c != None and i==None: ret = len(filter(lambda x:k == x['labels'] and c == x['coder'], self.data)) else: print "You must pass either i or c, not both!" logging.debug("Count on N[%s,%s,%s]: %d", k, i, c, ret) return float(ret) def Ao(self, cA, cB): """Observed agreement between two coders on all items. """ ret = float(sum(map(lambda x:self.agr(cA, cB, x), self.I))) / float(len(self.I)) logging.debug("Observed agreement between %s and %s: %f", cA, cB, ret) return ret def avg_Ao(self): """Average observed agreement across all coders and items. """ s = self.C.copy() counter = 0.0 total = 0.0 for cA in self.C: s.remove(cA) for cB in s: total += self.Ao(cA, cB) counter += 1.0 ret = total / counter logging.debug("Average observed agreement: %f", ret) return ret #TODO: VERY slow, speed this up! def Do_alpha(self): """The observed disagreement for the alpha coefficient. The alpha coefficient, unlike the other metrics, uses this rather than observed agreement. """ total = 0.0 for i in self.I: for j in self.K: for l in self.K: total += float(self.N(i = i, k = j) * self.N(i = i, k = l)) * self.distance(l, j) ret = (1.0 / float((len(self.I) * len(self.C) * (len(self.C) - 1)))) * total logging.debug("Observed disagreement: %f", ret) return ret def Do_Kw_pairwise(self,cA,cB,max_distance=1.0): """The observed disagreement for the weighted kappa coefficient. """ total = 0.0 for i in self.I: total += self.distance(filter(lambda x:x['coder']==cA and x['item']==i, self.data)[0]['labels'], filter(lambda x:x['coder']==cB and x['item']==i, self.data)[0]['labels']) ret = total / (len(self.I) * max_distance) logging.debug("Observed disagreement between %s and %s: %f", cA, cB, ret) return ret def Do_Kw(self, max_distance=1.0): """Averaged over all labelers """ vals = {} for cA in self.C: for cB in self.C: if (not frozenset([cA,cB]) in vals.keys() and not cA == cB): vals[frozenset([cA, cB])] = self.Do_Kw_pairwise(cA, cB, max_distance) ret = sum(vals.values()) / len(vals) logging.debug("Observed disagreement: %f", ret) return ret # Agreement Coefficients def S(self): """Bennett, Albert and Goldstein 1954 """ Ae = 1.0 / float(len(self.K)) ret = (self.avg_Ao() - Ae) / (1.0 - Ae) return ret def pi(self): """Scott 1955 """ total = 0.0 for k in self.K: total += self.N(k=k) ** 2 Ae = (1.0 / (4.0 * float(len(self.I) ** 2))) * total ret = (self.avg_Ao() - Ae) / (1 - Ae) return ret def pi_avg(self): pass def kappa_pairwise(self, cA, cB): """ """ Ae = 0.0 for k in self.K: Ae += (float(self.N(c=cA, k=k)) / float(len(self.I))) * (float(self.N(c=cB, k=k)) / float(len(self.I))) ret = (self.Ao(cA, cB) - Ae) / (1.0 - Ae) logging.debug("Expected agreement between %s and %s: %f", cA, cB, Ae) logging.info("Kappa between %s and %s: %f", cA, cB, ret) return ret def kappa(self): """Cohen 1960 """ vals = {} for a in self.C: for b in self.C: if a == b or "%s%s" % (b, a) in vals: continue vals["%s%s" % (a, b)] = self.kappa_pairwise(a, b) ret = sum(vals.values()) / float(len(vals)) return ret def alpha(self): """Krippendorff 1980 """ De = 0.0 for j in self.K: for l in self.K: De += float(self.N(k=j) * self.N(k=l)) * self.distance(j, l) De = (1.0 / (len(self.I) * len(self.C) * (len(self.I) * len(self.C) - 1))) * De logging.debug("Expected disagreement: %f", De) ret = 1.0 - (self.Do_alpha() / De) return ret def weighted_kappa_pairwise(self, cA, cB, max_distance=1.0): """Cohen 1968 """ total = 0.0 for j in self.K: for l in self.K: total += self.N(c=cA, k=j) * self.N(c=cB, k=l) * self.distance(j, l) De = total / (max_distance * pow(len(self.I), 2)) logging.debug("Expected disagreement between %s and %s: %f", cA, cB, De) Do = self.Do_Kw_pairwise(cA, cB) ret = 1.0 - (Do / De) logging.warning("Weighted kappa between %s and %s: %f", cA, cB, ret) return ret def weighted_kappa(self): """Cohen 1968 """ vals = {} for a in self.C: for b in self.C: if a == b or frozenset([a, b]) in vals: continue vals[frozenset([a, b])] = self.weighted_kappa_pairwise(a, b) ret = sum(vals.values()) / float(len(vals)) return ret if __name__ == '__main__': import re import optparse import distance # process command-line arguments parser = optparse.OptionParser() parser.add_option("-d", "--distance", dest="distance", default="binary_distance", help="distance metric to use") parser.add_option("-a", "--agreement", dest="agreement", default="kappa", help="agreement coefficient to calculate") parser.add_option("-e", "--exclude", dest="exclude", action="append", default=[], help="coder names to exclude (may be specified multiple times)") parser.add_option("-i", "--include", dest="include", action="append", default=[], help="coder names to include, same format as exclude") parser.add_option("-f", "--file", dest="file", help="file to read labelings from, each line with three columns: 'labeler item labels'") parser.add_option("-v", "--verbose", dest="verbose", default='0', help="how much debugging to print on stderr (0-4)") parser.add_option("-c", "--columnsep", dest="columnsep", default="\t", help="char/string that separates the three columns in the file, defaults to tab") parser.add_option("-l", "--labelsep", dest="labelsep", default=",", help="char/string that separates labels (if labelers can assign more than one), defaults to comma") parser.add_option("-p", "--presence", dest="presence", default=None, help="convert each labeling into 1 or 0, based on presence of LABEL") parser.add_option("-T", "--thorough", dest="thorough", default=False, action="store_true", help="calculate agreement for every subset of the annotators") (options, remainder) = parser.parse_args() if not options.file: parser.print_help() exit() logging.basicConfig(level=50 - 10 * int(options.verbose)) # read in data from the specified file data = [] for l in open(options.file): toks = l.split(options.columnsep) coder, object, labels = toks[0], str(toks[1:-1]), frozenset(toks[-1].strip().split(options.labelsep)) if ((options.include == options.exclude) or (len(options.include) > 0 and coder in options.include) or (len(options.exclude) > 0 and coder not in options.exclude)): data.append((coder, object, labels)) if options.presence: task = AnnotationTask(data, getattr(distance, options.distance)(options.presence)) else: task = AnnotationTask(data, getattr(distance, options.distance)) if options.thorough: pass else: print getattr(task, options.agreement)() logging.shutdown() nltk-2.0~b9/nltk/metrics/__init__.py0000644000175000017500000000204711327451602017311 0ustar bhavanibhavani# Natural Language Toolkit: Metrics # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT # """ Classes and methods for scoring processing modules. """ from scores import * from confusionmatrix import * from distance import * from windowdiff import * from agreement import * from association import * from spearman import * __all__ = ['ConfusionMatrix', 'accuracy', 'f_measure', 'log_likelihood', 'precision', 'recall', 'approxrand', 'edit_distance', 'edit_dist', 'windowdiff', 'AnnotationTask', 'spearman_correlation', 'ranks_from_sequence', 'ranks_from_scores', 'NgramAssocMeasures', 'BigramAssocMeasures', 'TrigramAssocMeasures', 'ContingencyMeasures', 'binary_distance', 'jaccard_distance', 'masi_distance', 'interval_distance', 'custom_distance', 'presence', 'fractional_presence'] nltk-2.0~b9/nltk/inference/tableau.py0000644000175000017500000006141611331672325017466 0ustar bhavanibhavani# Natural Language Toolkit: First-Order Tableau Theorem Prover # # Copyright (C) 2001-2010 NLTK Project # Author: Dan Garrette # # URL: # For license information, see LICENSE.TXT from nltk.sem.logic import * from api import Prover, BaseProverCommand """ Module for a tableau-based First Order theorem prover. """ _counter = Counter() class ProverParseError(Exception): pass class TableauProver(Prover): _assume_false=False def _prove(self, goal=None, assumptions=None, verbose=False): if not assumptions: assumptions = [] result = None try: agenda = Agenda() if goal: agenda.put(-goal) agenda.put_all(assumptions) debugger = Debug(verbose) result = self._attempt_proof(agenda, set(), set(), debugger) except RuntimeError, e: if self._assume_false and str(e).startswith('maximum recursion depth exceeded'): result = False else: if verbose: print e else: raise e return (result, '\n'.join(debugger.lines)) def _attempt_proof(self, agenda, accessible_vars, atoms, debug): (current, context), category = agenda.pop_first() #if there's nothing left in the agenda, and we haven't closed the path if not current: debug.line('AGENDA EMPTY') return False proof_method = { Categories.ATOM: self._attempt_proof_atom, Categories.PROP: self._attempt_proof_prop, Categories.N_ATOM: self._attempt_proof_n_atom, Categories.N_PROP: self._attempt_proof_n_prop, Categories.APP: self._attempt_proof_app, Categories.N_APP: self._attempt_proof_n_app, Categories.N_EQ: self._attempt_proof_n_eq, Categories.D_NEG: self._attempt_proof_d_neg, Categories.N_ALL: self._attempt_proof_n_all, Categories.N_EXISTS: self._attempt_proof_n_some, Categories.AND: self._attempt_proof_and, Categories.N_OR: self._attempt_proof_n_or, Categories.N_IMP: self._attempt_proof_n_imp, Categories.OR: self._attempt_proof_or, Categories.IMP: self._attempt_proof_imp, Categories.N_AND: self._attempt_proof_n_and, Categories.IFF: self._attempt_proof_iff, Categories.N_IFF: self._attempt_proof_n_iff, Categories.EQ: self._attempt_proof_eq, Categories.EXISTS: self._attempt_proof_some, Categories.ALL: self._attempt_proof_all, }[category] debug.line((current, context)) return proof_method(current, context, agenda, accessible_vars, atoms, debug) def _attempt_proof_atom(self, current, context, agenda, accessible_vars, atoms, debug): # Check if the branch is closed. Return 'True' if it is if (current, True) in atoms: debug.line('CLOSED', 1) return True if context: if isinstance(context.term, NegatedExpression): current = current.negate() agenda.put(context(current).simplify()) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) else: #mark all AllExpressions as 'not exhausted' into the agenda since we are (potentially) adding new accessible vars agenda.mark_alls_fresh(); return self._attempt_proof(agenda, accessible_vars|set(current.args), atoms|set([(current, False)]), debug+1) def _attempt_proof_n_atom(self, current, context, agenda, accessible_vars, atoms, debug): # Check if the branch is closed. Return 'True' if it is if (current.term, False) in atoms: debug.line('CLOSED', 1) return True if context: if isinstance(context.term, NegatedExpression): current = current.negate() agenda.put(context(current).simplify()) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) else: #mark all AllExpressions as 'not exhausted' into the agenda since we are (potentially) adding new accessible vars agenda.mark_alls_fresh(); return self._attempt_proof(agenda, accessible_vars|set(current.term.args), atoms|set([(current.term, True)]), debug+1) def _attempt_proof_prop(self, current, context, agenda, accessible_vars, atoms, debug): # Check if the branch is closed. Return 'True' if it is if (current, True) in atoms: debug.line('CLOSED', 1) return True #mark all AllExpressions as 'not exhausted' into the agenda since we are (potentially) adding new accessible vars agenda.mark_alls_fresh(); return self._attempt_proof(agenda, accessible_vars, atoms|set([(current, False)]), debug+1) def _attempt_proof_n_prop(self, current, context, agenda, accessible_vars, atoms, debug): # Check if the branch is closed. Return 'True' if it is if (current.term, False) in atoms: debug.line('CLOSED', 1) return True #mark all AllExpressions as 'not exhausted' into the agenda since we are (potentially) adding new accessible vars agenda.mark_alls_fresh(); return self._attempt_proof(agenda, accessible_vars, atoms|set([(current.term, True)]), debug+1) def _attempt_proof_app(self, current, context, agenda, accessible_vars, atoms, debug): f, args = current.uncurry() for i, arg in enumerate(args): if not TableauProver.is_atom(arg): ctx = f nv = Variable('X%s' % _counter.get()) for j,a in enumerate(args): if i==j: ctx = ctx(VariableExpression(nv)) else: ctx = ctx(a) if context: ctx = context(ctx).simplify() ctx = LambdaExpression(nv, ctx) agenda.put(arg, ctx) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) raise Exception('If this method is called, there must be a non-atomic argument') def _attempt_proof_n_app(self, current, context, agenda, accessible_vars, atoms, debug): f, args = current.term.uncurry() for i, arg in enumerate(args): if not TableauProver.is_atom(arg): ctx = f nv = Variable('X%s' % _counter.get()) for j,a in enumerate(args): if i==j: ctx = ctx(VariableExpression(nv)) else: ctx = ctx(a) if context: #combine new context with existing ctx = context(ctx).simplify() ctx = LambdaExpression(nv, -ctx) agenda.put(-arg, ctx) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) raise Exception('If this method is called, there must be a non-atomic argument') def _attempt_proof_n_eq(self, current, context, agenda, accessible_vars, atoms, debug): ########################################################################### # Since 'current' is of type '~(a=b)', the path is closed if 'a' == 'b' ########################################################################### if current.term.first == current.term.second: debug.line('CLOSED', 1) return True agenda[Categories.N_EQ].add((current,context)) current._exhausted = True return self._attempt_proof(agenda, accessible_vars|set([current.term.first, current.term.second]), atoms, debug+1) def _attempt_proof_d_neg(self, current, context, agenda, accessible_vars, atoms, debug): agenda.put(current.term.term, context) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) def _attempt_proof_n_all(self, current, context, agenda, accessible_vars, atoms, debug): agenda[Categories.EXISTS].add((ExistsExpression(current.term.variable, -current.term.term), context)) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) def _attempt_proof_n_some(self, current, context, agenda, accessible_vars, atoms, debug): agenda[Categories.ALL].add((AllExpression(current.term.variable, -current.term.term), context)) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) def _attempt_proof_and(self, current, context, agenda, accessible_vars, atoms, debug): agenda.put(current.first, context) agenda.put(current.second, context) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) def _attempt_proof_n_or(self, current, context, agenda, accessible_vars, atoms, debug): agenda.put(-current.term.first, context) agenda.put(-current.term.second, context) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) def _attempt_proof_n_imp(self, current, context, agenda, accessible_vars, atoms, debug): agenda.put(current.term.first, context) agenda.put(-current.term.second, context) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) def _attempt_proof_or(self, current, context, agenda, accessible_vars, atoms, debug): new_agenda = agenda.clone() agenda.put(current.first, context) new_agenda.put(current.second, context) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) and \ self._attempt_proof(new_agenda, accessible_vars, atoms, debug+1) def _attempt_proof_imp(self, current, context, agenda, accessible_vars, atoms, debug): new_agenda = agenda.clone() agenda.put(-current.first, context) new_agenda.put(current.second, context) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) and \ self._attempt_proof(new_agenda, accessible_vars, atoms, debug+1) def _attempt_proof_n_and(self, current, context, agenda, accessible_vars, atoms, debug): new_agenda = agenda.clone() agenda.put(-current.term.first, context) new_agenda.put(-current.term.second, context) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) and \ self._attempt_proof(new_agenda, accessible_vars, atoms, debug+1) def _attempt_proof_iff(self, current, context, agenda, accessible_vars, atoms, debug): new_agenda = agenda.clone() agenda.put(current.first, context) agenda.put(current.second, context) new_agenda.put(-current.first, context) new_agenda.put(-current.second, context) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) and \ self._attempt_proof(new_agenda, accessible_vars, atoms, debug+1) def _attempt_proof_n_iff(self, current, context, agenda, accessible_vars, atoms, debug): new_agenda = agenda.clone() agenda.put(current.term.first, context) agenda.put(-current.term.second, context) new_agenda.put(-current.term.first, context) new_agenda.put(current.term.second, context) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) and \ self._attempt_proof(new_agenda, accessible_vars, atoms, debug+1) def _attempt_proof_eq(self, current, context, agenda, accessible_vars, atoms, debug): ######################################################################### # Since 'current' is of the form '(a = b)', replace ALL free instances # of 'a' with 'b' ######################################################################### agenda.put_atoms(atoms) agenda.replace_all(current.first, current.second) accessible_vars.discard(current.first) agenda.mark_neqs_fresh(); return self._attempt_proof(agenda, accessible_vars, set(), debug+1) def _attempt_proof_some(self, current, context, agenda, accessible_vars, atoms, debug): new_unique_variable = VariableExpression(unique_variable()) agenda.put(current.term.replace(current.variable, new_unique_variable), context) agenda.mark_alls_fresh() return self._attempt_proof(agenda, accessible_vars|set([new_unique_variable]), atoms, debug+1) def _attempt_proof_all(self, current, context, agenda, accessible_vars, atoms, debug): try: current._used_vars except AttributeError: current._used_vars = set() #if there are accessible_vars on the path if accessible_vars: # get the set of bound variables that have not be used by this AllExpression bv_available = accessible_vars - current._used_vars if bv_available: variable_to_use = list(bv_available)[0] debug.line('--> Using \'%s\'' % variable_to_use, 2) current._used_vars |= set([variable_to_use]) agenda.put(current.term.replace(current.variable, variable_to_use), context) agenda[Categories.ALL].add((current,context)) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) else: #no more available variables to substitute debug.line('--> Variables Exhausted', 2) current._exhausted = True agenda[Categories.ALL].add((current,context)) return self._attempt_proof(agenda, accessible_vars, atoms, debug+1) else: new_unique_variable = VariableExpression(unique_variable()) debug.line('--> Using \'%s\'' % new_unique_variable, 2) current._used_vars |= set([new_unique_variable]) agenda.put(current.term.replace(current.variable, new_unique_variable), context) agenda[Categories.ALL].add((current,context)) agenda.mark_alls_fresh() return self._attempt_proof(agenda, accessible_vars|set([new_unique_variable]), atoms, debug+1) @staticmethod def is_atom(e): if isinstance(e, NegatedExpression): e = e.term if isinstance(e, ApplicationExpression): for arg in e.args: if not TableauProver.is_atom(arg): return False return True elif isinstance(e, AbstractVariableExpression) or \ isinstance(e, LambdaExpression): return True else: return False class TableauProverCommand(BaseProverCommand): def __init__(self, goal=None, assumptions=None, prover=None): """ @param goal: Input expression to prove @type goal: L{logic.Expression} @param assumptions: Input expressions to use as assumptions in the proof. @type assumptions: C{list} of L{logic.Expression} """ if prover is not None: assert isinstance(prover, TableauProver) else: prover = TableauProver() BaseProverCommand.__init__(self, prover, goal, assumptions) class Agenda(object): def __init__(self): self.sets = tuple(set() for i in range(21)) def clone(self): new_agenda = Agenda() set_list = [s.copy() for s in self.sets] new_allExs = set() for allEx,_ in set_list[Categories.ALL]: new_allEx = AllExpression(allEx.variable, allEx.term) try: new_allEx._used_vars = set(used for used in allEx._used_vars) except AttributeError: new_allEx._used_vars = set() new_allExs.add((new_allEx,None)) set_list[Categories.ALL] = new_allExs set_list[Categories.N_EQ] = set((NegatedExpression(n_eq.term),ctx) for (n_eq,ctx) in set_list[Categories.N_EQ]) new_agenda.sets = tuple(set_list) return new_agenda def __getitem__(self, index): return self.sets[index] def put(self, expression, context=None): if isinstance(expression, AllExpression): ex_to_add = AllExpression(expression.variable, expression.term) try: ex_to_add._used_vars = set(used for used in expression._used_vars) except AttributeError: ex_to_add._used_vars = set() else: ex_to_add = expression self.sets[self._categorize_expression(ex_to_add)].add((ex_to_add, context)) def put_all(self, expressions): for expression in expressions: self.put(expression) def put_atoms(self, atoms): for atom, neg in atoms: if neg: self[Categories.N_ATOM].add((-atom,None)) else: self[Categories.ATOM].add((atom,None)) def pop_first(self): """ Pop the first expression that appears in the agenda """ for i,s in enumerate(self.sets): if s: if i in [Categories.N_EQ, Categories.ALL]: for ex in s: try: if not ex[0]._exhausted: s.remove(ex) return (ex, i) except AttributeError: s.remove(ex) return (ex, i) else: return (s.pop(), i) return ((None, None), None) def replace_all(self, old, new): for s in self.sets: for ex,ctx in s: ex.replace(old.variable, new) if ctx is not None: ctx.replace(old.variable, new) def mark_alls_fresh(self): for u,_ in self.sets[Categories.ALL]: u._exhausted = False def mark_neqs_fresh(self): for neq,_ in self.sets[Categories.N_EQ]: neq._exhausted = False def _categorize_expression(self, current): if isinstance(current, NegatedExpression): return self._categorize_NegatedExpression(current) elif isinstance(current, FunctionVariableExpression): return Categories.PROP elif TableauProver.is_atom(current): return Categories.ATOM elif isinstance(current, AllExpression): return Categories.ALL elif isinstance(current, AndExpression): return Categories.AND elif isinstance(current, OrExpression): return Categories.OR elif isinstance(current, ImpExpression): return Categories.IMP elif isinstance(current, IffExpression): return Categories.IFF elif isinstance(current, EqualityExpression): return Categories.EQ elif isinstance(current, ExistsExpression): return Categories.EXISTS elif isinstance(current, ApplicationExpression): return Categories.APP else: raise ProverParseError("cannot categorize %s" % \ current.__class__.__name__) def _categorize_NegatedExpression(self, current): negated = current.term if isinstance(negated, NegatedExpression): return Categories.D_NEG elif isinstance(negated, FunctionVariableExpression): return Categories.N_PROP elif TableauProver.is_atom(negated): return Categories.N_ATOM elif isinstance(negated, AllExpression): return Categories.N_ALL elif isinstance(negated, AndExpression): return Categories.N_AND elif isinstance(negated, OrExpression): return Categories.N_OR elif isinstance(negated, ImpExpression): return Categories.N_IMP elif isinstance(negated, IffExpression): return Categories.N_IFF elif isinstance(negated, EqualityExpression): return Categories.N_EQ elif isinstance(negated, ExistsExpression): return Categories.N_EXISTS elif isinstance(negated, ApplicationExpression): return Categories.N_APP else: raise ProverParseError("cannot categorize %s" % \ negated.__class__.__name__) class Debug(object): def __init__(self, verbose, indent=0, lines=None): self.verbose = verbose self.indent = indent if not lines: lines = [] self.lines = lines def __add__(self, increment): return Debug(self.verbose, self.indent+1, self.lines) def line(self, data, indent=0): if isinstance(data, tuple): ex, ctx = data if ctx: data = '%s, %s' % (ex, ctx) else: data = str(ex) if isinstance(ex, AllExpression): try: data += ': %s' % str([ve.variable.name for ve in ex._used_vars]) except AttributeError: data += ': []' newline = '%s%s' % (' '*(self.indent+indent), data) self.lines.append(newline) if self.verbose: print newline class Categories(object): ATOM = 0 PROP = 1 N_ATOM = 2 N_PROP = 3 APP = 4 N_APP = 5 N_EQ = 6 D_NEG = 7 N_ALL = 8 N_EXISTS = 9 AND = 10 N_OR = 11 N_IMP = 12 OR = 13 IMP = 14 N_AND = 15 IFF = 16 N_IFF = 17 EQ = 18 EXISTS = 19 ALL = 20 def testTableauProver(): tableau_test('P | -P') tableau_test('P & -P') tableau_test('Q', ['P', '(P -> Q)']) tableau_test('man(x)') tableau_test('(man(x) -> man(x))') tableau_test('(man(x) -> --man(x))') tableau_test('-(man(x) and -man(x))') tableau_test('(man(x) or -man(x))') tableau_test('(man(x) -> man(x))') tableau_test('-(man(x) and -man(x))') tableau_test('(man(x) or -man(x))') tableau_test('(man(x) -> man(x))') tableau_test('(man(x) iff man(x))') tableau_test('-(man(x) iff -man(x))') tableau_test('all x.man(x)') tableau_test('all x.all y.((x = y) -> (y = x))') tableau_test('all x.all y.all z.(((x = y) & (y = z)) -> (x = z))') # tableau_test('-all x.some y.F(x,y) & some x.all y.(-F(x,y))') # tableau_test('some x.all y.sees(x,y)') parse = LogicParser().parse p1 = 'all x.(man(x) -> mortal(x))' p2 = 'man(Socrates)' c = 'mortal(Socrates)' tableau_test(c, [p1, p2]) p1 = 'all x.(man(x) -> walks(x))' p2 = 'man(John)' c = 'some y.walks(y)' tableau_test(c, [p1, p2]) p = '((x = y) & walks(y))' c = 'walks(x)' tableau_test(c, [p]) p = '((x = y) & ((y = z) & (z = w)))' c = '(x = w)' tableau_test(c, [p]) p = 'some e1.some e2.(believe(e1,john,e2) & walk(e2,mary))' c = 'some e0.walk(e0,mary)' tableau_test(c, [p]) c = '(exists x.exists z3.((x = Mary) & ((z3 = John) & sees(z3,x))) <-> exists x.exists z4.((x = John) & ((z4 = Mary) & sees(x,z4))))' tableau_test(c) # p = 'some e1.some e2.((believe e1 john e2) and (walk e2 mary))' # c = 'some x.some e3.some e4.((believe e3 x e4) and (walk e4 mary))' # tableau_test(c, [p]) def testHigherOrderTableauProver(): tableau_test('believe(j, -lie(b))', ['believe(j, -lie(b) & -cheat(b))']) tableau_test('believe(j, lie(b) & cheat(b))', ['believe(j, lie(b))']) tableau_test('believe(j, lie(b))', ['lie(b)']) #how do we capture that John believes all things that are true tableau_test('believe(j, know(b, cheat(b)))', ['believe(j, know(b, lie(b)) & know(b, steals(b) & cheat(b)))']) tableau_test('P(Q(y), R(y) & R(z))', ['P(Q(x) & Q(y), R(y) & R(z))']) tableau_test('believe(j, cheat(b) & lie(b))', ['believe(j, lie(b) & cheat(b))']) tableau_test('believe(j, -cheat(b) & -lie(b))', ['believe(j, -lie(b) & -cheat(b))']) def tableau_test(c, ps=None, verbose=False): lp = LogicParser() pc = lp.parse(c) if ps: pps = [lp.parse(p) for p in ps] else: ps = [] pps = [] print '%s |- %s: %s' % (', '.join(ps), pc, TableauProver().prove(pc, pps, verbose=verbose)) def demo(): testTableauProver() testHigherOrderTableauProver() if __name__ == '__main__': demo() nltk-2.0~b9/nltk/inference/resolution.py0000755000175000017500000006155611327451576020274 0ustar bhavanibhavani# Natural Language Toolkit: First-order Resolution-based Theorem Prover # # Author: Dan Garrette # # Copyright (C) 2001-2010 NLTK Project # URL: # For license information, see LICENSE.TXT from nltk.compat import defaultdict from nltk.sem.logic import * from nltk.sem.util import skolemize from api import Prover, BaseProverCommand """ Module for a resolution-based First Order theorem prover. """ class ProverParseError(Exception): pass class ResolutionProver(Prover): ANSWER_KEY = 'ANSWER' _assume_false=True def _prove(self, goal=None, assumptions=None, verbose=False): """ @param goal: Input expression to prove @type goal: L{logic.Expression} @param assumptions: Input expressions to use as assumptions in the proof @type assumptions: L{list} of logic.Expression objects """ if not assumptions: assumptions = [] result = None try: clauses = [] if goal: clauses.extend(clausify(-goal)) for a in assumptions: clauses.extend(clausify(a)) result, clauses = self._attempt_proof(clauses) if verbose: print ResolutionProverCommand._decorate_clauses(clauses) except RuntimeError, e: if self._assume_false and str(e).startswith('maximum recursion depth exceeded'): result = False clauses = [] else: if verbose: print e else: raise e return (result, clauses) def _attempt_proof(self, clauses): #map indices to lists of indices, to store attempted unifications tried = defaultdict(list) i = 0 while i < len(clauses): if not clauses[i].is_tautology(): #since we try clauses in order, we should start after the last #index tried if tried[i]: j = tried[i][-1] + 1 else: j = i + 1 #nothing tried yet for 'i', so start with the next while j < len(clauses): #don't: 1) unify a clause with itself, # 2) use tautologies if i != j and j and not clauses[j].is_tautology(): tried[i].append(j) newclauses = clauses[i].unify(clauses[j]) if newclauses: for newclause in newclauses: newclause._parents = (i+1, j+1) clauses.append(newclause) if not len(newclause): #if there's an empty clause return (True, clauses) i=-1 #since we added a new clause, restart from the top break j += 1 i += 1 return (False, clauses) class ResolutionProverCommand(BaseProverCommand): def __init__(self, goal=None, assumptions=None, prover=None): """ @param goal: Input expression to prove @type goal: L{logic.Expression} @param assumptions: Input expressions to use as assumptions in the proof. @type assumptions: C{list} of L{logic.Expression} """ if prover is not None: assert isinstance(prover, ResolutionProver) else: prover = ResolutionProver() BaseProverCommand.__init__(self, prover, goal, assumptions) self._clauses = None def prove(self, verbose=False): """ Perform the actual proof. Store the result to prevent unnecessary re-proving. """ if self._result is None: self._result, clauses = self._prover._prove(self.goal(), self.assumptions(), verbose) self._clauses = clauses self._proof = ResolutionProverCommand._decorate_clauses(clauses) return self._result def find_answers(self, verbose=False): self.prove(verbose) answers = set() answer_ex = VariableExpression(Variable(ResolutionProver.ANSWER_KEY)) for clause in self._clauses: for term in clause: if isinstance(term, ApplicationExpression) and\ term.function == answer_ex and\ not isinstance(term.argument, IndividualVariableExpression): answers.add(term.argument) return answers @staticmethod def _decorate_clauses(clauses): """ Decorate the proof output. """ out = '' max_clause_len = max([len(str(clause)) for clause in clauses]) max_seq_len = len(str(len(clauses))) for i in range(len(clauses)): parents = 'A' taut = '' if clauses[i].is_tautology(): taut = 'Tautology' if clauses[i]._parents: parents = str(clauses[i]._parents) parents = ' '*(max_clause_len-len(str(clauses[i]))+1) + parents seq = ' '*(max_seq_len-len(str(i+1))) + str(i+1) out += '[%s] %s %s %s\n' % (seq, clauses[i], parents, taut) return out class Clause(list): def __init__(self, data): list.__init__(self, data) self._is_tautology = None self._parents = None def unify(self, other, bindings=None, used=None, skipped=None, debug=False): """ Attempt to unify this Clause with the other, returning a list of resulting, unified, Clauses. @param other: C{Clause} with which to unify @param bindings: C{BindingDict} containing bindings that should be used during the unification @param used: C{tuple} of two C{list}s of atoms. The first lists the atoms from 'self' that were successfully unified with atoms from 'other'. The second lists the atoms from 'other' that were successfully unified with atoms from 'self'. @param skipped: C{tuple} of two C{Clause}s. The first is a list of all the atoms from the 'self' Clause that have not been unified with anything on the path. The second is same thing for the 'other' Clause. @param debug: C{bool} indicating whether debug statements should print @return: C{list} containing all the resulting C{Clause}s that could be obtained by unification """ if bindings is None: bindings = BindingDict() if used is None: used = ([],[]) if skipped is None: skipped = ([],[]) if isinstance(debug, bool): debug = DebugObject(debug) newclauses = _iterate_first(self, other, bindings, used, skipped, _complete_unify_path, debug) #remove subsumed clauses. make a list of all indices of subsumed #clauses, and then remove them from the list subsumed = [] for i, c1 in enumerate(newclauses): if i not in subsumed: for j, c2 in enumerate(newclauses): if i!=j and j not in subsumed and c1.subsumes(c2): subsumed.append(j) result = [] for i in range(len(newclauses)): if i not in subsumed: result.append(newclauses[i]) return result def isSubsetOf(self, other): """ Return True iff every term in 'self' is a term in 'other'. @param other: C{Clause} @return: C{bool} """ for a in self: if a not in other: return False return True def subsumes(self, other): """ Return True iff 'self' subsumes 'other', this is, if there is a substitution such that every term in 'self' can be unified with a term in 'other'. @param other: C{Clause} @return: C{bool} """ negatedother = [] for atom in other: if isinstance(atom, NegatedExpression): negatedother.append(atom.term) else: negatedother.append(-atom) negatedotherClause = Clause(negatedother) bindings = BindingDict() used = ([],[]) skipped = ([],[]) debug = DebugObject(False) return len(_iterate_first(self, negatedotherClause, bindings, used, skipped, _subsumes_finalize, debug)) > 0 def __getslice__(self, start, end): return Clause(list.__getslice__(self, start, end)) def __sub__(self, other): return Clause([a for a in self if a not in other]) def __add__(self, other): return Clause(list.__add__(self, other)) def is_tautology(self): """ Self is a tautology if it contains ground terms P and -P. The ground term, P, must be an exact match, ie, not using unification. """ if self._is_tautology is not None: return self._is_tautology for i,a in enumerate(self): if not isinstance(a, EqualityExpression): j = len(self)-1 while j > i: b = self[j] if isinstance(a, NegatedExpression): if a.term == b: self._is_tautology = True return True elif isinstance(b, NegatedExpression): if a == b.term: self._is_tautology = True return True j -= 1 self._is_tautology = False return False def free(self): s = set() for atom in self: s |= atom.free(False) return s def replace(self, variable, expression): """ Replace every instance of variable with expression across every atom in the clause @param variable: C{Variable} @param expression: C{Expression} """ return Clause([atom.replace(variable, expression) for atom in self]) def substitute_bindings(self, bindings): """ Replace every binding @param bindings: A C{list} of tuples mapping Variable Expressions to the Expressions to which they are bound @return: C{Clause} """ return Clause([atom.substitute_bindings(bindings) for atom in self]) def __str__(self): return '{' + ', '.join([str(item) for item in self]) + '}' def __repr__(self): return str(self) def _iterate_first(first, second, bindings, used, skipped, finalize_method, debug): """ This method facilitates movement through the terms of 'self' """ debug.line('unify(%s,%s) %s'%(first, second, bindings)) if not len(first) or not len(second): #if no more recursions can be performed return finalize_method(first, second, bindings, used, skipped, debug) else: #explore this 'self' atom result = _iterate_second(first, second, bindings, used, skipped, finalize_method, debug+1) #skip this possible 'self' atom newskipped = (skipped[0]+[first[0]], skipped[1]) result += _iterate_first(first[1:], second, bindings, used, newskipped, finalize_method, debug+1) try: newbindings, newused, unused = _unify_terms(first[0], second[0], bindings, used) #Unification found, so progress with this line of unification #put skipped and unused terms back into play for later unification. newfirst = first[1:] + skipped[0] + unused[0] newsecond = second[1:] + skipped[1] + unused[1] result += _iterate_first(newfirst, newsecond, newbindings, newused, ([],[]), finalize_method, debug+1) except BindingException: #the atoms could not be unified, pass return result def _iterate_second(first, second, bindings, used, skipped, finalize_method, debug): """ This method facilitates movement through the terms of 'other' """ debug.line('unify(%s,%s) %s'%(first, second, bindings)) if not len(first) or not len(second): #if no more recursions can be performed return finalize_method(first, second, bindings, used, skipped, debug) else: #skip this possible pairing and move to the next newskipped = (skipped[0], skipped[1]+[second[0]]) result = _iterate_second(first, second[1:], bindings, used, newskipped, finalize_method, debug+1) try: newbindings, newused, unused = _unify_terms(first[0], second[0], bindings, used) #Unification found, so progress with this line of unification #put skipped and unused terms back into play for later unification. newfirst = first[1:] + skipped[0] + unused[0] newsecond = second[1:] + skipped[1] + unused[1] result += _iterate_second(newfirst, newsecond, newbindings, newused, ([],[]), finalize_method, debug+1) except BindingException: #the atoms could not be unified, pass return result def _unify_terms(a, b, bindings=None, used=None): """ This method attempts to unify two terms. Two expressions are unifiable if there exists a substitution function S such that S(a) == S(-b). @param a: C{Expression} @param b: C{Expression} @param bindings: C{BindingDict} a starting set of bindings with which the unification must be consistent @return: C{BindingDict} A dictionary of the bindings required to unify @raise C{BindingException}: If the terms cannot be unified """ assert isinstance(a, Expression) assert isinstance(b, Expression) if bindings is None: bindings = BindingDict() if used is None: used = ([],[]) # Use resolution if isinstance(a, NegatedExpression) and isinstance(b, ApplicationExpression): newbindings = most_general_unification(a.term, b, bindings) newused = (used[0]+[a], used[1]+[b]) unused = ([],[]) elif isinstance(a, ApplicationExpression) and isinstance(b, NegatedExpression): newbindings = most_general_unification(a, b.term, bindings) newused = (used[0]+[a], used[1]+[b]) unused = ([],[]) # Use demodulation elif isinstance(a, EqualityExpression): newbindings = BindingDict([(a.first.variable, a.second)]) newused = (used[0]+[a], used[1]) unused = ([],[b]) elif isinstance(b, EqualityExpression): newbindings = BindingDict([(b.first.variable, b.second)]) newused = (used[0], used[1]+[b]) unused = ([a],[]) else: raise BindingException((a, b)) return newbindings, newused, unused def _complete_unify_path(first, second, bindings, used, skipped, debug): if used[0] or used[1]: #if bindings were made along the path newclause = Clause(skipped[0] + skipped[1] + first + second) debug.line(' -> New Clause: %s' % newclause) return [newclause.substitute_bindings(bindings)] else: #no bindings made means no unification occurred. so no result debug.line(' -> End') return [] def _subsumes_finalize(first, second, bindings, used, skipped, debug): if not len(skipped[0]) and not len(first): #If there are no skipped terms and no terms left in 'first', then #all of the terms in the original 'self' were unified with terms #in 'other'. Therefore, there exists a binding (this one) such that #every term in self can be unified with a term in other, which #is the definition of subsumption. return [True] else: return [] def clausify(expression): """ Skolemize, clausify, and standardize the variables apart. """ clause_list = [] for clause in _clausify(skolemize(expression)): for free in clause.free(): if is_indvar(free.name): newvar = VariableExpression(unique_variable()) clause = clause.replace(free, newvar) clause_list.append(clause) return clause_list def _clausify(expression): """ @param expression: a skolemized expression in CNF """ if isinstance(expression, AndExpression): return _clausify(expression.first) + _clausify(expression.second) elif isinstance(expression, OrExpression): first = _clausify(expression.first) second = _clausify(expression.second) assert len(first) == 1 assert len(second) == 1 return [first[0] + second[0]] elif isinstance(expression, EqualityExpression): return [Clause([expression])] elif isinstance(expression, ApplicationExpression): return [Clause([expression])] elif isinstance(expression, NegatedExpression): if isinstance(expression.term, ApplicationExpression): return [Clause([expression])] elif isinstance(expression.term, EqualityExpression): return [Clause([expression])] raise ProverParseError() class BindingDict(object): def __init__(self, binding_list=None): """ @param binding_list: C{list} of (C{AbstractVariableExpression}, C{AtomicExpression}) to initialize the dictionary """ self.d = {} if binding_list: for (v, b) in binding_list: self[v] = b def __setitem__(self, variable, binding): """ A binding is consistent with the dict if its variable is not already bound, OR if its variable is already bound to its argument. @param variable: C{Variable} The variable to bind @param binding: C{Expression} The atomic to which 'variable' should be bound @raise BindingException: If the variable cannot be bound in this dictionary """ assert isinstance(variable, Variable) assert isinstance(binding, Expression) try: existing = self[variable] except KeyError: existing = None if not existing or binding == existing: self.d[variable] = binding elif isinstance(binding, IndividualVariableExpression): # Since variable is already bound, try to bind binding to variable try: existing = self[binding.variable] except KeyError: existing = None binding2 = VariableExpression(variable) if not existing or binding2 == existing: self.d[binding.variable] = binding2 else: raise BindingException('Variable %s already bound to another ' 'value' % (variable)) else: raise BindingException('Variable %s already bound to another ' 'value' % (variable)) def __getitem__(self, variable): """ Return the expression to which 'variable' is bound """ assert isinstance(variable, Variable) intermediate = self.d[variable] while intermediate: try: intermediate = self.d[intermediate] except KeyError: return intermediate def __contains__(self, item): return item in self.d def __add__(self, other): """ @param other: C{BindingDict} The dict with which to combine self @return: C{BindingDict} A new dict containing all the elements of both parameters @raise BindingException: If the parameter dictionaries are not consistent with each other """ try: combined = BindingDict() for v in self.d: combined[v] = self.d[v] for v in other.d: combined[v] = other.d[v] return combined except BindingException: raise BindingException("Attempting to add two contradicting " "BindingDicts: '%s' and '%s'" % (self, other)) def __len__(self): return len(self.d) def __str__(self): return '{' + ', '.join(['%s: %s' % (v, self.d[v]) for v in self.d]) + '}' def __repr__(self): return str(self) def most_general_unification(a, b, bindings=None): """ Find the most general unification of the two given expressions @param a: C{Expression} @param b: C{Expression} @param bindings: C{BindingDict} a starting set of bindings with which the unification must be consistent @return: a list of bindings @raise BindingException: if the Expressions cannot be unified """ if bindings is None: bindings = BindingDict() if a == b: return bindings elif isinstance(a, IndividualVariableExpression): return _mgu_var(a, b, bindings) elif isinstance(b, IndividualVariableExpression): return _mgu_var(b, a, bindings) elif isinstance(a, ApplicationExpression) and\ isinstance(b, ApplicationExpression): return most_general_unification(a.function, b.function, bindings) +\ most_general_unification(a.argument, b.argument, bindings) raise BindingException((a, b)) def _mgu_var(var, expression, bindings): if var.variable in expression.free(False): raise BindingException((var, expression)) else: return BindingDict([(var.variable, expression)]) + bindings class BindingException(Exception): def __init__(self, arg): if isinstance(arg, tuple): Exception.__init__(self, "'%s' cannot be bound to '%s'" % arg) else: Exception.__init__(self, arg) class UnificationException(Exception): def __init__(self, a, b): Exception.__init__(self, "'%s' cannot unify with '%s'" % (a,b)) class DebugObject(object): def __init__(self, enabled=True, indent=0): self.enabled = enabled self.indent = indent def __add__(self, i): return DebugObject(self.enabled, self.indent+i) def line(self, line): if self.enabled: print ' '*self.indent + line def testResolutionProver(): resolution_test(r'man(x)') resolution_test(r'(man(x) -> man(x))') resolution_test(r'(man(x) -> --man(x))') resolution_test(r'-(man(x) and -man(x))') resolution_test(r'(man(x) or -man(x))') resolution_test(r'(man(x) -> man(x))') resolution_test(r'-(man(x) and -man(x))') resolution_test(r'(man(x) or -man(x))') resolution_test(r'(man(x) -> man(x))') resolution_test(r'(man(x) iff man(x))') resolution_test(r'-(man(x) iff -man(x))') resolution_test('all x.man(x)') resolution_test('-all x.some y.F(x,y) & some x.all y.(-F(x,y))') resolution_test('some x.all y.sees(x,y)') p1 = LogicParser().parse(r'all x.(man(x) -> mortal(x))') p2 = LogicParser().parse(r'man(Socrates)') c = LogicParser().parse(r'mortal(Socrates)') print '%s, %s |- %s: %s' % (p1, p2, c, ResolutionProver().prove(c, [p1,p2])) p1 = LogicParser().parse(r'all x.(man(x) -> walks(x))') p2 = LogicParser().parse(r'man(John)') c = LogicParser().parse(r'some y.walks(y)') print '%s, %s |- %s: %s' % (p1, p2, c, ResolutionProver().prove(c, [p1,p2])) p = LogicParser().parse(r'some e1.some e2.(believe(e1,john,e2) & walk(e2,mary))') c = LogicParser().parse(r'some e0.walk(e0,mary)') print '%s |- %s: %s' % (p, c, ResolutionProver().prove(c, [p])) def resolution_test(e): f = LogicParser().parse(e) t = ResolutionProver().prove(f) print '|- %s: %s' % (f, t) def test_clausify(): lp = LogicParser() print clausify(lp.parse('P(x) | Q(x)')) print clausify(lp.parse('(P(x) & Q(x)) | R(x)')) print clausify(lp.parse('P(x) | (Q(x) & R(x))')) print clausify(lp.parse('(P(x) & Q(x)) | (R(x) & S(x))')) print clausify(lp.parse('P(x) | Q(x) | R(x)')) print clausify(lp.parse('P(x) | (Q(x) & R(x)) | S(x)')) print clausify(lp.parse('exists x.P(x) | Q(x)')) print clausify(lp.parse('-(-P(x) & Q(x))')) print clausify(lp.parse('P(x) <-> Q(x)')) print clausify(lp.parse('-(P(x) <-> Q(x))')) print clausify(lp.parse('-(all x.P(x))')) print clausify(lp.parse('-(some x.P(x))')) print clausify(lp.parse('some x.P(x)')) print clausify(lp.parse('some x.all y.P(x,y)')) print clausify(lp.parse('all y.some x.P(x,y)')) print clausify(lp.parse('all z.all y.some x.P(x,y,z)')) print clausify(lp.parse('all x.(all y.P(x,y) -> -all y.(Q(x,y) -> R(x,y)))')) def demo(): test_clausify() print testResolutionProver() print p = LogicParser().parse('man(x)') print ResolutionProverCommand(p, [p]).prove() if __name__ == '__main__': demo() nltk-2.0~b9/nltk/inference/prover9.py0000644000175000017500000003144111332125373017447 0ustar bhavanibhavani# Natural Language Toolkit: Interface to the Prover9 Theorem Prover # # Copyright (C) 2001-2010 NLTK Project # Author: Dan Garrette # Ewan Klein # # URL: # For license information, see LICENSE.TXT import os import tempfile import subprocess from string import join import nltk from nltk.sem.logic import LogicParser, Tokens as LogicTokens from api import BaseProverCommand, Prover """ A theorem prover that makes use of the external 'Prover9' package. """ # # Following is not yet used. Return code for 2 actually realized as 512. # p9_return_codes = { 0: True, 1: "(FATAL)", #A fatal error occurred (user's syntax error). 2: False, # (SOS_EMPTY) Prover9 ran out of things to do # (sos list exhausted). 3: "(MAX_MEGS)", # The max_megs (memory limit) parameter was exceeded. 4: "(MAX_SECONDS)", # The max_seconds parameter was exceeded. 5: "(MAX_GIVEN)", # The max_given parameter was exceeded. 6: "(MAX_KEPT)", # The max_kept parameter was exceeded. 7: "(ACTION)", # A Prover9 action terminated the search. 101: "(SIGSEGV)", # Prover9 crashed, most probably due to a bug. } class Prover9CommandParent(object): """ A common base class used by both L{Prover9Command} and L{MaceCommand }, which is responsible for maintaining a goal and a set of assumptions, and generating prover9-style input files from them. """ def print_assumptions(self, output_format='nltk'): """ Print the list of the current assumptions. """ if output_format.lower() == 'nltk': for a in self.assumptions(): print a elif output_format.lower() == 'prover9': for a in convert_to_prover9(self.assumptions()): print a else: raise NameError("Unrecognized value for 'output_format': %s" % output_format) class Prover9Command(Prover9CommandParent, BaseProverCommand): """ A L{ProverCommand} specific to the L{Prover9} prover. It contains the a print_assumptions() method that is used to print the list of assumptions in multiple formats. """ def __init__(self, goal=None, assumptions=None, timeout=60, prover=None): """ @param goal: Input expression to prove @type goal: L{logic.Expression} @param assumptions: Input expressions to use as assumptions in the proof. @type assumptions: C{list} of L{logic.Expression} @param timeout: number of seconds before timeout; set to 0 for no timeout. @type timeout: C{int} @param prover: a prover. If not set, one will be created. @type prover: C{Prover9} """ if not assumptions: assumptions = [] if prover is not None: assert isinstance(prover, Prover9) else: prover = Prover9(timeout) BaseProverCommand.__init__(self, prover, goal, assumptions) def decorate_proof(self, proof_string, simplify=True): """ @see BaseProverCommand.decorate_proof() """ if simplify: return self._prover._call_prooftrans(proof_string, ['striplabels'])[0].rstrip() else: return proof_string.rstrip() class Prover9Parent(object): """ A common class extended by both L{Prover9} and L{Mace }. It contains the functionality required to convert NLTK-style expressions into Prover9-style expressions. """ _binary_location = None def config_prover9(self, binary_location, verbose=False): if binary_location is None: self._binary_location = None self._prover9_bin = None else: name = 'prover9' self._prover9_bin = nltk.internals.find_binary( name, path_to_bin=binary_location, env_vars=['PROVER9HOME'], url='http://www.cs.unm.edu/~mccune/prover9/', binary_names=[name, name + '.exe'], verbose=verbose) self._binary_location = self._prover9_bin.rsplit(os.path.sep, 1) def prover9_input(self, goal, assumptions): """ @return: The input string that should be provided to the prover9 binary. This string is formed based on the goal, assumptions, and timeout value of this object. """ s = '' if assumptions: s += 'formulas(assumptions).\n' for p9_assumption in convert_to_prover9(assumptions): s += ' %s.\n' % p9_assumption s += 'end_of_list.\n\n' if goal: s += 'formulas(goals).\n' s += ' %s.\n' % convert_to_prover9(goal) s += 'end_of_list.\n\n' return s def binary_locations(self): """ A list of directories that should be searched for the prover9 executables. This list is used by L{config_prover9} when searching for the prover9 executables. """ return ['/usr/local/bin/prover9', '/usr/local/bin/prover9/bin', '/usr/local/bin', '/usr/bin', '/usr/local/prover9', '/usr/local/share/prover9'] def _find_binary(self, name, verbose=False): binary_locations = self.binary_locations() if self._binary_location is not None: binary_locations += [self._binary_location] return nltk.internals.find_binary(name, searchpath=binary_locations, env_vars=['PROVER9HOME'], url='http://www.cs.unm.edu/~mccune/prover9/', binary_names=[name, name + '.exe'], verbose=verbose) def _call(self, input_str, binary, args=[], verbose=False): """ Call the binary with the given input. @param input_str: A string whose contents are used as stdin. @param binary: The location of the binary to call @param args: A list of command-line arguments. @return: A tuple (stdout, returncode) @see: L{config_prover9} """ if verbose: print 'Calling:', binary print 'Args:', args print 'Input:\n', input_str, '\n' # Call prover9 via a subprocess cmd = [binary] + args p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE) (stdout, stderr) = p.communicate(input_str) if verbose: print 'Return code:', p.returncode if stdout: print 'stdout:\n', stdout, '\n' if stderr: print 'stderr:\n', stderr, '\n' return (stdout, p.returncode) def convert_to_prover9(input): """ Convert C{logic.Expression}s to Prover9 format. """ if isinstance(input, list): result = [] for s in input: try: result.append(s.simplify().str(LogicTokens.PROVER9)) except AssertionError: print 'input %s cannot be converted to Prover9 input syntax' % input return result else: try: return input.simplify().str(LogicTokens.PROVER9) except AssertionError: print 'input %s cannot be converted to Prover9 input syntax' % input class Prover9(Prover9Parent, Prover): _prover9_bin = None _prooftrans_bin = None def __init__(self, timeout=60): self._timeout = timeout """The timeout value for prover9. If a proof can not be found in this amount of time, then prover9 will return false. (Use 0 for no timeout.)""" def _prove(self, goal=None, assumptions=None, verbose=False): """ Use Prover9 to prove a theorem. @return: A pair whose first element is a boolean indicating if the proof was successful (i.e. returns value of 0) and whose second element is the output of the prover. """ if not assumptions: assumptions = [] stdout, returncode = self._call_prover9(self.prover9_input(goal, assumptions), verbose=verbose) return (returncode == 0, stdout) def prover9_input(self, goal, assumptions): """ @see: Prover9Parent.prover9_input """ s = 'clear(auto_denials).\n' #only one proof required return s + Prover9Parent.prover9_input(self, goal, assumptions) def _call_prover9(self, input_str, args=[], verbose=False): """ Call the C{prover9} binary with the given input. @param input_str: A string whose contents are used as stdin. @param args: A list of command-line arguments. @return: A tuple (stdout, returncode) @see: L{config_prover9} """ if self._prover9_bin is None: self._prover9_bin = self._find_binary('prover9', verbose) updated_input_str = '' if self._timeout > 0: updated_input_str += 'assign(max_seconds, %d).\n\n' % self._timeout updated_input_str += input_str return self._call(updated_input_str, self._prover9_bin, args, verbose) def _call_prooftrans(self, input_str, args=[], verbose=False): """ Call the C{prooftrans} binary with the given input. @param input_str: A string whose contents are used as stdin. @param args: A list of command-line arguments. @return: A tuple (stdout, returncode) @see: L{config_prover9} """ if self._prooftrans_bin is None: self._prooftrans_bin = self._find_binary('prooftrans', verbose) return self._call(input_str, self._prooftrans_bin, args, verbose) ###################################################################### #{ Tests and Demos ###################################################################### def test_config(): a = LogicParser().parse('(walk(j) & sing(j))') g = LogicParser().parse('walk(j)') p = Prover9Command(g, assumptions=[a]) p._executable_path = None p.prover9_search=[] p.prove() #config_prover9('/usr/local/bin') print p.prove() print p.proof() def test_convert_to_prover9(expr): """ Test that parsing works OK. """ for t in expr: e = LogicParser().parse(t) print convert_to_prover9(e) def test_prove(arguments): """ Try some proofs and exhibit the results. """ for (goal, assumptions) in arguments: g = LogicParser().parse(goal) alist = [LogicParser().parse(a) for a in assumptions] p = Prover9Command(g, assumptions=alist).prove() for a in alist: print ' %s' % a print '|- %s: %s\n' % (g, p) arguments = [ ('(man(x) <-> (not (not man(x))))', []), ('(not (man(x) & (not man(x))))', []), ('(man(x) | (not man(x)))', []), ('(man(x) & (not man(x)))', []), ('(man(x) -> man(x))', []), ('(not (man(x) & (not man(x))))', []), ('(man(x) | (not man(x)))', []), ('(man(x) -> man(x))', []), ('(man(x) <-> man(x))', []), ('(not (man(x) <-> (not man(x))))', []), ('mortal(Socrates)', ['all x.(man(x) -> mortal(x))', 'man(Socrates)']), ('((all x.(man(x) -> walks(x)) & man(Socrates)) -> some y.walks(y))', []), ('(all x.man(x) -> all x.man(x))', []), ('some x.all y.sees(x,y)', []), ('some e3.(walk(e3) & subj(e3, mary))', ['some e1.(see(e1) & subj(e1, john) & some e2.(pred(e1, e2) & walk(e2) & subj(e2, mary)))']), ('some x e1.(see(e1) & subj(e1, x) & some e2.(pred(e1, e2) & walk(e2) & subj(e2, mary)))', ['some e1.(see(e1) & subj(e1, john) & some e2.(pred(e1, e2) & walk(e2) & subj(e2, mary)))']) ] expressions = [r'some x y.sees(x,y)', r'some x.(man(x) & walks(x))', r'\x.(man(x) & walks(x))', r'\x y.sees(x,y)', r'walks(john)', r'\x.big(x, \y.mouse(y))', r'(walks(x) & (runs(x) & (threes(x) & fours(x))))', r'(walks(x) -> runs(x))', r'some x.(PRO(x) & sees(John, x))', r'some x.(man(x) & (not walks(x)))', r'all x.(man(x) -> walks(x))'] def spacer(num=45): print '-' * num def demo(): print "Testing configuration" spacer() test_config() print print "Testing conversion to Prover9 format" spacer() test_convert_to_prover9(expressions) print print "Testing proofs" spacer() test_prove(arguments) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/inference/nonmonotonic.py0000644000175000017500000004364111327451576020601 0ustar bhavanibhavani# Natural Language Toolkit: Nonmonotonic Reasoning # # Author: Daniel H. Garrette # # Copyright (C) 2001-2010 NLTK Project # URL: # For license information, see LICENSE.TXT """ A module to perform nonmonotonic reasoning. The ideas and demonstrations in this module are based on "Logical Foundations of Artificial Intelligence" by Michael R. Genesereth and Nils J. Nilsson. """ from nltk.sem.logic import * from api import Prover, ProverCommandDecorator from prover9 import Prover9, Prover9Command from nltk.compat import defaultdict class ProverParseError(Exception): pass def get_domain(goal, assumptions): if goal is None: all_expressions = assumptions else: all_expressions = assumptions + [-goal] domain = set() for a in all_expressions: domain |= (a.free(False) - a.free(True)) return domain class ClosedDomainProver(ProverCommandDecorator): """ This is a prover decorator that adds domain closure assumptions before proving. """ def assumptions(self): assumptions = [a for a in self._command.assumptions()] goal = self._command.goal() domain = get_domain(goal, assumptions) return list([self.replace_quants(ex, domain) for ex in assumptions]) def goal(self): goal = self._command.goal() domain = get_domain(goal, self._command.assumptions()) return self.replace_quants(goal, domain) def replace_quants(self, ex, domain): """ Apply the closed domain assumption to the expression - Domain = union([e.free(False) for e in all_expressions]) - translate "exists x.P" to "(z=d1 | z=d2 | ... ) & P.replace(x,z)" OR "P.replace(x, d1) | P.replace(x, d2) | ..." - translate "all x.P" to "P.replace(x, d1) & P.replace(x, d2) & ..." @param ex: C{Expression} @param domain: C{set} of {Variable}s @return: C{Expression} """ if isinstance(ex, AllExpression): conjuncts = [ex.term.replace(ex.variable, VariableExpression(d)) for d in domain] conjuncts = [self.replace_quants(c, domain) for c in conjuncts] return reduce(lambda x,y: x&y, conjuncts) elif isinstance(ex, BooleanExpression): return ex.__class__(self.replace_quants(ex.first, domain), self.replace_quants(ex.second, domain) ) elif isinstance(ex, NegatedExpression): return -self.replace_quants(ex.term, domain) elif isinstance(ex, ExistsExpression): disjuncts = [ex.term.replace(ex.variable, VariableExpression(d)) for d in domain] disjuncts = [self.replace_quants(d, domain) for d in disjuncts] return reduce(lambda x,y: x|y, disjuncts) else: return ex class UniqueNamesProver(ProverCommandDecorator): """ This is a prover decorator that adds unique names assumptions before proving. """ def assumptions(self): """ - Domain = union([e.free(False) for e in all_expressions]) - if "d1 = d2" cannot be proven from the premises, then add "d1 != d2" """ assumptions = self._command.assumptions() domain = list(get_domain(self._command.goal(), assumptions)) #build a dictionary of obvious equalities eq_sets = SetHolder() for a in assumptions: if isinstance(a, EqualityExpression): av = a.first.variable bv = a.second.variable #put 'a' and 'b' in the same set eq_sets[av].add(bv) new_assumptions = [] for i,a in enumerate(domain): for b in domain[i+1:]: #if a and b are not already in the same equality set if b not in eq_sets[a]: newEqEx = EqualityExpression(VariableExpression(a), VariableExpression(b)) if Prover9().prove(newEqEx, assumptions): #we can prove that the names are the same entity. #remember that they are equal so we don't re-check. eq_sets[a].add(b) else: #we can't prove it, so assume unique names new_assumptions.append(-newEqEx) return assumptions + new_assumptions class SetHolder(list): """ A list of sets of Variables. """ def __getitem__(self, item): """ @param item: C{Variable} @return: the C{set} containing 'item' """ assert isinstance(item, Variable) for s in self: if item in s: return s #item is not found in any existing set. so create a new set new = set([item]) self.append(new) return new class ClosedWorldProver(ProverCommandDecorator): """ This is a prover decorator that completes predicates before proving. If the assumptions contain "P(A)", then "all x.(P(x) -> (x=A))" is the completion of "P". If the assumptions contain "all x.(ostrich(x) -> bird(x))", then "all x.(bird(x) -> ostrich(x))" is the completion of "bird". If the assumptions don't contain anything that are "P", then "all x.-P(x)" is the completion of "P". walk(Socrates) Socrates != Bill + all x.(walk(x) -> (x=Socrates)) ---------------- -walk(Bill) see(Socrates, John) see(John, Mary) Socrates != John John != Mary + all x.all y.(see(x,y) -> ((x=Socrates & y=John) | (x=John & y=Mary))) ---------------- -see(Socrates, Mary) all x.(ostrich(x) -> bird(x)) bird(Tweety) -ostrich(Sam) Sam != Tweety + all x.(bird(x) -> (ostrich(x) | x=Tweety)) + all x.-ostrich(x) ------------------- -bird(Sam) """ def assumptions(self): assumptions = self._command.assumptions() predicates = self._make_predicate_dict(assumptions) new_assumptions = [] for p, predHolder in predicates.iteritems(): new_sig = self._make_unique_signature(predHolder) new_sig_exs = [VariableExpression(v) for v in new_sig] disjuncts = [] #Turn the signatures into disjuncts for sig in predHolder.signatures: equality_exs = [] for v1,v2 in zip(new_sig_exs, sig): equality_exs.append(EqualityExpression(v1,v2)) disjuncts.append(reduce(lambda x,y: x&y, equality_exs)) #Turn the properties into disjuncts for prop in predHolder.properties: #replace variables from the signature with new sig variables bindings = {} for v1,v2 in zip(new_sig_exs, prop[0]): bindings[v2] = v1 disjuncts.append(prop[1].substitute_bindings(bindings)) #make the assumption if disjuncts: #disjuncts exist, so make an implication antecedent = self._make_antecedent(p, new_sig) consequent = reduce(lambda x,y: x|y, disjuncts) accum = ImpExpression(antecedent, consequent) else: #nothing has property 'p' accum = NegatedExpression(self._make_antecedent(p, new_sig)) #quantify the implication for new_sig_var in new_sig[::-1]: accum = AllExpression(new_sig_var, accum) new_assumptions.append(accum) return assumptions + new_assumptions def _make_unique_signature(self, predHolder): """ This method figures out how many arguments the predicate takes and returns a tuple containing that number of unique variables. """ return tuple([unique_variable() for i in range(predHolder.signature_len)]) def _make_antecedent(self, predicate, signature): """ Return an application expression with 'predicate' as the predicate and 'signature' as the list of arguments. """ antecedent = predicate for v in signature: antecedent = antecedent(VariableExpression(v)) return antecedent def _make_predicate_dict(self, assumptions): """ Create a dictionary of predicates from the assumptions. @param assumptions: a C{list} of C{Expression}s @return: C{dict} mapping C{AbstractVariableExpression} to C{PredHolder} """ predicates = defaultdict(PredHolder) for a in assumptions: self._map_predicates(a, predicates) return predicates def _map_predicates(self, expression, predDict): if isinstance(expression, ApplicationExpression): (func, args) = expression.uncurry() if isinstance(func, AbstractVariableExpression): predDict[func].append_sig(tuple(args)) elif isinstance(expression, AndExpression): self._map_predicates(expression.first, predDict) self._map_predicates(expression.second, predDict) elif isinstance(expression, AllExpression): #collect all the universally quantified variables sig = [expression.variable] term = expression.term while isinstance(term, AllExpression): sig.append(term.variable) term = term.term if isinstance(term, ImpExpression): if isinstance(term.first, ApplicationExpression) and \ isinstance(term.second, ApplicationExpression): func1, args1 = term.first.uncurry() func2, args2 = term.second.uncurry() if isinstance(func1, AbstractVariableExpression) and \ isinstance(func2, AbstractVariableExpression) and \ sig == [v.variable for v in args1] and \ sig == [v.variable for v in args2]: predDict[func2].append_prop((tuple(sig), term.first)) predDict[func1].validate_sig_len(sig) class PredHolder(object): """ This class will be used by a dictionary that will store information about predicates to be used by the C{ClosedWorldProver}. The 'signatures' property is a list of tuples defining signatures for which the predicate is true. For instance, 'see(john, mary)' would be result in the signature '(john,mary)' for 'see'. The second element of the pair is a list of pairs such that the first element of the pair is a tuple of variables and the second element is an expression of those variables that makes the predicate true. For instance, 'all x.all y.(see(x,y) -> know(x,y))' would result in "((x,y),('see(x,y)'))" for 'know'. """ def __init__(self): self.signatures = [] self.properties = [] self.signature_len = None def append_sig(self, new_sig): self.validate_sig_len(new_sig) self.signatures.append(new_sig) def append_prop(self, new_prop): self.validate_sig_len(new_prop[0]) self.properties.append(new_prop) def validate_sig_len(self, new_sig): if self.signature_len is None: self.signature_len = len(new_sig) elif self.signature_len != len(new_sig): raise Exception("Signature lengths do not match") def __str__(self): return '(%s,%s,%s)' % (self.signatures, self.properties, self.signature_len) def __repr__(self): return str(self) def closed_domain_demo(): lp = LogicParser() p1 = lp.parse(r'exists x.walk(x)') p2 = lp.parse(r'man(Socrates)') c = lp.parse(r'walk(Socrates)') prover = Prover9Command(c, [p1,p2]) print prover.prove() cdp = ClosedDomainProver(prover) print 'assumptions:' for a in cdp.assumptions(): print ' ', a print 'goal:', cdp.goal() print cdp.prove() p1 = lp.parse(r'exists x.walk(x)') p2 = lp.parse(r'man(Socrates)') p3 = lp.parse(r'-walk(Bill)') c = lp.parse(r'walk(Socrates)') prover = Prover9Command(c, [p1,p2,p3]) print prover.prove() cdp = ClosedDomainProver(prover) print 'assumptions:' for a in cdp.assumptions(): print ' ', a print 'goal:', cdp.goal() print cdp.prove() p1 = lp.parse(r'exists x.walk(x)') p2 = lp.parse(r'man(Socrates)') p3 = lp.parse(r'-walk(Bill)') c = lp.parse(r'walk(Socrates)') prover = Prover9Command(c, [p1,p2,p3]) print prover.prove() cdp = ClosedDomainProver(prover) print 'assumptions:' for a in cdp.assumptions(): print ' ', a print 'goal:', cdp.goal() print cdp.prove() p1 = lp.parse(r'walk(Socrates)') p2 = lp.parse(r'walk(Bill)') c = lp.parse(r'all x.walk(x)') prover = Prover9Command(c, [p1,p2]) print prover.prove() cdp = ClosedDomainProver(prover) print 'assumptions:' for a in cdp.assumptions(): print ' ', a print 'goal:', cdp.goal() print cdp.prove() p1 = lp.parse(r'girl(mary)') p2 = lp.parse(r'dog(rover)') p3 = lp.parse(r'all x.(girl(x) -> -dog(x))') p4 = lp.parse(r'all x.(dog(x) -> -girl(x))') p5 = lp.parse(r'chase(mary, rover)') c = lp.parse(r'exists y.(dog(y) & all x.(girl(x) -> chase(x,y)))') prover = Prover9Command(c, [p1,p2,p3,p4,p5]) print prover.prove() cdp = ClosedDomainProver(prover) print 'assumptions:' for a in cdp.assumptions(): print ' ', a print 'goal:', cdp.goal() print cdp.prove() def unique_names_demo(): lp = LogicParser() p1 = lp.parse(r'man(Socrates)') p2 = lp.parse(r'man(Bill)') c = lp.parse(r'exists x.exists y.(x != y)') prover = Prover9Command(c, [p1,p2]) print prover.prove() unp = UniqueNamesProver(prover) print 'assumptions:' for a in unp.assumptions(): print ' ', a print 'goal:', unp.goal() print unp.prove() p1 = lp.parse(r'all x.(walk(x) -> (x = Socrates))') p2 = lp.parse(r'Bill = William') p3 = lp.parse(r'Bill = Billy') c = lp.parse(r'-walk(William)') prover = Prover9Command(c, [p1,p2,p3]) print prover.prove() unp = UniqueNamesProver(prover) print 'assumptions:' for a in unp.assumptions(): print ' ', a print 'goal:', unp.goal() print unp.prove() def closed_world_demo(): lp = LogicParser() p1 = lp.parse(r'walk(Socrates)') p2 = lp.parse(r'(Socrates != Bill)') c = lp.parse(r'-walk(Bill)') prover = Prover9Command(c, [p1,p2]) print prover.prove() cwp = ClosedWorldProver(prover) print 'assumptions:' for a in cwp.assumptions(): print ' ', a print 'goal:', cwp.goal() print cwp.prove() p1 = lp.parse(r'see(Socrates, John)') p2 = lp.parse(r'see(John, Mary)') p3 = lp.parse(r'(Socrates != John)') p4 = lp.parse(r'(John != Mary)') c = lp.parse(r'-see(Socrates, Mary)') prover = Prover9Command(c, [p1,p2,p3,p4]) print prover.prove() cwp = ClosedWorldProver(prover) print 'assumptions:' for a in cwp.assumptions(): print ' ', a print 'goal:', cwp.goal() print cwp.prove() p1 = lp.parse(r'all x.(ostrich(x) -> bird(x))') p2 = lp.parse(r'bird(Tweety)') p3 = lp.parse(r'-ostrich(Sam)') p4 = lp.parse(r'Sam != Tweety') c = lp.parse(r'-bird(Sam)') prover = Prover9Command(c, [p1,p2,p3,p4]) print prover.prove() cwp = ClosedWorldProver(prover) print 'assumptions:' for a in cwp.assumptions(): print ' ', a print 'goal:', cwp.goal() print cwp.prove() def combination_prover_demo(): lp = LogicParser() p1 = lp.parse(r'see(Socrates, John)') p2 = lp.parse(r'see(John, Mary)') c = lp.parse(r'-see(Socrates, Mary)') prover = Prover9Command(c, [p1,p2]) print prover.prove() command = ClosedDomainProver( UniqueNamesProver( ClosedWorldProver(prover))) for a in command.assumptions(): print a print command.prove() def default_reasoning_demo(): lp = LogicParser() premises = [] #define taxonomy premises.append(lp.parse(r'all x.(elephant(x) -> animal(x))')) premises.append(lp.parse(r'all x.(bird(x) -> animal(x))')) premises.append(lp.parse(r'all x.(dove(x) -> bird(x))')) premises.append(lp.parse(r'all x.(ostrich(x) -> bird(x))')) premises.append(lp.parse(r'all x.(flying_ostrich(x) -> ostrich(x))')) #default properties premises.append(lp.parse(r'all x.((animal(x) & -Ab1(x)) -> -fly(x))')) #normal animals don't fly premises.append(lp.parse(r'all x.((bird(x) & -Ab2(x)) -> fly(x))')) #normal birds fly premises.append(lp.parse(r'all x.((ostrich(x) & -Ab3(x)) -> -fly(x))')) #normal ostriches don't fly #specify abnormal entities premises.append(lp.parse(r'all x.(bird(x) -> Ab1(x))')) #flight premises.append(lp.parse(r'all x.(ostrich(x) -> Ab2(x))')) #non-flying bird premises.append(lp.parse(r'all x.(flying_ostrich(x) -> Ab3(x))')) #flying ostrich #define entities premises.append(lp.parse(r'elephant(E)')) premises.append(lp.parse(r'dove(D)')) premises.append(lp.parse(r'ostrich(O)')) #print the assumptions prover = Prover9Command(None, premises) command = UniqueNamesProver(ClosedWorldProver(prover)) for a in command.assumptions(): print a print_proof('-fly(E)', premises) print_proof('fly(D)', premises) print_proof('-fly(O)', premises) def print_proof(goal, premises): lp = LogicParser() prover = Prover9Command(lp.parse(goal), premises) command = UniqueNamesProver(ClosedWorldProver(prover)) print goal, prover.prove(), command.prove() def demo(): closed_domain_demo() unique_names_demo() closed_world_demo() combination_prover_demo() default_reasoning_demo() if __name__ == '__main__': demo() nltk-2.0~b9/nltk/inference/mace.py0000644000175000017500000002653511377057400016762 0ustar bhavanibhavani# Natural Language Toolkit: Interface to the Mace4 Model Builder # # Author: Dan Garrette # Ewan Klein # URL: # For license information, see LICENSE.TXT import os import tempfile from nltk.sem.logic import * from nltk.sem import Valuation from api import ModelBuilder, BaseModelBuilderCommand from prover9 import * """ A model builder that makes use of the external 'Mace4' package. """ class MaceCommand(Prover9CommandParent, BaseModelBuilderCommand): """ A L{MaceCommand} specific to the L{Mace} model builder. It contains a print_assumptions() method that is used to print the list of assumptions in multiple formats. """ _interpformat_bin = None def __init__(self, goal=None, assumptions=None, max_models=500, model_builder=None): """ @param goal: Input expression to prove @type goal: L{logic.Expression} @param assumptions: Input expressions to use as assumptions in the proof. @type assumptions: C{list} of L{logic.Expression} @param max_models: The maximum number of models that Mace will try before simply returning false. (Use 0 for no maximum.) @type max_models: C{int} """ if model_builder is not None: assert isinstance(model_builder, Mace) else: model_builder = Mace(max_models) BaseModelBuilderCommand.__init__(self, model_builder, goal, assumptions) valuation = property(lambda mbc: mbc.model('valuation')) def _convert2val(self, valuation_str): """ Transform the output file into an NLTK-style Valuation. @return: A model if one is generated; None otherwise. @rtype: L{nltk.sem.Valuation} """ valuation_standard_format = self._transform_output(valuation_str, 'standard') val = [] for line in valuation_standard_format.splitlines(False): l = line.strip() if l.startswith('interpretation'): # find the number of entities in the model num_entities = int(l[l.index('(')+1:l.index(',')].strip()) elif l.startswith('function') and l.find('_') == -1: # replace the integer identifier with a corresponding alphabetic character name = l[l.index('(')+1:l.index(',')].strip() if is_indvar(name): name = name.upper() value = int(l[l.index('[')+1:l.index(']')].strip()) val.append((name, MaceCommand._make_model_var(value))) elif l.startswith('relation'): l = l[l.index('(')+1:] if '(' in l: #relation is not nullary name = l[:l.index('(')].strip() values = [int(v.strip()) for v in l[l.index('[')+1:l.index(']')].split(',')] val.append((name, MaceCommand._make_relation_set(num_entities, values))) else: #relation is nullary name = l[:l.index(',')].strip() value = int(l[l.index('[')+1:l.index(']')].strip()) val.append((name, value == 1)) return Valuation(val) @staticmethod def _make_relation_set(num_entities, values): """ Convert a Mace4-style relation table into a dictionary. @parameter num_entities: the number of entities in the model; determines the row length in the table. @type num_entities: C{int} @parameter values: a list of 1's and 0's that represent whether a relation holds in a Mace4 model. @type values: C{list} of C{int} """ r = set() for position in [pos for (pos,v) in enumerate(values) if v == 1]: r.add(tuple(MaceCommand._make_relation_tuple(position, values, num_entities))) return r @staticmethod def _make_relation_tuple(position, values, num_entities): if len(values) == 1: return [] else: sublist_size = len(values) / num_entities sublist_start = position / sublist_size sublist_position = position % sublist_size sublist = values[sublist_start*sublist_size:(sublist_start+1)*sublist_size] return [MaceCommand._make_model_var(sublist_start)] + \ MaceCommand._make_relation_tuple(sublist_position, sublist, num_entities) @staticmethod def _make_model_var(value): """ Pick an alphabetic character as identifier for an entity in the model. @parameter value: where to index into the list of characters @type value: C{int} """ letter = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n', 'o','p','q','r','s','t','u','v','w','x','y','z'][value] num = int(value) / 26 if num > 0: return letter + str(num) else: return letter def _decorate_model(self, valuation_str, format): """ Print out a Mace4 model using any Mace4 C{interpformat} format. See U{http://www.cs.unm.edu/~mccune/mace4/manual/} for details. @param valuation_str: C{str} with the model builder's output @param format: C{str} indicating the format for displaying models. Defaults to 'standard' format. @return: C{str} """ if not format: return valuation_str elif format == 'valuation': return self._convert2val(valuation_str) else: return self._transform_output(valuation_str, format) def _transform_output(self, valuation_str, format): """ Transform the output file into any Mace4 C{interpformat} format. @parameter format: Output format for displaying models. @type format: C{str} """ if format in ['standard', 'standard2', 'portable', 'tabular', 'raw', 'cooked', 'xml', 'tex']: return self._call_interpformat(valuation_str, [format])[0] else: raise LookupError("The specified format does not exist") def _call_interpformat(self, input_str, args=[], verbose=False): """ Call the C{interpformat} binary with the given input. @param input_str: A string whose contents are used as stdin. @param args: A list of command-line arguments. @return: A tuple (stdout, returncode) @see: L{config_prover9} """ if self._interpformat_bin is None: self._interpformat_bin = self._modelbuilder._find_binary( 'interpformat', verbose) return self._modelbuilder._call(input_str, self._interpformat_bin, args, verbose) class Mace(Prover9Parent, ModelBuilder): _mace4_bin = None def __init__(self, end_size=500): self._end_size = end_size """The maximum model size that Mace will try before simply returning false. (Use -1 for no maximum.)""" def _build_model(self, goal=None, assumptions=None, verbose=False): """ Use Mace4 to build a first order model. @return: C{True} if a model was found (i.e. Mace returns value of 0), else C{False} """ if not assumptions: assumptions = [] stdout, returncode = self._call_mace4(self.prover9_input(goal, assumptions), verbose=verbose) return (returncode == 0, stdout) def _call_mace4(self, input_str, args=[], verbose=False): """ Call the C{mace4} binary with the given input. @param input_str: A string whose contents are used as stdin. @param args: A list of command-line arguments. @return: A tuple (stdout, returncode) @see: L{config_prover9} """ if self._mace4_bin is None: self._mace4_bin = self._find_binary('mace4', verbose) updated_input_str = '' if self._end_size > 0: updated_input_str += 'assign(end_size, %d).\n\n' % self._end_size updated_input_str += input_str return self._call(updated_input_str, self._mace4_bin, args, verbose) def spacer(num=30): print '-' * num def decode_result(found): """ Decode the result of model_found() @parameter found: The output of model_found() @type found: C{boolean} """ return {True: 'Countermodel found', False: 'No countermodel found', None: 'None'}[found] def test_model_found(arguments): """ Try some proofs and exhibit the results. """ lp = LogicParser() for (goal, assumptions) in arguments: g = lp.parse(goal) alist = [lp.parse(a) for a in assumptions] m = MaceCommand(g, assumptions=alist, end_size=50) found = m.build_model() for a in alist: print ' %s' % a print '|- %s: %s\n' % (g, decode_result(found)) def test_build_model(arguments): """ Try to build a L{nltk.sem.Valuation}. """ lp = LogicParser() g = lp.parse('all x.man(x)') alist = [lp.parse(a) for a in ['man(John)', 'man(Socrates)', 'man(Bill)', 'some x.(-(x = John) & man(x) & sees(John,x))', 'some x.(-(x = Bill) & man(x))', 'all x.some y.(man(x) -> gives(Socrates,x,y))']] m = MaceCommand(g, assumptions=alist) m.build_model() spacer() print "Assumptions and Goal" spacer() for a in alist: print ' %s' % a print '|- %s: %s\n' % (g, decode_result(m.build_model())) spacer() #print m.model('standard') #print m.model('cooked') print "Valuation" spacer() print m.valuation, '\n' def test_transform_output(argument_pair): """ Transform the model into various Mace4 C{interpformat} formats. """ lp = LogicParser() g = lp.parse(argument_pair[0]) alist = [lp.parse(a) for a in argument_pair[1]] m = MaceCommand(g, assumptions=alist) m.build_model() for a in alist: print ' %s' % a print '|- %s: %s\n' % (g, m.build_model()) for format in ['standard', 'portable', 'xml', 'cooked']: spacer() print "Using '%s' format" % format spacer() print m.model(format=format) def test_make_relation_set(): print MaceCommand._make_relation_set(num_entities=3, values=[1,0,1]) == set([('c',), ('a',)]) print MaceCommand._make_relation_set(num_entities=3, values=[0,0,0,0,0,0,1,0,0]) == set([('c', 'a')]) print MaceCommand._make_relation_set(num_entities=2, values=[0,0,1,0,0,0,1,0]) == set([('a', 'b', 'a'), ('b', 'b', 'a')]) arguments = [ ('mortal(Socrates)', ['all x.(man(x) -> mortal(x))', 'man(Socrates)']), ('(not mortal(Socrates))', ['all x.(man(x) -> mortal(x))', 'man(Socrates)']) ] def demo(): test_model_found(arguments) test_build_model(arguments) test_transform_output(arguments[1]) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/inference/discourse.py0000644000175000017500000005303311307071412020036 0ustar bhavanibhavani# Natural Language Toolkit: Discourse Processing # # Author: Ewan Klein # Dan Garrette # # URL: # For license information, see LICENSE.TXT # $Id: discourse.py 8435 2009-11-28 18:56:53Z dhgarrette $ import os from operator import and_, add import nltk from nltk.data import show_cfg from nltk.sem import root_semrep, Expression from mace import MaceCommand from prover9 import Prover9Command from nltk.tag import RegexpTagger from nltk.parse.malt import MaltParser from nltk.sem.drt import resolve_anaphora, AnaphoraResolutionException from nltk.sem.glue import DrtGlue """ Module for incrementally developing simple discourses, and checking for semantic ambiguity, consistency and informativeness. Many of the ideas are based on the CURT family of programs of Blackburn and Bos (see U{http://homepages.inf.ed.ac.uk/jbos/comsem/book1.html}). Consistency checking is carried out by using the L{mace} module to call the Mace4 model builder. Informativeness checking is carried out with a call to C{Prover.prove()} from the L{inference} module. C{DiscourseTester} is a constructor for discourses. The basic data structure is a list of sentences, stored as C{self._sentences}. Each sentence in the list is assigned a I{sentence ID} (C{sid}) of the form C{s}I{i}. For example:: s0: A boxer walks s1: Every boxer chases a girl Each sentence can be ambiguous between a number of readings, each of which receives a I{reading ID} (C{rid}) of the form C{s}I{i} -C{r}I{j}. For example:: s0 readings: s0-r1: some x.(boxer(x) & walk(x)) s0-r0: some x.(boxerdog(x) & walk(x)) A I{thread} is a list of readings, represented as a list of C{rid}s. Each thread receives a I{thread ID} (C{tid}) of the form C{d}I{i}. For example:: d0: ['s0-r0', 's1-r0'] The set of all threads for a discourse is the Cartesian product of all the readings of the sequences of sentences. (This is not intended to scale beyond very short discourses!) The method L{readings(filter=True)} will only show those threads which are consistent (taking into account any background assumptions). """ class ReadingCommand(object): def parse_to_readings(self, sentence): """ @param sentence: the sentence to read @type sentence: C{str} """ raise NotImplementedError() def process_thread(self, sentence_readings): """ This method should be used to handle dependencies between readings such as resolving anaphora. @param sentence_readings: readings to process @type sentence_readings: C{list} of C{Expression} @return: the list of readings after processing @rtype: C{list} of C{Expression} """ return sentence_readings def combine_readings(self, readings): """ @param readings: readings to combine @type readings: C{list} of C{Expression} @return: one combined reading @rtype: C{Expression} """ raise NotImplementedError() class CfgReadingCommand(ReadingCommand): def __init__(self, gramfile=None): """ @parameter gramfile: name of file where grammar can be loaded @type gramfile: C{str} """ if gramfile is None: self._gramfile = 'grammars/book_grammars/discourse.fcfg' else: self._gramfile = gramfile self._parser = nltk.parse.load_parser(self._gramfile) def parse_to_readings(self, sentence): """@see: ReadingCommand.parse_to_readings()""" tokens = sentence.split() trees = self._parser.nbest_parse(tokens) return [root_semrep(tree) for tree in trees] def combine_readings(self, readings): """@see: ReadingCommand.combine_readings()""" return reduce(and_, readings) class DrtGlueReadingCommand(ReadingCommand): def __init__(self, semtype_file=None, remove_duplicates=False, depparser=None): """ @param semtype_file: name of file where grammar can be loaded @param remove_duplicates: should duplicates be removed? @param depparser: the dependency parser """ if semtype_file is None: semtype_file = 'drt_glue.semtype' self._glue = DrtGlue(semtype_file=semtype_file, remove_duplicates=remove_duplicates, depparser=depparser) def parse_to_readings(self, sentence): """@see: ReadingCommand.parse_to_readings()""" return self._glue.parse_to_meaning(sentence) def process_thread(self, sentence_readings): """@see: ReadingCommand.process_thread()""" try: return [self.combine_readings(sentence_readings)] except AnaphoraResolutionException: return [] def combine_readings(self, readings): """@see: ReadingCommand.combine_readings()""" thread_reading = reduce(add, readings) return resolve_anaphora(thread_reading.simplify()) class DiscourseTester(object): """ Check properties of an ongoing discourse. """ def __init__(self, input, reading_command=None, background=None): """ Initialize a C{DiscourseTester}. @parameter input: the discourse sentences @type input: C{list} of C{str} @parameter background: Formulas which express background assumptions @type background: C{list} of L{logic.Expression}. """ self._input = input self._sentences = dict([('s%s' % i, sent) for i, sent in enumerate(input)]) self._models = None self._readings = {} if reading_command is None: self._reading_command = CfgReadingCommand() else: self._reading_command = reading_command self._threads = {} self._filtered_threads = {} if background is not None: for e in background: assert isinstance(e, Expression) self._background = background else: self._background = [] ############################### # Sentences ############################### def sentences(self): """ Display the list of sentences in the current discourse. """ for id in sorted(self._sentences.keys()): print "%s: %s" % (id, self._sentences[id]) def add_sentence(self, sentence, informchk=False, consistchk=False,): """ Add a sentence to the current discourse. Updates C{self._input} and C{self._sentences}. @parameter sentence: An input sentence @type sentence: C{str} @parameter informchk: if C{True}, check that the result of adding the sentence is thread-informative. Updates C{self._readings}. @parameter consistchk: if C{True}, check that the result of adding the sentence is thread-consistent. Updates C{self._readings}. """ # check whether the new sentence is informative (i.e. not entailed by the previous discourse) if informchk: self.readings(verbose=False) for tid in sorted(self._threads.keys()): assumptions = [reading for (rid, reading) in self.expand_threads(tid)] assumptions += self._background for sent_reading in self._get_readings(sentence): tp = Prover9Command(goal=sent_reading, assumptions=assumptions) if tp.prove(): print "Sentence '%s' under reading '%s':" % (sentence, str(sent_reading)) print "Not informative relative to thread '%s'" % tid self._input.append(sentence) self._sentences = dict([('s%s' % i, sent) for i, sent in enumerate(self._input)]) # check whether adding the new sentence to the discourse preserves consistency (i.e. a model can be found for the combined set of # of assumptions if consistchk: self.readings(verbose=False) self.models(show=False) def retract_sentence(self, sentence, verbose=True): """ Remove a sentence from the current discourse. Updates C{self._input}, C{self._sentences} and C{self._readings}. @parameter sentence: An input sentence @type sentence: C{str} @parameter verbose: If C{True}, report on the updated list of sentences. """ try: self._input.remove(sentence) except ValueError: print "Retraction failed. The sentence '%s' is not part of the current discourse:" % sentence self.sentences() return None self._sentences = dict([('s%s' % i, sent) for i, sent in enumerate(self._input)]) self.readings(verbose=False) if verbose: print "Current sentences are " self.sentences() def grammar(self): """ Print out the grammar in use for parsing input sentences """ show_cfg(self._reading_command._gramfile) ############################### # Readings and Threads ############################### def _get_readings(self, sentence): """ Build a list of semantic readings for a sentence. @rtype: C{list} of L{logic.Expression}. """ return self._reading_command.parse_to_readings(sentence) def _construct_readings(self): """ Use C{self._sentences} to construct a value for C{self._readings}. """ # re-initialize self._readings in case we have retracted a sentence self._readings = {} for sid, sentence in self._sentences.iteritems(): readings = self._get_readings(sentence) self._readings[sid] = dict([("%s-r%s" % (sid, rid), reading.simplify()) for rid, reading in enumerate(readings)]) def _construct_threads(self): """ Use C{self._readings} to construct a value for C{self._threads} and use the model builder to construct a value for C{self._filtered_threads} """ thread_list = [[]] for sid in sorted(self._readings.keys()): thread_list = self.multiply(thread_list, sorted(self._readings[sid].keys())) self._threads = dict([("d%s" % tid, thread) for tid, thread in enumerate(thread_list)]) # re-initialize the filtered threads self._filtered_threads = {} # keep the same ids, but only include threads which get models consistency_checked = self._check_consistency(self._threads) for (tid, thread) in self._threads.items(): if (tid, True) in consistency_checked: self._filtered_threads[tid] = thread def _show_readings(self, sentence=None): """ Print out the readings for the discourse (or a single sentence). """ if sentence is not None: print "The sentence '%s' has these readings:" % sentence for r in [str(reading) for reading in (self._get_readings(sentence))]: print " %s" % r else: for sid in sorted(self._readings.keys()): print print '%s readings:' % sid print #'-' * 30 for rid in sorted(self._readings[sid]): lf = self._readings[sid][rid] #TODO lf = lf.normalize('[xyz]\d*', 'z%d') print "%s: %s" % (rid, lf) def _show_threads(self, filter=False, show_thread_readings=False): """ Print out the value of C{self._threads} or C{self._filtered_hreads} """ if filter: threads = self._filtered_threads else: threads = self._threads for tid in sorted(threads.keys()): if show_thread_readings: readings = [self._readings[rid.split('-')[0]][rid] for rid in self._threads[tid]] try: thread_reading = ": %s" % \ self._reading_command.combine_readings(readings) except Exception, e: thread_reading = ': INVALID: %s' % e.__class__.__name__ else: thread_reading = '' print "%s:" % tid, self._threads[tid], thread_reading def readings(self, sentence=None, threaded=False, verbose=True, filter=False, show_thread_readings=False): """ Construct and show the readings of the discourse (or of a single sentence). @parameter sentence: test just this sentence @type sentence: C{str} @parameter threaded: if C{True}, print out each thread ID and the corresponding thread. @parameter filter: if C{True}, only print out consistent thread IDs and threads. """ self._construct_readings() self._construct_threads() # if we are filtering or showing thread readings, show threads if filter or show_thread_readings: threaded = True if verbose: if not threaded: self._show_readings(sentence=sentence) else: self._show_threads(filter=filter, show_thread_readings=show_thread_readings) def expand_threads(self, thread_id, threads=None): """ Given a thread ID, find the list of L{logic.Expression}s corresponding to the reading IDs in that thread. @parameter thread_id: thread ID @type thread_id: C{str} @parameter threads: a mapping from thread IDs to lists of reading IDs @type threads: C{dict} @return: A list of pairs (C{rid}, I{reading}) where I{reading} is the L{logic.Expression} associated with a reading ID @rtype: C{list} of C{tuple} """ if threads is None: threads = self._threads return [(rid, self._readings[sid][rid]) for rid in threads[thread_id] for sid in rid.split('-')[:1]] ############################### # Models and Background ############################### def _check_consistency(self, threads, show=False, verbose=False): results = [] for tid in sorted(threads.keys()): assumptions = [reading for (rid, reading) in self.expand_threads(tid, threads=threads)] assumptions = self._reading_command.process_thread(assumptions) if assumptions: assumptions += self._background # if Mace4 finds a model, it always seems to find it quickly mb = MaceCommand(None, assumptions, max_models=20) modelfound = mb.build_model() else: modelfound = False results.append((tid, modelfound)) if show: spacer(80) print "Model for Discourse Thread %s" % tid spacer(80) if verbose: for a in assumptions: print a spacer(80) if modelfound: print mb.model(format='cooked') else: print "No model found!\n" return results def models(self, thread_id=None, show=True, verbose=False): """ Call Mace4 to build a model for each current discourse thread. @parameter thread_id: thread ID @type thread_id: C{str} @parameter show: If C{True}, display the model that has been found. """ self._construct_readings() self._construct_threads() if thread_id is None: threads = self._threads else: threads = {thread_id: self._threads[thread_id]} for (tid, modelfound) in self._check_consistency(threads, show=show, verbose=verbose): idlist = [rid for rid in threads[tid]] if not modelfound: print "Inconsistent discourse: %s %s:" % (tid, idlist) for rid, reading in [(rid, str(reading)) for (rid, reading) in self.expand_threads(tid)]: print " %s: %s" % (rid, reading) print else: print "Consistent discourse: %s %s:" % (tid, idlist) for rid, reading in [(rid, str(reading)) for (rid, reading) in self.expand_threads(tid)]: print " %s: %s" % (rid, reading) print def add_background(self, background, verbose=False): """ Add a list of background assumptions for reasoning about the discourse. When called, this method also updates the discourse model's set of readings and threads. @parameter background: Formulas which contain background information @type background: C{list} of L{logic.Expression}. """ for (count, e) in enumerate(background): assert isinstance(e, Expression) if verbose: print "Adding assumption %s to background" % count self._background.append(e) #update the state self._construct_readings() self._construct_threads() def background(self): """ Show the current background assumptions. """ for e in self._background: print str(e) ############################### # Misc ############################### @staticmethod def multiply(discourse, readings): """ Multiply every thread in C{discourse} by every reading in C{readings}. Given discourse = [['A'], ['B']], readings = ['a', 'b', 'c'] , returns [['A', 'a'], ['A', 'b'], ['A', 'c'], ['B', 'a'], ['B', 'b'], ['B', 'c']] @parameter discourse: the current list of readings @type discourse: C{list} of C{list}s @parameter readings: an additional list of readings @type readings: C{list} of C{logic.Expression}s @rtype: A C{list} of C{list}s """ result = [] for sublist in discourse: for r in readings: new = [] new += sublist new.append(r) result.append(new) return result #multiply = DiscourseTester.multiply #L1 = [['A'], ['B']] #L2 = ['a', 'b', 'c'] #print multiply(L1,L2) def parse_fol(s): """ Temporarily duplicated from L{nltk.sem.util}. Convert a file of First Order Formulas into a list of C{Expression}s. @parameter s: the contents of the file @type s: C{str} @return: a list of parsed formulas. @rtype: C{list} of L{Expression} """ from nltk.sem import LogicParser statements = [] lp = LogicParser() for linenum, line in enumerate(s.splitlines()): line = line.strip() if line.startswith('#') or line=='': continue try: statements.append(lp.parse(line)) except Error: raise ValueError, 'Unable to parse line %s: %s' % (linenum, line) return statements ############################### # Demo ############################### def discourse_demo(reading_command=None): """ Illustrate the various methods of C{DiscourseTester} """ dt = DiscourseTester(['A boxer walks', 'Every boxer chases a girl'], reading_command) dt.models() print #dt.grammar() print dt.sentences() print dt.readings() print dt.readings(threaded=True) print dt.models('d1') dt.add_sentence('John is a boxer') print dt.sentences() print dt.readings(threaded=True) print dt = DiscourseTester(['A student dances', 'Every student is a person'], reading_command) print dt.add_sentence('No person dances', consistchk=True) print dt.readings() print dt.retract_sentence('No person dances', verbose=True) print dt.models() print dt.readings('A person dances') print dt.add_sentence('A person dances', informchk=True) dt = DiscourseTester(['Vincent is a boxer', 'Fido is a boxer', 'Vincent is married', 'Fido barks'], reading_command) dt.readings(filter=True) import nltk.data background = nltk.data.load('/grammars/book_grammars/background.fol') print dt.add_background(background, verbose=False) dt.background() print dt.readings(filter=True) print dt.models() def drt_discourse_demo(reading_command=None): """ Illustrate the various methods of C{DiscourseTester} """ dt = DiscourseTester(['every dog chases a boy', 'he runs'], reading_command) dt.models() print dt.sentences() print dt.readings() print dt.readings(show_thread_readings=True) print dt.readings(filter=True, show_thread_readings=True) def spacer(num=30): print '-' * num def demo(): discourse_demo() tagger = RegexpTagger( [('^(chases|runs)$', 'VB'), ('^(a)$', 'ex_quant'), ('^(every)$', 'univ_quant'), ('^(dog|boy)$', 'NN'), ('^(he)$', 'PRP') ]) depparser = MaltParser(tagger=tagger) drt_discourse_demo(DrtGlueReadingCommand(remove_duplicates=False, depparser=depparser)) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/inference/api.py0000644000175000017500000004675111303614105016616 0ustar bhavanibhavani# Natural Language Toolkit: Classifier Interface # # Author: Ewan Klein # Dan Garrette # # URL: # For license information, see LICENSE.TXT """ Interfaces and base classes for theorem provers and model builders. L{Prover} is a standard interface for a theorem prover which tries to prove a goal from a list of assumptions. L{ModelBuilder} is a standard interface for a model builder. Given just a set of assumptions. the model builder tries to build a model for the assumptions. Given a set of assumptions and a goal M{G}, the model builder tries to find a counter-model, in the sense of a model that will satisfy the assumptions plus the negation of M{G}. """ import threading import time class Prover(object): """ Interface for trying to prove a goal from assumptions. Both the goal and the assumptions are constrained to be formulas of L{logic.Expression}. """ def prove(self, goal=None, assumptions=None, verbose=False): """ @return: Whether the proof was successful or not. @rtype: C{bool} """ return self._prove(goal, assumptions, verbose)[0] def _prove(self, goal=None, assumptions=None, verbose=False): """ @return: Whether the proof was successful or not, along with the proof @rtype: C{tuple}: (C{bool}, C{str}) """ raise NotImplementedError() class ModelBuilder(object): """ Interface for trying to build a model of set of formulas. Open formulas are assumed to be universally quantified. Both the goal and the assumptions are constrained to be formulas of L{logic.Expression}. """ def build_model(self, goal=None, assumptions=None, verbose=False): """ Perform the actual model building. @return: Whether a model was generated @rtype: C{bool} """ return self._build_model(goal, assumptions, verbose)[0] def _build_model(self, goal=None, assumptions=None, verbose=False): """ Perform the actual model building. @return: Whether a model was generated, and the model itself @rtype: C{tuple} of (C{bool}, C{nltk.sem.evaluate.Valuation}) """ raise NotImplementedError() class TheoremToolCommand(object): """ This class holds a goal and a list of assumptions to be used in proving or model building. """ def add_assumptions(self, new_assumptions): """ Add new assumptions to the assumption list. @param new_assumptions: new assumptions @type new_assumptions: C{list} of C{Expression}s """ raise NotImplementedError() def retract_assumptions(self, retracted, debug=False): """ Retract assumptions from the assumption list. @param debug: If True, give warning when C{retracted} is not present on assumptions list. @type debug: C{bool} @param retracted: assumptions to be retracted @type retracted: C{list} of L{sem.logic.Expression}s """ raise NotImplementedError() def assumptions(self): """ List the current assumptions. @return: C{list} of C{Expression} """ raise NotImplementedError() def goal(self): """ Return the goal @return: C{Expression} """ raise NotImplementedError() def print_assumptions(self): """ Print the list of the current assumptions. """ raise NotImplementedError() class ProverCommand(TheoremToolCommand): """ This class holds a C{Prover}, a goal, and a list of assumptions. When prove() is called, the C{Prover} is executed with the goal and assumptions. """ def prove(self, verbose=False): """ Perform the actual proof. """ raise NotImplementedError() def proof(self, simplify=True): """ Return the proof string @param simplify: C{boolean} simplify the proof? @return: C{str} """ raise NotImplementedError() def get_prover(self): """ Return the prover object @return: C{Prover} """ raise NotImplementedError() class ModelBuilderCommand(TheoremToolCommand): """ This class holds a C{ModelBuilder}, a goal, and a list of assumptions. When build_model() is called, the C{ModelBuilder} is executed with the goal and assumptions. """ def build_model(self, verbose=False): """ Perform the actual model building. @return: A model if one is generated; None otherwise. @rtype: C{nltk.sem.evaluate.Valuation} """ raise NotImplementedError() def model(self, format=None): """ Return a string representation of the model @param simplify: C{boolean} simplify the proof? @return: C{str} """ raise NotImplementedError() def get_model_builder(self): """ Return the model builder object @return: C{ModelBuilder} """ raise NotImplementedError() class BaseTheoremToolCommand(TheoremToolCommand): """ This class holds a goal and a list of assumptions to be used in proving or model building. """ def __init__(self, goal=None, assumptions=None): """ @param goal: Input expression to prove @type goal: L{logic.Expression} @param assumptions: Input expressions to use as assumptions in the proof. @type assumptions: C{list} of L{logic.Expression} """ self._goal = goal if not assumptions: self._assumptions = [] else: self._assumptions = list(assumptions) self._result = None """A holder for the result, to prevent unnecessary re-proving""" def add_assumptions(self, new_assumptions): """ Add new assumptions to the assumption list. @param new_assumptions: new assumptions @type new_assumptions: C{list} of L{sem.logic.Expression}s """ self._assumptions.extend(new_assumptions) self._result = None def retract_assumptions(self, retracted, debug=False): """ Retract assumptions from the assumption list. @param debug: If True, give warning when C{retracted} is not present on assumptions list. @type debug: C{bool} @param retracted: assumptions to be retracted @type retracted: C{list} of L{sem.logic.Expression}s """ retracted = set(retracted) result_list = filter(lambda a: a not in retracted, self._assumptions) if debug and result_list == self._assumptions: print Warning("Assumptions list has not been changed:") self.print_assumptions() self._assumptions = result_list self._result = None def assumptions(self): """ List the current assumptions. @return: C{list} of C{Expression} """ return self._assumptions def goal(self): """ Return the goal @return: C{Expression} """ return self._goal def print_assumptions(self): """ Print the list of the current assumptions. """ for a in self.assumptions(): print a class BaseProverCommand(BaseTheoremToolCommand, ProverCommand): """ This class holds a C{Prover}, a goal, and a list of assumptions. When prove() is called, the C{Prover} is executed with the goal and assumptions. """ def __init__(self, prover, goal=None, assumptions=None): """ @param prover: The theorem tool to execute with the assumptions @type prover: C{Prover} @see: C{BaseTheoremToolCommand} """ self._prover = prover """The theorem tool to execute with the assumptions""" BaseTheoremToolCommand.__init__(self, goal, assumptions) self._proof = None def prove(self, verbose=False): """ Perform the actual proof. Store the result to prevent unnecessary re-proving. """ if self._result is None: self._result, self._proof = self._prover._prove(self.goal(), self.assumptions(), verbose) return self._result def proof(self, simplify=True): """ Return the proof string @param simplify: C{boolean} simplify the proof? @return: C{str} """ if self._result is None: raise LookupError("You have to call prove() first to get a proof!") else: return self.decorate_proof(self._proof, simplify) def decorate_proof(self, proof_string, simplify=True): """ Modify and return the proof string @param proof_string: C{str} the proof to decorate @param simplify: C{boolean} simplify the proof? @return: C{str} """ return proof_string def get_prover(self): return self._prover class BaseModelBuilderCommand(BaseTheoremToolCommand, ModelBuilderCommand): """ This class holds a C{ModelBuilder}, a goal, and a list of assumptions. When build_model() is called, the C{ModelBuilder} is executed with the goal and assumptions. """ def __init__(self, modelbuilder, goal=None, assumptions=None): """ @param modelbuilder: The theorem tool to execute with the assumptions @type modelbuilder: C{ModelBuilder} @see: C{BaseTheoremToolCommand} """ self._modelbuilder = modelbuilder """The theorem tool to execute with the assumptions""" BaseTheoremToolCommand.__init__(self, goal, assumptions) self._model = None def build_model(self, verbose=False): """ Attempt to build a model. Store the result to prevent unnecessary re-building. """ if self._result is None: self._result, self._model = \ self._modelbuilder._build_model(self.goal(), self.assumptions(), verbose) return self._result def model(self, format=None): """ Return a string representation of the model @param simplify: C{boolean} simplify the proof? @return: C{str} """ if self._result is None: raise LookupError('You have to call build_model() first to ' 'get a model!') else: return self._decorate_model(self._model, format) def _decorate_model(self, valuation_str, format=None): """ @param valuation_str: C{str} with the model builder's output @param format: C{str} indicating the format for displaying @return: C{str} """ return valuation_str def get_model_builder(self): return self._modelbuilder class TheoremToolCommandDecorator(TheoremToolCommand): """ A base decorator for the C{ProverCommandDecorator} and C{ModelBuilderCommandDecorator} classes from which decorators can extend. """ def __init__(self, command): """ @param command: C{TheoremToolCommand} to decorate """ self._command = command #The decorator has its own versions of 'result' different from the #underlying command self._result = None def assumptions(self): return self._command.assumptions() def goal(self): return self._command.goal() def add_assumptions(self, new_assumptions): self._command.add_assumptions(new_assumptions) self._result = None def retract_assumptions(self, retracted, debug=False): self._command.retract_assumptions(retracted, debug) self._result = None def print_assumptions(self): self._command.print_assumptions() class ProverCommandDecorator(TheoremToolCommandDecorator, ProverCommand): """ A base decorator for the C{ProverCommand} class from which other prover command decorators can extend. """ def __init__(self, proverCommand): """ @param proverCommand: C{ProverCommand} to decorate """ TheoremToolCommandDecorator.__init__(self, proverCommand) #The decorator has its own versions of 'result' and 'proof' #because they may be different from the underlying command self._proof = None def prove(self, verbose=False): if self._result is None: prover = self.get_prover() self._result, self._proof = prover._prove(self.goal(), self.assumptions(), verbose) return self._result def proof(self, simplify=True): """ Return the proof string @param simplify: C{boolean} simplify the proof? @return: C{str} """ if self._result is None: raise LookupError("You have to call prove() first to get a proof!") else: return self.decorate_proof(self._proof, simplify) def decorate_proof(self, proof_string, simplify=True): """ Modify and return the proof string @param proof_string: C{str} the proof to decorate @param simplify: C{boolean} simplify the proof? @return: C{str} """ return self._command.decorate_proof(proof_string, simplify) def get_prover(self): return self._command.get_prover() class ModelBuilderCommandDecorator(TheoremToolCommandDecorator, ModelBuilderCommand): """ A base decorator for the C{ModelBuilderCommand} class from which other prover command decorators can extend. """ def __init__(self, modelBuilderCommand): """ @param modelBuilderCommand: C{ModelBuilderCommand} to decorate """ TheoremToolCommandDecorator.__init__(self, modelBuilderCommand) #The decorator has its own versions of 'result' and 'valuation' #because they may be different from the underlying command self._model = None def build_model(self, verbose=False): """ Attempt to build a model. Store the result to prevent unnecessary re-building. """ if self._result is None: modelbuilder = self.get_model_builder() self._result, self._model = \ modelbuilder._build_model(self.goal(), self.assumptions(), verbose) return self._result def model(self, format=None): """ Return a string representation of the model @param simplify: C{boolean} simplify the proof? @return: C{str} """ if self._result is None: raise LookupError('You have to call build_model() first to ' 'get a model!') else: return self._decorate_model(self._model, format) def _decorate_model(self, valuation_str, format=None): """ Modify and return the proof string @param valuation_str: C{str} with the model builder's output @param format: C{str} indicating the format for displaying @return: C{str} """ return self._command._decorate_model(valuation_str, format) def get_model_builder(self): return self._command.get_prover() class ParallelProverBuilder(Prover, ModelBuilder): """ This class stores both a prover and a model builder and when either prove() or build_model() is called, then both theorem tools are run in parallel. Whichever finishes first, the prover or the model builder, is the result that will be used. """ def __init__(self, prover, modelbuilder): self._prover = prover self._modelbuilder = modelbuilder def _prove(self, goal=None, assumptions=None, verbose=False): return self._run(goal, assumptions, verbose), '' def _build_model(self, goal=None, assumptions=None, verbose=False): return not self._run(goal, assumptions, verbose), '' def _run(self, goal, assumptions, verbose): # Set up two thread, Prover and ModelBuilder to run in parallel tp_thread = TheoremToolThread(lambda: self._prover.prove(goal, assumptions, verbose), verbose, 'TP') mb_thread = TheoremToolThread(lambda: self._modelbuilder.build_model(goal, assumptions, verbose), verbose, 'MB') tp_thread.start() mb_thread.start() while tp_thread.isAlive() and mb_thread.isAlive(): # wait until either the prover or the model builder is done pass if tp_thread.result is not None: return tp_thread.result elif mb_thread.result is not None: return not mb_thread.result else: return None class ParallelProverBuilderCommand(BaseProverCommand, BaseModelBuilderCommand): """ This command stores both a prover and a model builder and when either prove() or build_model() is called, then both theorem tools are run in parallel. Whichever finishes first, the prover or the model builder, is the result that will be used. Because the theorem prover result is the opposite of the model builder result, we will treat self._result as meaning "proof found/no model found". """ def __init__(self, prover, modelbuilder, goal=None, assumptions=None): BaseProverCommand.__init__(self, prover, goal, assumptions) BaseModelBuilderCommand.__init__(self, modelbuilder, goal, assumptions) def prove(self, verbose=False): return self._run(verbose) def build_model(self, verbose=False): return not self._run(verbose) def _run(self, verbose): # Set up two thread, Prover and ModelBuilder to run in parallel tp_thread = TheoremToolThread(lambda: BaseProverCommand.prove(self, verbose), verbose, 'TP') mb_thread = TheoremToolThread(lambda: BaseModelBuilderCommand.build_model(self, verbose), verbose, 'MB') tp_thread.start() mb_thread.start() while tp_thread.isAlive() and mb_thread.isAlive(): # wait until either the prover or the model builder is done pass if tp_thread.result is not None: self._result = tp_thread.result elif mb_thread.result is not None: self._result = not mb_thread.result return self._result class TheoremToolThread(threading.Thread): def __init__(self, command, verbose, name=None): threading.Thread.__init__(self) self._command = command self._result = None self._verbose = verbose self._name = name def run(self): try: self._result = self._command() if self._verbose: print 'Thread %s finished with result %s at %s' % \ (self._name, self._result, time.localtime(time.time())) except Exception, e: print e print 'Thread %s completed abnormally' % (self._name) result = property(lambda self: self._result) nltk-2.0~b9/nltk/inference/__init__.py0000644000175000017500000000146311327451576017614 0ustar bhavanibhavani# Natural Language Toolkit: Inference # # Copyright (C) 2001-2010 NLTK Project # Author: Dan Garrette # Ewan Klein # # URL: # For license information, see LICENSE.TXT """ Classes and interfaces for theorem proving and model building. """ from api import * from mace import * from prover9 import * from resolution import * from tableau import * from discourse import * __all__ = [ # inference tools 'Prover9', 'Prover9Command', 'TableauProver', 'TableauProverCommand', 'ResolutionProver', 'ResolutionProverCommand', 'Mace', 'MaceCommand', 'ParallelProverBuilder', 'ParallelProverBuilderCommand', # discourse 'ReadingCommand', 'CfgReadingCommand', 'DrtGlueReadingCommand', 'DiscourseTester' ] nltk-2.0~b9/nltk/examples/pt.py0000644000175000017500000000372511332125236016346 0ustar bhavanibhavani# -*- coding: iso-8859-1 -*- # Natural Language Toolkit: Some Portuguese texts for exploration in chapter 1 of the book # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT from nltk.corpus import machado, mac_morpho, floresta, genesis from nltk.text import Text from nltk.probability import FreqDist from nltk.util import bigrams from nltk.misc import babelize_shell print "*** Introductory Examples for the NLTK Book ***" print "Loading ptext1, ... and psent1, ..." print "Type the name of the text or sentence to view it." print "Type: 'texts()' or 'sents()' to list the materials." ptext1 = Text(machado.words('romance/marm05.txt'), name="Memórias Póstumas de Brás Cubas (1881)") print "ptext1:", ptext1.name.decode('latin-1') ptext2 = Text(machado.words('romance/marm08.txt'), name="Dom Casmurro (1899)") print "ptext2:", ptext2.name.decode('latin-1') ptext3 = Text(genesis.words('portuguese.txt'), name="Gênesis") print "ptext3:", ptext3.name.decode('latin-1') ptext4 = Text(mac_morpho.words('mu94se01.txt'), name="Folha de Sau Paulo (1994)") print "ptext4:", ptext4.name.decode('latin-1') def texts(): print "ptext1:", ptext1.name.decode('latin-1') print "ptext2:", ptext2.name.decode('latin-1') print "ptext3:", ptext3.name.decode('latin-1') print "ptext4:", ptext4.name.decode('latin-1') psent1 = "o amor da glória era a coisa mais verdadeiramente humana que há no homem , e , conseqüentemente , a sua mais genuína feição .".split() psent2 = "Não consultes dicionários .".split() psent3 = "No princípio, criou Deus os céus e a terra.".split() psent4 = "A Cáritas acredita que outros cubanos devem chegar ao Brasil .".split() def sents(): print "psent1:", " ".join(psent1).decode('latin-1') print "psent2:", " ".join(psent2).decode('latin-1') print "psent3:", " ".join(psent3).decode('latin-1') print "psent4:", " ".join(psent4).decode('latin-1') nltk-2.0~b9/nltk/examples/__init__.py0000644000175000017500000000000011303614106017436 0ustar bhavanibhavaninltk-2.0~b9/nltk/etree/ElementTree.py0000644000175000017500000012014011140171432017405 0ustar bhavanibhavani# # ElementTree # $Id: ElementTree.py 2326 2005-03-17 07:45:21Z fredrik $ # # light-weight XML support for Python 1.5.2 and later. # # history: # 2001-10-20 fl created (from various sources) # 2001-11-01 fl return root from parse method # 2002-02-16 fl sort attributes in lexical order # 2002-04-06 fl TreeBuilder refactoring, added PythonDoc markup # 2002-05-01 fl finished TreeBuilder refactoring # 2002-07-14 fl added basic namespace support to ElementTree.write # 2002-07-25 fl added QName attribute support # 2002-10-20 fl fixed encoding in write # 2002-11-24 fl changed default encoding to ascii; fixed attribute encoding # 2002-11-27 fl accept file objects or file names for parse/write # 2002-12-04 fl moved XMLTreeBuilder back to this module # 2003-01-11 fl fixed entity encoding glitch for us-ascii # 2003-02-13 fl added XML literal factory # 2003-02-21 fl added ProcessingInstruction/PI factory # 2003-05-11 fl added tostring/fromstring helpers # 2003-05-26 fl added ElementPath support # 2003-07-05 fl added makeelement factory method # 2003-07-28 fl added more well-known namespace prefixes # 2003-08-15 fl fixed typo in ElementTree.findtext (Thomas Dartsch) # 2003-09-04 fl fall back on emulator if ElementPath is not installed # 2003-10-31 fl markup updates # 2003-11-15 fl fixed nested namespace bug # 2004-03-28 fl added XMLID helper # 2004-06-02 fl added default support to findtext # 2004-06-08 fl fixed encoding of non-ascii element/attribute names # 2004-08-23 fl take advantage of post-2.1 expat features # 2005-02-01 fl added iterparse implementation # 2005-03-02 fl fixed iterparse support for pre-2.2 versions # # Copyright (c) 1999-2005 by Fredrik Lundh. All rights reserved. # # fredrik@pythonware.com # http://www.pythonware.com # # -------------------------------------------------------------------- # The ElementTree toolkit is # # Copyright (c) 1999-2005 by Fredrik Lundh # # By obtaining, using, and/or copying this software and/or its # associated documentation, you agree that you have read, understood, # and will comply with the following terms and conditions: # # Permission to use, copy, modify, and distribute this software and # its associated documentation for any purpose and without fee is # hereby granted, provided that the above copyright notice appears in # all copies, and that both that copyright notice and this permission # notice appear in supporting documentation, and that the name of # Secret Labs AB or the author not be used in advertising or publicity # pertaining to distribution of the software without specific, written # prior permission. # # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE # OF THIS SOFTWARE. # -------------------------------------------------------------------- # Licensed to PSF under a Contributor Agreement. # See http://www.python.org/2.4/license for licensing details. __all__ = [ # public symbols "Comment", "dump", "Element", "ElementTree", "fromstring", "iselement", "iterparse", "parse", "PI", "ProcessingInstruction", "QName", "SubElement", "tostring", "TreeBuilder", "VERSION", "XML", "XMLParser", "XMLTreeBuilder", ] ## # The Element type is a flexible container object, designed to # store hierarchical data structures in memory. The type can be # described as a cross between a list and a dictionary. #

# Each element has a number of properties associated with it: #

    #
  • a tag. This is a string identifying what kind of data # this element represents (the element type, in other words).
  • #
  • a number of attributes, stored in a Python dictionary.
  • #
  • a text string.
  • #
  • an optional tail string.
  • #
  • a number of child elements, stored in a Python sequence
  • #
# # To create an element instance, use the {@link #Element} or {@link # #SubElement} factory functions. #

# The {@link #ElementTree} class can be used to wrap an element # structure, and convert it from and to XML. ## import string, sys, re class _SimpleElementPath: # emulate pre-1.2 find/findtext/findall behaviour def find(self, element, tag): for elem in element: if elem.tag == tag: return elem return None def findtext(self, element, tag, default=None): for elem in element: if elem.tag == tag: return elem.text or "" return default def findall(self, element, tag): if tag[:3] == ".//": return element.getiterator(tag[3:]) result = [] for elem in element: if elem.tag == tag: result.append(elem) return result try: import ElementPath except ImportError: # FIXME: issue warning in this case? ElementPath = _SimpleElementPath() # TODO: add support for custom namespace resolvers/default namespaces # TODO: add improved support for incremental parsing VERSION = "1.2.6" ## # Internal element class. This class defines the Element interface, # and provides a reference implementation of this interface. #

# You should not create instances of this class directly. Use the # appropriate factory functions instead, such as {@link #Element} # and {@link #SubElement}. # # @see Element # @see SubElement # @see Comment # @see ProcessingInstruction class _ElementInterface: # text...tail ## # (Attribute) Element tag. tag = None ## # (Attribute) Element attribute dictionary. Where possible, use # {@link #_ElementInterface.get}, # {@link #_ElementInterface.set}, # {@link #_ElementInterface.keys}, and # {@link #_ElementInterface.items} to access # element attributes. attrib = None ## # (Attribute) Text before first subelement. This is either a # string or the value None, if there was no text. text = None ## # (Attribute) Text after this element's end tag, but before the # next sibling element's start tag. This is either a string or # the value None, if there was no text. tail = None # text after end tag, if any def __init__(self, tag, attrib): self.tag = tag self.attrib = attrib self._children = [] def __repr__(self): return "" % (self.tag, id(self)) ## # Creates a new element object of the same type as this element. # # @param tag Element tag. # @param attrib Element attributes, given as a dictionary. # @return A new element instance. def makeelement(self, tag, attrib): return Element(tag, attrib) ## # Returns the number of subelements. # # @return The number of subelements. def __len__(self): return len(self._children) ## # Returns the given subelement. # # @param index What subelement to return. # @return The given subelement. # @exception IndexError If the given element does not exist. def __getitem__(self, index): return self._children[index] ## # Replaces the given subelement. # # @param index What subelement to replace. # @param element The new element value. # @exception IndexError If the given element does not exist. # @exception AssertionError If element is not a valid object. def __setitem__(self, index, element): assert iselement(element) self._children[index] = element ## # Deletes the given subelement. # # @param index What subelement to delete. # @exception IndexError If the given element does not exist. def __delitem__(self, index): del self._children[index] ## # Returns a list containing subelements in the given range. # # @param start The first subelement to return. # @param stop The first subelement that shouldn't be returned. # @return A sequence object containing subelements. def __getslice__(self, start, stop): return self._children[start:stop] ## # Replaces a number of subelements with elements from a sequence. # # @param start The first subelement to replace. # @param stop The first subelement that shouldn't be replaced. # @param elements A sequence object with zero or more elements. # @exception AssertionError If a sequence member is not a valid object. def __setslice__(self, start, stop, elements): for element in elements: assert iselement(element) self._children[start:stop] = list(elements) ## # Deletes a number of subelements. # # @param start The first subelement to delete. # @param stop The first subelement to leave in there. def __delslice__(self, start, stop): del self._children[start:stop] ## # Adds a subelement to the end of this element. # # @param element The element to add. # @exception AssertionError If a sequence member is not a valid object. def append(self, element): assert iselement(element) self._children.append(element) ## # Inserts a subelement at the given position in this element. # # @param index Where to insert the new subelement. # @exception AssertionError If the element is not a valid object. def insert(self, index, element): assert iselement(element) self._children.insert(index, element) ## # Removes a matching subelement. Unlike the find methods, # this method compares elements based on identity, not on tag # value or contents. # # @param element What element to remove. # @exception ValueError If a matching element could not be found. # @exception AssertionError If the element is not a valid object. def remove(self, element): assert iselement(element) self._children.remove(element) ## # Returns all subelements. The elements are returned in document # order. # # @return A list of subelements. # @defreturn list of Element instances def getchildren(self): return self._children ## # Finds the first matching subelement, by tag name or path. # # @param path What element to look for. # @return The first matching element, or None if no element was found. # @defreturn Element or None def find(self, path): return ElementPath.find(self, path) ## # Finds text for the first matching subelement, by tag name or path. # # @param path What element to look for. # @param default What to return if the element was not found. # @return The text content of the first matching element, or the # default value no element was found. Note that if the element # has is found, but has no text content, this method returns an # empty string. # @defreturn string def findtext(self, path, default=None): return ElementPath.findtext(self, path, default) ## # Finds all matching subelements, by tag name or path. # # @param path What element to look for. # @return A list or iterator containing all matching elements, # in document order. # @defreturn list of Element instances def findall(self, path): return ElementPath.findall(self, path) ## # Resets an element. This function removes all subelements, clears # all attributes, and sets the text and tail attributes to None. def clear(self): self.attrib.clear() self._children = [] self.text = self.tail = None ## # Gets an element attribute. # # @param key What attribute to look for. # @param default What to return if the attribute was not found. # @return The attribute value, or the default value, if the # attribute was not found. # @defreturn string or None def get(self, key, default=None): return self.attrib.get(key, default) ## # Sets an element attribute. # # @param key What attribute to set. # @param value The attribute value. def set(self, key, value): self.attrib[key] = value ## # Gets a list of attribute names. The names are returned in an # arbitrary order (just like for an ordinary Python dictionary). # # @return A list of element attribute names. # @defreturn list of strings def keys(self): return self.attrib.keys() ## # Gets element attributes, as a sequence. The attributes are # returned in an arbitrary order. # # @return A list of (name, value) tuples for all attributes. # @defreturn list of (string, string) tuples def items(self): return self.attrib.items() ## # Creates a tree iterator. The iterator loops over this element # and all subelements, in document order, and returns all elements # with a matching tag. #

# If the tree structure is modified during iteration, the result # is undefined. # # @param tag What tags to look for (default is to return all elements). # @return A list or iterator containing all the matching elements. # @defreturn list or iterator def getiterator(self, tag=None): nodes = [] if tag == "*": tag = None if tag is None or self.tag == tag: nodes.append(self) for node in self._children: nodes.extend(node.getiterator(tag)) return nodes # compatibility _Element = _ElementInterface ## # Element factory. This function returns an object implementing the # standard Element interface. The exact class or type of that object # is implementation dependent, but it will always be compatible with # the {@link #_ElementInterface} class in this module. #

# The element name, attribute names, and attribute values can be # either 8-bit ASCII strings or Unicode strings. # # @param tag The element name. # @param attrib An optional dictionary, containing element attributes. # @param **extra Additional attributes, given as keyword arguments. # @return An element instance. # @defreturn Element def Element(tag, attrib={}, **extra): attrib = attrib.copy() attrib.update(extra) return _ElementInterface(tag, attrib) ## # Subelement factory. This function creates an element instance, and # appends it to an existing element. #

# The element name, attribute names, and attribute values can be # either 8-bit ASCII strings or Unicode strings. # # @param parent The parent element. # @param tag The subelement name. # @param attrib An optional dictionary, containing element attributes. # @param **extra Additional attributes, given as keyword arguments. # @return An element instance. # @defreturn Element def SubElement(parent, tag, attrib={}, **extra): attrib = attrib.copy() attrib.update(extra) element = parent.makeelement(tag, attrib) parent.append(element) return element ## # Comment element factory. This factory function creates a special # element that will be serialized as an XML comment. #

# The comment string can be either an 8-bit ASCII string or a Unicode # string. # # @param text A string containing the comment string. # @return An element instance, representing a comment. # @defreturn Element def Comment(text=None): element = Element(Comment) element.text = text return element ## # PI element factory. This factory function creates a special element # that will be serialized as an XML processing instruction. # # @param target A string containing the PI target. # @param text A string containing the PI contents, if any. # @return An element instance, representing a PI. # @defreturn Element def ProcessingInstruction(target, text=None): element = Element(ProcessingInstruction) element.text = target if text: element.text = element.text + " " + text return element PI = ProcessingInstruction ## # QName wrapper. This can be used to wrap a QName attribute value, in # order to get proper namespace handling on output. # # @param text A string containing the QName value, in the form {uri}local, # or, if the tag argument is given, the URI part of a QName. # @param tag Optional tag. If given, the first argument is interpreted as # an URI, and this argument is interpreted as a local name. # @return An opaque object, representing the QName. class QName: def __init__(self, text_or_uri, tag=None): if tag: text_or_uri = "{%s}%s" % (text_or_uri, tag) self.text = text_or_uri def __str__(self): return self.text def __hash__(self): return hash(self.text) def __cmp__(self, other): if isinstance(other, QName): return cmp(self.text, other.text) return cmp(self.text, other) ## # ElementTree wrapper class. This class represents an entire element # hierarchy, and adds some extra support for serialization to and from # standard XML. # # @param element Optional root element. # @keyparam file Optional file handle or name. If given, the # tree is initialized with the contents of this XML file. class ElementTree: def __init__(self, element=None, file=None): assert element is None or iselement(element) self._root = element # first node if file: self.parse(file) ## # Gets the root element for this tree. # # @return An element instance. # @defreturn Element def getroot(self): return self._root ## # Replaces the root element for this tree. This discards the # current contents of the tree, and replaces it with the given # element. Use with care. # # @param element An element instance. def _setroot(self, element): assert iselement(element) self._root = element ## # Loads an external XML document into this element tree. # # @param source A file name or file object. # @param parser An optional parser instance. If not given, the # standard {@link XMLTreeBuilder} parser is used. # @return The document root element. # @defreturn Element def parse(self, source, parser=None): if not hasattr(source, "read"): source = open(source, "rb") if not parser: parser = XMLTreeBuilder() while 1: data = source.read(32768) if not data: break parser.feed(data) self._root = parser.close() return self._root ## # Creates a tree iterator for the root element. The iterator loops # over all elements in this tree, in document order. # # @param tag What tags to look for (default is to return all elements) # @return An iterator. # @defreturn iterator def getiterator(self, tag=None): assert self._root is not None return self._root.getiterator(tag) ## # Finds the first toplevel element with given tag. # Same as getroot().find(path). # # @param path What element to look for. # @return The first matching element, or None if no element was found. # @defreturn Element or None def find(self, path): assert self._root is not None if path[:1] == "/": path = "." + path return self._root.find(path) ## # Finds the element text for the first toplevel element with given # tag. Same as getroot().findtext(path). # # @param path What toplevel element to look for. # @param default What to return if the element was not found. # @return The text content of the first matching element, or the # default value no element was found. Note that if the element # has is found, but has no text content, this method returns an # empty string. # @defreturn string def findtext(self, path, default=None): assert self._root is not None if path[:1] == "/": path = "." + path return self._root.findtext(path, default) ## # Finds all toplevel elements with the given tag. # Same as getroot().findall(path). # # @param path What element to look for. # @return A list or iterator containing all matching elements, # in document order. # @defreturn list of Element instances def findall(self, path): assert self._root is not None if path[:1] == "/": path = "." + path return self._root.findall(path) ## # Writes the element tree to a file, as XML. # # @param file A file name, or a file object opened for writing. # @param encoding Optional output encoding (default is US-ASCII). def write(self, file, encoding="us-ascii"): assert self._root is not None if not hasattr(file, "write"): file = open(file, "wb") if not encoding: encoding = "us-ascii" elif encoding != "utf-8" and encoding != "us-ascii": file.write("\n" % encoding) self._write(file, self._root, encoding, {}) def _write(self, file, node, encoding, namespaces): # write XML to file tag = node.tag if tag is Comment: file.write("" % _escape_cdata(node.text, encoding)) elif tag is ProcessingInstruction: file.write("" % _escape_cdata(node.text, encoding)) else: items = node.items() xmlns_items = [] # new namespaces in this scope try: if isinstance(tag, QName) or tag[:1] == "{": tag, xmlns = fixtag(tag, namespaces) if xmlns: xmlns_items.append(xmlns) except TypeError: _raise_serialization_error(tag) file.write("<" + _encode(tag, encoding)) if items or xmlns_items: items.sort() # lexical order for k, v in items: try: if isinstance(k, QName) or k[:1] == "{": k, xmlns = fixtag(k, namespaces) if xmlns: xmlns_items.append(xmlns) except TypeError: _raise_serialization_error(k) try: if isinstance(v, QName): v, xmlns = fixtag(v, namespaces) if xmlns: xmlns_items.append(xmlns) except TypeError: _raise_serialization_error(v) file.write(" %s=\"%s\"" % (_encode(k, encoding), _escape_attrib(v, encoding))) for k, v in xmlns_items: file.write(" %s=\"%s\"" % (_encode(k, encoding), _escape_attrib(v, encoding))) if node.text or len(node): file.write(">") if node.text: file.write(_escape_cdata(node.text, encoding)) for n in node: self._write(file, n, encoding, namespaces) file.write("") else: file.write(" />") for k, v in xmlns_items: del namespaces[v] if node.tail: file.write(_escape_cdata(node.tail, encoding)) # -------------------------------------------------------------------- # helpers ## # Checks if an object appears to be a valid element object. # # @param An element instance. # @return A true value if this is an element object. # @defreturn flag def iselement(element): # FIXME: not sure about this; might be a better idea to look # for tag/attrib/text attributes return isinstance(element, _ElementInterface) or hasattr(element, "tag") ## # Writes an element tree or element structure to sys.stdout. This # function should be used for debugging only. #

# The exact output format is implementation dependent. In this # version, it's written as an ordinary XML file. # # @param elem An element tree or an individual element. def dump(elem): # debugging if not isinstance(elem, ElementTree): elem = ElementTree(elem) elem.write(sys.stdout) tail = elem.getroot().tail if not tail or tail[-1] != "\n": sys.stdout.write("\n") def _encode(s, encoding): try: return s.encode(encoding) except AttributeError: return s # 1.5.2: assume the string uses the right encoding if sys.version[:3] == "1.5": _escape = re.compile(r"[&<>\"\x80-\xff]+") # 1.5.2 else: _escape = re.compile(eval(r'u"[&<>\"\u0080-\uffff]+"')) _escape_map = { "&": "&", "<": "<", ">": ">", '"': """, } _namespace_map = { # "well-known" namespace prefixes "http://www.w3.org/XML/1998/namespace": "xml", "http://www.w3.org/1999/xhtml": "html", "http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf", "http://schemas.xmlsoap.org/wsdl/": "wsdl", } def _raise_serialization_error(text): raise TypeError( "cannot serialize %r (type %s)" % (text, type(text).__name__) ) def _encode_entity(text, pattern=_escape): # map reserved and non-ascii characters to numerical entities def escape_entities(m, map=_escape_map): out = [] append = out.append for char in m.group(): text = map.get(char) if text is None: text = "&#%d;" % ord(char) append(text) return string.join(out, "") try: return _encode(pattern.sub(escape_entities, text), "ascii") except TypeError: _raise_serialization_error(text) # # the following functions assume an ascii-compatible encoding # (or "utf-16") def _escape_cdata(text, encoding=None, replace=string.replace): # escape character data try: if encoding: try: text = _encode(text, encoding) except UnicodeError: return _encode_entity(text) text = replace(text, "&", "&") text = replace(text, "<", "<") text = replace(text, ">", ">") return text except (TypeError, AttributeError): _raise_serialization_error(text) def _escape_attrib(text, encoding=None, replace=string.replace): # escape attribute value try: if encoding: try: text = _encode(text, encoding) except UnicodeError: return _encode_entity(text) text = replace(text, "&", "&") text = replace(text, "'", "'") # FIXME: overkill text = replace(text, "\"", """) text = replace(text, "<", "<") text = replace(text, ">", ">") return text except (TypeError, AttributeError): _raise_serialization_error(text) def fixtag(tag, namespaces): # given a decorated tag (of the form {uri}tag), return prefixed # tag and namespace declaration, if any if isinstance(tag, QName): tag = tag.text namespace_uri, tag = string.split(tag[1:], "}", 1) prefix = namespaces.get(namespace_uri) if prefix is None: prefix = _namespace_map.get(namespace_uri) if prefix is None: prefix = "ns%d" % len(namespaces) namespaces[namespace_uri] = prefix if prefix == "xml": xmlns = None else: xmlns = ("xmlns:%s" % prefix, namespace_uri) else: xmlns = None return "%s:%s" % (prefix, tag), xmlns ## # Parses an XML document into an element tree. # # @param source A filename or file object containing XML data. # @param parser An optional parser instance. If not given, the # standard {@link XMLTreeBuilder} parser is used. # @return An ElementTree instance def parse(source, parser=None): tree = ElementTree() tree.parse(source, parser) return tree ## # Parses an XML document into an element tree incrementally, and reports # what's going on to the user. # # @param source A filename or file object containing XML data. # @param events A list of events to report back. If omitted, only "end" # events are reported. # @return A (event, elem) iterator. class iterparse: def __init__(self, source, events=None): if not hasattr(source, "read"): source = open(source, "rb") self._file = source self._events = [] self._index = 0 self.root = self._root = None self._parser = XMLTreeBuilder() # wire up the parser for event reporting parser = self._parser._parser append = self._events.append if events is None: events = ["end"] for event in events: if event == "start": try: parser.ordered_attributes = 1 parser.specified_attributes = 1 def handler(tag, attrib_in, event=event, append=append, start=self._parser._start_list): append((event, start(tag, attrib_in))) parser.StartElementHandler = handler except AttributeError: def handler(tag, attrib_in, event=event, append=append, start=self._parser._start): append((event, start(tag, attrib_in))) parser.StartElementHandler = handler elif event == "end": def handler(tag, event=event, append=append, end=self._parser._end): append((event, end(tag))) parser.EndElementHandler = handler elif event == "start-ns": def handler(prefix, uri, event=event, append=append): try: uri = _encode(uri, "ascii") except UnicodeError: pass append((event, (prefix or "", uri))) parser.StartNamespaceDeclHandler = handler elif event == "end-ns": def handler(prefix, event=event, append=append): append((event, None)) parser.EndNamespaceDeclHandler = handler def next(self): while 1: try: item = self._events[self._index] except IndexError: if self._parser is None: self.root = self._root try: raise StopIteration except NameError: raise IndexError # load event buffer del self._events[:] self._index = 0 data = self._file.read(16384) if data: self._parser.feed(data) else: self._root = self._parser.close() self._parser = None else: self._index = self._index + 1 return item try: iter def __iter__(self): return self except NameError: def __getitem__(self, index): return self.next() ## # Parses an XML document from a string constant. This function can # be used to embed "XML literals" in Python code. # # @param source A string containing XML data. # @return An Element instance. # @defreturn Element def XML(text): parser = XMLTreeBuilder() parser.feed(text) return parser.close() ## # Parses an XML document from a string constant, and also returns # a dictionary which maps from element id:s to elements. # # @param source A string containing XML data. # @return A tuple containing an Element instance and a dictionary. # @defreturn (Element, dictionary) def XMLID(text): parser = XMLTreeBuilder() parser.feed(text) tree = parser.close() ids = {} for elem in tree.getiterator(): id = elem.get("id") if id: ids[id] = elem return tree, ids ## # Parses an XML document from a string constant. Same as {@link #XML}. # # @def fromstring(text) # @param source A string containing XML data. # @return An Element instance. # @defreturn Element fromstring = XML ## # Generates a string representation of an XML element, including all # subelements. # # @param element An Element instance. # @return An encoded string containing the XML data. # @defreturn string def tostring(element, encoding=None): class dummy: pass data = [] file = dummy() file.write = data.append ElementTree(element).write(file, encoding) return string.join(data, "") ## # Generic element structure builder. This builder converts a sequence # of {@link #TreeBuilder.start}, {@link #TreeBuilder.data}, and {@link # #TreeBuilder.end} method calls to a well-formed element structure. #

# You can use this class to build an element structure using a custom XML # parser, or a parser for some other XML-like format. # # @param element_factory Optional element factory. This factory # is called to create new Element instances, as necessary. class TreeBuilder: def __init__(self, element_factory=None): self._data = [] # data collector self._elem = [] # element stack self._last = None # last element self._tail = None # true if we're after an end tag if element_factory is None: element_factory = _ElementInterface self._factory = element_factory ## # Flushes the parser buffers, and returns the toplevel documen # element. # # @return An Element instance. # @defreturn Element def close(self): assert len(self._elem) == 0, "missing end tags" assert self._last != None, "missing toplevel element" return self._last def _flush(self): if self._data: if self._last is not None: text = string.join(self._data, "") if self._tail: assert self._last.tail is None, "internal error (tail)" self._last.tail = text else: assert self._last.text is None, "internal error (text)" self._last.text = text self._data = [] ## # Adds text to the current element. # # @param data A string. This should be either an 8-bit string # containing ASCII text, or a Unicode string. def data(self, data): self._data.append(data) ## # Opens a new element. # # @param tag The element name. # @param attrib A dictionary containing element attributes. # @return The opened element. # @defreturn Element def start(self, tag, attrs): self._flush() self._last = elem = self._factory(tag, attrs) if self._elem: self._elem[-1].append(elem) self._elem.append(elem) self._tail = 0 return elem ## # Closes the current element. # # @param tag The element name. # @return The closed element. # @defreturn Element def end(self, tag): self._flush() self._last = self._elem.pop() assert self._last.tag == tag,\ "end tag mismatch (expected %s, got %s)" % ( self._last.tag, tag) self._tail = 1 return self._last ## # Element structure builder for XML source data, based on the # expat parser. # # @keyparam target Target object. If omitted, the builder uses an # instance of the standard {@link #TreeBuilder} class. # @keyparam html Predefine HTML entities. This flag is not supported # by the current implementation. # @see #ElementTree # @see #TreeBuilder class XMLTreeBuilder: def __init__(self, html=0, target=None): try: from xml.parsers import expat except ImportError: raise ImportError( "No module named expat; use SimpleXMLTreeBuilder instead" ) self._parser = parser = expat.ParserCreate(None, "}") if target is None: target = TreeBuilder() self._target = target self._names = {} # name memo cache # callbacks parser.DefaultHandlerExpand = self._default parser.StartElementHandler = self._start parser.EndElementHandler = self._end parser.CharacterDataHandler = self._data # let expat do the buffering, if supported try: self._parser.buffer_text = 1 except AttributeError: pass # use new-style attribute handling, if supported try: self._parser.ordered_attributes = 1 self._parser.specified_attributes = 1 parser.StartElementHandler = self._start_list except AttributeError: pass encoding = None if not parser.returns_unicode: encoding = "utf-8" # target.xml(encoding, None) self._doctype = None self.entity = {} def _fixtext(self, text): # convert text string to ascii, if possible try: return _encode(text, "ascii") except UnicodeError: return text def _fixname(self, key): # expand qname, and convert name string to ascii, if possible try: name = self._names[key] except KeyError: name = key if "}" in name: name = "{" + name self._names[key] = name = self._fixtext(name) return name def _start(self, tag, attrib_in): fixname = self._fixname tag = fixname(tag) attrib = {} for key, value in attrib_in.items(): attrib[fixname(key)] = self._fixtext(value) return self._target.start(tag, attrib) def _start_list(self, tag, attrib_in): fixname = self._fixname tag = fixname(tag) attrib = {} if attrib_in: for i in range(0, len(attrib_in), 2): attrib[fixname(attrib_in[i])] = self._fixtext(attrib_in[i+1]) return self._target.start(tag, attrib) def _data(self, text): return self._target.data(self._fixtext(text)) def _end(self, tag): return self._target.end(self._fixname(tag)) def _default(self, text): prefix = text[:1] if prefix == "&": # deal with undefined entities try: self._target.data(self.entity[text[1:-1]]) except KeyError: from xml.parsers import expat raise expat.error( "undefined entity %s: line %d, column %d" % (text, self._parser.ErrorLineNumber, self._parser.ErrorColumnNumber) ) elif prefix == "<" and text[:9] == "": self._doctype = None return text = string.strip(text) if not text: return self._doctype.append(text) n = len(self._doctype) if n > 2: type = self._doctype[1] if type == "PUBLIC" and n == 4: name, type, pubid, system = self._doctype elif type == "SYSTEM" and n == 3: name, type, system = self._doctype pubid = None else: return if pubid: pubid = pubid[1:-1] self.doctype(name, pubid, system[1:-1]) self._doctype = None ## # Handles a doctype declaration. # # @param name Doctype name. # @param pubid Public identifier. # @param system System identifier. def doctype(self, name, pubid, system): pass ## # Feeds data to the parser. # # @param data Encoded data. def feed(self, data): self._parser.Parse(data, 0) ## # Finishes feeding data to the parser. # # @return An element structure. # @defreturn Element def close(self): self._parser.Parse("", 1) # end of data tree = self._target.close() del self._target, self._parser # get rid of circular references return tree # compatibility XMLParser = XMLTreeBuilder nltk-2.0~b9/nltk/etree/ElementPath.py0000644000175000017500000001366211140171432017414 0ustar bhavanibhavani# # ElementTree # $Id: ElementPath.py 1858 2004-06-17 21:31:41Z Fredrik $ # # limited xpath support for element trees # # history: # 2003-05-23 fl created # 2003-05-28 fl added support for // etc # 2003-08-27 fl fixed parsing of periods in element names # # Copyright (c) 2003-2004 by Fredrik Lundh. All rights reserved. # # fredrik@pythonware.com # http://www.pythonware.com # # -------------------------------------------------------------------- # The ElementTree toolkit is # # Copyright (c) 1999-2004 by Fredrik Lundh # # By obtaining, using, and/or copying this software and/or its # associated documentation, you agree that you have read, understood, # and will comply with the following terms and conditions: # # Permission to use, copy, modify, and distribute this software and # its associated documentation for any purpose and without fee is # hereby granted, provided that the above copyright notice appears in # all copies, and that both that copyright notice and this permission # notice appear in supporting documentation, and that the name of # Secret Labs AB or the author not be used in advertising or publicity # pertaining to distribution of the software without specific, written # prior permission. # # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE # OF THIS SOFTWARE. # -------------------------------------------------------------------- # Licensed to PSF under a Contributor Agreement. # See http://www.python.org/2.4/license for licensing details. ## # Implementation module for XPath support. There's usually no reason # to import this module directly; the ElementTree does this for # you, if needed. ## import re xpath_tokenizer = re.compile( "(::|\.\.|\(\)|[/.*:\[\]\(\)@=])|((?:\{[^}]+\})?[^/:\[\]\(\)@=\s]+)|\s+" ).findall class xpath_descendant_or_self: pass ## # Wrapper for a compiled XPath. class Path: ## # Create an Path instance from an XPath expression. def __init__(self, path): tokens = xpath_tokenizer(path) # the current version supports 'path/path'-style expressions only self.path = [] self.tag = None if tokens and tokens[0][0] == "/": raise SyntaxError("cannot use absolute path on element") while tokens: op, tag = tokens.pop(0) if tag or op == "*": self.path.append(tag or op) elif op == ".": pass elif op == "/": self.path.append(xpath_descendant_or_self()) continue else: raise SyntaxError("unsupported path syntax (%s)" % op) if tokens: op, tag = tokens.pop(0) if op != "/": raise SyntaxError( "expected path separator (%s)" % (op or tag) ) if self.path and isinstance(self.path[-1], xpath_descendant_or_self): raise SyntaxError("path cannot end with //") if len(self.path) == 1 and isinstance(self.path[0], type("")): self.tag = self.path[0] ## # Find first matching object. def find(self, element): tag = self.tag if tag is None: nodeset = self.findall(element) if not nodeset: return None return nodeset[0] for elem in element: if elem.tag == tag: return elem return None ## # Find text for first matching object. def findtext(self, element, default=None): tag = self.tag if tag is None: nodeset = self.findall(element) if not nodeset: return default return nodeset[0].text or "" for elem in element: if elem.tag == tag: return elem.text or "" return default ## # Find all matching objects. def findall(self, element): nodeset = [element] index = 0 while 1: try: path = self.path[index] index = index + 1 except IndexError: return nodeset set = [] if isinstance(path, xpath_descendant_or_self): try: tag = self.path[index] if not isinstance(tag, type("")): tag = None else: index = index + 1 except IndexError: tag = None # invalid path for node in nodeset: new = list(node.getiterator(tag)) if new and new[0] is node: set.extend(new[1:]) else: set.extend(new) else: for node in nodeset: for node in node: if path == "*" or node.tag == path: set.append(node) if not set: return [] nodeset = set _cache = {} ## # (Internal) Compile path. def _compile(path): p = _cache.get(path) if p is not None: return p p = Path(path) if len(_cache) >= 100: _cache.clear() _cache[path] = p return p ## # Find first matching object. def find(element, path): return _compile(path).find(element) ## # Find text for first matching object. def findtext(element, path, default=None): return _compile(path).findtext(element, default) ## # Find all matching objects. def findall(element, path): return _compile(path).findall(element) nltk-2.0~b9/nltk/etree/ElementInclude.py0000644000175000017500000001165611140171432020104 0ustar bhavanibhavani# # ElementTree # $Id: ElementInclude.py 1862 2004-06-18 07:31:02Z Fredrik $ # # limited xinclude support for element trees # # history: # 2003-08-15 fl created # 2003-11-14 fl fixed default loader # # Copyright (c) 2003-2004 by Fredrik Lundh. All rights reserved. # # fredrik@pythonware.com # http://www.pythonware.com # # -------------------------------------------------------------------- # The ElementTree toolkit is # # Copyright (c) 1999-2004 by Fredrik Lundh # # By obtaining, using, and/or copying this software and/or its # associated documentation, you agree that you have read, understood, # and will comply with the following terms and conditions: # # Permission to use, copy, modify, and distribute this software and # its associated documentation for any purpose and without fee is # hereby granted, provided that the above copyright notice appears in # all copies, and that both that copyright notice and this permission # notice appear in supporting documentation, and that the name of # Secret Labs AB or the author not be used in advertising or publicity # pertaining to distribution of the software without specific, written # prior permission. # # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE # OF THIS SOFTWARE. # -------------------------------------------------------------------- # Licensed to PSF under a Contributor Agreement. # See http://www.python.org/2.4/license for licensing details. ## # Limited XInclude support for the ElementTree package. ## import copy import ElementTree XINCLUDE = "{http://www.w3.org/2001/XInclude}" XINCLUDE_INCLUDE = XINCLUDE + "include" XINCLUDE_FALLBACK = XINCLUDE + "fallback" ## # Fatal include error. class FatalIncludeError(SyntaxError): pass ## # Default loader. This loader reads an included resource from disk. # # @param href Resource reference. # @param parse Parse mode. Either "xml" or "text". # @param encoding Optional text encoding. # @return The expanded resource. If the parse mode is "xml", this # is an ElementTree instance. If the parse mode is "text", this # is a Unicode string. If the loader fails, it can return None # or raise an IOError exception. # @throws IOError If the loader fails to load the resource. def default_loader(href, parse, encoding=None): file = open(href) if parse == "xml": data = ElementTree.parse(file).getroot() else: data = file.read() if encoding: data = data.decode(encoding) file.close() return data ## # Expand XInclude directives. # # @param elem Root element. # @param loader Optional resource loader. If omitted, it defaults # to {@link default_loader}. If given, it should be a callable # that implements the same interface as default_loader. # @throws FatalIncludeError If the function fails to include a given # resource, or if the tree contains malformed XInclude elements. # @throws IOError If the function fails to load a given resource. def include(elem, loader=None): if loader is None: loader = default_loader # look for xinclude elements i = 0 while i < len(elem): e = elem[i] if e.tag == XINCLUDE_INCLUDE: # process xinclude directive href = e.get("href") parse = e.get("parse", "xml") if parse == "xml": node = loader(href, parse) if node is None: raise FatalIncludeError( "cannot load %r as %r" % (href, parse) ) node = copy.copy(node) if e.tail: node.tail = (node.tail or "") + e.tail elem[i] = node elif parse == "text": text = loader(href, parse, e.get("encoding")) if text is None: raise FatalIncludeError( "cannot load %r as %r" % (href, parse) ) if i: node = elem[i-1] node.tail = (node.tail or "") + text else: elem.text = (elem.text or "") + text + (e.tail or "") del elem[i] continue else: raise FatalIncludeError( "unknown parse type in xi:include tag (%r)" % parse ) elif e.tag == XINCLUDE_FALLBACK: raise FatalIncludeError( "xi:fallback tag must be child of xi:include (%r)" % e.tag ) else: include(e, loader) i = i + 1 nltk-2.0~b9/nltk/etree/__init__.py0000644000175000017500000000310411140171432016733 0ustar bhavanibhavani# $Id: __init__.py 1821 2004-06-03 16:57:49Z fredrik $ # elementtree package # -------------------------------------------------------------------- # The ElementTree toolkit is # # Copyright (c) 1999-2004 by Fredrik Lundh # # By obtaining, using, and/or copying this software and/or its # associated documentation, you agree that you have read, understood, # and will comply with the following terms and conditions: # # Permission to use, copy, modify, and distribute this software and # its associated documentation for any purpose and without fee is # hereby granted, provided that the above copyright notice appears in # all copies, and that both that copyright notice and this permission # notice appear in supporting documentation, and that the name of # Secret Labs AB or the author not be used in advertising or publicity # pertaining to distribution of the software without specific, written # prior permission. # # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE # OF THIS SOFTWARE. # -------------------------------------------------------------------- # Licensed to PSF under a Contributor Agreement. # See http://www.python.org/2.4/license for licensing details. nltk-2.0~b9/nltk/draw/util.py0000644000175000017500000025210211327451575016026 0ustar bhavanibhavani# Natural Language Toolkit: Drawing utilities # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # # $Id: util.py 6339 2008-07-30 01:54:47Z stevenbird $ """ Tools for graphically displaying and interacting with the objects and processing classes defined by the Toolkit. These tools are primarily intended to help students visualize the objects that they create. The graphical tools are typically built using X{canvas widgets}, each of which encapsulates the graphical elements and bindings used to display a complex object on a Tkinter C{Canvas}. For example, NLTK defines canvas widgets for displaying trees and directed graphs, as well as a number of simpler widgets. These canvas widgets make it easier to build new graphical tools and demos. See the class documentation for L{CanvasWidget} for more information. The C{nltk.draw} module defines the abstract C{CanvasWidget} base class, and a number of simple canvas widgets. The remaining canvas widgets are defined by submodules, such as L{nltk.draw.tree}. The C{nltk.draw} module also defines L{CanvasFrame}, which encapsulates a C{Canvas} and its scrollbars. It uses a L{ScrollWatcherWidget} to ensure that all canvas widgets contained on its canvas are within the scroll region. Acknowledgements: Many of the ideas behind the canvas widget system are derived from C{CLIG}, a Tk-based grapher for linguistic data structures. For more information, see U{the CLIG homepage}. """ from Tkinter import * import tkFont, tkMessageBox, tkFileDialog from nltk.util import in_idle ##////////////////////////////////////////////////////// ## CanvasWidget ##////////////////////////////////////////////////////// class CanvasWidget(object): """ A collection of graphical elements and bindings used to display a complex object on a Tkinter C{Canvas}. A canvas widget is responsible for managing the C{Canvas} tags and callback bindings necessary to display and interact with the object. Canvas widgets are often organized into hierarchies, where parent canvas widgets control aspects of their child widgets. Each canvas widget is bound to a single C{Canvas}. This C{Canvas} is specified as the first argument to the C{CanvasWidget}'s constructor. Attributes ========== Each canvas widget can support a variety of X{attributes}, which control how the canvas widget is displayed. Some typical examples attributes are C{color}, C{font}, and C{radius}. Each attribute has a default value. This default value can be overridden in the constructor, using keyword arguments of the form C{attribute=value}: >>> cn = CanvasText(c, 'test', color='red') Attribute values can also be changed after a canvas widget has been constructed, using the C{__setitem__} operator: >>> cn['font'] = 'times' The current value of an attribute value can be queried using the C{__getitem__} operator: >>> cn['color'] red For a list of the attributes supported by a type of canvas widget, see its class documentation. Interaction =========== The attribute C{'draggable'} controls whether the user can drag a canvas widget around the canvas. By default, canvas widgets are not draggable. C{CanvasWidget} provides callback support for two types of user interaction: clicking and dragging. The method C{bind_click} registers a callback function that is called whenever the canvas widget is clicked. The method C{bind_drag} registers a callback function that is called after the canvas widget is dragged. If the user clicks or drags a canvas widget with no registered callback function, then the interaction event will propagate to its parent. For each canvas widget, only one callback function may be registered for an interaction event. Callback functions can be deregistered with the C{unbind_click} and C{unbind_drag} methods. Subclassing =========== C{CanvasWidget} is an abstract class. Subclasses are required to implement the following methods: - C{__init__}: Builds a new canvas widget. It must perform the following three tasks (in order): - Create any new graphical elements. - Call C{_add_child_widget} on each child widget. - Call the C{CanvasWidget} constructor. - C{_tags}: Returns a list of the canvas tags for all graphical elements managed by this canvas widget, not including graphical elements managed by its child widgets. - C{_manage}: Arranges the child widgets of this canvas widget. This is typically only called when the canvas widget is created. - C{_update}: Update this canvas widget in response to a change in a single child. For C{CanvasWidget}s with no child widgets, the default definitions for C{_manage} and C{_update} may be used. If a subclass defines any attributes, then it should implement C{__getitem__} and C{__setitem__}. If either of these methods is called with an unknown attribute, then they should propagate the request to C{CanvasWidget}. Most subclasses implement a number of additional methods that modify the C{CanvasWidget} in some way. These methods must call C{parent.update(self)} after making any changes to the canvas widget's graphical elements. The canvas widget must also call C{parent.update(self)} after changing any attribute value that affects the shape or position of the canvas widget's graphical elements. @type __canvas: C{Tkinter.Canvas} @ivar __canvas: This C{CanvasWidget}'s canvas. @type __parent: C{CanvasWidget} or C{None} @ivar __parent: This C{CanvasWidget}'s hierarchical parent widget. @type __children: C{list} of C{CanvasWidget} @ivar __children: This C{CanvasWidget}'s hierarchical child widgets. @type __updating: C{boolean} @ivar __updating: Is this canvas widget currently performing an update? If it is, then it will ignore any new update requests from child widgets. @type __draggable: C{boolean} @ivar __draggable: Is this canvas widget draggable? @type __press: C{event} @ivar __press: The ButtonPress event that we're currently handling. @type __drag_x: C{int} @ivar __drag_x: Where it's been moved to (to find dx) @type __drag_y: C{int} @ivar __drag_y: Where it's been moved to (to find dy) @type __callbacks: C{dictionary} @ivar __callbacks: Registered callbacks. Currently, four keys are used: C{1}, C{2}, C{3}, and C{'drag'}. The values are callback functions. Each callback function takes a single argument, which is the C{CanvasWidget} that triggered the callback. """ def __init__(self, canvas, parent=None, **attribs): """ Create a new canvas widget. This constructor should only be called by subclass constructors; and it should be called only X{after} the subclass has constructed all graphical canvas objects and registered all child widgets. @param canvas: This canvas widget's canvas. @type canvas: C{Tkinter.Canvas} @param parent: This canvas widget's hierarchical parent. @type parent: C{CanvasWidget} @param attribs: The new canvas widget's attributes. """ if self.__class__ == CanvasWidget: raise TypeError, 'CanvasWidget is an abstract base class' if not isinstance(canvas, Canvas): raise TypeError('Expected a canvas!') self.__canvas = canvas self.__parent = parent # If the subclass constructor called _add_child_widget, then # self.__children will already exist. if not hasattr(self, '_CanvasWidget__children'): self.__children = [] # Is this widget hidden? self.__hidden = 0 # Update control (prevents infinite loops) self.__updating = 0 # Button-press and drag callback handling. self.__press = None self.__drag_x = self.__drag_y = 0 self.__callbacks = {} self.__draggable = 0 # Set up attributes. for (attr, value) in attribs.items(): self[attr] = value # Manage this canvas widget self._manage() # Register any new bindings for tag in self._tags(): self.__canvas.tag_bind(tag, '', self.__press_cb) self.__canvas.tag_bind(tag, '', self.__press_cb) self.__canvas.tag_bind(tag, '', self.__press_cb) ##////////////////////////////////////////////////////// ## Inherited methods. ##////////////////////////////////////////////////////// def bbox(self): """ @return: A bounding box for this C{CanvasWidget}. The bounding box is a tuple of four coordinates, M{(xmin, ymin, xmax, ymax)}, for a rectangle which encloses all of the canvas widget's graphical elements. Bounding box coordinates are specified with respect to the C{Canvas}'s coordinate space. @rtype: C{4-tuple} of C{int}s """ if self.__hidden: return (0,0,0,0) if len(self.tags()) == 0: raise ValueError('No tags') return self.__canvas.bbox(*self.tags()) def width(self): """ @return: The width of this canvas widget's bounding box, in its C{Canvas}'s coordinate space. @rtype: C{int} """ if len(self.tags()) == 0: raise ValueError('No tags') bbox = self.__canvas.bbox(*self.tags()) return bbox[2]-bbox[0] def height(self): """ @return: The height of this canvas widget's bounding box, in its C{Canvas}'s coordinate space. @rtype: C{int} """ if len(self.tags()) == 0: raise ValueError('No tags') bbox = self.__canvas.bbox(*self.tags()) return bbox[3]-bbox[1] def parent(self): """ @return: The hierarchical parent of this canvas widget. C{self} is considered a subpart of its parent for purposes of user interaction. @rtype: C{CanvasWidget} or C{None} """ return self.__parent def child_widgets(self): """ @return: A list of the hierarchical children of this canvas widget. These children are considered part of C{self} for purposes of user interaction. @rtype: C{list} of C{CanvasWidget} """ return self.__children def canvas(self): """ @return: The canvas that this canvas widget is bound to. @rtype: C{Tkinter.Canvas} """ return self.__canvas def move(self, dx, dy): """ Move this canvas widget by a given distance. In particular, shift the canvas widget right by C{dx} pixels, and down by C{dy} pixels. Both C{dx} and C{dy} may be negative, resulting in leftward or upward movement. @type dx: C{int} @param dx: The number of pixels to move this canvas widget rightwards. @type dy: C{int} @param dy: The number of pixels to move this canvas widget downwards. @rtype: C{None} """ if dx == dy == 0: return for tag in self.tags(): self.__canvas.move(tag, dx, dy) if self.__parent: self.__parent.update(self) def moveto(self, x, y, anchor='NW'): """ Move this canvas widget to the given location. In particular, shift the canvas widget such that the corner or side of the bounding box specified by C{anchor} is at location (C{x}, C{y}). @param x,y: The location that the canvas widget should be moved to. @param anchor: The corner or side of the canvas widget that should be moved to the specified location. C{'N'} specifies the top center; C{'NE'} specifies the top right corner; etc. """ x1,y1,x2,y2 = self.bbox() if anchor == 'NW': self.move(x-x1, y-y1) if anchor == 'N': self.move(x-x1/2-x2/2, y-y1) if anchor == 'NE': self.move(x-x2, y-y1) if anchor == 'E': self.move(x-x2, y-y1/2-y2/2) if anchor == 'SE': self.move(x-x2, y-y2) if anchor == 'S': self.move(x-x1/2-x2/2, y-y2) if anchor == 'SW': self.move(x-x1, y-y2) if anchor == 'W': self.move(x-x1, y-y1/2-y2/2) def destroy(self): """ Remove this C{CanvasWidget} from its C{Canvas}. After a C{CanvasWidget} has been destroyed, it should not be accessed. Note that you only need to destroy a top-level C{CanvasWidget}; its child widgets will be destroyed automatically. If you destroy a non-top-level C{CanvasWidget}, then the entire top-level widget will be destroyed. @raise ValueError: if this C{CanvasWidget} has a parent. @rtype: C{None} """ if self.__parent is not None: self.__parent.destroy() return for tag in self.tags(): self.__canvas.tag_unbind(tag, '') self.__canvas.tag_unbind(tag, '') self.__canvas.tag_unbind(tag, '') self.__canvas.delete(*self.tags()) self.__canvas = None def update(self, child): """ Update the graphical display of this canvas widget, and all of its ancestors, in response to a change in one of this canvas widget's children. @param child: The child widget that changed. @type child: C{CanvasWidget} """ if self.__hidden or child.__hidden: return # If we're already updating, then do nothing. This prevents # infinite loops when _update modifies its children. if self.__updating: return self.__updating = 1 # Update this CanvasWidget. self._update(child) # Propagate update request to the parent. if self.__parent: self.__parent.update(self) # We're done updating. self.__updating = 0 def manage(self): """ Arrange this canvas widget and all of its descendants. @rtype: C{None} """ if self.__hidden: return for child in self.__children: child.manage() self._manage() def tags(self): """ @return: a list of the canvas tags for all graphical elements managed by this canvas widget, including graphical elements managed by its child widgets. @rtype: C{list} of C{int} """ if self.__canvas is None: raise ValueError('Attempt to access a destroyed canvas widget') tags = [] tags += self._tags() for child in self.__children: tags += child.tags() return tags def __setitem__(self, attr, value): """ Set the value of the attribute C{attr} to C{value}. See the class documentation for a list of attributes supported by this canvas widget. @rtype: C{None} """ if attr == 'draggable': self.__draggable = value else: raise ValueError('Unknown attribute %r' % attr) def __getitem__(self, attr): """ @return: the value of the attribute C{attr}. See the class documentation for a list of attributes supported by this canvas widget. @rtype: (any) """ if attr == 'draggable': return self.__draggable else: raise ValueError('Unknown attribute %r' % attr) def __repr__(self): """ @return: a string representation of this canvas widget. @rtype: C{string} """ return '<%s>' % self.__class__.__name__ def hide(self): """ Temporarily hide this canvas widget. @rtype: C{None} """ self.__hidden = 1 for tag in self.tags(): self.__canvas.itemconfig(tag, state='hidden') def show(self): """ Show a hidden canvas widget. @rtype: C{None} """ self.__hidden = 0 for tag in self.tags(): self.__canvas.itemconfig(tag, state='normal') def hidden(self): """ @return: True if this canvas widget is hidden. @rtype: C{boolean} """ return self.__hidden ##////////////////////////////////////////////////////// ## Callback interface ##////////////////////////////////////////////////////// def bind_click(self, callback, button=1): """ Register a new callback that will be called whenever this C{CanvasWidget} is clicked on. @type callback: C{function} @param callback: The callback function that will be called whenever this C{CanvasWidget} is clicked. This function will be called with this C{CanvasWidget} as its argument. @type button: C{int} @param button: Which button the user should use to click on this C{CanvasWidget}. Typically, this should be 1 (left button), 3 (right button), or 2 (middle button). """ self.__callbacks[button] = callback def bind_drag(self, callback): """ Register a new callback that will be called after this C{CanvasWidget} is dragged. This implicitly makes this C{CanvasWidget} draggable. @type callback: C{function} @param callback: The callback function that will be called whenever this C{CanvasWidget} is clicked. This function will be called with this C{CanvasWidget} as its argument. """ self.__draggable = 1 self.__callbacks['drag'] = callback def unbind_click(self, button=1): """ Remove a callback that was registered with C{bind_click}. @type button: C{int} @param button: Which button the user should use to click on this C{CanvasWidget}. Typically, this should be 1 (left button), 3 (right button), or 2 (middle button). """ try: del self.__callbacks[button] except: pass def unbind_drag(self): """ Remove a callback that was registered with C{bind_drag}. """ try: del self.__callbacks['drag'] except: pass ##////////////////////////////////////////////////////// ## Callback internals ##////////////////////////////////////////////////////// def __press_cb(self, event): """ Handle a button-press event: - record the button press event in C{self.__press} - register a button-release callback. - if this CanvasWidget or any of its ancestors are draggable, then register the appropriate motion callback. """ # If we're already waiting for a button release, then ignore # this new button press. if (self.__canvas.bind('') or self.__canvas.bind('') or self.__canvas.bind('')): return # Unbind motion (just in case; this shouldn't be necessary) self.__canvas.unbind('') # Record the button press event. self.__press = event # If any ancestor is draggable, set up a motion callback. # (Only if they pressed button number 1) if event.num == 1: widget = self while widget is not None: if widget['draggable']: widget.__start_drag(event) break widget = widget.parent() # Set up the button release callback. self.__canvas.bind('' % event.num, self.__release_cb) def __start_drag(self, event): """ Begin dragging this object: - register a motion callback - record the drag coordinates """ self.__canvas.bind('', self.__motion_cb) self.__drag_x = event.x self.__drag_y = event.y def __motion_cb(self, event): """ Handle a motion event: - move this object to the new location - record the new drag coordinates """ self.move(event.x-self.__drag_x, event.y-self.__drag_y) self.__drag_x = event.x self.__drag_y = event.y def __release_cb(self, event): """ Handle a release callback: - unregister motion & button release callbacks. - decide whether they clicked, dragged, or cancelled - call the appropriate handler. """ # Unbind the button release & motion callbacks. self.__canvas.unbind('' % event.num) self.__canvas.unbind('') # Is it a click or a drag? if (event.time - self.__press.time < 100 and abs(event.x-self.__press.x) + abs(event.y-self.__press.y) < 5): # Move it back, if we were dragging. if self.__draggable and event.num == 1: self.move(self.__press.x - self.__drag_x, self.__press.y - self.__drag_y) self.__click(event.num) elif event.num == 1: self.__drag() self.__press = None def __drag(self): """ If this C{CanvasWidget} has a drag callback, then call it; otherwise, find the closest ancestor with a drag callback, and call it. If no ancestors have a drag callback, do nothing. """ if self.__draggable: if self.__callbacks.has_key('drag'): cb = self.__callbacks['drag'] try: cb(self) except: print 'Error in drag callback for %r' % self elif self.__parent is not None: self.__parent.__drag() def __click(self, button): """ If this C{CanvasWidget} has a drag callback, then call it; otherwise, find the closest ancestor with a click callback, and call it. If no ancestors have a click callback, do nothing. """ if self.__callbacks.has_key(button): cb = self.__callbacks[button] #try: cb(self) #except: # print 'Error in click callback for %r' % self # raise elif self.__parent is not None: self.__parent.__click(button) ##////////////////////////////////////////////////////// ## Child/parent Handling ##////////////////////////////////////////////////////// def _add_child_widget(self, child): """ Register a hierarchical child widget. The child will be considered part of this canvas widget for purposes of user interaction. C{_add_child_widget} has two direct effects: - It sets C{child}'s parent to this canvas widget. - It adds C{child} to the list of canvas widgets returned by the C{child_widgets} member function. @param child: The new child widget. C{child} must not already have a parent. @type child: C{CanvasWidget} """ if not hasattr(self, '_CanvasWidget__children'): self.__children = [] if child.__parent is not None: raise ValueError('%s already has a parent', child) child.__parent = self self.__children.append(child) def _remove_child_widget(self, child): """ Remove a hierarchical child widget. This child will no longer be considered part of this canvas widget for purposes of user interaction. C{_add_child_widget} has two direct effects: - It sets C{child}'s parent to C{None}. - It removes C{child} from the list of canvas widgets returned by the C{child_widgets} member function. @param child: The child widget to remove. C{child} must be a child of this canvas widget. @type child: C{CanvasWidget} """ self.__children.remove(child) child.__parent = None ##////////////////////////////////////////////////////// ## Defined by subclass ##////////////////////////////////////////////////////// def _tags(self): """ @return: a list of canvas tags for all graphical elements managed by this canvas widget, not including graphical elements managed by its child widgets. @rtype: C{list} of C{int} """ raise AssertionError() def _manage(self): """ Arrange the child widgets of this canvas widget. This method is called when the canvas widget is initially created. It is also called if the user calls the C{manage} method on this canvas widget or any of its ancestors. @rtype: C{None} """ pass def _update(self, child): """ Update this canvas widget in response to a change in one of its children. @param child: The child that changed. @type child: C{CanvasWidget} @rtype: C{None} """ pass ##////////////////////////////////////////////////////// ## Basic widgets. ##////////////////////////////////////////////////////// class TextWidget(CanvasWidget): """ A canvas widget that displays a single string of text. Attributes: - C{color}: the color of the text. - C{font}: the font used to display the text. - C{justify}: justification for multi-line texts. Valid values are C{left}, C{center}, and C{right}. - C{width}: the width of the text. If the text is wider than this width, it will be line-wrapped at whitespace. - C{draggable}: whether the text can be dragged by the user. """ def __init__(self, canvas, text, **attribs): """ Create a new text widget. @type canvas: C{Tkinter.Canvas} @param canvas: This canvas widget's canvas. @type text: C{string} @param text: The string of text to display. @param attribs: The new canvas widget's attributes. """ self._text = text self._tag = canvas.create_text(1, 1, text=text) CanvasWidget.__init__(self, canvas, **attribs) def __setitem__(self, attr, value): if attr in ('color', 'font', 'justify', 'width'): if attr == 'color': attr = 'fill' self.canvas().itemconfig(self._tag, {attr:value}) else: CanvasWidget.__setitem__(self, attr, value) def __getitem__(self, attr): if attr == 'width': return int(self.canvas().itemcget(self._tag, attr)) elif attr in ('color', 'font', 'justify'): if attr == 'color': attr = 'fill' return self.canvas().itemcget(self._tag, attr) else: return CanvasWidget.__getitem__(self, attr) def _tags(self): return [self._tag] def text(self): """ @return: The text displayed by this text widget. @rtype: C{string} """ return self.canvas().itemcget(self._tag, 'TEXT') def set_text(self, text): """ Change the text that is displayed by this text widget. @type text: C{string} @param text: The string of text to display. @rtype: C{None} """ self.canvas().itemconfig(self._tag, text=text) if self.parent() is not None: self.parent().update(self) def __repr__(self): return '[Text: %r]' % self._text class SymbolWidget(TextWidget): """ A canvas widget that displays special symbols, such as the negation sign and the exists operator. Symbols are specified by name. Currently, the following symbol names are defined: C{neg}, C{disj}, C{conj}, C{lambda}, C{merge}, C{forall}, C{exists}, C{subseteq}, C{subset}, C{notsubset}, C{emptyset}, C{imp}, C{rightarrow}, C{equal}, C{notequal}, C{epsilon}. Attributes: - C{color}: the color of the text. - C{draggable}: whether the text can be dragged by the user. @cvar SYMBOLS: A dictionary mapping from symbols to the character in the C{symbol} font used to render them. """ SYMBOLS = {'neg':'\330', 'disj':'\332', 'conj': '\331', 'lambda': '\154', 'merge': '\304', 'forall': '\042', 'exists': '\044', 'subseteq': '\315', 'subset': '\314', 'notsubset': '\313', 'emptyset': '\306', 'imp': '\336', 'rightarrow': chr(222), #'\256', 'equal': '\75', 'notequal': '\271', 'intersection': '\307', 'union': '\310', 'epsilon': 'e', } def __init__(self, canvas, symbol, **attribs): """ Create a new symbol widget. @type canvas: C{Tkinter.Canvas} @param canvas: This canvas widget's canvas. @type symbol: C{string} @param symbol: The name of the symbol to display. @param attribs: The new canvas widget's attributes. """ attribs['font'] = 'symbol' TextWidget.__init__(self, canvas, '', **attribs) self.set_symbol(symbol) def symbol(self): """ @return: the name of the symbol that is displayed by this symbol widget. @rtype: C{string} """ return self._symbol def set_symbol(self, symbol): """ Change the symbol that is displayed by this symbol widget. @type symbol: C{string} @param symbol: The name of the symbol to display. """ if not SymbolWidget.SYMBOLS.has_key(symbol): raise ValueError('Unknown symbol: %s' % symbol) self._symbol = symbol self.set_text(SymbolWidget.SYMBOLS[symbol]) def __repr__(self): return '[Symbol: %r]' % self._symbol # A staticmethod that displays all symbols. def symbolsheet(size=20): """ Open a new Tkinter window that displays the entire alphabet for the symbol font. This is useful for constructing the L{SymbolWidget.SYMBOLS} dictionary. """ top = Tk() def destroy(e, top=top): top.destroy() top.bind('q', destroy) Button(top, text='Quit', command=top.destroy).pack(side='bottom') text = Text(top, font=('helvetica', -size), width=20, height=30) text.pack(side='left') sb=Scrollbar(top, command=text.yview) text['yscrollcommand']=sb.set sb.pack(side='right', fill='y') text.tag_config('symbol', font=('symbol', -size)) for i in range(256): if i in (0,10): continue # null and newline for k,v in SymbolWidget.SYMBOLS.items(): if v == chr(i): text.insert('end', '%-10s\t' % k) break else: text.insert('end', '%-10d \t' % i) text.insert('end', '[%s]\n' % chr(i), 'symbol') top.mainloop() symbolsheet = staticmethod(symbolsheet) class AbstractContainerWidget(CanvasWidget): """ An abstract class for canvas widgets that contain a single child, such as C{BoxWidget} and C{OvalWidget}. Subclasses must define a constructor, which should create any new graphical elements and then call the C{AbstractCanvasContainer} constructor. Subclasses must also define the C{_update} method and the C{_tags} method; and any subclasses that define attributes should define C{__setitem__} and C{__getitem__}. """ def __init__(self, canvas, child, **attribs): """ Create a new container widget. This constructor should only be called by subclass constructors. @type canvas: C{Tkinter.Canvas} @param canvas: This canvas widget's canvas. @param child: The container's child widget. C{child} must not have a parent. @type child: C{CanvasWidget} @param attribs: The new canvas widget's attributes. """ self._child = child self._add_child_widget(child) CanvasWidget.__init__(self, canvas, **attribs) def _manage(self): self._update(self._child) def child(self): """ @return: The child widget contained by this container widget. @rtype: C{CanvasWidget} """ return self._child def set_child(self, child): """ Change the child widget contained by this container widget. @param child: The new child widget. C{child} must not have a parent. @type child: C{CanvasWidget} @rtype: C{None} """ self._remove_child_widget(self._child) self._add_child_widget(child) self._child = child self.update(child) def __repr__(self): name = self.__class__.__name__ if name[-6:] == 'Widget': name = name[:-6] return '[%s: %r]' % (name, self._child) class BoxWidget(AbstractContainerWidget): """ A canvas widget that places a box around a child widget. Attributes: - C{fill}: The color used to fill the interior of the box. - C{outline}: The color used to draw the outline of the box. - C{width}: The width of the outline of the box. - C{margin}: The number of pixels space left between the child and the box. - C{draggable}: whether the text can be dragged by the user. """ def __init__(self, canvas, child, **attribs): """ Create a new box widget. @type canvas: C{Tkinter.Canvas} @param canvas: This canvas widget's canvas. @param child: The child widget. C{child} must not have a parent. @type child: C{CanvasWidget} @param attribs: The new canvas widget's attributes. """ self._child = child self._margin = 1 self._box = canvas.create_rectangle(1,1,1,1) canvas.tag_lower(self._box) AbstractContainerWidget.__init__(self, canvas, child, **attribs) def __setitem__(self, attr, value): if attr == 'margin': self._margin = value elif attr in ('outline', 'fill', 'width'): self.canvas().itemconfig(self._box, {attr:value}) else: CanvasWidget.__setitem__(self, attr, value) def __getitem__(self, attr): if attr == 'margin': return self._margin elif attr == 'width': return float(self.canvas().itemcget(self._box, attr)) elif attr in ('outline', 'fill', 'width'): return self.canvas().itemcget(self._box, attr) else: return CanvasWidget.__getitem__(self, attr) def _update(self, child): (x1, y1, x2, y2) = child.bbox() margin = self._margin + self['width']/2 self.canvas().coords(self._box, x1-margin, y1-margin, x2+margin, y2+margin) def _tags(self): return [self._box] class OvalWidget(AbstractContainerWidget): """ A canvas widget that places a oval around a child widget. Attributes: - C{fill}: The color used to fill the interior of the oval. - C{outline}: The color used to draw the outline of the oval. - C{width}: The width of the outline of the oval. - C{margin}: The number of pixels space left between the child and the oval. - C{draggable}: whether the text can be dragged by the user. - C{double}: If true, then a double-oval is drawn. """ def __init__(self, canvas, child, **attribs): """ Create a new oval widget. @type canvas: C{Tkinter.Canvas} @param canvas: This canvas widget's canvas. @param child: The child widget. C{child} must not have a parent. @type child: C{CanvasWidget} @param attribs: The new canvas widget's attributes. """ self._child = child self._margin = 1 self._oval = canvas.create_oval(1,1,1,1) self._circle = attribs.pop('circle', False) self._double = attribs.pop('double', False) if self._double: self._oval2 = canvas.create_oval(1,1,1,1) else: self._oval2 = None canvas.tag_lower(self._oval) AbstractContainerWidget.__init__(self, canvas, child, **attribs) def __setitem__(self, attr, value): c = self.canvas() if attr == 'margin': self._margin = value elif attr == 'double': if value==True and self._oval2 is None: # Copy attributes & position from self._oval. x1, y1, x2, y2 = c.bbox(self._oval) w = self['width']*2 self._oval2 = c.create_oval(x1-w, y1-w, x2+w, y2+w, outline=c.itemcget(self._oval, 'outline'), width=c.itemcget(self._oval, 'width')) c.tag_lower(self._oval2) if value==False and self._oval2 is not None: c.delete(self._oval2) self._oval2 = None elif attr in ('outline', 'fill', 'width'): c.itemconfig(self._oval, {attr:value}) if self._oval2 is not None and attr!='fill': c.itemconfig(self._oval2, {attr:value}) if self._oval2 is not None and attr!='fill': self.canvas().itemconfig(self._oval2, {attr:value}) else: CanvasWidget.__setitem__(self, attr, value) def __getitem__(self, attr): if attr == 'margin': return self._margin elif attr == 'double': return self._double is not None elif attr == 'width': return float(self.canvas().itemcget(self._oval, attr)) elif attr in ('outline', 'fill', 'width'): return self.canvas().itemcget(self._oval, attr) else: return CanvasWidget.__getitem__(self, attr) # The ratio between inscribed & circumscribed ovals RATIO = 1.4142135623730949 def _update(self, child): R = OvalWidget.RATIO (x1, y1, x2, y2) = child.bbox() margin = self._margin # If we're a circle, pretend our contents are square. if self._circle: dx, dy = abs(x1-x2), abs(y1-y2) if dx > dy: y = (y1+y2)/2 y1, y2 = y-dx/2, y+dx/2 elif dy > dx: x = (x1+x2)/2 x1, x2 = x-dy/2, x+dy/2 # Find the four corners. left = int(( x1*(1+R) + x2*(1-R) ) / 2) right = left + int((x2-x1)*R) top = int(( y1*(1+R) + y2*(1-R) ) / 2) bot = top + int((y2-y1)*R) self.canvas().coords(self._oval, left-margin, top-margin, right+margin, bot+margin) if self._oval2 is not None: self.canvas().coords(self._oval2, left-margin+2, top-margin+2, right+margin-2, bot+margin-2) def _tags(self): if self._oval2 is None: return [self._oval] else: return [self._oval, self._oval2] class ParenWidget(AbstractContainerWidget): """ A canvas widget that places a pair of parenthases around a child widget. Attributes: - C{color}: The color used to draw the parenthases. - C{width}: The width of the parenthases. - C{draggable}: whether the text can be dragged by the user. """ def __init__(self, canvas, child, **attribs): """ Create a new parenthasis widget. @type canvas: C{Tkinter.Canvas} @param canvas: This canvas widget's canvas. @param child: The child widget. C{child} must not have a parent. @type child: C{CanvasWidget} @param attribs: The new canvas widget's attributes. """ self._child = child self._oparen = canvas.create_arc(1,1,1,1, style='arc', start=90, extent=180) self._cparen = canvas.create_arc(1,1,1,1, style='arc', start=-90, extent=180) AbstractContainerWidget.__init__(self, canvas, child, **attribs) def __setitem__(self, attr, value): if attr == 'color': self.canvas().itemconfig(self._oparen, outline=value) self.canvas().itemconfig(self._cparen, outline=value) elif attr == 'width': self.canvas().itemconfig(self._oparen, width=value) self.canvas().itemconfig(self._cparen, width=value) else: CanvasWidget.__setitem__(self, attr, value) def __getitem__(self, attr): if attr == 'color': return self.canvas().itemcget(self._oparen, 'outline') elif attr == 'width': return self.canvas().itemcget(self._oparen, 'width') else: return CanvasWidget.__getitem__(self, attr) def _update(self, child): (x1, y1, x2, y2) = child.bbox() width = max((y2-y1)/6, 4) self.canvas().coords(self._oparen, x1-width, y1, x1+width, y2) self.canvas().coords(self._cparen, x2-width, y1, x2+width, y2) def _tags(self): return [self._oparen, self._cparen] class BracketWidget(AbstractContainerWidget): """ A canvas widget that places a pair of brackets around a child widget. Attributes: - C{color}: The color used to draw the brackets. - C{width}: The width of the brackets. - C{draggable}: whether the text can be dragged by the user. """ def __init__(self, canvas, child, **attribs): """ Create a new bracket widget. @type canvas: C{Tkinter.Canvas} @param canvas: This canvas widget's canvas. @param child: The child widget. C{child} must not have a parent. @type child: C{CanvasWidget} @param attribs: The new canvas widget's attributes. """ self._child = child self._obrack = canvas.create_line(1,1,1,1,1,1,1,1) self._cbrack = canvas.create_line(1,1,1,1,1,1,1,1) AbstractContainerWidget.__init__(self, canvas, child, **attribs) def __setitem__(self, attr, value): if attr == 'color': self.canvas().itemconfig(self._obrack, fill=value) self.canvas().itemconfig(self._cbrack, fill=value) elif attr == 'width': self.canvas().itemconfig(self._obrack, width=value) self.canvas().itemconfig(self._cbrack, width=value) else: CanvasWidget.__setitem__(self, attr, value) def __getitem__(self, attr): if attr == 'color': return self.canvas().itemcget(self._obrack, 'outline') elif attr == 'width': return self.canvas().itemcget(self._obrack, 'width') else: return CanvasWidget.__getitem__(self, attr) def _update(self, child): (x1, y1, x2, y2) = child.bbox() width = max((y2-y1)/8, 2) self.canvas().coords(self._obrack, x1, y1, x1-width, y1, x1-width, y2, x1, y2) self.canvas().coords(self._cbrack, x2, y1, x2+width, y1, x2+width, y2, x2, y2) def _tags(self): return [self._obrack, self._cbrack] class SequenceWidget(CanvasWidget): """ A canvas widget that keeps a list of canvas widgets in a horizontal line. Attributes: - C{align}: The vertical alignment of the children. Possible values are C{'top'}, C{'center'}, and C{'bottom'}. By default, children are center-aligned. - C{space}: The amount of horizontal space to place between children. By default, one pixel of space is used. - C{ordered}: If true, then keep the children in their original order. """ def __init__(self, canvas, *children, **attribs): """ Create a new sequence widget. @type canvas: C{Tkinter.Canvas} @param canvas: This canvas widget's canvas. @param children: The widgets that should be aligned horizontally. Each child must not have a parent. @type children: C{list} of C{CanvasWidget} @param attribs: The new canvas widget's attributes. """ self._align = 'center' self._space = 1 self._ordered = False self._children = list(children) for child in children: self._add_child_widget(child) CanvasWidget.__init__(self, canvas, **attribs) def __setitem__(self, attr, value): if attr == 'align': if value not in ('top', 'bottom', 'center'): raise ValueError('Bad alignment: %r' % value) self._align = value elif attr == 'space': self._space = value elif attr == 'ordered': self._ordered = value else: CanvasWidget.__setitem__(self, attr, value) def __getitem__(self, attr): if attr == 'align': return value elif attr == 'space': return self._space elif attr == 'ordered': return self._ordered else: return CanvasWidget.__getitem__(self, attr) def _tags(self): return [] def _yalign(self, top, bot): if self._align == 'top': return top if self._align == 'bottom': return bot if self._align == 'center': return (top+bot)/2 def _update(self, child): # Align all children with child. (left, top, right, bot) = child.bbox() y = self._yalign(top, bot) for c in self._children: (x1, y1, x2, y2) = c.bbox() c.move(0, y-self._yalign(y1,y2)) if self._ordered and len(self._children) > 1: index = self._children.index(child) x = right + self._space for i in range(index+1, len(self._children)): (x1, y1, x2, y2) = self._children[i].bbox() if x > x1: self._children[i].move(x-x1, 0) x += x2-x1 + self._space x = left - self._space for i in range(index-1, -1, -1): (x1, y1, x2, y2) = self._children[i].bbox() if x < x2: self._children[i].move(x-x2, 0) x -= x2-x1 + self._space def _manage(self): if len(self._children) == 0: return child = self._children[0] # Align all children with child. (left, top, right, bot) = child.bbox() y = self._yalign(top, bot) index = self._children.index(child) # Line up children to the right of child. x = right + self._space for i in range(index+1, len(self._children)): (x1, y1, x2, y2) = self._children[i].bbox() self._children[i].move(x-x1, y-self._yalign(y1,y2)) x += x2-x1 + self._space # Line up children to the left of child. x = left - self._space for i in range(index-1, -1, -1): (x1, y1, x2, y2) = self._children[i].bbox() self._children[i].move(x-x2, y-self._yalign(y1,y2)) x -= x2-x1 + self._space def __repr__(self): return '[Sequence: ' + `self._children`[1:-1]+']' # Provide an alias for the child_widgets() member. children = CanvasWidget.child_widgets def replace_child(self, oldchild, newchild): """ Replace the child canvas widget C{oldchild} with C{newchild}. C{newchild} must not have a parent. C{oldchild}'s parent will be set to C{None}. @type oldchild: C{CanvasWidget} @param oldchild: The child canvas widget to remove. @type newchild: C{CanvasWidget} @param newchild: The canvas widget that should replace C{oldchild}. """ index = self._children.index(oldchild) self._children[index] = newchild self._remove_child_widget(oldchild) self._add_child_widget(newchild) self.update(newchild) def remove_child(self, child): """ Remove the given child canvas widget. C{child}'s parent will be set ot None. @type child: C{CanvasWidget} @param child: The child canvas widget to remove. """ index = self._children.index(child) del self._children[index] self._remove_child_widget(child) if len(self._children) > 0: self.update(self._children[0]) def insert_child(self, index, child): """ Insert a child canvas widget before a given index. @type child: C{CanvasWidget} @param child: The canvas widget that should be inserted. @type index: C{int} @param index: The index where the child widget should be inserted. In particular, the index of C{child} will be C{index}; and the index of any children whose indices were greater than equal to C{index} before C{child} was inserted will be incremented by one. """ self._children.insert(index, child) self._add_child_widget(child) class StackWidget(CanvasWidget): """ A canvas widget that keeps a list of canvas widgets in a vertical line. Attributes: - C{align}: The horizontal alignment of the children. Possible values are C{'left'}, C{'center'}, and C{'right'}. By default, children are center-aligned. - C{space}: The amount of vertical space to place between children. By default, one pixel of space is used. - C{ordered}: If true, then keep the children in their original order. """ def __init__(self, canvas, *children, **attribs): """ Create a new stack widget. @type canvas: C{Tkinter.Canvas} @param canvas: This canvas widget's canvas. @param children: The widgets that should be aligned vertically. Each child must not have a parent. @type children: C{list} of C{CanvasWidget} @param attribs: The new canvas widget's attributes. """ self._align = 'center' self._space = 1 self._ordered = False self._children = list(children) for child in children: self._add_child_widget(child) CanvasWidget.__init__(self, canvas, **attribs) def __setitem__(self, attr, value): if attr == 'align': if value not in ('left', 'right', 'center'): raise ValueError('Bad alignment: %r' % value) self._align = value elif attr == 'space': self._space = value elif attr == 'ordered': self._ordered = value else: CanvasWidget.__setitem__(self, attr, value) def __getitem__(self, attr): if attr == 'align': return value elif attr == 'space': return self._space elif attr == 'ordered': return self._ordered else: return CanvasWidget.__getitem__(self, attr) def _tags(self): return [] def _xalign(self, left, right): if self._align == 'left': return left if self._align == 'right': return right if self._align == 'center': return (left+right)/2 def _update(self, child): # Align all children with child. (left, top, right, bot) = child.bbox() x = self._xalign(left, right) for c in self._children: (x1, y1, x2, y2) = c.bbox() c.move(x-self._xalign(x1,x2), 0) if self._ordered and len(self._children) > 1: index = self._children.index(child) y = bot + self._space for i in range(index+1, len(self._children)): (x1, y1, x2, y2) = self._children[i].bbox() if y > y1: self._children[i].move(0, y-y1) y += y2-y1 + self._space y = top - self._space for i in range(index-1, -1, -1): (x1, y1, x2, y2) = self._children[i].bbox() if y < y2: self._children[i].move(0, y-y2) y -= y2-y1 + self._space def _manage(self): if len(self._children) == 0: return child = self._children[0] # Align all children with child. (left, top, right, bot) = child.bbox() x = self._xalign(left, right) index = self._children.index(child) # Line up children below the child. y = bot + self._space for i in range(index+1, len(self._children)): (x1, y1, x2, y2) = self._children[i].bbox() self._children[i].move(x-self._xalign(x1,x2), y-y1) y += y2-y1 + self._space # Line up children above the child. y = top - self._space for i in range(index-1, -1, -1): (x1, y1, x2, y2) = self._children[i].bbox() self._children[i].move(x-self._xalign(x1,x2), y-y2) y -= y2-y1 + self._space def __repr__(self): return '[Stack: ' + `self._children`[1:-1]+']' # Provide an alias for the child_widgets() member. children = CanvasWidget.child_widgets def replace_child(self, oldchild, newchild): """ Replace the child canvas widget C{oldchild} with C{newchild}. C{newchild} must not have a parent. C{oldchild}'s parent will be set to C{None}. @type oldchild: C{CanvasWidget} @param oldchild: The child canvas widget to remove. @type newchild: C{CanvasWidget} @param newchild: The canvas widget that should replace C{oldchild}. """ index = self._children.index(oldchild) self._children[index] = newchild self._remove_child_widget(oldchild) self._add_child_widget(newchild) self.update(newchild) def remove_child(self, child): """ Remove the given child canvas widget. C{child}'s parent will be set ot None. @type child: C{CanvasWidget} @param child: The child canvas widget to remove. """ index = self._children.index(child) del self._children[index] self._remove_child_widget(child) if len(self._children) > 0: self.update(self._children[0]) def insert_child(self, index, child): """ Insert a child canvas widget before a given index. @type child: C{CanvasWidget} @param child: The canvas widget that should be inserted. @type index: C{int} @param index: The index where the child widget should be inserted. In particular, the index of C{child} will be C{index}; and the index of any children whose indices were greater than equal to C{index} before C{child} was inserted will be incremented by one. """ self._children.insert(index, child) self._add_child_widget(child) class SpaceWidget(CanvasWidget): """ A canvas widget that takes up space but does not display anything. C{SpaceWidget}s can be used to add space between elements. Each space widget is characterized by a width and a height. If you wish to only create horizontal space, then use a height of zero; and if you wish to only create vertical space, use a width of zero. """ def __init__(self, canvas, width, height, **attribs): """ Create a new space widget. @type canvas: C{Tkinter.Canvas} @param canvas: This canvas widget's canvas. @type width: C{int} @param width: The width of the new space widget. @type height: C{int} @param height: The height of the new space widget. @param attribs: The new canvas widget's attributes. """ # For some reason, if width > 4: width -= 4 if height > 4: height -= 4 self._tag = canvas.create_line(1, 1, width, height, fill='') CanvasWidget.__init__(self, canvas, **attribs) # note: width() and height() are already defined by CanvasWidget. def set_width(self, width): """ Change the width of this space widget. @param width: The new width. @type width: C{int} @rtype: C{None} """ [x1, y1, x2, y2] = self.bbox() self.canvas().coords(self._tag, x1, y1, x1+width, y2) def set_height(self, height): """ Change the height of this space widget. @param height: The new height. @type height: C{int} @rtype: C{None} """ [x1, y1, x2, y2] = self.bbox() self.canvas().coords(self._tag, x1, y1, x2, y1+height) def _tags(self): return [self._tag] def __repr__(self): return '[Space]' class ScrollWatcherWidget(CanvasWidget): """ A special canvas widget that adjusts its C{Canvas}'s scrollregion to always include the bounding boxes of all of its children. The scroll-watcher widget will only increase the size of the C{Canvas}'s scrollregion; it will never decrease it. """ def __init__(self, canvas, *children, **attribs): """ Create a new scroll-watcher widget. @type canvas: C{Tkinter.Canvas} @param canvas: This canvas widget's canvas. @type children: C{list} of C{CanvasWidget} @param children: The canvas widgets watched by the scroll-watcher. The scroll-watcher will ensure that these canvas widgets are always contained in their canvas's scrollregion. @param attribs: The new canvas widget's attributes. """ for child in children: self._add_child_widget(child) CanvasWidget.__init__(self, canvas, **attribs) def add_child(self, canvaswidget): """ Add a new canvas widget to the scroll-watcher. The scroll-watcher will ensure that the new canvas widget is always contained in its canvas's scrollregion. @param canvaswidget: The new canvas widget. @type canvaswidget: C{CanvasWidget} @rtype: C{None} """ self._add_child_widget(canvaswidget) self.update(canvaswidget) def remove_child(self, canvaswidget): """ Remove a canvas widget from the scroll-watcher. The scroll-watcher will no longer ensure that the new canvas widget is always contained in its canvas's scrollregion. @param canvaswidget: The canvas widget to remove. @type canvaswidget: C{CanvasWidget} @rtype: C{None} """ self._remove_child_widget(canvaswidget) def _tags(self): return [] def _update(self, child): self._adjust_scrollregion() def _adjust_scrollregion(self): """ Adjust the scrollregion of this scroll-watcher's C{Canvas} to include the bounding boxes of all of its children. """ bbox = self.bbox() canvas = self.canvas() scrollregion = [int(n) for n in canvas['scrollregion'].split()] if len(scrollregion) != 4: return if (bbox[0] < scrollregion[0] or bbox[1] < scrollregion[1] or bbox[2] > scrollregion[2] or bbox[3] > scrollregion[3]): scrollregion = ('%d %d %d %d' % (min(bbox[0], scrollregion[0]), min(bbox[1], scrollregion[1]), max(bbox[2], scrollregion[2]), max(bbox[3], scrollregion[3]))) canvas['scrollregion'] = scrollregion ##////////////////////////////////////////////////////// ## Canvas Frame ##////////////////////////////////////////////////////// class CanvasFrame(object): """ A C{Tkinter} frame containing a canvas and scrollbars. C{CanvasFrame} uses a C{ScrollWatcherWidget} to ensure that all of the canvas widgets contained on its canvas are within its scrollregion. In order for C{CanvasFrame} to make these checks, all canvas widgets must be registered with C{add_widget} when they are added to the canvas; and destroyed with C{destroy_widget} when they are no longer needed. If a C{CanvasFrame} is created with no parent, then it will create its own main window, including a "Done" button and a "Print" button. """ def __init__(self, parent=None, **kw): """ Create a new C{CanvasFrame}. @type parent: C{Tkinter.BaseWidget} or C{Tkinter.Tk} @param parent: The parent C{Tkinter} widget. If no parent is specified, then C{CanvasFrame} will create a new main window. @param kw: Keyword arguments for the new C{Canvas}. See the documentation for C{Tkinter.Canvas} for more information. """ # If no parent was given, set up a top-level window. if parent is None: self._parent = Tk() self._parent.title('NLTK') self._parent.bind('', lambda e: self.print_to_file()) self._parent.bind('', self.destroy) self._parent.bind('', self.destroy) else: self._parent = parent # Create a frame for the canvas & scrollbars self._frame = frame = Frame(self._parent) self._canvas = canvas = Canvas(frame, **kw) xscrollbar = Scrollbar(self._frame, orient='horizontal') yscrollbar = Scrollbar(self._frame, orient='vertical') xscrollbar['command'] = canvas.xview yscrollbar['command'] = canvas.yview canvas['xscrollcommand'] = xscrollbar.set canvas['yscrollcommand'] = yscrollbar.set yscrollbar.pack(fill='y', side='right') xscrollbar.pack(fill='x', side='bottom') canvas.pack(expand=1, fill='both', side='left') # Set initial scroll region. scrollregion = '0 0 %s %s' % (canvas['width'], canvas['height']) canvas['scrollregion'] = scrollregion self._scrollwatcher = ScrollWatcherWidget(canvas) # If no parent was given, pack the frame, and add a menu. if parent is None: self.pack(expand=1, fill='both') self._init_menubar() def _init_menubar(self): menubar = Menu(self._parent) filemenu = Menu(menubar, tearoff=0) filemenu.add_command(label='Print to Postscript', underline=0, command=self.print_to_file, accelerator='Ctrl-p') filemenu.add_command(label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-x') menubar.add_cascade(label='File', underline=0, menu=filemenu) self._parent.config(menu=menubar) def print_to_file(self, filename=None): """ Print the contents of this C{CanvasFrame} to a postscript file. If no filename is given, then prompt the user for one. @param filename: The name of the file to print the tree to. @type filename: C{string} @rtype: C{None} """ if filename is None: from tkFileDialog import asksaveasfilename ftypes = [('Postscript files', '.ps'), ('All files', '*')] filename = asksaveasfilename(filetypes=ftypes, defaultextension='.ps') if not filename: return (x0, y0, w, h) = self.scrollregion() self._canvas.postscript(file=filename, x=x0, y=y0, width=w+2, height=h+2, pagewidth=w+2, # points = 1/72 inch pageheight=h+2, # points = 1/72 inch pagex=0, pagey=0) def scrollregion(self): """ @return: The current scroll region for the canvas managed by this C{CanvasFrame}. @rtype: 4-tuple of C{int} """ (x1, y1, x2, y2) = self._canvas['scrollregion'].split() return (int(x1), int(y1), int(x2), int(y2)) def canvas(self): """ @return: The canvas managed by this C{CanvasFrame}. @rtype: C{Tkinter.Canvas} """ return self._canvas def add_widget(self, canvaswidget, x=None, y=None): """ Register a canvas widget with this C{CanvasFrame}. The C{CanvasFrame} will ensure that this canvas widget is always within the C{Canvas}'s scrollregion. If no coordinates are given for the canvas widget, then the C{CanvasFrame} will attempt to find a clear area of the canvas for it. @type canvaswidget: C{CanvasWidget} @param canvaswidget: The new canvas widget. C{canvaswidget} must have been created on this C{CanvasFrame}'s canvas. @type x: C{int} @param x: The initial x coordinate for the upper left hand corner of C{canvaswidget}, in the canvas's coordinate space. @type y: C{int} @param y: The initial y coordinate for the upper left hand corner of C{canvaswidget}, in the canvas's coordinate space. """ if x is None or y is None: (x, y) = self._find_room(canvaswidget, x, y) # Move to (x,y) (x1,y1,x2,y2) = canvaswidget.bbox() canvaswidget.move(x-x1,y-y1) # Register with scrollwatcher. self._scrollwatcher.add_child(canvaswidget) def _find_room(self, widget, desired_x, desired_y): """ Try to find a space for a given widget. """ (left, top, right, bot) = self.scrollregion() w = widget.width() h = widget.height() if w >= (right-left): return (0,0) if h >= (bot-top): return (0,0) # Move the widget out of the way, for now. (x1,y1,x2,y2) = widget.bbox() widget.move(left-x2-50, top-y2-50) if desired_x is not None: x = desired_x for y in range(top, bot-h, (bot-top-h)/10): if not self._canvas.find_overlapping(x-5, y-5, x+w+5, y+h+5): return (x,y) if desired_y is not None: y = desired_y for x in range(left, right-w, (right-left-w)/10): if not self._canvas.find_overlapping(x-5, y-5, x+w+5, y+h+5): return (x,y) for y in range(top, bot-h, (bot-top-h)/10): for x in range(left, right-w, (right-left-w)/10): if not self._canvas.find_overlapping(x-5, y-5, x+w+5, y+h+5): return (x,y) return (0,0) def destroy_widget(self, canvaswidget): """ Remove a canvas widget from this C{CanvasFrame}. This deregisters the canvas widget, and destroys it. """ self.remove_widget(canvaswidget) canvaswidget.destroy() def remove_widget(self, canvaswidget): # Deregister with scrollwatcher. self._scrollwatcher.remove_child(canvaswidget) def pack(self, cnf={}, **kw): """ Pack this C{CanvasFrame}. See the documentation for C{Tkinter.Pack} for more information. """ self._frame.pack(cnf, **kw) # Adjust to be big enough for kids? def destroy(self, *e): """ Destroy this C{CanvasFrame}. If this C{CanvasFrame} created a top-level window, then this will close that window. """ if self._parent is None: return self._parent.destroy() self._parent = None def mainloop(self, *args, **kwargs): """ Enter the Tkinter mainloop. This function must be called if this frame is created from a non-interactive program (e.g. from a secript); otherwise, the frame will close as soon as the script completes. """ if in_idle(): return self._parent.mainloop(*args, **kwargs) ##////////////////////////////////////////////////////// ## Text display ##////////////////////////////////////////////////////// class ShowText(object): """ A C{Tkinter} window used to display a text. C{ShowText} is typically used by graphical tools to display help text, or similar information. """ def __init__(self, root, title, text, width=None, height=None, **textbox_options): if width is None or height is None: (width, height) = self.find_dimentions(text, width, height) # Create the main window. if root is None: self._top = top = Tk() else: self._top = top = Toplevel(root) top.title(title) b = Button(top, text='Ok', command=self.destroy) b.pack(side='bottom') tbf = Frame(top) tbf.pack(expand=1, fill='both') scrollbar = Scrollbar(tbf, orient='vertical') scrollbar.pack(side='right', fill='y') textbox = Text(tbf, wrap='word', width=width, height=height, **textbox_options) textbox.insert('end', text) textbox['state'] = 'disabled' textbox.pack(side='left', expand=1, fill='both') scrollbar['command'] = textbox.yview textbox['yscrollcommand'] = scrollbar.set # Make it easy to close the window. top.bind('q', self.destroy) top.bind('x', self.destroy) top.bind('c', self.destroy) top.bind('', self.destroy) top.bind('', self.destroy) # Focus the scrollbar, so they can use up/down, etc. scrollbar.focus() def find_dimentions(self, text, width, height): lines = text.split('\n') if width is None: maxwidth = max([len(line) for line in lines]) width = min(maxwidth, 80) # Now, find height. height = 0 for line in lines: while len(line) > width: brk = line[:width].rfind(' ') line = line[brk:] height += 1 height += 1 height = min(height, 25) return (width, height) def destroy(self, *e): if self._top is None: return self._top.destroy() self._top = None def mainloop(self, *args, **kwargs): """ Enter the Tkinter mainloop. This function must be called if this window is created from a non-interactive program (e.g. from a secript); otherwise, the window will close as soon as the script completes. """ if in_idle(): return self._top.mainloop(*args, **kwargs) ##////////////////////////////////////////////////////// ## Entry dialog ##////////////////////////////////////////////////////// class EntryDialog(object): """ A dialog box for entering """ def __init__(self, parent, original_text='', instructions='', set_callback=None, title=None): self._parent = parent self._original_text = original_text self._set_callback = set_callback width = max(30, len(original_text)*3/2) self._top = Toplevel(parent) if title: self._top.title(title) # The text entry box. entryframe = Frame(self._top) entryframe.pack(expand=1, fill='both', padx=5, pady=5,ipady=10) if instructions: l=Label(entryframe, text=instructions) l.pack(side='top', anchor='w', padx=30) self._entry = Entry(entryframe, width=width) self._entry.pack(expand=1, fill='x', padx=30) self._entry.insert(0, original_text) # A divider divider = Frame(self._top, borderwidth=1, relief='sunken') divider.pack(fill='x', ipady=1, padx=10) # The buttons. buttons = Frame(self._top) buttons.pack(expand=0, fill='x', padx=5, pady=5) b = Button(buttons, text='Cancel', command=self._cancel, width=8) b.pack(side='right', padx=5) b = Button(buttons, text='Ok', command=self._ok, width=8, default='active') b.pack(side='left', padx=5) b = Button(buttons, text='Apply', command=self._apply, width=8) b.pack(side='left') self._top.bind('', self._ok) self._top.bind('', self._cancel) self._top.bind('', self._cancel) self._entry.focus() def _reset(self, *e): self._entry.delete(0,'end') self._entry.insert(0, self._original_text) if self._set_callback: self._set_callback(self._original_text) def _cancel(self, *e): try: self._reset() except: pass self._destroy() def _ok(self, *e): self._apply() self._destroy() def _apply(self, *e): if self._set_callback: self._set_callback(self._entry.get()) def _destroy(self, *e): if self._top is None: return self._top.destroy() self._top = None ##////////////////////////////////////////////////////// ## Colorized List ##////////////////////////////////////////////////////// class ColorizedList(object): """ An abstract base class for displaying a colorized list of items. Subclasses should define: - L{_init_colortags}, which sets up Text color tags that will be used by the list. - L{_item_repr}, which returns a list of (text,colortag) tuples that make up the colorized representation of the item. @note: Typically, you will want to register a callback for C{'select'} that calls L{mark} on the given item. """ def __init__(self, parent, items=[], **options): """ Construct a new list. @param parent: The Tk widget that contains the colorized list @param items: The initial contents of the colorized list. @param options: """ self._parent = parent self._callbacks = {} # Which items are marked? self._marks = {} # Initialize the Tkinter frames. self._init_itemframe(options.copy()) # Set up key & mouse bindings. self._textwidget.bind('', self._keypress) self._textwidget.bind('', self._buttonpress) # Fill in the given CFG's items. self._items = None self.set(items) #//////////////////////////////////////////////////////////// # Abstract methods #//////////////////////////////////////////////////////////// def _init_colortags(self, textwidget, options): """ Set up any colortags that will be used by this colorized list. E.g.: >>> textwidget.tag_config('terminal', foreground='black') """ raise AssertionError, 'Abstract base class' def _item_repr(self, item): """ Return a list of (text, colortag) tuples that make up the colorized representation of the item. Colorized representations may not span multiple lines. I.e., the text strings returned may not contain newline characters. """ raise AssertionError, 'Abstract base class' #//////////////////////////////////////////////////////////// # Item Access #//////////////////////////////////////////////////////////// def get(self, index=None): """ @return: A list of the items contained by this list. """ if index is None: return self._items[:] else: return self._items[index] def set(self, items): """ Modify the list of items contained by this list. """ items = list(items) if self._items == items: return self._items = list(items) self._textwidget['state'] = 'normal' self._textwidget.delete('1.0', 'end') for item in items: for (text, colortag) in self._item_repr(item): assert '\n' not in text, 'item repr may not contain newline' self._textwidget.insert('end', text, colortag) self._textwidget.insert('end', '\n') # Remove the final newline self._textwidget.delete('end-1char', 'end') self._textwidget.mark_set('insert', '1.0') self._textwidget['state'] = 'disabled' # Clear all marks self._marks.clear() def unmark(self, item=None): """ Remove highlighting from the given item; or from every item, if no item is given. @raise ValueError: If C{item} is not contained in the list. @raise KeyError: If C{item} is not marked. """ if item is None: self._marks.clear() self._textwidget.tag_remove('highlight', '1.0', 'end+1char') else: index = self._items.index(item) del self._marks[item] (start, end) = ('%d.0' % (index+1), '%d.0' % (index+2)) self._textwidget.tag_remove('highlight', start, end) def mark(self, item): """ Highlight the given item. @raise ValueError: If C{item} is not contained in the list. """ self._marks[item] = 1 index = self._items.index(item) (start, end) = ('%d.0' % (index+1), '%d.0' % (index+2)) self._textwidget.tag_add('highlight', start, end) def markonly(self, item): """ Remove any current highlighting, and mark the given item. @raise ValueError: If C{item} is not contained in the list. """ self.unmark() self.mark(item) def view(self, item): """ Adjust the view such that the given item is visible. If the item is already visible, then do nothing. """ index = self._items.index(item) self._textwidget.see('%d.0' % (index+1)) #//////////////////////////////////////////////////////////// # Callbacks #//////////////////////////////////////////////////////////// def add_callback(self, event, func): """ Register a callback function with the list. This function will be called whenever the given event occurs. @param event: The event that will trigger the callback function. Valid events are: click1, click2, click3, space, return, select, up, down, next, prior, move @param func: The function that should be called when the event occurs. C{func} will be called with a single item as its argument. (The item selected or the item moved to). """ if event == 'select': events = ['click1', 'space', 'return'] elif event == 'move': events = ['up', 'down', 'next', 'prior'] else: events = [event] for e in events: self._callbacks.setdefault(e,{})[func] = 1 def remove_callback(self, event, func=None): """ Deregister a callback function. If C{func} is none, then all callbacks are removed for the given event. """ if event is None: events = self._callbacks.keys() elif event == 'select': events = ['click1', 'space', 'return'] elif event == 'move': events = ['up', 'down', 'next', 'prior'] else: events = [event] for e in events: if func is None: del self._callbacks[e] else: try: del self._callbacks[e][func] except: pass #//////////////////////////////////////////////////////////// # Tkinter Methods #//////////////////////////////////////////////////////////// def pack(self, cnf={}, **kw): # "@include: Tkinter.Pack.pack" self._itemframe.pack(cnf, **kw) def grid(self, cnf={}, **kw): # "@include: Tkinter.Grid.grid" self._itemframe.grid(cnf, *kw) def focus(self): # "@include: Tkinter.Widget.focus" self._textwidget.focus() #//////////////////////////////////////////////////////////// # Internal Methods #//////////////////////////////////////////////////////////// def _init_itemframe(self, options): self._itemframe = Frame(self._parent) # Create the basic Text widget & scrollbar. options.setdefault('background', '#e0e0e0') self._textwidget = Text(self._itemframe, **options) self._textscroll = Scrollbar(self._itemframe, takefocus=0, orient='vertical') self._textwidget.config(yscrollcommand = self._textscroll.set) self._textscroll.config(command=self._textwidget.yview) self._textscroll.pack(side='right', fill='y') self._textwidget.pack(expand=1, fill='both', side='left') # Initialize the colorization tags self._textwidget.tag_config('highlight', background='#e0ffff', border='1', relief='raised') self._init_colortags(self._textwidget, options) # How do I want to mark keyboard selection? self._textwidget.tag_config('sel', foreground='') self._textwidget.tag_config('sel', foreground='', background='', border='', underline=1) self._textwidget.tag_lower('highlight', 'sel') def _fire_callback(self, event, itemnum): if not self._callbacks.has_key(event): return if 0 <= itemnum < len(self._items): item = self._items[itemnum] else: item = None for cb_func in self._callbacks[event].keys(): cb_func(item) def _buttonpress(self, event): clickloc = '@%d,%d' % (event.x,event.y) insert_point = self._textwidget.index(clickloc) itemnum = int(insert_point.split('.')[0])-1 self._fire_callback('click%d' % event.num, itemnum) def _keypress(self, event): if event.keysym == 'Return' or event.keysym == 'space': insert_point = self._textwidget.index('insert') itemnum = int(insert_point.split('.')[0])-1 self._fire_callback(event.keysym.lower(), itemnum) return elif event.keysym == 'Down': delta='+1line' elif event.keysym == 'Up': delta='-1line' elif event.keysym == 'Next': delta='+10lines' elif event.keysym == 'Prior': delta='-10lines' else: return 'continue' self._textwidget.mark_set('insert', 'insert'+delta) self._textwidget.see('insert') self._textwidget.tag_remove('sel', '1.0', 'end+1char') self._textwidget.tag_add('sel', 'insert linestart', 'insert lineend') insert_point = self._textwidget.index('insert') itemnum = int(insert_point.split('.')[0])-1 self._fire_callback(event.keysym.lower(), itemnum) return 'break' ##////////////////////////////////////////////////////// ## Improved OptionMenu ##////////////////////////////////////////////////////// class MutableOptionMenu(Menubutton): def __init__(self, master, values, **options): self._callback = options.get('command') if 'command' in options: del options['command'] # Create a variable self._variable = variable = StringVar() if len(values) > 0: variable.set(values[0]) kw = {"borderwidth": 2, "textvariable": variable, "indicatoron": 1, "relief": RAISED, "anchor": "c", "highlightthickness": 2} kw.update(options) Widget.__init__(self, master, "menubutton", kw) self.widgetName = 'tk_optionMenu' self._menu = Menu(self, name="menu", tearoff=0,) self.menuname = self._menu._w self._values = [] for value in values: self.add(value) self["menu"] = self._menu def add(self, value): if value in self._values: return def set(value=value): self.set(value) self._menu.add_command(label=value, command=set) self._values.append(value) def set(self, value): self._variable.set(value) if self._callback: self._callback(value) def remove(self, value): # Might raise indexerror: pass to parent. i = self._values.index(value) del self._values[i] self._menu.delete(i, i) def __getitem__(self, name): if name == 'menu': return self.__menu return Widget.__getitem__(self, name) def destroy(self): """Destroy this widget and the associated menu.""" Menubutton.destroy(self) self._menu = None ##////////////////////////////////////////////////////// ## Test code. ##////////////////////////////////////////////////////// def demo(): """ A simple demonstration showing how to use canvas widgets. """ def fill(cw): from random import randint cw['fill'] = '#00%04d' % randint(0,9999) def color(cw): from random import randint cw['color'] = '#ff%04d' % randint(0,9999) cf = CanvasFrame(closeenough=10, width=300, height=300) c = cf.canvas() ct3 = TextWidget(c, 'hiya there', draggable=1) ct2 = TextWidget(c, 'o o\n||\n___\n U', draggable=1, justify='center') co = OvalWidget(c, ct2, outline='red') ct = TextWidget(c, 'o o\n||\n\\___/', draggable=1, justify='center') cp = ParenWidget(c, ct, color='red') cb = BoxWidget(c, cp, fill='cyan', draggable=1, width=3, margin=10) equation = SequenceWidget(c, SymbolWidget(c, 'forall'), TextWidget(c, 'x'), SymbolWidget(c, 'exists'), TextWidget(c, 'y: '), TextWidget(c, 'x'), SymbolWidget(c, 'notequal'), TextWidget(c, 'y')) space = SpaceWidget(c, 0, 30) cstack = StackWidget(c, cb, ct3, space, co, equation, align='center') foo = TextWidget(c, 'try clicking\nand dragging', draggable=1, justify='center') cs = SequenceWidget(c, cstack, foo) zz = BracketWidget(c, cs, color='green4', width=3) cf.add_widget(zz, 60, 30) cb.bind_click(fill) ct.bind_click(color) co.bind_click(fill) ct2.bind_click(color) ct3.bind_click(color) cf.mainloop() #ShowText(None, 'title', ((('this is text'*150)+'\n')*5)) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/draw/tree.py0000644000175000017500000011110411327451575016004 0ustar bhavanibhavani# Natural Language Toolkit: Graphical Representations for Trees # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # # $Id: tree.py 8479 2010-01-13 05:40:34Z StevenBird1 $ """ Graphically display a C{Tree}. """ import sys from nltk.tree import Tree, bracket_parse from util import * ##////////////////////////////////////////////////////// ## Tree Segment ##////////////////////////////////////////////////////// class TreeSegmentWidget(CanvasWidget): """ A canvas widget that displays a single segment of a hierarchical tree. Each C{TreeSegmentWidget} connects a single X{node widget} to a sequence of zero or more X{subtree widgets}. By default, the bottom of the node is connected to the top of each subtree by a single line. However, if the C{roof} attribute is set, then a single triangular "roof" will connect the node to all of its children. Attributes: - C{roof}: What sort of connection to draw between the node and its subtrees. If C{roof} is true, draw a single triangular "roof" over the subtrees. If C{roof} is false, draw a line between each subtree and the node. Default value is false. - C{xspace}: The amount of horizontal space to leave between subtrees when managing this widget. Default value is 10. - C{yspace}: The amount of space to place between the node and its children when managing this widget. Default value is 15. - C{color}: The color of the lines connecting the node to its subtrees; and of the outline of the triangular roof. Default value is C{'#006060'}. - C{fill}: The fill color for the triangular roof. Default value is C{''} (no fill). - C{width}: The width of the lines connecting the node to its subtrees; and of the outline of the triangular roof. Default value is 1. - C{orientation}: Determines whether the tree branches downwards or rightwards. Possible values are C{'horizontal'} and C{'vertical'}. The default value is C{'vertical'} (i.e., branch downwards). - C{draggable}: whether the widget can be dragged by the user. The following attributes may also be added in the near future: - C{lineM{n}_color}: The color of the line connecting the node to its C{M{n}}th subtree. - C{lineM{n}_color}: The width of the line connecting the node to its C{M{n}}th subtree. - C{lineM{n}_color}: The dash pattern of the line connecting the node to its C{M{n}}th subtree. """ def __init__(self, canvas, node, subtrees, **attribs): """ @type node: @type subtrees: C{list} of C{CanvasWidgetI} """ self._node = node self._subtrees = subtrees # Attributes self._horizontal = 0 self._roof = 0 self._xspace = 10 self._yspace = 15 self._ordered = False # Create canvas objects. self._lines = [canvas.create_line(0,0,0,0, fill='#006060') for c in subtrees] self._polygon = canvas.create_polygon(0,0, fill='', state='hidden', outline='#006060') # Register child widgets (node + subtrees) self._add_child_widget(node) for subtree in subtrees: self._add_child_widget(subtree) # Are we currently managing? self._managing = False CanvasWidget.__init__(self, canvas, **attribs) def __setitem__(self, attr, value): canvas = self.canvas() if attr is 'roof': self._roof = value if self._roof: for l in self._lines: canvas.itemconfig(l, state='hidden') canvas.itemconfig(self._polygon, state='normal') else: for l in self._lines: canvas.itemconfig(l, state='normal') canvas.itemconfig(self._polygon, state='hidden') elif attr == 'orientation': if value == 'horizontal': self._horizontal = 1 elif value == 'vertical': self._horizontal = 0 else: raise ValueError('orientation must be horizontal or vertical') elif attr == 'color': for l in self._lines: canvas.itemconfig(l, fill=value) canvas.itemconfig(self._polygon, outline=value) elif isinstance(attr, tuple) and attr[0] == 'color': # Set the color of an individual line. l = self._lines[int(attr[1])] canvas.itemconfig(l, fill=value) elif attr == 'fill': canvas.itemconfig(self._polygon, fill=value) elif attr == 'width': canvas.itemconfig(self._polygon, {attr:value}) for l in self._lines: canvas.itemconfig(l, {attr:value}) elif attr in ('xspace', 'yspace'): if attr == 'xspace': self._xspace = value elif attr == 'yspace': self._yspace = value self.update(self._node) elif attr == 'ordered': self._ordered = value else: CanvasWidget.__setitem__(self, attr, value) def __getitem__(self, attr): if attr == 'roof': return self._roof elif attr == 'width': return self.canvas().itemcget(self._polygon, attr) elif attr == 'color': return self.canvas().itemcget(self._polygon, 'outline') elif isinstance(attr, tuple) and attr[0] == 'color': l = self._lines[int(attr[1])] return self.canvas().itemcget(l, 'fill') elif attr == 'xspace': return self._xspace elif attr == 'yspace': return self._yspace elif attr == 'orientation': if self._horizontal: return 'horizontal' else: return 'vertical' elif attr == 'ordered': return self._ordered else: return CanvasWidget.__getitem__(self, attr) def node(self): return self._node def subtrees(self): return self._subtrees[:] def set_node(self, node): """ Set the node to C{node}. """ self._remove_child_widget(self._node) self._add_child_widget(node) self._node = node self.update(self._node) def replace_child(self, oldchild, newchild): """ Replace the child C{oldchild} with C{newchild}. """ index = self._subtrees.index(oldchild) self._subtrees[index] = newchild self._remove_child_widget(oldchild) self._add_child_widget(newchild) self.update(newchild) def remove_child(self, child): index = self._subtrees.index(child) del self._subtrees[index] self._remove_child_widget(child) self.canvas().delete(self._lines.pop()) self.update(self._node) def insert_child(self, index, child): self._subtrees.insert(index, child) self._add_child_widget(child) self._lines.append(canvas.create_line(0,0,0,0, fill='#006060')) self.update(self._node) # but.. lines??? def _tags(self): if self._roof: return [self._polygon] else: return self._lines def _subtree_top(self, child): if isinstance(child, TreeSegmentWidget): bbox = child.node().bbox() else: bbox = child.bbox() if self._horizontal: return (bbox[0], (bbox[1]+bbox[3])/2.0) else: return ((bbox[0]+bbox[2])/2.0, bbox[1]) def _node_bottom(self): bbox = self._node.bbox() if self._horizontal: return (bbox[2], (bbox[1]+bbox[3])/2.0) else: return ((bbox[0]+bbox[2])/2.0, bbox[3]) def _update(self, child): if len(self._subtrees) == 0: return if self._node.bbox() is None: return # [XX] ??? # Which lines need to be redrawn? if child is self._node: need_update = self._subtrees else: need_update = [child] if self._ordered and not self._managing: need_update = self._maintain_order(child) # Update the polygon. (nodex, nodey) = self._node_bottom() (xmin, ymin, xmax, ymax) = self._subtrees[0].bbox() for subtree in self._subtrees[1:]: bbox = subtree.bbox() xmin = min(xmin, bbox[0]) ymin = min(ymin, bbox[1]) xmax = max(xmax, bbox[2]) ymax = max(ymax, bbox[3]) if self._horizontal: self.canvas().coords(self._polygon, nodex, nodey, xmin, ymin, xmin, ymax, nodex, nodey) else: self.canvas().coords(self._polygon, nodex, nodey, xmin, ymin, xmax, ymin, nodex, nodey) # Redraw all lines that need it. for subtree in need_update: (nodex, nodey) = self._node_bottom() line = self._lines[self._subtrees.index(subtree)] (subtreex, subtreey) = self._subtree_top(subtree) self.canvas().coords(line, nodex, nodey, subtreex, subtreey) def _maintain_order(self, child): if self._horizontal: return self._maintain_order_horizontal(child) else: return self._maintain_order_vertical(child) def _maintain_order_vertical(self, child): (left, top, right, bot) = child.bbox() if child is self._node: # Check all the leaves for subtree in self._subtrees: (x1, y1, x2, y2) = subtree.bbox() if bot+self._yspace > y1: subtree.move(0,bot+self._yspace-y1) return self._subtrees else: moved = [child] index = self._subtrees.index(child) # Check leaves to our right. x = right + self._xspace for i in range(index+1, len(self._subtrees)): (x1, y1, x2, y2) = self._subtrees[i].bbox() if x > x1: self._subtrees[i].move(x-x1, 0) x += x2-x1 + self._xspace moved.append(self._subtrees[i]) # Check leaves to our left. x = left - self._xspace for i in range(index-1, -1, -1): (x1, y1, x2, y2) = self._subtrees[i].bbox() if x < x2: self._subtrees[i].move(x-x2, 0) x -= x2-x1 + self._xspace moved.append(self._subtrees[i]) # Check the node (x1, y1, x2, y2) = self._node.bbox() if y2 > top-self._yspace: self._node.move(0, top-self._yspace-y2) moved = self._subtrees # Return a list of the nodes we moved return moved def _maintain_order_horizontal(self, child): (left, top, right, bot) = child.bbox() if child is self._node: # Check all the leaves for subtree in self._subtrees: (x1, y1, x2, y2) = subtree.bbox() if right+self._xspace > x1: subtree.move(right+self._xspace-x1) return self._subtrees else: moved = [child] index = self._subtrees.index(child) # Check leaves below us. y = bot + self._yspace for i in range(index+1, len(self._subtrees)): (x1, y1, x2, y2) = self._subtrees[i].bbox() if y > y1: self._subtrees[i].move(0, y-y1) y += y2-y1 + self._yspace moved.append(self._subtrees[i]) # Check leaves above us y = top - self._yspace for i in range(index-1, -1, -1): (x1, y1, x2, y2) = self._subtrees[i].bbox() if y < y2: self._subtrees[i].move(0, y-y2) y -= y2-y1 + self._yspace moved.append(self._subtrees[i]) # Check the node (x1, y1, x2, y2) = self._node.bbox() if x2 > left-self._xspace: self._node.move(left-self._xspace-x2, 0) moved = self._subtrees # Return a list of the nodes we moved return moved def _manage_horizontal(self): (nodex, nodey) = self._node_bottom() # Put the subtrees in a line. y = 20 for subtree in self._subtrees: subtree_bbox = subtree.bbox() dx = nodex - subtree_bbox[0] + self._xspace dy = y - subtree_bbox[1] subtree.move(dx, dy) y += subtree_bbox[3] - subtree_bbox[1] + self._yspace # Find the center of their tops. center = 0.0 for subtree in self._subtrees: center += self._subtree_top(subtree)[1] center /= len(self._subtrees) # Center the subtrees with the node. for subtree in self._subtrees: subtree.move(0, nodey-center) def _manage_vertical(self): (nodex, nodey) = self._node_bottom() # Put the subtrees in a line. x = 0 for subtree in self._subtrees: subtree_bbox = subtree.bbox() dy = nodey - subtree_bbox[1] + self._yspace dx = x - subtree_bbox[0] subtree.move(dx, dy) x += subtree_bbox[2] - subtree_bbox[0] + self._xspace # Find the center of their tops. center = 0.0 for subtree in self._subtrees: center += self._subtree_top(subtree)[0]/len(self._subtrees) # Center the subtrees with the node. for subtree in self._subtrees: subtree.move(nodex-center, 0) def _manage(self): self._managing = True (nodex, nodey) = self._node_bottom() if len(self._subtrees) == 0: return if self._horizontal: self._manage_horizontal() else: self._manage_vertical() # Update lines to subtrees. for subtree in self._subtrees: self._update(subtree) self._managing = False def __repr__(self): return '[TreeSeg %s: %s]' % (self._node, self._subtrees) def _tree_to_treeseg(canvas, t, make_node, make_leaf, tree_attribs, node_attribs, leaf_attribs, loc_attribs): if isinstance(t, Tree): node = make_node(canvas, t.node, **node_attribs) subtrees = [_tree_to_treeseg(canvas, child, make_node, make_leaf, tree_attribs, node_attribs, leaf_attribs, loc_attribs) for child in t] return TreeSegmentWidget(canvas, node, subtrees, **tree_attribs) else: return make_leaf(canvas, t, **leaf_attribs) def tree_to_treesegment(canvas, t, make_node=TextWidget, make_leaf=TextWidget, **attribs): """ Convert a C{Tree} into a C{TreeSegmentWidget}. @param make_node: A C{CanvasWidget} constructor or a function that creates C{CanvasWidgets}. C{make_node} is used to convert the C{Tree}'s nodes into C{CanvasWidgets}. If no constructor is specified, then C{TextWidget} will be used. @param make_leaf: A C{CanvasWidget} constructor or a function that creates C{CanvasWidgets}. C{make_leaf} is used to convert the C{Tree}'s leafs into C{CanvasWidgets}. If no constructor is specified, then C{TextWidget} will be used. @param attribs: Attributes for the canvas widgets that make up the returned C{TreeSegmentWidget}. Any attribute beginning with C{'tree_'} will be passed to all C{TreeSegmentWidget}s (with the C{'tree_'} prefix removed. Any attribute beginning with C{'node_'} will be passed to all nodes. Any attribute beginning with C{'leaf_'} will be passed to all leaves. And any attribute beginning with C{'loc_'} will be passed to all text locations (for C{Tree}s). """ # Process attribs. tree_attribs = {} node_attribs = {} leaf_attribs = {} loc_attribs = {} for (key, value) in attribs.items(): if key[:5] == 'tree_': tree_attribs[key[5:]] = value elif key[:5] == 'node_': node_attribs[key[5:]] = value elif key[:5] == 'leaf_': leaf_attribs[key[5:]] = value elif key[:4] == 'loc_': loc_attribs[key[4:]] = value else: raise ValueError('Bad attribute: %s' % key) return _tree_to_treeseg(canvas, t, make_node, make_leaf, tree_attribs, node_attribs, leaf_attribs, loc_attribs) ##////////////////////////////////////////////////////// ## Tree Widget ##////////////////////////////////////////////////////// class TreeWidget(CanvasWidget): """ A canvas widget that displays a single C{Tree}. C{TreeWidget} manages a group of C{TreeSegmentWidget}s that are used to display a C{Tree}. Attributes: - C{node_M{attr}}: Sets the attribute C{M{attr}} on all of the node widgets for this C{TreeWidget}. - C{node_M{attr}}: Sets the attribute C{M{attr}} on all of the leaf widgets for this C{TreeWidget}. - C{loc_M{attr}}: Sets the attribute C{M{attr}} on all of the location widgets for this C{TreeWidget} (if it was built from a C{Tree}). Note that location widgets are C{TextWidget}s. - C{xspace}: The amount of horizontal space to leave between subtrees when managing this widget. Default value is 10. - C{yspace}: The amount of space to place between the node and its children when managing this widget. Default value is 15. - C{line_color}: The color of the lines connecting each expanded node to its subtrees. - C{roof_color}: The color of the outline of the triangular roof for collapsed trees. - C{roof_fill}: The fill color for the triangular roof for collapsed trees. - C{width} - C{orientation}: Determines whether the tree branches downwards or rightwards. Possible values are C{'horizontal'} and C{'vertical'}. The default value is C{'vertical'} (i.e., branch downwards). - C{shapeable}: whether the subtrees can be independantly dragged by the user. THIS property simply sets the C{DRAGGABLE} property on all of the C{TreeWidget}'s tree segments. - C{draggable}: whether the widget can be dragged by the user. """ def __init__(self, canvas, t, make_node=TextWidget, make_leaf=TextWidget, **attribs): # Node & leaf canvas widget constructors self._make_node = make_node self._make_leaf = make_leaf self._tree = t # Attributes. self._nodeattribs = {} self._leafattribs = {} self._locattribs = {'color': '#008000'} self._line_color = '#008080' self._line_width = 1 self._roof_color = '#008080' self._roof_fill = '#c0c0c0' self._shapeable = False self._xspace = 10 self._yspace = 10 self._orientation = 'vertical' self._ordered = False # Build trees. self._keys = {} # treeseg -> key self._expanded_trees = {} self._collapsed_trees = {} self._nodes = [] self._leaves = [] #self._locs = [] self._make_collapsed_trees(canvas, t, ()) self._treeseg = self._make_expanded_tree(canvas, t, ()) self._add_child_widget(self._treeseg) CanvasWidget.__init__(self, canvas, **attribs) def expanded_tree(self, *path_to_tree): """ Return the C{TreeSegmentWidget} for the specified subtree. @param path_to_tree: A list of indices i1, i2, ..., in, where the desired widget is the widget corresponding to C{tree.children()[i1].children()[i2]....children()[in]}. For the root, the path is C{()}. """ return self._expanded_trees[path_to_tree] def collapsed_tree(self, *path_to_tree): """ Return the C{TreeSegmentWidget} for the specified subtree. @param path_to_tree: A list of indices i1, i2, ..., in, where the desired widget is the widget corresponding to C{tree.children()[i1].children()[i2]....children()[in]}. For the root, the path is C{()}. """ return self._collapsed_trees[path_to_tree] def bind_click_trees(self, callback, button=1): """ Add a binding to all tree segments. """ for tseg in self._expanded_trees.values(): tseg.bind_click(callback, button) for tseg in self._collapsed_trees.values(): tseg.bind_click(callback, button) def bind_drag_trees(self, callback, button=1): """ Add a binding to all tree segments. """ for tseg in self._expanded_trees.values(): tseg.bind_drag(callback, button) for tseg in self._collapsed_trees.values(): tseg.bind_drag(callback, button) def bind_click_leaves(self, callback, button=1): """ Add a binding to all leaves. """ for leaf in self._leaves: leaf.bind_click(callback, button) for leaf in self._leaves: leaf.bind_click(callback, button) def bind_drag_leaves(self, callback, button=1): """ Add a binding to all leaves. """ for leaf in self._leaves: leaf.bind_drag(callback, button) for leaf in self._leaves: leaf.bind_drag(callback, button) def bind_click_nodes(self, callback, button=1): """ Add a binding to all nodes. """ for node in self._nodes: node.bind_click(callback, button) for node in self._nodes: node.bind_click(callback, button) def bind_drag_nodes(self, callback, button=1): """ Add a binding to all nodes. """ for node in self._nodes: node.bind_drag(callback, button) for node in self._nodes: node.bind_drag(callback, button) def _make_collapsed_trees(self, canvas, t, key): if not isinstance(t, Tree): return make_node = self._make_node make_leaf = self._make_leaf node = make_node(canvas, t.node, **self._nodeattribs) self._nodes.append(node) leaves = [make_leaf(canvas, l, **self._leafattribs) for l in t.leaves()] self._leaves += leaves treeseg = TreeSegmentWidget(canvas, node, leaves, roof=1, color=self._roof_color, fill=self._roof_fill, width=self._line_width) self._collapsed_trees[key] = treeseg self._keys[treeseg] = key #self._add_child_widget(treeseg) treeseg.hide() # Build trees for children. for i in range(len(t)): child = t[i] self._make_collapsed_trees(canvas, child, key + (i,)) def _make_expanded_tree(self, canvas, t, key): make_node = self._make_node make_leaf = self._make_leaf if isinstance(t, Tree): node = make_node(canvas, t.node, **self._nodeattribs) self._nodes.append(node) children = t subtrees = [self._make_expanded_tree(canvas, children[i], key+(i,)) for i in range(len(children))] treeseg = TreeSegmentWidget(canvas, node, subtrees, color=self._line_color, width=self._line_width) self._expanded_trees[key] = treeseg self._keys[treeseg] = key return treeseg else: leaf = make_leaf(canvas, t, **self._leafattribs) self._leaves.append(leaf) return leaf def __setitem__(self, attr, value): if attr[:5] == 'node_': for node in self._nodes: node[attr[5:]] = value elif attr[:5] == 'leaf_': for leaf in self._leaves: leaf[attr[5:]] = value elif attr == 'line_color': self._line_color = value for tseg in self._expanded_trees.values(): tseg['color'] = value elif attr == 'line_width': self._line_width = value for tseg in self._expanded_trees.values(): tseg['width'] = value for tseg in self._collapsed_trees.values(): tseg['width'] = value elif attr == 'roof_color': self._roof_color = value for tseg in self._collapsed_trees.values(): tseg['color'] = value elif attr == 'roof_fill': self._roof_fill = value for tseg in self._collapsed_trees.values(): tseg['fill'] = value elif attr == 'shapeable': self._shapeable = value for tseg in self._expanded_trees.values(): tseg['draggable'] = value for tseg in self._collapsed_trees.values(): tseg['draggable'] = value for leaf in self._leaves: leaf['draggable'] = value elif attr == 'xspace': self._xspace = value for tseg in self._expanded_trees.values(): tseg['xspace'] = value for tseg in self._collapsed_trees.values(): tseg['xspace'] = value self.manage() elif attr == 'yspace': self._yspace = value for tseg in self._expanded_trees.values(): tseg['yspace'] = value for tseg in self._collapsed_trees.values(): tseg['yspace'] = value self.manage() elif attr == 'orientation': self._orientation = value for tseg in self._expanded_trees.values(): tseg['orientation'] = value for tseg in self._collapsed_trees.values(): tseg['orientation'] = value self.manage() elif attr == 'ordered': self._ordered = value for tseg in self._expanded_trees.values(): tseg['ordered'] = value for tseg in self._collapsed_trees.values(): tseg['ordered'] = value else: CanvasWidget.__setitem__(self, attr, value) def __getitem__(self, attr): if attr[:5] == 'node_': return self._nodeattribs.get(attr[5:], None) elif attr[:5] == 'leaf_': return self._leafattribs.get(attr[5:], None) elif attr[:4] == 'loc_': return self._locattribs.get(attr[4:], None) elif attr == 'line_color': return self._line_color elif attr == 'line_width': return self._line_width elif attr == 'roof_color': return self._roof_color elif attr == 'roof_fill': return self._roof_fill elif attr == 'shapeable': return self._shapeable elif attr == 'xspace': return self._xspace elif attr == 'yspace': return self._yspace elif attr == 'orientation': return self._orientation else: return CanvasWidget.__getitem__(self, attr) def _tags(self): return [] def _manage(self): segs = self._expanded_trees.values() + self._collapsed_trees.values() for tseg in segs: if tseg.hidden(): tseg.show() tseg.manage() tseg.hide() def toggle_collapsed(self, treeseg): """ Collapse/expand a tree. """ old_treeseg = treeseg if old_treeseg['roof']: new_treeseg = self._expanded_trees[self._keys[old_treeseg]] else: new_treeseg = self._collapsed_trees[self._keys[old_treeseg]] # Replace the old tree with the new tree. if old_treeseg.parent() is self: self._remove_child_widget(old_treeseg) self._add_child_widget(new_treeseg) self._treeseg = new_treeseg else: old_treeseg.parent().replace_child(old_treeseg, new_treeseg) # Move the new tree to where the old tree was. Show it first, # so we can find its bounding box. new_treeseg.show() (newx, newy) = new_treeseg.node().bbox()[:2] (oldx, oldy) = old_treeseg.node().bbox()[:2] new_treeseg.move(oldx-newx, oldy-newy) # Hide the old tree old_treeseg.hide() # We could do parent.manage() here instead, if we wanted. new_treeseg.parent().update(new_treeseg) ##////////////////////////////////////////////////////// ## draw_trees ##////////////////////////////////////////////////////// class TreeView(object): def __init__(self, *trees): from nltk.draw import CanvasFrame from math import sqrt, ceil self._trees = trees self._top = Tk() self._top.title('NLTK') self._top.bind('', self.destroy) self._top.bind('', self.destroy) cf = self._cframe = CanvasFrame(self._top) self._top.bind('', self._cframe.print_to_file) # Size is variable. self._size = IntVar(self._top) self._size.set(12) bold = ('helvetica', -self._size.get(), 'bold') helv = ('helvetica', -self._size.get()) # Lay the trees out in a square. self._width = int(ceil(sqrt(len(trees)))) self._widgets = [] for i in range(len(trees)): widget = TreeWidget(cf.canvas(), trees[i], node_font=bold, leaf_color='#008040', node_color='#004080', roof_color='#004040', roof_fill='white', line_color='#004040', draggable=1, leaf_font=helv) widget.bind_click_trees(widget.toggle_collapsed) self._widgets.append(widget) cf.add_widget(widget, 0, 0) self._layout() self._cframe.pack(expand=1, fill='both') self._init_menubar() def _layout(self): i = x = y = ymax = 0 width = self._width for i in range(len(self._widgets)): widget = self._widgets[i] (oldx, oldy) = widget.bbox()[:2] if i % width == 0: y = ymax x = 0 widget.move(x-oldx, y-oldy) x = widget.bbox()[2] + 10 ymax = max(ymax, widget.bbox()[3] + 10) def _init_menubar(self): menubar = Menu(self._top) filemenu = Menu(menubar, tearoff=0) filemenu.add_command(label='Print to Postscript', underline=0, command=self._cframe.print_to_file, accelerator='Ctrl-p') filemenu.add_command(label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-x') menubar.add_cascade(label='File', underline=0, menu=filemenu) zoommenu = Menu(menubar, tearoff=0) zoommenu.add_radiobutton(label='Tiny', variable=self._size, underline=0, value=10, command=self.resize) zoommenu.add_radiobutton(label='Small', variable=self._size, underline=0, value=12, command=self.resize) zoommenu.add_radiobutton(label='Medium', variable=self._size, underline=0, value=14, command=self.resize) zoommenu.add_radiobutton(label='Large', variable=self._size, underline=0, value=28, command=self.resize) zoommenu.add_radiobutton(label='Huge', variable=self._size, underline=0, value=50, command=self.resize) menubar.add_cascade(label='Zoom', underline=0, menu=zoommenu) self._top.config(menu=menubar) def resize(self, *e): bold = ('helvetica', -self._size.get(), 'bold') helv = ('helvetica', -self._size.get()) xspace = self._size.get() yspace = self._size.get() for widget in self._widgets: widget['node_font'] = bold widget['leaf_font'] = helv widget['xspace'] = xspace widget['yspace'] = yspace if self._size.get() < 20: widget['line_width'] = 1 elif self._size.get() < 30: widget['line_width'] = 2 else: widget['line_width'] = 3 self._layout() def destroy(self, *e): if self._top is None: return self._top.destroy() self._top = None def mainloop(self, *args, **kwargs): """ Enter the Tkinter mainloop. This function must be called if this demo is created from a non-interactive program (e.g. from a secript); otherwise, the demo will close as soon as the script completes. """ if in_idle(): return self._top.mainloop(*args, **kwargs) def draw_trees(*trees): """ Open a new window containing a graphical diagram of the given trees. @rtype: None """ TreeView(*trees).mainloop() return ##////////////////////////////////////////////////////// ## Demo Code ##////////////////////////////////////////////////////// def demo(): import random def fill(cw): cw['fill'] = '#%06d' % random.randint(0,999999) cf = CanvasFrame(width=550, height=450, closeenough=2) t = bracket_parse(''' (S (NP the very big cat) (VP (Adv sorta) (V saw) (NP (Det the) (N dog))))''') tc = TreeWidget(cf.canvas(), t, draggable=1, node_font=('helvetica', -14, 'bold'), leaf_font=('helvetica', -12, 'italic'), roof_fill='white', roof_color='black', leaf_color='green4', node_color='blue2') cf.add_widget(tc,10,10) def boxit(canvas, text): big = ('helvetica', -16, 'bold') return BoxWidget(canvas, TextWidget(canvas, text, font=big), fill='green') def ovalit(canvas, text): return OvalWidget(canvas, TextWidget(canvas, text), fill='cyan') treetok = bracket_parse('(S (NP this tree) (VP (V is) (AdjP shapeable)))') tc2 = TreeWidget(cf.canvas(), treetok, boxit, ovalit, shapeable=1) def color(node): node['color'] = '#%04d00' % random.randint(0,9999) def color2(treeseg): treeseg.node()['fill'] = '#%06d' % random.randint(0,9999) treeseg.node().child()['color'] = 'white' tc.bind_click_trees(tc.toggle_collapsed) tc2.bind_click_trees(tc2.toggle_collapsed) tc.bind_click_nodes(color, 3) tc2.expanded_tree(1).bind_click(color2, 3) tc2.expanded_tree().bind_click(color2, 3) paren = ParenWidget(cf.canvas(), tc2) cf.add_widget(paren, tc.bbox()[2]+10, 10) tree3 = bracket_parse(''' (S (NP this tree) (AUX was) (VP (V built) (PP (P with) (NP (N tree_to_treesegment)))))''') tc3 = tree_to_treesegment(cf.canvas(), tree3, tree_color='green4', tree_xspace=2, tree_width=2) tc3['draggable'] = 1 cf.add_widget(tc3, 10, tc.bbox()[3]+10) def orientswitch(treewidget): if treewidget['orientation'] == 'horizontal': treewidget.expanded_tree(1,1).subtrees()[0].set_text('vertical') treewidget.collapsed_tree(1,1).subtrees()[0].set_text('vertical') treewidget.collapsed_tree(1).subtrees()[1].set_text('vertical') treewidget.collapsed_tree().subtrees()[3].set_text('vertical') treewidget['orientation'] = 'vertical' else: treewidget.expanded_tree(1,1).subtrees()[0].set_text('horizontal') treewidget.collapsed_tree(1,1).subtrees()[0].set_text('horizontal') treewidget.collapsed_tree(1).subtrees()[1].set_text('horizontal') treewidget.collapsed_tree().subtrees()[3].set_text('horizontal') treewidget['orientation'] = 'horizontal' text = """ Try clicking, right clicking, and dragging different elements of each of the trees. The top-left tree is a TreeWidget built from a Tree. The top-right is a TreeWidget built from a Tree, using non-default widget constructors for the nodes & leaves (BoxWidget and OvalWidget). The bottom-left tree is built from tree_to_treesegment.""" twidget = TextWidget(cf.canvas(), text.strip()) textbox = BoxWidget(cf.canvas(), twidget, fill='white', draggable=1) cf.add_widget(textbox, tc3.bbox()[2]+10, tc2.bbox()[3]+10) tree4 = bracket_parse('(S (NP this tree) (VP (V is) (Adj horizontal)))') tc4 = TreeWidget(cf.canvas(), tree4, draggable=1, line_color='brown2', roof_color='brown2', node_font=('helvetica', -12, 'bold'), node_color='brown4', orientation='horizontal') tc4.manage() cf.add_widget(tc4, tc3.bbox()[2]+10, textbox.bbox()[3]+10) tc4.bind_click(orientswitch) tc4.bind_click_trees(tc4.toggle_collapsed, 3) # Run mainloop cf.mainloop() if __name__ == '__main__': demo() nltk-2.0~b9/nltk/draw/table.py0000644000175000017500000012700411327451576016143 0ustar bhavanibhavani# Natural Language Toolkit: Table widget # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # # $Id:$ """ Tkinter widgets for displaying multi-column listboxes and tables. """ from util import * import operator ###################################################################### # Multi-Column Listbox ###################################################################### class MultiListbox(Frame): """ A multi-column listbox, where the current selection applies to an entire row. Based on the 'U{MultiListbox Tkinter widget }' recipe from the Python Cookbook. For the most part, C{MultiListbox}'s methods just delegate to its contained listboxes. For any methods that do not have docstrings, see C{Tkinter.Listbox} for a description of what that method does. """ #///////////////////////////////////////////////////////////////// # Configuration #///////////////////////////////////////////////////////////////// #: Default configuration values for the frame. FRAME_CONFIG = dict(background='#888', takefocus=True, highlightthickness=1) #: Default configurations for the column labels. LABEL_CONFIG = dict(borderwidth=1, relief='raised', font='helvetica -16 bold', background='#444', foreground='white') #: Default configuration for the column listboxes. LISTBOX_CONFIG = dict(borderwidth=1, selectborderwidth=0, highlightthickness=0, exportselection=False, selectbackground='#888', activestyle='none', takefocus=False) #///////////////////////////////////////////////////////////////// # Constructor #///////////////////////////////////////////////////////////////// def __init__(self, master, columns, column_weights=None, cnf={}, **kw): """ Construct a new multi-column listbox widget. @param master: The widget that should contain the new multi-column listbox. @param columns: Specifies what columns should be included in the new multi-column listbox. If C{columns} is an integer, the it is the number of columns to include. If it is a list, then its length indicates the number of columns to include; and each element of the list will be used as a label for the corresponding column. @param cnf, kw: Configuration parameters for this widget. Use C{label_*} to configure all labels; and C{listbox_*} to configure all listboxes. E.g.: >>> mlb = MultiListbox(master, 5, label_foreground='red') """ # If columns was specified as an int, convert it to a list. if isinstance(columns, int): columns = range(columns) include_labels = False else: include_labels = True if len(columns) == 0: raise ValuError("Expected at least one column") # Instance variables self._column_names = tuple(columns) self._listboxes = [] self._labels = [] # Pick a default value for column_weights, if none was specified. if column_weights is None: column_weights = [1] * len(columns) elif len(column_weights) != len(columns): raise ValueError('Expected one column_weight for each column') self._column_weights = column_weights # Configure our widgets. Frame.__init__(self, master, **self.FRAME_CONFIG) self.grid_rowconfigure(1, weight=1) for i, label in enumerate(self._column_names): self.grid_columnconfigure(i, weight=column_weights[i]) # Create a label for the column if include_labels: l = Label(self, text=label, **self.LABEL_CONFIG) self._labels.append(l) l.grid(column=i, row=0, sticky='news', padx=0, pady=0) l.column_index = i # Create a listbox for the column lb = Listbox(self, **self.LISTBOX_CONFIG) self._listboxes.append(lb) lb.grid(column=i, row=1, sticky='news', padx=0, pady=0) lb.column_index = i # Clicking or dragging selects: lb.bind('', self._select) lb.bind('', self._select) # Scroll whell scrolls: lb.bind('', lambda e: self._scroll(-1)) lb.bind('', lambda e: self._scroll(+1)) lb.bind('', lambda e: self._scroll(e.delta)) # Button 2 can be used to scan: lb.bind('', lambda e: self.scan_mark(e.x, e.y)) lb.bind('', lambda e: self.scan_dragto(e.x, e.y)) # Dragging outside the window has no effect (diable # the default listbox behavior, which scrolls): lb.bind('', lambda e: 'break') # Columns can be resized by dragging them: l.bind('', self._resize_column) # Columns can be resized by dragging them. (This binding is # used if they click on the grid between columns:) self.bind('', self._resize_column) # Set up key bindings for the widget: self.bind('', lambda e: self.select(delta=-1)) self.bind('', lambda e: self.select(delta=1)) self.bind('', lambda e: self.select(delta=-self._pagesize())) self.bind('', lambda e: self.select(delta=self._pagesize())) # Configuration customizations self.configure(cnf, **kw) #///////////////////////////////////////////////////////////////// # Column Resizing #///////////////////////////////////////////////////////////////// def _resize_column(self, event): """ Callback used to resize a column of the table. Return C{True} if the column is actually getting resized (if the user clicked on the far left or far right 5 pixels of a label); and C{False} otherwies. """ # If we're already waiting for a button release, then ignore # the new button press. if event.widget.bind(''): return False # Decide which column (if any) to resize. self._resize_column_index = None if event.widget is self: for i, lb in enumerate(self._listboxes): if abs(event.x-(lb.winfo_x()+lb.winfo_width())) < 10: self._resize_column_index = i elif event.x > (event.widget.winfo_width()-5): self._resize_column_index = event.widget.column_index elif event.x < 5 and event.widget.column_index != 0: self._resize_column_index = event.widget.column_index-1 # Bind callbacks that are used to resize it. if self._resize_column_index is not None: event.widget.bind('', self._resize_column_motion_cb) event.widget.bind('' % event.num, self._resize_column_buttonrelease_cb) return True else: return False def _resize_column_motion_cb(self, event): lb = self._listboxes[self._resize_column_index] charwidth = lb.winfo_width() / float(lb['width']) x1 = event.x + event.widget.winfo_x() x2 = lb.winfo_x() + lb.winfo_width() lb['width'] = max(3, lb['width'] + int((x1-x2)/charwidth)) def _resize_column_buttonrelease_cb(self, event): event.widget.unbind('' % event.num) event.widget.unbind('') #///////////////////////////////////////////////////////////////// # Properties #///////////////////////////////////////////////////////////////// column_names = property(lambda self: self._column_names, doc=""" A tuple containing the names of the columns used by this multi-column listbox.""") column_labels = property(lambda self: tuple(self._labels), doc=""" A tuple containing the C{Tkinter.Label} widgets used to display the label of each column. If this multi-column listbox was created without labels, then this will be an empty tuple. These widgets will all be augmented with a C{column_index} attribute, which can be used to determine which column they correspond to. This can be convenient, e.g., when defining callbacks for bound events.""") listboxes = property(lambda self: tuple(self._listboxes), doc=""" A tuple containing the C{Tkinter.Listbox} widgets used to display individual columns. These widgets will all be augmented with a C{column_index} attribute, which can be used to determine which column they correspond to. This can be convenient, e.g., when defining callbacks for bound events.""") #///////////////////////////////////////////////////////////////// # Mouse & Keyboard Callback Functions #///////////////////////////////////////////////////////////////// def _select(self, e): i = e.widget.nearest(e.y) self.selection_clear(0, 'end') self.selection_set(i) self.activate(i) self.focus() def _scroll(self, delta): for lb in self._listboxes: lb.yview_scroll(delta, 'unit') return 'break' def _pagesize(self): """@return: The number of rows that makes up one page""" return int(self.index('@0,1000000')) - int(self.index('@0,0')) #///////////////////////////////////////////////////////////////// # Row selection #///////////////////////////////////////////////////////////////// def select(self, index=None, delta=None, see=True): """ Set the selected row. If C{index} is specified, then select row C{index}. Otherwise, if C{delta} is specified, then move the current selection by C{delta} (negative numbers for up, positive numbers for down). This will not move the selection past the top or the bottom of the list. @param see: If true, then call C{self.see()} with the newly selected index, to ensure that it is visible. """ if (index is not None) and (delta is not None): raise ValueError('specify index or delta, but not both') # If delta was given, then calculate index. if delta is not None: if len(self.curselection()) == 0: index = -1 + delta else: index = int(self.curselection()[0]) + delta # Clear all selected rows. self.selection_clear(0, 'end') # Select the specified index if index is not None: index = min(max(index, 0), self.size()-1) #self.activate(index) self.selection_set(index) if see: self.see(index) #///////////////////////////////////////////////////////////////// # Configuration #///////////////////////////////////////////////////////////////// def configure(self, cnf={}, **kw): """ Configure this widget. Use C{label_*} to configure all labels; and C{listbox_*} to configure all listboxes. E.g.: >>> mlb = MultiListbox(master, 5) >>> mlb.configure(label_foreground='red') >>> mlb.configure(listbox_foreground='red') """ cnf = dict(cnf.items() + kw.items()) for (key, val) in cnf.items(): if key.startswith('label_') or key.startswith('label-'): for label in self._labels: label.configure({key[6:]: val}) elif key.startswith('listbox_') or key.startswith('listbox-'): for listbox in self._listboxes: listbox.configure({key[8:]: val}) else: Frame.configure(self, {key:val}) def __setitem__(self, key, val): """ Configure this widget. This is equivalent to C{self.configure({key,val})}. See L{configure()}. """ self.configure({key:val}) def rowconfigure(self, row_index, cnf={}, **kw): """ Configure all table cells in the given row. Valid keyword arguments are: C{background}, C{bg}, C{foreground}, C{fg}, C{selectbackground}, C{selectforeground}. """ for lb in self._listboxes: lb.itemconfigure(row_index, cnf, **kw) def columnconfigure(self, col_index, cnf={}, **kw): """ Configure all table cells in the given column. Valid keyword arguments are: C{background}, C{bg}, C{foreground}, C{fg}, C{selectbackground}, C{selectforeground}. """ lb = self._listboxes[col_index] cnf = dict(cnf.items() + kw.items()) for (key, val) in cnf.items(): if key in ('background', 'bg', 'foreground', 'fg', 'selectbackground', 'selectforeground'): for i in range(lb.size()): lb.itemconfigure(i, {key:val}) else: lb.configure({key:val}) def itemconfigure(self, row_index, col_index, cnf=None, **kw): """ Configure the table cell at the given row and column. Valid keyword arguments are: C{background}, C{bg}, C{foreground}, C{fg}, C{selectbackground}, C{selectforeground}. """ lb = self._listboxes[col_index] return lb.itemconfigure(row_index, cnf, **kw) #///////////////////////////////////////////////////////////////// # Value Access #///////////////////////////////////////////////////////////////// def insert(self, index, *rows): """ Insert the given row or rows into the table, at the given index. Each row value should be a tuple of cell values, one for each column in the row. Index may be an integer or any of the special strings (such as C{'end'}) accepted by C{Tkinter.Listbox}. """ for elt in rows: if len(elt) != len(self._column_names): raise ValueError('rows should be tuples whose length ' 'is equal to the number of columns') for (lb,elts) in zip(self._listboxes, zip(*rows)): lb.insert(index, *elts) def get(self, first, last=None): """ Return the value(s) of the specified row(s). If C{last} is not specified, then return a single row value; otherwise, return a list of row values. Each row value is a tuple of cell values, one for each column in the row. """ values = [lb.get(first, last) for lb in self._listboxes] if last: return [tuple(row) for row in zip(*values)] else: return tuple(values) def bbox(self, row, col): """ Return the bounding box for the given table cell, relative to this widget's top-left corner. The bounding box is a tuple of integers C{(left, top, width, height)}. """ dx, dy, _, _ = self.grid_bbox(row=0, column=col) x, y, w, h = self._listboxes[col].bbox(row) return int(x)+int(dx), int(y)+int(dy), int(w), int(h) #///////////////////////////////////////////////////////////////// # Hide/Show Columns #///////////////////////////////////////////////////////////////// def hide_column(self, col_index): """ Hide the given column. The column's state is still maintained: its values will still be returned by L{get()}, and you must supply its values when calling L{insert()}. It is safe to call this on a column that is already hidden. @see: L{show_column()} """ if self._labels: self._labels[col_index].grid_forget() self.listboxes[col_index].grid_forget() self.grid_columnconfigure(col_index, weight=0) def show_column(self, col_index): """ Display a column that has been hidden using L{hide_column()}. It is safe to call this on a column that is not hidden. """ weight = self._column_weights[col_index] if self._labels: self._labels[col_index].grid(column=col_index, row=0, sticky='news', padx=0, pady=0) self._listboxes[col_index].grid(column=col_index, row=1, sticky='news', padx=0, pady=0) self.grid_columnconfigure(col_index, weight=weight) #///////////////////////////////////////////////////////////////// # Binding Methods #///////////////////////////////////////////////////////////////// def bind_to_labels(self, sequence=None, func=None, add=None): """ Add a binding to each C{Tkinter.Label} widget in this mult-column listbox that will call C{func} in response to the event C{sequence}. @return: A list of the identifiers of replaced binding functions (if any), allowing for their deletion (to prevent a memory leak). """ return [label.bind(sequence, func, add) for label in self.column_labels] def bind_to_listboxes(self, sequence=None, func=None, add=None): """ Add a binding to each C{Tkinter.Listbox} widget in this mult-column listbox that will call C{func} in response to the event C{sequence}. @return: A list of the identifiers of replaced binding functions (if any), allowing for their deletion (to prevent a memory leak). """ for listbox in self.listboxes: listbox.bind(sequence, func, add) def bind_to_columns(self, sequence=None, func=None, add=None): """ Add a binding to each C{Tkinter.Label} and C{Tkinter.Listbox} widget in this mult-column listbox that will call C{func} in response to the event C{sequence}. @return: A list of the identifiers of replaced binding functions (if any), allowing for their deletion (to prevent a memory leak). """ return (self.bind_to_labels(sequence, func, add) + self.bind_to_listboxes(sequence, func, add)) #///////////////////////////////////////////////////////////////// # Simple Delegation #///////////////////////////////////////////////////////////////// # These methods delegate to the first listbox: def curselection(self, *args, **kwargs): return self._listboxes[0].curselection(*args, **kwargs) def selection_includes(self, *args, **kwargs): return self._listboxes[0].selection_includes(*args, **kwargs) def itemcget(self, *args, **kwargs): return self._listboxes[0].itemcget(*args, **kwargs) def size(self, *args, **kwargs): return self._listboxes[0].size(*args, **kwargs) def index(self, *args, **kwargs): return self._listboxes[0].index(*args, **kwargs) def nearest(self, *args, **kwargs): return self._listboxes[0].nearest(*args, **kwargs) # These methods delegate to each listbox (and return None): def activate(self, *args, **kwargs): for lb in self._listboxes: lb.activate(*args, **kwargs) def delete(self, *args, **kwargs): for lb in self._listboxes: lb.delete(*args, **kwargs) def scan_mark(self, *args, **kwargs): for lb in self._listboxes: lb.scan_mark(*args, **kwargs) def scan_dragto(self, *args, **kwargs): for lb in self._listboxes: lb.scan_dragto(*args, **kwargs) def see(self, *args, **kwargs): for lb in self._listboxes: lb.see(*args, **kwargs) def selection_anchor(self, *args, **kwargs): for lb in self._listboxes: lb.selection_anchor(*args, **kwargs) def selection_clear(self, *args, **kwargs): for lb in self._listboxes: lb.selection_clear(*args, **kwargs) def selection_set(self, *args, **kwargs): for lb in self._listboxes: lb.selection_set(*args, **kwargs) def yview(self, *args, **kwargs): for lb in self._listboxes: v = lb.yview(*args, **kwargs) return v # if called with no arguments def yview_moveto(self, *args, **kwargs): for lb in self._listboxes: lb.yview_moveto(*args, **kwargs) def yview_scroll(self, *args, **kwargs): for lb in self._listboxes: lb.yview_scroll(*args, **kwargs) #///////////////////////////////////////////////////////////////// # Aliases #///////////////////////////////////////////////////////////////// itemconfig = itemconfigure rowconfig = rowconfigure columnconfig = columnconfigure select_anchor = selection_anchor select_clear = selection_clear select_includes = selection_includes select_set = selection_set #///////////////////////////////////////////////////////////////// # These listbox methods are not defined for multi-listbox #///////////////////////////////////////////////////////////////// # def xview(self, *what): pass # def xview_moveto(self, fraction): pass # def xview_scroll(self, number, what): pass ###################################################################### # Table ###################################################################### class Table(object): """ A display widget for a table of values, based on a L{MultiListbox} widget. For many purposes, C{Table} can be treated as a list-of-lists. E.g., table[i] is a list of the values for row i; and table.append(row) adds a new row with the given lits of values. Individual cells can be accessed using table[i,j], which refers to the j-th column of the i-th row. This can be used to both read and write values from the table. E.g.: >>> table[i,j] = 'hello' The column (j) can be given either as an index number, or as a column name. E.g., the following prints the value in the 3rd row for the 'First Name' column: >>> print table[3, 'First Name'] John You can configure the colors for individual rows, columns, or cells using L{rowconfig()}, L{columnconfig()}, and L{itemconfig()}. The color configuration for each row will be preserved if the table is modified; however, when new rows are added, any color configurations that have been made for I{columns} will not be applied to the new row. Note: Although C{Table} acts like a widget in some ways (e.g., it defines L{grid()}, L{pack()}, and L{bind()}), it is not itself a widget; it just contains one. This is because widgets need to define C{__getitem__()}, C{__setitem__()}, and C{__nonzero__()} in a way that's incompatible with the fact that C{Table} behaves as a list-of-lists. @ivar _mlb: The multi-column listbox used to display this table's data. @ivar _rows: A list-of-lists used to hold the cell values of this table. Each element of _rows is a row value, i.e., a list of cell values, one for each column in the row. """ def __init__(self, master, column_names, rows=None, column_weights=None, scrollbar=True, click_to_sort=True, reprfunc=None, cnf={}, **kw): """ Construct a new Table widget. @type master: C{Tkinter.Widget} @param master: The widget that should contain the new table. @type column_names: C{list} of C{str} @param column_names: A list of names for the columns; these names will be used to create labels for each column; and can be used as an index when reading or writing cell values from the table. @type rows: C{list} of C{list} @param rows: A list of row values used to initialze the table. Each row value should be a tuple of cell values, one for each column in the row. @type scrollbar: C{bool} @param scrollbar: If true, then create a scrollbar for the new table widget. @type click_to_sort: C{bool} @param click_to_sort: If true, then create bindings that will sort the table's rows by a given column's values if the user clicks on that colum's label. @type reprfunc: C{function} @param reprfunc: If specified, then use this function to convert each table cell value to a string suitable for display. C{reprfunc} has the following signature: >>> reprfunc(row_index, col_index, cell_value) -> str (Note that the column is specified by index, not by name.) @param cnf, kw: Configuration parameters for this widget's contained C{MultiListbox}. See L{MultiListbox.__init__()} for details. """ self._num_columns = len(column_names) self._reprfunc = reprfunc self._frame = Frame(master) self._column_name_to_index = dict((c,i) for (i,c) in enumerate(column_names)) # Make a copy of the rows & check that it's valid. if rows is None: self._rows = [] else: self._rows = [[v for v in row] for row in rows] for row in self._rows: self._checkrow(row) # Create our multi-list box. self._mlb = MultiListbox(self._frame, column_names, column_weights, cnf, **kw) self._mlb.pack(side='left', expand=True, fill='both') # Optional scrollbar if scrollbar: sb = Scrollbar(self._frame, orient='vertical', command=self._mlb.yview) self._mlb.listboxes[0]['yscrollcommand'] = sb.set #for listbox in self._mlb.listboxes: # listbox['yscrollcommand'] = sb.set sb.pack(side='right', fill='y') self._scrollbar = sb # Set up sorting self._sortkey = None if click_to_sort: for i, l in enumerate(self._mlb.column_labels): l.bind('', self._sort) # Fill in our multi-list box. self._fill_table() #///////////////////////////////////////////////////////////////// #{ Widget-like Methods #///////////////////////////////////////////////////////////////// # These all just delegate to either our frame or our MLB. def pack(self, *args, **kwargs): """Position this table's main frame widget in its parent widget. See C{Tkinter.Frame.pack()} for more info.""" self._frame.pack(*args, **kwargs) def grid(self, *args, **kwargs): """Position this table's main frame widget in its parent widget. See C{Tkinter.Frame.grid()} for more info.""" self._frame.grid(*args, **kwargs) def focus(self): """Direct (keyboard) input foxus to this widget.""" self._mlb.focus() def bind(self, sequence=None, func=None, add=None): """Add a binding to this table's main frame that will call C{func} in response to the event C{sequence}.""" self._mlb.bind(sequence, func, add) def rowconfigure(self, row_index, cnf={}, **kw): """@see: L{MultiListbox.rowconfigure()}""" self._mlb.rowconfigure(row_index, cnf, **kw) def columnconfigure(self, col_index, cnf={}, **kw): """@see: L{MultiListbox.columnconfigure()}""" col_index = self.column_index(col_index) self._mlb.columnconfigure(col_index, cnf, **kw) def itemconfigure(self, row_index, col_index, cnf=None, **kw): """@see: L{MultiListbox.itemconfigure()}""" col_index = self.column_index(col_index) return self._mlb.itemconfigure(row_index, col_index, cnf, **kw) def bind_to_labels(self, sequence=None, func=None, add=None): """@see: L{MultiListbox.bind_to_labels()}""" return self._mlb.bind_to_labels(sequence, func, add) def bind_to_listboxes(self, sequence=None, func=None, add=None): """@see: L{MultiListbox.bind_to_listboxes()}""" return self._mlb.bind_to_listboxes(sequence, func, add) def bind_to_columns(self, sequence=None, func=None, add=None): """@see: L{MultiListbox.bind_to_columns()}""" return self._mlb.bind_to_columns(sequence, func, add) rowconfig = rowconfigure columnconfig = columnconfigure itemconfig = itemconfigure #///////////////////////////////////////////////////////////////// #{ Table as list-of-lists #///////////////////////////////////////////////////////////////// def insert(self, row_index, rowvalue): """ Insert a new row into the table, so that its row index will be C{row_index}. If the table contains any rows whose row index is greater than or equal to C{row_index}, then they will be shifted down. @param rowvalue: A tuple of cell values, one for each column in the new row. """ self._checkrow(rowvalue) self._rows.insert(row_index, rowvalue) if self._reprfunc is not None: rowvalue = [self._reprfunc(row_index,j,v) for (j,v) in enumerate(rowvalue)] self._mlb.insert(row_index, rowvalue) if self._DEBUG: self._check_table_vs_mlb() def extend(self, rowvalues): """ Add new rows at the end of the table. @param rowvalues: A list of row values used to initialze the table. Each row value should be a tuple of cell values, one for each column in the row. """ for rowvalue in rowvalues: self.append(rowvalue) if self._DEBUG: self._check_table_vs_mlb() def append(self, rowvalue): """ Add a new row to the end of the table. @param rowvalue: A tuple of cell values, one for each column in the new row. """ self.insert(len(self._rows), rowvalue) if self._DEBUG: self._check_table_vs_mlb() def clear(self): """ Delete all rows in this table. """ self._rows = [] self._mlb.delete(0, 'end') if self._DEBUG: self._check_table_vs_mlb() def __getitem__(self, index): """ Return the value of a row or a cell in this table. If C{index} is an integer, then the row value for the C{index}th row. This row value consists of a tuple of cell values, one for each column in the row. If C{index} is a tuple of two integers, C{(i,j)}, then return the value of the cell in the C{i}th row and the C{j}th column. """ if isinstance(index, slice): raise ValueError('Slicing not supported') elif isinstance(index, tuple) and len(index)==2: return self._rows[index[0]][self.column_index(index[1])] else: return tuple(self._rows[index]) def __setitem__(self, index, val): """ Replace the value of a row or a cell in this table with C{val}. If C{index} is an integer, then C{val} should be a row value (i.e., a tuple of cell values, one for each column). In this case, the values of the C{index}th row of the table will be replaced with the values in C{val}. If C{index} is a tuple of integers, C{(i,j)}, then replace the value of the cell in the C{i}th row and C{j}th column with C{val}. """ if isinstance(index, slice): raise ValueError('Slicing not supported') # table[i,j] = val elif isinstance(index, tuple) and len(index)==2: i, j = index[0], self.column_index(index[1]) config_cookie = self._save_config_info([i]) self._rows[i][j] = val if self._reprfunc is not None: val = self._reprfunc(i, j, val) self._mlb.listboxes[j].insert(i, val) self._mlb.listboxes[j].delete(i+1) self._restore_config_info(config_cookie) # table[i] = val else: config_cookie = self._save_config_info([index]) self._checkrow(val) self._rows[index] = list(val) if self._reprfunc is not None: val = [self._reprfunc(index,j,v) for (j,v) in enumerate(val)] self._mlb.insert(index, val) self._mlb.delete(index+1) self._restore_config_info(config_cookie) def __delitem__(self, row_index): """ Delete the C{row_index}th row from this table. """ if isinstance(index, slice): raise ValueError('Slicing not supported') if isinstance(row_index, tuple) and len(row_index)==2: raise ValueError('Cannot delete a single cell!') del self._rows[row_index] self._mlb.delete(row_index) if self._DEBUG: self._check_table_vs_mlb() def __len__(self): """ @return: the number of rows in this table. """ return len(self._rows) def _checkrow(self, rowvalue): """ Helper function: check that a given row value has the correct number of elements; and if not, raise an exception. """ if len(rowvalue) != self._num_columns: raise ValueError('Row %r has %d columns; expected %d' % (rowvalue, len(rowvalue), self._num_columns)) #///////////////////////////////////////////////////////////////// # Columns #///////////////////////////////////////////////////////////////// column_names = property(lambda self: self._mlb.column_names, doc=""" A list of the names of the columns in this table.""") def column_index(self, i): """ If C{i} is a valid column index integer, then return it as is. Otherwise, check if C{i} is used as the name for any column; if so, return that column's index. Otherwise, raise a C{KeyError} exception. """ if isinstance(i, int) and 0 <= i < self._num_columns: return i else: # This raises a key error if the column is not found. return self._column_name_to_index[i] def hide_column(self, column_index): """@see: L{MultiListbox.hide_column()}""" self._mlb.hide_column(self.column_index(column_index)) def show_column(self, column_index): """@see: L{MultiListbox.show_column()}""" self._mlb.show_column(self.column_index(column_index)) #///////////////////////////////////////////////////////////////// # Selection #///////////////////////////////////////////////////////////////// def selected_row(self): """ Return the index of the currently selected row, or C{None} if no row is selected. To get the row value itself, use C{table[table.selected_row()]}. """ sel = self._mlb.curselection() if sel: return int(sel[0]) else: return None def select(self, index=None, delta=None, see=True): """@see: L{MultiListbox.select()}""" self._mlb.select(index, delta, see) #///////////////////////////////////////////////////////////////// # Sorting #///////////////////////////////////////////////////////////////// def sort_by(self, column_index, order='toggle'): """ Sort the rows in this table, using the specified column's values as a sort key. @param column_index: Specifies which column to sort, using either a column index (C{int}) or a column's label name (C{str}). @param order: Specifies whether to sort the values in ascending or descending order: - C{'ascending'}: Sort from least to greatest. - C{'descending'}: Sort from greatest to least. - C{'toggle'}: If the most recent call to C{sort_by()} sorted the table by the same column C({column_index}), then reverse the rows; otherwise sort in ascending order. """ if order not in ('ascending', 'descending', 'toggle'): raise ValueError('sort_by(): order should be "ascending", ' '"descending", or "toggle".') column_index = self.column_index(column_index) config_cookie = self._save_config_info(index_by_id=True) # Sort the rows. if order == 'toggle' and column_index == self._sortkey: self._rows.reverse() else: self._rows.sort(key=operator.itemgetter(column_index), reverse=(order=='descending')) self._sortkey = column_index # Redraw the table. self._fill_table() self._restore_config_info(config_cookie, index_by_id=True, see=True) if self._DEBUG: self._check_table_vs_mlb() def _sort(self, event): """Event handler for clicking on a column label -- sort by that column.""" column_index = event.widget.column_index # If they click on the far-left of far-right of a column's # label, then resize rather than sorting. if self._mlb._resize_column(event): return 'continue' # Otherwise, sort. else: self.sort_by(column_index) return 'continue' #///////////////////////////////////////////////////////////////// #{ Table Drawing Helpers #///////////////////////////////////////////////////////////////// def _fill_table(self, save_config=True): """ Re-draw the table from scratch, by clearing out the table's multi-column listbox; and then filling it in with values from C{self._rows}. Note that any cell-, row-, or column-specific color configuration that has been done will be lost. The selection will also be lost -- i.e., no row will be selected after this call completes. """ self._mlb.delete(0, 'end') for i, row in enumerate(self._rows): if self._reprfunc is not None: row = [self._reprfunc(i,j,v) for (j,v) in enumerate(row)] self._mlb.insert('end', row) def _get_itemconfig(self, r, c): return dict( (k, self._mlb.itemconfig(r, c, k)[-1]) for k in ('foreground', 'selectforeground', 'background', 'selectbackground') ) def _save_config_info(self, row_indices=None, index_by_id=False): """ Return a 'cookie' containing information about which row is selected, and what color configurations have been applied. this information can the be re-applied to the table (after making modifications) using L{_restore_config_info()}. Color configuration information will be saved for any rows in C{row_indices}, or in the entire table, if C{row_indices=None}. If C{index_by_id=True}, the the cookie will associate rows with their configuration information based on the rows' python id. This is useful when performing operations that re-arrange the rows (e.g. C{sort}). If C{index_by_id=False}, then it is assumed that all rows will be in the same order when C{_restore_config_info()} is called. """ # Default value for row_indices is all rows. if row_indices is None: row_indices = range(len(self._rows)) # Look up our current selection. selection = self.selected_row() if index_by_id and selection is not None: selection = id(self._rows[selection]) # Look up the color configuration info for each row. if index_by_id: config = dict((id(self._rows[r]), [self._get_itemconfig(r, c) for c in range(self._num_columns)]) for r in row_indices) else: config = dict((r, [self._get_itemconfig(r, c) for c in range(self._num_columns)]) for r in row_indices) return selection, config def _restore_config_info(self, cookie, index_by_id=False, see=False): """ Restore selection & color configuration information that was saved using L{_save_config_info}. """ selection, config = cookie # Clear the selection. if selection is None: self._mlb.selection_clear(0, 'end') # Restore selection & color config if index_by_id: for r, row in enumerate(self._rows): if id(row) in config: for c in range(self._num_columns): self._mlb.itemconfigure(r, c, config[id(row)][c]) if id(row) == selection: self._mlb.select(r, see=see) else: if selection is not None: self._mlb.select(selection, see=see) for r in config: for c in range(self._num_columns): self._mlb.itemconfigure(r, c, config[r][c]) #///////////////////////////////////////////////////////////////// # Debugging (Invariant Checker) #///////////////////////////////////////////////////////////////// _DEBUG = False """If true, then run L{_check_table_vs_mlb()} after any operation that modifies the table.""" def _check_table_vs_mlb(self): """ Verify that the contents of the table's L{_rows} variable match the contents of its multi-listbox (L{_mlb}). This is just included for debugging purposes, to make sure that the list-modifying operations are working correctly. """ for col in self._mlb.listboxes: assert len(self) == col.size() for row in self: assert len(row) == self._num_columns assert self._num_columns == len(self._mlb.column_names) #assert self._column_names == self._mlb.column_names for i, row in enumerate(self): for j, cell in enumerate(row): if self._reprfunc is not None: cell = self._reprfunc(i, j, cell) assert self._mlb.get(i)[j] == cell ###################################################################### # Demo/Test Function ###################################################################### # update this to use new WordNet API def demo(): root = Tk() root.bind('', lambda e: root.destroy()) table = Table(root, 'Word Synset Hypernym Hyponym'.split(), column_weights=[0, 1, 1, 1], reprfunc=(lambda i,j,s: ' %s' % s)) table.pack(expand=True, fill='both') from nltk.corpus import wordnet from nltk.corpus import brown for word, pos in sorted(set(brown.tagged_words()[:500])): if pos[0] != 'N': continue word = word.lower() for synset in wordnet.synsets(word): hyper = (synset.hypernyms()+[''])[0] hypo = (synset.hyponyms()+[''])[0] table.append([word, getattr(synset, 'definition', '*none*'), getattr(hyper, 'definition', '*none*'), getattr(hypo, 'definition', '*none*')]) table.columnconfig('Word', background='#afa') table.columnconfig('Synset', background='#efe') table.columnconfig('Hypernym', background='#fee') table.columnconfig('Hyponym', background='#ffe') for row in range(len(table)): for column in ('Hypernym', 'Hyponym'): if table[row, column] == '*none*': table.itemconfig(row, column, foreground='#666', selectforeground='#666') root.mainloop() if __name__ == '__main__': demo() nltk-2.0~b9/nltk/draw/dispersion.py0000644000175000017500000000251011327451575017224 0ustar bhavanibhavani# Natural Language Toolkit: Dispersion Plots # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT """ A utility for displaying lexical dispersion. """ def dispersion_plot(text, words): """ Generate a lexical dispersion plot. @param text: The source text @type text: C{list} or C{enum} of C{str} @param words: The target words @type words: C{list} of C{str} """ try: import pylab except ImportError: raise ValueError('The plot function requires the matplotlib package.' 'See http://matplotlib.sourceforge.net/') text = list(text) words.reverse() points = [(x,y) for x in range(len(text)) for y in range(len(words)) if text[x] == words[y]] if points: x, y = zip(*points) else: x = y = () pylab.plot(x, y, "b|", scalex=.1) pylab.yticks(range(len(words)), words, color="b") pylab.ylim(-1, len(words)) pylab.title("Lexical Dispersion Plot") pylab.xlabel("Word Offset") pylab.show() if __name__ == '__main__': from nltk.corpus import gutenberg words = ['Elinor', 'Marianne', 'Edward', 'Willoughby'] dispersion_plot(gutenberg.words('austen-sense.txt'), words) nltk-2.0~b9/nltk/draw/cfg.py0000644000175000017500000007145011327451575015615 0ustar bhavanibhavani# Natural Language Toolkit: CFG visualization # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # # $Id: cfg.py 8479 2010-01-13 05:40:34Z StevenBird1 $ """ Visualization tools for CFGs. """ import re """ Idea for a nice demo: - 3 panes: grammar, treelet, working area - grammar is a list of productions - when you select a production, the treelet that it licenses appears in the treelet area - the working area has the text on the bottom, and S at top. When you select a production, it shows (ghosted) the locations where that production's treelet could be attached to either the text or the tree rooted at S. - the user can drag the treelet onto one of those (or click on them?) - the user can delete pieces of the tree from the working area (right click?) - connecting top to bottom? drag one NP onto another? +-------------------------------------------------------------+ | S -> NP VP | S | |[NP -> Det N ]| / \ | | ... | NP VP | | N -> 'dog' | | | N -> 'cat' | | | ... | | +--------------+ | | NP | Det N | | / \ | | | | | Det N | the cat saw the dog | | | | +--------------+----------------------------------------------+ Operations: - connect a new treelet -- drag or click shadow - delete a treelet -- right click - if only connected to top, delete everything below - if only connected to bottom, delete everything above - connect top & bottom -- drag a leaf to a root or a root to a leaf - disconnect top & bottom -- right click - if connected to top & bottom, then disconnect """ from nltk.grammar import ContextFreeGrammar, Nonterminal, parse_cfg_production from nltk.tree import Tree from util import * from tree import * ###################################################################### # Production List ###################################################################### class ProductionList(ColorizedList): ARROW = SymbolWidget.SYMBOLS['rightarrow'] def _init_colortags(self, textwidget, options): textwidget.tag_config('terminal', foreground='#006000') textwidget.tag_config('arrow', font='symbol', underline='0') textwidget.tag_config('nonterminal', foreground='blue', font=('helvetica', -12, 'bold')) def _item_repr(self, item): contents = [] contents.append(('%s\t' % item.lhs(), 'nonterminal')) contents.append((self.ARROW, 'arrow')) for elt in item.rhs(): if isinstance(elt, Nonterminal): contents.append((' %s' % elt.symbol(), 'nonterminal')) else: contents.append((' %r' % elt, 'terminal')) return contents ###################################################################### # CFG Editor ###################################################################### _CFGEditor_HELP = """ The CFG Editor can be used to create or modify context free grammars. A context free grammar consists of a start symbol and a list of productions. The start symbol is specified by the text entry field in the upper right hand corner of the editor; and the list of productions are specified in the main text editing box. Every non-blank line specifies a single production. Each production has the form "LHS -> RHS," where LHS is a single nonterminal, and RHS is a list of nonterminals and terminals. Nonterminals must be a single word, such as S or NP or NP_subj. Currently, nonterminals must consists of alphanumeric characters and underscores (_). Nonterminals are colored blue. If you place the mouse over any nonterminal, then all occurances of that nonterminal will be highlighted. Termianals must be surrounded by single quotes (') or double quotes(\"). For example, "dog" and "New York" are terminals. Currently, the string within the quotes must consist of alphanumeric characters, underscores, and spaces. To enter a new production, go to a blank line, and type a nonterminal, followed by an arrow (->), followed by a sequence of terminals and nonterminals. Note that "->" (dash + greater-than) is automatically converted to an arrow symbol. When you move your cursor to a different line, your production will automatically be colorized. If there are any errors, they will be highlighted in red. Note that the order of the productions is signifigant for some algorithms. To re-order the productions, use cut and paste to move them. Use the buttons at the bottom of the window when you are done editing the CFG: - Ok: apply the new CFG, and exit the editor. - Apply: apply the new CFG, and do not exit the editor. - Reset: revert to the original CFG, and do not exit the editor. - Cancel: revert to the original CFG, and exit the editor. """ class CFGEditor(object): """ A dialog window for creating and editing context free grammars. C{CFGEditor} places the following restrictions on what C{CFG}s can be edited: - All nonterminals must be strings consisting of word characters. - All terminals must be strings consisting of word characters and space characters. """ # Regular expressions used by _analyze_line. Precompile them, so # we can process the text faster. ARROW = SymbolWidget.SYMBOLS['rightarrow'] _LHS_RE = re.compile(r"(^\s*\w+\s*)(->|("+ARROW+"))") _ARROW_RE = re.compile("\s*(->|("+ARROW+"))\s*") _PRODUCTION_RE = re.compile(r"(^\s*\w+\s*)" + # LHS "(->|("+ARROW+"))\s*" + # arrow r"((\w+|'[\w ]*'|\"[\w ]*\"|\|)\s*)*$") # RHS _TOKEN_RE = re.compile("\\w+|->|'[\\w ]+'|\"[\\w ]+\"|("+ARROW+")") _BOLD = ('helvetica', -12, 'bold') def __init__(self, parent, cfg=None, set_cfg_callback=None): self._parent = parent if cfg is not None: self._cfg = cfg else: self._cfg = ContextFreeGrammar(Nonterminal('S'), []) self._set_cfg_callback = set_cfg_callback self._highlight_matching_nonterminals = 1 # Create the top-level window. self._top = Toplevel(parent) self._init_bindings() self._init_startframe() self._startframe.pack(side='top', fill='x', expand=0) self._init_prodframe() self._prodframe.pack(side='top', fill='both', expand=1) self._init_buttons() self._buttonframe.pack(side='bottom', fill='x', expand=0) self._textwidget.focus() def _init_startframe(self): frame = self._startframe = Frame(self._top) self._start = Entry(frame) self._start.pack(side='right') Label(frame, text='Start Symbol:').pack(side='right') Label(frame, text='Productions:').pack(side='left') self._start.insert(0, self._cfg.start().symbol()) def _init_buttons(self): frame = self._buttonframe = Frame(self._top) Button(frame, text='Ok', command=self._ok, underline=0, takefocus=0).pack(side='left') Button(frame, text='Apply', command=self._apply, underline=0, takefocus=0).pack(side='left') Button(frame, text='Reset', command=self._reset, underline=0, takefocus=0,).pack(side='left') Button(frame, text='Cancel', command=self._cancel, underline=0, takefocus=0).pack(side='left') Button(frame, text='Help', command=self._help, underline=0, takefocus=0).pack(side='right') def _init_bindings(self): self._top.title('CFG Editor') self._top.bind('', self._cancel) self._top.bind('', self._cancel) self._top.bind('', self._cancel) #self._top.bind('', self._cancel) self._top.bind('', self._cancel) self._top.bind('', self._cancel) #self._top.bind('', self._cancel) self._top.bind('', self._cancel) self._top.bind('', self._ok) self._top.bind('', self._ok) self._top.bind('', self._apply) self._top.bind('', self._apply) self._top.bind('', self._reset) self._top.bind('', self._reset) self._top.bind('', self._help) self._top.bind('', self._help) self._top.bind('', self._help) def _init_prodframe(self): self._prodframe = Frame(self._top) # Create the basic Text widget & scrollbar. self._textwidget = Text(self._prodframe, background='#e0e0e0', exportselection=1) self._textscroll = Scrollbar(self._prodframe, takefocus=0, orient='vertical') self._textwidget.config(yscrollcommand = self._textscroll.set) self._textscroll.config(command=self._textwidget.yview) self._textscroll.pack(side='right', fill='y') self._textwidget.pack(expand=1, fill='both', side='left') # Initialize the colorization tags. Each nonterminal gets its # own tag, so they aren't listed here. self._textwidget.tag_config('terminal', foreground='#006000') self._textwidget.tag_config('arrow', font='symbol') self._textwidget.tag_config('error', background='red') # Keep track of what line they're on. We use that to remember # to re-analyze a line whenever they leave it. self._linenum = 0 # Expand "->" to an arrow. self._top.bind('>', self._replace_arrows) # Re-colorize lines when appropriate. self._top.bind('<>', self._analyze) self._top.bind('', self._check_analyze) self._top.bind('', self._check_analyze) # Tab cycles focus. (why doesn't this work??) def cycle(e, textwidget=self._textwidget): textwidget.tk_focusNext().focus() self._textwidget.bind('', cycle) prod_tuples = [(p.lhs(),[p.rhs()]) for p in self._cfg.productions()] for i in range(len(prod_tuples)-1,0,-1): if (prod_tuples[i][0] == prod_tuples[i-1][0]): if () in prod_tuples[i][1]: continue if () in prod_tuples[i-1][1]: continue print prod_tuples[i-1][1] print prod_tuples[i][1] prod_tuples[i-1][1].extend(prod_tuples[i][1]) del prod_tuples[i] for lhs, rhss in prod_tuples: print lhs, rhss s = '%s ->' % lhs for rhs in rhss: for elt in rhs: if isinstance(elt, Nonterminal): s += ' %s' % elt else: s += ' %r' % elt s += ' |' s = s[:-2] + '\n' self._textwidget.insert('end', s) self._analyze() # # Add the producitons to the text widget, and colorize them. # prod_by_lhs = {} # for prod in self._cfg.productions(): # if len(prod.rhs()) > 0: # prod_by_lhs.setdefault(prod.lhs(),[]).append(prod) # for (lhs, prods) in prod_by_lhs.items(): # self._textwidget.insert('end', '%s ->' % lhs) # self._textwidget.insert('end', self._rhs(prods[0])) # for prod in prods[1:]: # print '\t|'+self._rhs(prod), # self._textwidget.insert('end', '\t|'+self._rhs(prod)) # print # self._textwidget.insert('end', '\n') # for prod in self._cfg.productions(): # if len(prod.rhs()) == 0: # self._textwidget.insert('end', '%s' % prod) # self._analyze() # def _rhs(self, prod): # s = '' # for elt in prod.rhs(): # if isinstance(elt, Nonterminal): s += ' %s' % elt.symbol() # else: s += ' %r' % elt # return s def _clear_tags(self, linenum): """ Remove all tags (except C{arrow} and C{sel}) from the given line of the text widget used for editing the productions. """ start = '%d.0'%linenum end = '%d.end'%linenum for tag in self._textwidget.tag_names(): if tag not in ('arrow', 'sel'): self._textwidget.tag_remove(tag, start, end) def _check_analyze(self, *e): """ Check if we've moved to a new line. If we have, then remove all colorization from the line we moved to, and re-colorize the line that we moved from. """ linenum = int(self._textwidget.index('insert').split('.')[0]) if linenum != self._linenum: self._clear_tags(linenum) self._analyze_line(self._linenum) self._linenum = linenum def _replace_arrows(self, *e): """ Replace any C{'->'} text strings with arrows (char \\256, in symbol font). This searches the whole buffer, but is fast enough to be done anytime they press '>'. """ arrow = '1.0' while 1: arrow = self._textwidget.search('->', arrow, 'end+1char') if arrow == '': break self._textwidget.delete(arrow, arrow+'+2char') self._textwidget.insert(arrow, self.ARROW, 'arrow') self._textwidget.insert(arrow, '\t') arrow = '1.0' while 1: arrow = self._textwidget.search(self.ARROW, arrow+'+1char', 'end+1char') if arrow == '': break self._textwidget.tag_add('arrow', arrow, arrow+'+1char') def _analyze_token(self, match, linenum): """ Given a line number and a regexp match for a token on that line, colorize the token. Note that the regexp match gives us the token's text, start index (on the line), and end index (on the line). """ # What type of token is it? if match.group()[0] in "'\"": tag = 'terminal' elif match.group() in ('->', self.ARROW): tag = 'arrow' else: # If it's a nonterminal, then set up new bindings, so we # can highlight all instances of that nonterminal when we # put the mouse over it. tag = 'nonterminal_'+match.group() if tag not in self._textwidget.tag_names(): self._init_nonterminal_tag(tag) start = '%d.%d' % (linenum, match.start()) end = '%d.%d' % (linenum, match.end()) self._textwidget.tag_add(tag, start, end) def _init_nonterminal_tag(self, tag, foreground='blue'): self._textwidget.tag_config(tag, foreground=foreground, font=CFGEditor._BOLD) if not self._highlight_matching_nonterminals: return def enter(e, textwidget=self._textwidget, tag=tag): textwidget.tag_config(tag, background='#80ff80') def leave(e, textwidget=self._textwidget, tag=tag): textwidget.tag_config(tag, background='') self._textwidget.tag_bind(tag, '', enter) self._textwidget.tag_bind(tag, '', leave) def _analyze_line(self, linenum): """ Colorize a given line. """ # Get rid of any tags that were previously on the line. self._clear_tags(linenum) # Get the line line's text string. line = self._textwidget.get(`linenum`+'.0', `linenum`+'.end') # If it's a valid production, then colorize each token. if CFGEditor._PRODUCTION_RE.match(line): # It's valid; Use _TOKEN_RE to tokenize the production, # and call analyze_token on each token. def analyze_token(match, self=self, linenum=linenum): self._analyze_token(match, linenum) return '' CFGEditor._TOKEN_RE.sub(analyze_token, line) elif line.strip() != '': # It's invalid; show the user where the error is. self._mark_error(linenum, line) def _mark_error(self, linenum, line): """ Mark the location of an error in a line. """ arrowmatch = CFGEditor._ARROW_RE.search(line) if not arrowmatch: # If there's no arrow at all, highlight the whole line. start = '%d.0' % linenum end = '%d.end' % linenum elif not CFGEditor._LHS_RE.match(line): # Otherwise, if the LHS is bad, highlight it. start = '%d.0' % linenum end = '%d.%d' % (linenum, arrowmatch.start()) else: # Otherwise, highlight the RHS. start = '%d.%d' % (linenum, arrowmatch.end()) end = '%d.end' % linenum # If we're highlighting 0 chars, highlight the whole line. if self._textwidget.compare(start, '==', end): start = '%d.0' % linenum end = '%d.end' % linenum self._textwidget.tag_add('error', start, end) def _analyze(self, *e): """ Replace C{->} with arrows, and colorize the entire buffer. """ self._replace_arrows() numlines = int(self._textwidget.index('end').split('.')[0]) for linenum in range(1, numlines+1): # line numbers start at 1. self._analyze_line(linenum) def _parse_productions(self): """ Parse the current contents of the textwidget buffer, to create a list of productions. """ productions = [] # Get the text, normalize it, and split it into lines. text = self._textwidget.get('1.0', 'end') text = re.sub(self.ARROW, '->', text) text = re.sub('\t', ' ', text) lines = text.split('\n') # Convert each line to a CFG production for line in lines: line = line.strip() if line=='': continue productions += parse_cfg_production(line) #if line.strip() == '': continue #if not CFGEditor._PRODUCTION_RE.match(line): # raise ValueError('Bad production string %r' % line) # #(lhs_str, rhs_str) = line.split('->') #lhs = Nonterminal(lhs_str.strip()) #rhs = [] #def parse_token(match, rhs=rhs): # token = match.group() # if token[0] in "'\"": rhs.append(token[1:-1]) # else: rhs.append(Nonterminal(token)) # return '' #CFGEditor._TOKEN_RE.sub(parse_token, rhs_str) # #productions.append(Production(lhs, *rhs)) return productions def _destroy(self, *e): if self._top is None: return self._top.destroy() self._top = None def _ok(self, *e): self._apply() self._destroy() def _apply(self, *e): productions = self._parse_productions() start = Nonterminal(self._start.get()) cfg = ContextFreeGrammar(start, productions) if self._set_cfg_callback is not None: self._set_cfg_callback(cfg) def _reset(self, *e): self._textwidget.delete('1.0', 'end') for production in self._cfg.productions(): self._textwidget.insert('end', '%s\n' % production) self._analyze() if self._set_cfg_callback is not None: self._set_cfg_callback(self._cfg) def _cancel(self, *e): try: self._reset() except: pass self._destroy() def _help(self, *e): # The default font's not very legible; try using 'fixed' instead. try: ShowText(self._parent, 'Help: Chart Parser Demo', (_CFGEditor_HELP).strip(), width=75, font='fixed') except: ShowText(self._parent, 'Help: Chart Parser Demo', (_CFGEditor_HELP).strip(), width=75) ###################################################################### # New Demo (built tree based on cfg) ###################################################################### class CFGDemo(object): def __init__(self, grammar, text): self._grammar = grammar self._text = text # Set up the main window. self._top = Tk() self._top.title('Context Free Grammar Demo') # Base font size self._size = IntVar(self._top) self._size.set(12) # = medium # Set up the key bindings self._init_bindings(self._top) # Create the basic frames frame1 = Frame(self._top) frame1.pack(side='left', fill='y', expand=0) self._init_menubar(self._top) self._init_buttons(self._top) self._init_grammar(frame1) self._init_treelet(frame1) self._init_workspace(self._top) #////////////////////////////////////////////////// # Initialization #////////////////////////////////////////////////// def _init_bindings(self, top): top.bind('', self.destroy) def _init_menubar(self, parent): pass def _init_buttons(self, parent): pass def _init_grammar(self, parent): self._prodlist = ProductionList(parent, self._grammar, width=20) self._prodlist.pack(side='top', fill='both', expand=1) self._prodlist.focus() self._prodlist.add_callback('select', self._selectprod_cb) self._prodlist.add_callback('move', self._selectprod_cb) def _init_treelet(self, parent): self._treelet_canvas = Canvas(parent, background='white') self._treelet_canvas.pack(side='bottom', fill='x') self._treelet = None def _init_workspace(self, parent): self._workspace = CanvasFrame(parent, background='white') self._workspace.pack(side='right', fill='both', expand=1) self._tree = None self.reset_workspace() #////////////////////////////////////////////////// # Workspace #////////////////////////////////////////////////// def reset_workspace(self): c = self._workspace.canvas() fontsize = int(self._size.get()) node_font = ('helvetica', -(fontsize+4), 'bold') leaf_font = ('helvetica', -(fontsize+2)) # Remove the old tree if self._tree is not None: self._workspace.remove_widget(self._tree) # The root of the tree. start = self._grammar.start().symbol() rootnode = TextWidget(c, start, font=node_font, draggable=1) # The leaves of the tree. leaves = [] for word in self._text: if isinstance(word, Token): word = word.type() leaves.append(TextWidget(c, word, font=leaf_font, draggable=1)) # Put it all together into one tree self._tree = TreeSegmentWidget(c, rootnode, leaves, color='white') # Add it to the workspace. self._workspace.add_widget(self._tree) # Move the leaves to the bottom of the workspace. for leaf in leaves: leaf.move(0,100) #self._nodes = {start:1} #self._leaves = dict([(l,1) for l in leaves]) def workspace_markprod(self, production): pass def _markproduction(self, prod, tree=None): if tree is None: tree = self._tree for i in range(len(tree.subtrees())-len(prod.rhs())): if tree['color', i] == 'white': self._markproduction for j, node in enumerate(prod.rhs()): widget = tree.subtrees()[i+j] if (isinstance(node, Nonterminal) and isinstance(widget, TreeSegmentWidget) and node.symbol == widget.node().text()): pass # matching nonterminal elif (isinstance(node, (str, unicode)) and isinstance(widget, TextWidget) and node == widget.text()): pass # matching nonterminal else: break else: # Everything matched! print 'MATCH AT', i #////////////////////////////////////////////////// # Grammar #////////////////////////////////////////////////// def _selectprod_cb(self, production): canvas = self._treelet_canvas self._prodlist.highlight(production) if self._treelet is not None: self._treelet.destroy() # Convert the production to a tree. rhs = production.rhs() for (i, elt) in enumerate(rhs): if isinstance(elt, Nonterminal): elt = Tree(elt) tree = Tree(production.lhs().symbol(), *rhs) # Draw the tree in the treelet area. fontsize = int(self._size.get()) node_font = ('helvetica', -(fontsize+4), 'bold') leaf_font = ('helvetica', -(fontsize+2)) self._treelet = tree_to_treesegment(canvas, tree, node_font=node_font, leaf_font=leaf_font) self._treelet['draggable'] = 1 # Center the treelet. (x1, y1, x2, y2) = self._treelet.bbox() w, h = int(canvas['width']), int(canvas['height']) self._treelet.move((w-x1-x2)/2, (h-y1-y2)/2) # Mark the places where we can add it to the workspace. self._markproduction(production) def destroy(self, *args): self._top.destroy() def mainloop(self, *args, **kwargs): self._top.mainloop(*args, **kwargs) def demo2(): from nltk import Nonterminal, Production, ContextFreeGrammar nonterminals = 'S VP NP PP P N Name V Det' (S, VP, NP, PP, P, N, Name, V, Det) = [Nonterminal(s) for s in nonterminals.split()] productions = ( # Syntactic Productions Production(S, [NP, VP]), Production(NP, [Det, N]), Production(NP, [NP, PP]), Production(VP, [VP, PP]), Production(VP, [V, NP, PP]), Production(VP, [V, NP]), Production(PP, [P, NP]), Production(PP, []), Production(PP, ['up', 'over', NP]), # Lexical Productions Production(NP, ['I']), Production(Det, ['the']), Production(Det, ['a']), Production(N, ['man']), Production(V, ['saw']), Production(P, ['in']), Production(P, ['with']), Production(N, ['park']), Production(N, ['dog']), Production(N, ['statue']), Production(Det, ['my']), ) grammar = ContextFreeGrammar(S, productions) text = 'I saw a man in the park'.split() d=CFGDemo(grammar, text) d.mainloop() ###################################################################### # Old Demo ###################################################################### def demo(): from nltk import Nonterminal, parse_cfg nonterminals = 'S VP NP PP P N Name V Det' (S, VP, NP, PP, P, N, Name, V, Det) = [Nonterminal(s) for s in nonterminals.split()] grammar = parse_cfg(""" S -> NP VP PP -> P NP NP -> Det N NP -> NP PP VP -> V NP VP -> VP PP Det -> 'a' Det -> 'the' Det -> 'my' NP -> 'I' N -> 'dog' N -> 'man' N -> 'park' N -> 'statue' V -> 'saw' P -> 'in' P -> 'up' P -> 'over' P -> 'with' """) def cb(grammar): print grammar top = Tk() editor = CFGEditor(top, grammar, cb) Label(top, text='\nTesting CFG Editor\n').pack() Button(top, text='Quit', command=top.destroy).pack() top.mainloop() def demo3(): from nltk import Production (S, VP, NP, PP, P, N, Name, V, Det) = \ nonterminals('S, VP, NP, PP, P, N, Name, V, Det') productions = ( # Syntactic Productions Production(S, [NP, VP]), Production(NP, [Det, N]), Production(NP, [NP, PP]), Production(VP, [VP, PP]), Production(VP, [V, NP, PP]), Production(VP, [V, NP]), Production(PP, [P, NP]), Production(PP, []), Production(PP, ['up', 'over', NP]), # Lexical Productions Production(NP, ['I']), Production(Det, ['the']), Production(Det, ['a']), Production(N, ['man']), Production(V, ['saw']), Production(P, ['in']), Production(P, ['with']), Production(N, ['park']), Production(N, ['dog']), Production(N, ['statue']), Production(Det, ['my']), ) t = Tk() def destroy(e, t=t): t.destroy() t.bind('q', destroy) p = ProductionList(t, productions) p.pack(expand=1, fill='both') p.add_callback('select', p.markonly) p.add_callback('move', p.markonly) p.focus() p.mark(productions[2]) p.mark(productions[8]) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/draw/__init__.py0000644000175000017500000000144511327451575016612 0ustar bhavanibhavani# Natural Language Toolkit: graphical representations package # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT # # $Id: __init__.py 8479 2010-01-13 05:40:34Z StevenBird1 $ # Import Tkinter-based modules if Tkinter is installed try: import Tkinter except ImportError: import warnings warnings.warn("nltk.draw package not loaded " "(please install Tkinter library).") else: from cfg import * from tree import * from dispersion import dispersion_plot # Make sure that nltk.draw.cfg and nltk.draw.tree refer to the correct # modules (and not to nltk.cfg & nltk.tree) import cfg, tree nltk-2.0~b9/nltk/corpus/util.py0000644000175000017500000000557211327451601016401 0ustar bhavanibhavani# Natural Language Toolkit: Corpus Reader Utility Functions # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT ###################################################################### #{ Lazy Corpus Loader ###################################################################### import re import nltk TRY_ZIPFILE_FIRST = False class LazyCorpusLoader(object): """ A proxy object which is used to stand in for a corpus object before the corpus is loaded. This allows NLTK to create an object for each corpus, but defer the costs associated with loading those corpora until the first time that they're actually accessed. The first time this object is accessed in any way, it will load the corresponding corpus, and transform itself into that corpus (by modifying its own C{__class__} and C{__dict__} attributes). If the corpus can not be found, then accessing this object will raise an exception, displaying installation instructions for the NLTK data package. Once they've properly installed the data package (or modified C{nltk.data.path} to point to its location), they can then use the corpus object without restarting python. """ def __init__(self, name, reader_cls, *args, **kwargs): from nltk.corpus.reader.api import CorpusReader assert issubclass(reader_cls, CorpusReader) self.__name = self.__name__ = name self.__reader_cls = reader_cls self.__args = args self.__kwargs = kwargs def __load(self): # Find the corpus root directory. zip_name = re.sub(r'(([^/]*)(/.*)?)', r'\2.zip/\1/', self.__name) if TRY_ZIPFILE_FIRST: try: root = nltk.data.find('corpora/%s' % zip_name) except LookupError: raise root = nltk.data.find('corpora/%s' % self.__name) else: try: root = nltk.data.find('corpora/%s' % self.__name) except LookupError, e: try: root = nltk.data.find('corpora/%s' % zip_name) except LookupError: raise e # Load the corpus. corpus = self.__reader_cls(root, *self.__args, **self.__kwargs) # This is where the magic happens! Transform ourselves into # the corpus by modifying our own __dict__ and __class__ to # match that of the corpus. self.__dict__ = corpus.__dict__ self.__class__ = corpus.__class__ def __getattr__(self, attr): self.__load() # This looks circular, but its not, since __load() changes our # __class__ to something new: return getattr(self, attr) def __repr__(self): return '<%s in %r (not loaded yet)>' % ( self.__reader_cls.__name__, '.../corpora/'+self.__name) nltk-2.0~b9/nltk/corpus/europarl_raw.py0000644000175000017500000000303111327451601020112 0ustar bhavanibhavani# Natural Language Toolkit: Europarl Corpus Readers # # Copyright (C) 2001-2010 NLTK Project # Author: Nitin Madnani # URL: # For license information, see LICENSE.TXT import re from util import LazyCorpusLoader from reader import * # Create a new corpus reader instance for each European language danish = LazyCorpusLoader( 'europarl_raw/danish', EuroparlCorpusReader, r'ep-.*\.da', encoding='utf-8') dutch = LazyCorpusLoader( 'europarl_raw/dutch', EuroparlCorpusReader, r'ep-.*\.nl', encoding='utf-8') english = LazyCorpusLoader( 'europarl_raw/english', EuroparlCorpusReader, r'ep-.*\.en', encoding='utf-8') finnish = LazyCorpusLoader( 'europarl_raw/finnish', EuroparlCorpusReader, r'ep-.*\.fi', encoding='utf-8') french = LazyCorpusLoader( 'europarl_raw/french', EuroparlCorpusReader, r'ep-.*\.fr', encoding='utf-8') german = LazyCorpusLoader( 'europarl_raw/german', EuroparlCorpusReader, r'ep-.*\.de', encoding='utf-8') greek = LazyCorpusLoader( 'europarl_raw/greek', EuroparlCorpusReader, r'ep-.*\.el', encoding='utf-8') italian = LazyCorpusLoader( 'europarl_raw/italian', EuroparlCorpusReader, r'ep-.*\.it', encoding='utf-8') portuguese = LazyCorpusLoader( 'europarl_raw/portuguese', EuroparlCorpusReader, r'ep-.*\.pt', encoding='utf-8') spanish = LazyCorpusLoader( 'europarl_raw/spanish', EuroparlCorpusReader, r'ep-.*\.es', encoding='utf-8') swedish = LazyCorpusLoader( 'europarl_raw/swedish', EuroparlCorpusReader, r'ep-.*\.sv', encoding='utf-8') nltk-2.0~b9/nltk/corpus/__init__.py0000644000175000017500000002337611374105242017164 0ustar bhavanibhavani# Natural Language Toolkit: Corpus Readers # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # [xx] this docstring isnt' up-to-date! """ NLTK corpus readers. The modules in this package provide functions that can be used to read corpus files in a variety of formats. These functions can be used to read both the corpus files that are distributed in the NLTK corpus package, and corpus files that are part of external corpora. Available Corpora ================= Please see http://nltk.googlecode.com/svn/trunk/nltk_data/index.xml for a complete list. Install corpora using nltk.download(). Corpus Reader Functions ======================= Each corpus module defines one or more X{corpus reader functions}, which can be used to read documents from that corpus. These functions take an argument, C{item}, which is used to indicate which document should be read from the corpus: - If C{item} is one of the unique identifiers listed in the corpus module's C{items} variable, then the corresponding document will be loaded from the NLTK corpus package. - If C{item} is a filename, then that file will be read. Additionally, corpus reader functions can be given lists of item names; in which case, they will return a concatenation of the corresponding documents. Corpus reader functions are named based on the type of information they return. Some common examples, and their return types, are: - I{corpus}.words(): list of str - I{corpus}.sents(): list of (list of str) - I{corpus}.paras(): list of (list of (list of str)) - I{corpus}.tagged_words(): list of (str,str) tuple - I{corpus}.tagged_sents(): list of (list of (str,str)) - I{corpus}.tagged_paras(): list of (list of (list of (str,str))) - I{corpus}.chunked_sents(): list of (Tree w/ (str,str) leaves) - I{corpus}.parsed_sents(): list of (Tree with str leaves) - I{corpus}.parsed_paras(): list of (list of (Tree with str leaves)) - I{corpus}.xml(): A single xml ElementTree - I{corpus}.raw(): unprocessed corpus contents For example, to read a list of the words in the Brown Corpus, use C{nltk.corpus.brown.words()}: >>> from nltk.corpus import brown >>> print brown.words() ['The', 'Fulton', 'County', 'Grand', 'Jury', 'said', ...] Corpus Metadata =============== Metadata about the NLTK corpora, and their individual documents, is stored using U{Open Language Archives Community (OLAC) } metadata records. These records can be accessed using C{nltk.corpus.I{corpus}.olac()}. """ import re from nltk.tokenize import RegexpTokenizer from nltk.tag import simplify_brown_tag, simplify_wsj_tag,\ simplify_alpino_tag, simplify_indian_tag,\ simplify_tag from util import LazyCorpusLoader from reader import * abc = LazyCorpusLoader( 'abc', PlaintextCorpusReader, r'(?!\.).*\.txt') alpino = LazyCorpusLoader( 'alpino', AlpinoCorpusReader, tag_mapping_function=simplify_alpino_tag) brown = LazyCorpusLoader( 'brown', CategorizedTaggedCorpusReader, r'c[a-z]\d\d', cat_file='cats.txt', tag_mapping_function=simplify_brown_tag) cess_cat = LazyCorpusLoader( 'cess_cat', BracketParseCorpusReader, r'(?!\.).*\.tbf', tag_mapping_function=simplify_tag) cess_esp = LazyCorpusLoader( 'cess_esp', BracketParseCorpusReader, r'(?!\.).*\.tbf', tag_mapping_function=simplify_tag) cmudict = LazyCorpusLoader( 'cmudict', CMUDictCorpusReader, ['cmudict']) conll2000 = LazyCorpusLoader( 'conll2000', ConllChunkCorpusReader, ['train.txt', 'test.txt'], ('NP','VP','PP')) conll2002 = LazyCorpusLoader( 'conll2002', ConllChunkCorpusReader, '.*\.(test|train).*', ('LOC', 'PER', 'ORG', 'MISC'), encoding='utf-8') conll2007 = LazyCorpusLoader( 'conll2007', DependencyCorpusReader, '.*\.(test|train).*', encoding='utf-8') dependency_treebank = LazyCorpusLoader( 'dependency_treebank', DependencyCorpusReader, '.*\.dp') floresta = LazyCorpusLoader( 'floresta', BracketParseCorpusReader, r'(?!\.).*\.ptb', '#', tag_mapping_function=simplify_tag) gazetteers = LazyCorpusLoader( 'gazetteers', WordListCorpusReader, r'(?!LICENSE|\.).*\.txt') genesis = LazyCorpusLoader( 'genesis', PlaintextCorpusReader, r'(?!\.).*\.txt', encoding=[ ('finnish|french|german', 'latin_1'), ('swedish', 'cp865'), ('.*', 'utf_8')]) gutenberg = LazyCorpusLoader( 'gutenberg', PlaintextCorpusReader, r'(?!\.).*\.txt') # corpus not available with NLTK; these lines caused help(nltk.corpus) to break #hebrew_treebank = LazyCorpusLoader( # 'hebrew_treebank', BracketParseCorpusReader, r'.*\.txt') ieer = LazyCorpusLoader( 'ieer', IEERCorpusReader, r'(?!README|\.).*') inaugural = LazyCorpusLoader( 'inaugural', PlaintextCorpusReader, r'(?!\.).*\.txt') # [XX] This should probably just use TaggedCorpusReader: indian = LazyCorpusLoader( 'indian', IndianCorpusReader, r'(?!\.).*\.pos', tag_mapping_function=simplify_indian_tag) ipipan = LazyCorpusLoader( 'ipipan', IPIPANCorpusReader, r'(?!\.).*morph\.xml') mac_morpho = LazyCorpusLoader( 'mac_morpho', MacMorphoCorpusReader, r'(?!\.).*\.txt', tag_mapping_function=simplify_tag, encoding='latin-1') machado = LazyCorpusLoader( 'machado', PortugueseCategorizedPlaintextCorpusReader, r'(?!\.).*\.txt', cat_pattern=r'([a-z]*)/.*', encoding='latin-1') movie_reviews = LazyCorpusLoader( 'movie_reviews', CategorizedPlaintextCorpusReader, r'(?!\.).*\.txt', cat_pattern=r'(neg|pos)/.*') names = LazyCorpusLoader( 'names', WordListCorpusReader, r'(?!\.).*\.txt') nps_chat = LazyCorpusLoader( 'nps_chat', NPSChatCorpusReader, r'(?!README|\.).*\.xml', tag_mapping_function=simplify_wsj_tag) pl196x = LazyCorpusLoader( 'pl196x', Pl196xCorpusReader, r'[a-z]-.*\.xml', cat_file='cats.txt', textid_file='textids.txt') ppattach = LazyCorpusLoader( 'ppattach', PPAttachmentCorpusReader, ['training', 'test', 'devset']) qc = LazyCorpusLoader( 'qc', StringCategoryCorpusReader, ['train.txt', 'test.txt']) reuters = LazyCorpusLoader( 'reuters', CategorizedPlaintextCorpusReader, '(training|test).*', cat_file='cats.txt') rte = LazyCorpusLoader( 'rte', RTECorpusReader, r'(?!\.).*\.xml') semcor = LazyCorpusLoader( 'semcor', XMLCorpusReader, r'brown./tagfiles/br-.*\.xml') senseval = LazyCorpusLoader( 'senseval', SensevalCorpusReader, r'(?!\.).*\.pos') shakespeare = LazyCorpusLoader( 'shakespeare', XMLCorpusReader, r'(?!\.).*\.xml') sinica_treebank = LazyCorpusLoader( 'sinica_treebank', SinicaTreebankCorpusReader, ['parsed'], tag_mapping_function=simplify_tag) state_union = LazyCorpusLoader( 'state_union', PlaintextCorpusReader, r'(?!\.).*\.txt') stopwords = LazyCorpusLoader( 'stopwords', WordListCorpusReader, r'(?!README|\.).*') swadesh = LazyCorpusLoader( 'swadesh', SwadeshCorpusReader, r'(?!README|\.).*') switchboard = LazyCorpusLoader( 'switchboard', SwitchboardCorpusReader) timit = LazyCorpusLoader( 'timit', TimitCorpusReader) toolbox = LazyCorpusLoader( 'toolbox', ToolboxCorpusReader, r'(?!.*(README|\.)).*\.(dic|txt)') treebank = LazyCorpusLoader( 'treebank/combined', BracketParseCorpusReader, r'wsj_.*\.mrg', tag_mapping_function=simplify_wsj_tag) treebank_chunk = LazyCorpusLoader( 'treebank/tagged', ChunkedCorpusReader, r'wsj_.*\.pos', sent_tokenizer=RegexpTokenizer(r'(?<=/\.)\s*(?![^\[]*\])', gaps=True), para_block_reader=tagged_treebank_para_block_reader) treebank_raw = LazyCorpusLoader( 'treebank/raw', PlaintextCorpusReader, r'wsj_.*') udhr = LazyCorpusLoader( 'udhr', PlaintextCorpusReader, r'(?!README|\.).*', # Encodings specified in filenames but not mapped to anything: # DallakHelv, VIQR, Cyrillic+Abkh, WinResearcher, font, # Afenegus6..60375, VG2Main, VPS, Turkish, TCVN, Az.Times.Lat0117, # EUC, Baltic, err, Az.Times.Cyr.Normal0117, T61, Amahuaca, Agra encoding=[('.*-UTF8$', 'utf-8'), ('.*-Latin1$', 'latin-1'), ('.*-Hebrew$', 'hebrew'), ('.*-Arabic$', 'arabic'), ('.*-Cyrillic$', 'cyrillic'), ('.*-SJIS$', 'SJIS'), ('.*-GB2312$', 'GB2312'), ('.*-Latin2$', 'ISO-8859-2'), ('.*-Greek$', 'greek'), ('.*-UFT8$', 'utf-8'), ('Hungarian_Magyar-Unicode', 'utf-16-le')] ) verbnet = LazyCorpusLoader( 'verbnet', VerbnetCorpusReader, r'(?!\.).*\.xml') webtext = LazyCorpusLoader( 'webtext', PlaintextCorpusReader, r'(?!README|\.).*\.txt') wordnet = LazyCorpusLoader( 'wordnet', WordNetCorpusReader) wordnet_ic = LazyCorpusLoader( 'wordnet_ic', WordNetICCorpusReader, '.*\.dat') words = LazyCorpusLoader( 'words', WordListCorpusReader, r'(?!README|\.).*') ycoe = LazyCorpusLoader( 'ycoe', YCOECorpusReader) # defined after treebank propbank = LazyCorpusLoader( 'propbank', PropbankCorpusReader, 'prop.txt', 'frames/.*\.xml', 'verbs.txt', lambda filename: re.sub(r'^wsj/\d\d/', '', filename), treebank) # Must be defined *after* treebank corpus. nombank = LazyCorpusLoader( 'nombank.1.0', NombankCorpusReader, 'nombank.1.0', 'frames/.*\.xml', 'nombank.1.0.words', lambda filename: re.sub(r'^wsj/\d\d/', '', filename), treebank) # Must be defined *after* treebank corpus. def demo(): # This is out-of-date: abc.demo() brown.demo() # chat80.demo() cmudict.demo() conll2000.demo() conll2002.demo() genesis.demo() gutenberg.demo() ieer.demo() inaugural.demo() indian.demo() names.demo() ppattach.demo() senseval.demo() shakespeare.demo() sinica_treebank.demo() state_union.demo() stopwords.demo() timit.demo() toolbox.demo() treebank.demo() udhr.demo() webtext.demo() words.demo() # ycoe.demo() if __name__ == '__main__': #demo() pass nltk-2.0~b9/nltk/cluster/util.py0000644000175000017500000002251411363770677016563 0ustar bhavanibhavani# Natural Language Toolkit: Clusterer Utilities # # Copyright (C) 2001-2010 NLTK Project # Author: Trevor Cohn # URL: # For license information, see LICENSE.TXT import copy import sys import math import numpy from api import * class VectorSpaceClusterer(ClusterI): """ Abstract clusterer which takes tokens and maps them into a vector space. Optionally performs singular value decomposition to reduce the dimensionality. """ def __init__(self, normalise=False, svd_dimensions=None): """ @param normalise: should vectors be normalised to length 1 @type normalise: boolean @param svd_dimensions: number of dimensions to use in reducing vector dimensionsionality with SVD @type svd_dimensions: int """ self._Tt = None self._should_normalise = normalise self._svd_dimensions = svd_dimensions def cluster(self, vectors, assign_clusters=False, trace=False): assert len(vectors) > 0 # normalise the vectors if self._should_normalise: vectors = map(self._normalise, vectors) # use SVD to reduce the dimensionality if self._svd_dimensions and self._svd_dimensions < len(vectors[0]): [u, d, vt] = numpy.linalg.svd(numpy.transpose(array(vectors))) S = d[:self._svd_dimensions] * \ numpy.identity(self._svd_dimensions, numpy.Float64) T = u[:,:self._svd_dimensions] Dt = vt[:self._svd_dimensions,:] vectors = numpy.transpose(numpy.matrixmultiply(S, Dt)) self._Tt = numpy.transpose(T) # call abstract method to cluster the vectors self.cluster_vectorspace(vectors, trace) # assign the vectors to clusters if assign_clusters: print self._Tt, vectors return [self.classify(vector) for vector in vectors] def cluster_vectorspace(self, vectors, trace): """ Finds the clusters using the given set of vectors. """ raise AssertionError() def classify(self, vector): if self._should_normalise: vector = self._normalise(vector) if self._Tt != None: vector = numpy.matrixmultiply(self._Tt, vector) cluster = self.classify_vectorspace(vector) return self.cluster_name(cluster) def classify_vectorspace(self, vector): """ Returns the index of the appropriate cluster for the vector. """ raise AssertionError() def likelihood(self, vector, label): if self._should_normalise: vector = self._normalise(vector) if self._Tt != None: vector = numpy.matrixmultiply(self._Tt, vector) return self.likelihood_vectorspace(vector, label) def likelihood_vectorspace(self, vector, cluster): """ Returns the likelihood of the vector belonging to the cluster. """ predicted = self.classify_vectorspace(vector) if cluster == predicted: return 1.0 else: return 0.0 def vector(self, vector): """ Returns the vector after normalisation and dimensionality reduction """ if self._should_normalise: vector = self._normalise(vector) if self._Tt != None: vector = numpy.matrixmultiply(self._Tt, vector) return vector def _normalise(self, vector): """ Normalises the vector to unit length. """ return vector / math.sqrt(numpy.dot(vector, vector)) def euclidean_distance(u, v): """ Returns the euclidean distance between vectors u and v. This is equivalent to the length of the vector (u - v). """ diff = u - v return math.sqrt(numpy.dot(diff, diff)) def cosine_distance(u, v): """ Returns the cosine of the angle between vectors v and u. This is equal to u.v / |u||v|. """ return numpy.dot(u, v) / (math.sqrt(numpy.dot(u, u)) * math.sqrt(numpy.dot(v, v))) class _DendrogramNode(object): """ Tree node of a dendrogram. """ def __init__(self, value, *children): self._value = value self._children = children def leaves(self, values=True): if self._children: leaves = [] for child in self._children: leaves.extend(child.leaves(values)) return leaves elif values: return [self._value] else: return [self] def groups(self, n): queue = [(self._value, self)] while len(queue) < n: priority, node = queue.pop() if not node._children: queue.push((priority, node)) break for child in node._children: if child._children: queue.append((child._value, child)) else: queue.append((0, child)) # makes the earliest merges at the start, latest at the end queue.sort() groups = [] for priority, node in queue: groups.append(node.leaves()) return groups class Dendrogram(object): """ Represents a dendrogram, a tree with a specified branching order. This must be initialised with the leaf items, then iteratively call merge for each branch. This class constructs a tree representing the order of calls to the merge function. """ def __init__(self, items=[]): """ @param items: the items at the leaves of the dendrogram @type items: sequence of (any) """ self._items = [_DendrogramNode(item) for item in items] self._original_items = copy.copy(self._items) self._merge = 1 def merge(self, *indices): """ Merges nodes at given indices in the dendrogram. The nodes will be combined which then replaces the first node specified. All other nodes involved in the merge will be removed. @param indices: indices of the items to merge (at least two) @type indices: seq of int """ assert len(indices) >= 2 node = _DendrogramNode(self._merge, *[self._items[i] for i in indices]) self._merge += 1 self._items[indices[0]] = node for i in indices[1:]: del self._items[i] def groups(self, n): """ Finds the n-groups of items (leaves) reachable from a cut at depth n. @param n: number of groups @type n: int """ if len(self._items) > 1: root = _DendrogramNode(self._merge, *self._items) else: root = self._items[0] return root.groups(n) def show(self, leaf_labels=[]): """ Print the dendrogram in ASCII art to standard out. @param leaf_labels: an optional list of strings to use for labeling the leaves @type leaf_labels: list """ # ASCII rendering characters JOIN, HLINK, VLINK = '+', '-', '|' # find the root (or create one) if len(self._items) > 1: root = _DendrogramNode(self._merge, *self._items) else: root = self._items[0] leaves = self._original_items if leaf_labels: last_row = leaf_labels else: last_row = [str(leaf._value) for leaf in leaves] # find the bottom row and the best cell width width = max(map(len, last_row)) + 1 lhalf = width / 2 rhalf = width - lhalf - 1 # display functions def format(centre, left=' ', right=' '): return '%s%s%s' % (lhalf*left, centre, right*rhalf) def display(str): sys.stdout.write(str) # for each merge, top down queue = [(root._value, root)] verticals = [ format(' ') for leaf in leaves ] while queue: priority, node = queue.pop() child_left_leaf = map(lambda c: c.leaves(False)[0], node._children) indices = map(leaves.index, child_left_leaf) if child_left_leaf: min_idx = min(indices) max_idx = max(indices) for i in range(len(leaves)): if leaves[i] in child_left_leaf: if i == min_idx: display(format(JOIN, ' ', HLINK)) elif i == max_idx: display(format(JOIN, HLINK, ' ')) else: display(format(JOIN, HLINK, HLINK)) verticals[i] = format(VLINK) elif min_idx <= i <= max_idx: display(format(HLINK, HLINK, HLINK)) else: display(verticals[i]) display('\n') for child in node._children: if child._children: queue.append((child._value, child)) queue.sort() for vertical in verticals: display(vertical) display('\n') # finally, display the last line display(''.join(item.center(width) for item in last_row)) display('\n') def __repr__(self): if len(self._items) > 1: root = _DendrogramNode(self._merge, *self._items) else: root = self._items[0] leaves = root.leaves(False) return '' % len(leaves) nltk-2.0~b9/nltk/cluster/kmeans.py0000644000175000017500000001657611327451574017067 0ustar bhavanibhavani# Natural Language Toolkit: K-Means Clusterer # # Copyright (C) 2001-2010 NLTK Project # Author: Trevor Cohn # URL: # For license information, see LICENSE.TXT import numpy import random from api import * from util import * class KMeansClusterer(VectorSpaceClusterer): """ The K-means clusterer starts with k arbitrary chosen means then allocates each vector to the cluster with the closest mean. It then recalculates the means of each cluster as the centroid of the vectors in the cluster. This process repeats until the cluster memberships stabilise. This is a hill-climbing algorithm which may converge to a local maximum. Hence the clustering is often repeated with random initial means and the most commonly occuring output means are chosen. """ def __init__(self, num_means, distance, repeats=1, conv_test=1e-6, initial_means=None, normalise=False, svd_dimensions=None, rng=None): """ @param num_means: the number of means to use (may use fewer) @type num_means: int @param distance: measure of distance between two vectors @type distance: function taking two vectors and returing a float @param repeats: number of randomised clustering trials to use @type repeats: int @param conv_test: maximum variation in mean differences before deemed convergent @type conv_test: number @param initial_means: set of k initial means @type initial_means: sequence of vectors @param normalise: should vectors be normalised to length 1 @type normalise: boolean @param svd_dimensions: number of dimensions to use in reducing vector dimensionsionality with SVD @type svd_dimensions: int @param rng: random number generator (or None) @type rng: Random """ VectorSpaceClusterer.__init__(self, normalise, svd_dimensions) self._num_means = num_means self._distance = distance self._max_difference = conv_test assert not initial_means or len(initial_means) == num_means self._means = initial_means assert repeats >= 1 assert not (initial_means and repeats > 1) self._repeats = repeats if rng: self._rng = rng else: self._rng = random.Random() def cluster_vectorspace(self, vectors, trace=False): if self._means and self._repeats > 1: print 'Warning: means will be discarded for subsequent trials' meanss = [] for trial in range(self._repeats): if trace: print 'k-means trial', trial if not self._means or trial > 1: self._means = self._rng.sample(vectors, self._num_means) self._cluster_vectorspace(vectors, trace) meanss.append(self._means) if len(meanss) > 1: # sort the means first (so that different cluster numbering won't # effect the distance comparison) for means in meanss: means.sort(cmp = _vector_compare) # find the set of means that's minimally different from the others min_difference = min_means = None for i in range(len(meanss)): d = 0 for j in range(len(meanss)): if i != j: d += self._sum_distances(meanss[i], meanss[j]) if min_difference == None or d < min_difference: min_difference, min_means = d, meanss[i] # use the best means self._means = min_means def _cluster_vectorspace(self, vectors, trace=False): if self._num_means < len(vectors): # perform k-means clustering converged = False while not converged: # assign the tokens to clusters based on minimum distance to # the cluster means clusters = [[] for m in range(self._num_means)] for vector in vectors: index = self.classify_vectorspace(vector) clusters[index].append(vector) if trace: print 'iteration' #for i in range(self._num_means): #print ' mean', i, 'allocated', len(clusters[i]), 'vectors' # recalculate cluster means by computing the centroid of each cluster new_means = map(self._centroid, clusters) # measure the degree of change from the previous step for convergence difference = self._sum_distances(self._means, new_means) if difference < self._max_difference: converged = True # remember the new means self._means = new_means def classify_vectorspace(self, vector): # finds the closest cluster centroid # returns that cluster's index best_distance = best_index = None for index in range(len(self._means)): mean = self._means[index] dist = self._distance(vector, mean) if best_distance == None or dist < best_distance: best_index, best_distance = index, dist return best_index def num_clusters(self): if self._means: return len(self._means) else: return self._num_means def means(self): """ The means used for clustering. """ return self._means def _sum_distances(self, vectors1, vectors2): difference = 0.0 for u, v in zip(vectors1, vectors2): difference += self._distance(u, v) return difference def _centroid(self, cluster): assert len(cluster) > 0 centroid = copy.copy(cluster[0]) for vector in cluster[1:]: centroid += vector return centroid / float(len(cluster)) def __repr__(self): return '' % \ (self._means, self._repeats) def _vector_compare(x, y): xs, ys = sum(x), sum(y) if xs < ys: return -1 elif xs > ys: return 1 else: return 0 ################################################################################# def demo(): # example from figure 14.9, page 517, Manning and Schutze from nltk import cluster vectors = [numpy.array(f) for f in [[2, 1], [1, 3], [4, 7], [6, 7]]] means = [[4, 3], [5, 5]] clusterer = cluster.KMeansClusterer(2, euclidean_distance, initial_means=means) clusters = clusterer.cluster(vectors, True, trace=True) print 'Clustered:', vectors print 'As:', clusters print 'Means:', clusterer.means() print vectors = [numpy.array(f) for f in [[3, 3], [1, 2], [4, 2], [4, 0], [2, 3], [3, 1]]] # test k-means using the euclidean distance metric, 2 means and repeat # clustering 10 times with random seeds clusterer = cluster.KMeansClusterer(2, euclidean_distance, repeats=10) clusters = clusterer.cluster(vectors, True) print 'Clustered:', vectors print 'As:', clusters print 'Means:', clusterer.means() print # classify a new vector vector = numpy.array([3, 3]) print 'classify(%s):' % vector, print clusterer.classify(vector) print if __name__ == '__main__': demo() nltk-2.0~b9/nltk/cluster/gaac.py0000644000175000017500000001200411327451574016462 0ustar bhavanibhavani# Natural Language Toolkit: Group Average Agglomerative Clusterer # # Copyright (C) 2001-2010 NLTK Project # Author: Trevor Cohn # URL: # For license information, see LICENSE.TXT import numpy from nltk.internals import deprecated from api import * from util import * class GAAClusterer(VectorSpaceClusterer): """ The Group Average Agglomerative starts with each of the N vectors as singleton clusters. It then iteratively merges pairs of clusters which have the closest centroids. This continues until there is only one cluster. The order of merges gives rise to a dendrogram: a tree with the earlier merges lower than later merges. The membership of a given number of clusters c, 1 <= c <= N, can be found by cutting the dendrogram at depth c. This clusterer uses the cosine similarity metric only, which allows for efficient speed-up in the clustering process. """ def __init__(self, num_clusters=1, normalise=True, svd_dimensions=None): VectorSpaceClusterer.__init__(self, normalise, svd_dimensions) self._num_clusters = num_clusters self._dendrogram = None self._groups_values = None def cluster(self, vectors, assign_clusters=False, trace=False): # stores the merge order self._dendrogram = Dendrogram( [numpy.array(vector, numpy.float64) for vector in vectors]) return VectorSpaceClusterer.cluster(self, vectors, assign_clusters, trace) def cluster_vectorspace(self, vectors, trace=False): # create a cluster for each vector clusters = [[vector] for vector in vectors] # the sum vectors vector_sum = copy.copy(vectors) while len(clusters) > max(self._num_clusters, 1): # find the two best candidate clusters to merge, based on their # S(union c_i, c_j) best = None for i in range(len(clusters)): for j in range(i + 1, len(clusters)): sim = self._average_similarity( vector_sum[i], len(clusters[i]), vector_sum[j], len(clusters[j])) if not best or sim > best[0]: best = (sim, i, j) # merge them and replace in cluster list i, j = best[1:] sum = clusters[i] + clusters[j] if trace: print 'merging %d and %d' % (i, j) clusters[i] = sum del clusters[j] vector_sum[i] = vector_sum[i] + vector_sum[j] del vector_sum[j] self._dendrogram.merge(i, j) self.update_clusters(self._num_clusters) def update_clusters(self, num_clusters): clusters = self._dendrogram.groups(num_clusters) self._centroids = [] for cluster in clusters: assert len(cluster) > 0 if self._should_normalise: centroid = self._normalise(cluster[0]) else: centroid = numpy.array(cluster[0]) for vector in cluster[1:]: if self._should_normalise: centroid += self._normalise(vector) else: centroid += vector centroid /= float(len(cluster)) self._centroids.append(centroid) self._num_clusters = len(self._centroids) def classify_vectorspace(self, vector): best = None for i in range(self._num_clusters): centroid = self._centroids[i] sim = self._average_similarity(vector, 1, centroid, 1) if not best or sim > best[0]: best = (sim, i) return best[1] @deprecated("Use GAAClusterer.dendrogram instead.") def dendogram(self): return dendrogram(self) def dendrogram(self): """ @return: The dendrogram representing the current clustering @rtype: Dendrogram """ return self._dendrogram def num_clusters(self): return self._num_clusters def _average_similarity(self, v1, l1, v2, l2): sum = v1 + v2 length = l1 + l2 return (numpy.dot(sum, sum) - length) / (length * (length - 1)) def __repr__(self): return '' % self._num_clusters def demo(): """ Non-interactive demonstration of the clusterers with simple 2-D data. """ from nltk import cluster # use a set of tokens with 2D indices vectors = [numpy.array(f) for f in [[3, 3], [1, 2], [4, 2], [4, 0], [2, 3], [3, 1]]] # test the GAAC clusterer with 4 clusters clusterer = cluster.GAAClusterer(4) clusters = clusterer.cluster(vectors, True) print 'Clusterer:', clusterer print 'Clustered:', vectors print 'As:', clusters print # show the dendrogram clusterer.dendrogram().show() # classify a new vector vector = numpy.array([3, 3]) print 'classify(%s):' % vector, print clusterer.classify(vector) print if __name__ == '__main__': demo() nltk-2.0~b9/nltk/cluster/em.py0000644000175000017500000002243511327451574016201 0ustar bhavanibhavani# Natural Language Toolkit: Expectation Maximization Clusterer # # Copyright (C) 2001-2010 NLTK Project # Author: Trevor Cohn # URL: # For license information, see LICENSE.TXT import numpy from api import * from util import * class EMClusterer(VectorSpaceClusterer): """ The Gaussian EM clusterer models the vectors as being produced by a mixture of k Gaussian sources. The parameters of these sources (prior probability, mean and covariance matrix) are then found to maximise the likelihood of the given data. This is done with the expectation maximisation algorithm. It starts with k arbitrarily chosen means, priors and covariance matrices. It then calculates the membership probabilities for each vector in each of the clusters; this is the 'E' step. The cluster parameters are then updated in the 'M' step using the maximum likelihood estimate from the cluster membership probabilities. This process continues until the likelihood of the data does not significantly increase. """ def __init__(self, initial_means, priors=None, covariance_matrices=None, conv_threshold=1e-6, bias=0.1, normalise=False, svd_dimensions=None): """ Creates an EM clusterer with the given starting parameters, convergence threshold and vector mangling parameters. @param initial_means: the means of the gaussian cluster centers @type initial_means: [seq of] numpy array or seq of SparseArray @param priors: the prior probability for each cluster @type priors: numpy array or seq of float @param covariance_matrices: the covariance matrix for each cluster @type covariance_matrices: [seq of] numpy array @param conv_threshold: maximum change in likelihood before deemed convergent @type conv_threshold: int or float @param bias: variance bias used to ensure non-singular covariance matrices @type bias: float @param normalise: should vectors be normalised to length 1 @type normalise: boolean @param svd_dimensions: number of dimensions to use in reducing vector dimensionsionality with SVD @type svd_dimensions: int """ VectorSpaceClusterer.__init__(self, normalise, svd_dimensions) self._means = numpy.array(initial_means, numpy.float64) self._num_clusters = len(initial_means) self._conv_threshold = conv_threshold self._covariance_matrices = covariance_matrices self._priors = priors self._bias = bias def num_clusters(self): return self._num_clusters def cluster_vectorspace(self, vectors, trace=False): assert len(vectors) > 0 # set the parameters to initial values dimensions = len(vectors[0]) means = self._means priors = self._priors if not priors: priors = self._priors = numpy.ones(self._num_clusters, numpy.float64) / self._num_clusters covariances = self._covariance_matrices if not covariances: covariances = self._covariance_matrices = \ [ numpy.identity(dimensions, numpy.float64) for i in range(self._num_clusters) ] # do the E and M steps until the likelihood plateaus lastl = self._loglikelihood(vectors, priors, means, covariances) converged = False while not converged: if trace: print 'iteration; loglikelihood', lastl # E-step, calculate hidden variables, h[i,j] h = numpy.zeros((len(vectors), self._num_clusters), numpy.float64) for i in range(len(vectors)): for j in range(self._num_clusters): h[i,j] = priors[j] * self._gaussian(means[j], covariances[j], vectors[i]) h[i,:] /= sum(h[i,:]) # M-step, update parameters - cvm, p, mean for j in range(self._num_clusters): covariance_before = covariances[j] new_covariance = numpy.zeros((dimensions, dimensions), numpy.float64) new_mean = numpy.zeros(dimensions, numpy.float64) sum_hj = 0.0 for i in range(len(vectors)): delta = vectors[i] - means[j] new_covariance += h[i,j] * \ numpy.multiply.outer(delta, delta) sum_hj += h[i,j] new_mean += h[i,j] * vectors[i] covariances[j] = new_covariance / sum_hj means[j] = new_mean / sum_hj priors[j] = sum_hj / len(vectors) # bias term to stop covariance matrix being singular covariances[j] += self._bias * \ numpy.identity(dimensions, numpy.float64) # calculate likelihood - FIXME: may be broken l = self._loglikelihood(vectors, priors, means, covariances) # check for convergence if abs(lastl - l) < self._conv_threshold: converged = True lastl = l def classify_vectorspace(self, vector): best = None for j in range(self._num_clusters): p = self._priors[j] * self._gaussian(self._means[j], self._covariance_matrices[j], vector) if not best or p > best[0]: best = (p, j) return best[1] def likelihood_vectorspace(self, vector, cluster): cid = self.cluster_names().index(cluster) return self._priors[cluster] * self._gaussian(self._means[cluster], self._covariance_matrices[cluster], vector) def _gaussian(self, mean, cvm, x): m = len(mean) assert cvm.shape == (m, m), \ 'bad sized covariance matrix, %s' % str(cvm.shape) try: det = numpy.linalg.det(cvm) inv = numpy.linalg.inv(cvm) a = det ** -0.5 * (2 * numpy.pi) ** (-m / 2.0) dx = x - mean print dx, inv b = -0.5 * numpy.dot( numpy.dot(dx, inv), dx) return a * numpy.exp(b) except OverflowError: # happens when the exponent is negative infinity - i.e. b = 0 # i.e. the inverse of cvm is huge (cvm is almost zero) return 0 def _loglikelihood(self, vectors, priors, means, covariances): llh = 0.0 for vector in vectors: p = 0 for j in range(len(priors)): p += priors[j] * \ self._gaussian(means[j], covariances[j], vector) llh += numpy.log(p) return llh def __repr__(self): return '' % list(self._means) def demo(): """ Non-interactive demonstration of the clusterers with simple 2-D data. """ from nltk import cluster # example from figure 14.10, page 519, Manning and Schutze vectors = [numpy.array(f) for f in [[0.5, 0.5], [1.5, 0.5], [1, 3]]] means = [[4, 2], [4, 2.01]] clusterer = cluster.EMClusterer(means, bias=0.1) clusters = clusterer.cluster(vectors, True, trace=True) print 'Clustered:', vectors print 'As: ', clusters print for c in range(2): print 'Cluster:', c print 'Prior: ', clusterer._priors[c] print 'Mean: ', clusterer._means[c] print 'Covar: ', clusterer._covariance_matrices[c] print # classify a new vector vector = numpy.array([2, 2]) print 'classify(%s):' % vector, print clusterer.classify(vector) # show the classification probabilities vector = numpy.array([2, 2]) print 'classification_probdist(%s):' % vector pdist = clusterer.classification_probdist(vector) for sample in pdist.samples(): print '%s => %.0f%%' % (sample, pdist.prob(sample) *100) # # The following demo code is broken. # # # use a set of tokens with 2D indices # vectors = [numpy.array(f) for f in [[3, 3], [1, 2], [4, 2], [4, 0], [2, 3], [3, 1]]] # # test the EM clusterer with means given by k-means (2) and # # dimensionality reduction # clusterer = cluster.KMeans(2, euclidean_distance, svd_dimensions=1) # print 'Clusterer:', clusterer # clusters = clusterer.cluster(vectors) # means = clusterer.means() # print 'Means:', clusterer.means() # print # clusterer = cluster.EMClusterer(means, svd_dimensions=1) # clusters = clusterer.cluster(vectors, True) # print 'Clusterer:', clusterer # print 'Clustered:', str(vectors)[:60], '...' # print 'As:', str(clusters)[:60], '...' # print # # classify a new vector # vector = numpy.array([3, 3]) # print 'classify(%s):' % vector, # print clusterer.classify(vector) # print # # show the classification probabilities # vector = numpy.array([2.2, 2]) # print 'classification_probdist(%s)' % vector # pdist = clusterer.classification_probdist(vector) # for sample in pdist: # print '%s => %.0f%%' % (sample, pdist.prob(sample) *100) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/cluster/api.py0000644000175000017500000000377011327451574016352 0ustar bhavanibhavani# Natural Language Toolkit: Clusterer Interfaces # # Copyright (C) 2001-2010 NLTK Project # Author: Trevor Cohn # Porting: Steven Bird # URL: # For license information, see LICENSE.TXT from nltk.probability import DictionaryProbDist class ClusterI(object): """ Interface covering basic clustering functionality. """ def cluster(self, vectors, assign_clusters=False): """ Assigns the vectors to clusters, learning the clustering parameters from the data. Returns a cluster identifier for each vector. """ raise AssertionError() def classify(self, token): """ Classifies the token into a cluster, setting the token's CLUSTER parameter to that cluster identifier. """ raise AssertionError() def likelihood(self, vector, label): """ Returns the likelihood (a float) of the token having the corresponding cluster. """ if self.classify(vector) == label: return 1.0 else: return 0.0 def classification_probdist(self, vector): """ Classifies the token into a cluster, returning a probability distribution over the cluster identifiers. """ likelihoods = {} sum = 0.0 for cluster in self.cluster_names(): likelihoods[cluster] = self.likelihood(vector, cluster) sum += likelihoods[cluster] for cluster in self.cluster_names(): likelihoods[cluster] /= sum return DictionaryProbDist(likelihoods) def num_clusters(self): """ Returns the number of clusters. """ raise AssertError() def cluster_names(self): """ Returns the names of the clusters. """ return range(self.num_clusters()) def cluster_name(self, index): """ Returns the names of the cluster at index. """ return index nltk-2.0~b9/nltk/cluster/__init__.py0000644000175000017500000001017111423114516017316 0ustar bhavanibhavani# Natural Language Toolkit: Clusterers # # Copyright (C) 2001-2010 NLTK Project # Author: Trevor Cohn # URL: # For license information, see LICENSE.TXT """ This module contains a number of basic clustering algorithms. Clustering describes the task of discovering groups of similar items with a large collection. It is also describe as unsupervised machine learning, as the data from which it learns is unannotated with class information, as is the case for supervised learning. Annotated data is difficult and expensive to obtain in the quantities required for the majority of supervised learning algorithms. This problem, the knowledge acquisition bottleneck, is common to most natural language processing tasks, thus fueling the need for quality unsupervised approaches. This module contains a k-means clusterer, E-M clusterer and a group average agglomerative clusterer (GAAC). All these clusterers involve finding good cluster groupings for a set of vectors in multi-dimensional space. The K-means clusterer starts with k arbitrary chosen means then allocates each vector to the cluster with the closest mean. It then recalculates the means of each cluster as the centroid of the vectors in the cluster. This process repeats until the cluster memberships stabilise. This is a hill-climbing algorithm which may converge to a local maximum. Hence the clustering is often repeated with random initial means and the most commonly occurring output means are chosen. The GAAC clusterer starts with each of the M{N} vectors as singleton clusters. It then iteratively merges pairs of clusters which have the closest centroids. This continues until there is only one cluster. The order of merges gives rise to a dendrogram - a tree with the earlier merges lower than later merges. The membership of a given number of clusters M{c}, M{1 <= c <= N}, can be found by cutting the dendrogram at depth M{c}. The Gaussian EM clusterer models the vectors as being produced by a mixture of k Gaussian sources. The parameters of these sources (prior probability, mean and covariance matrix) are then found to maximise the likelihood of the given data. This is done with the expectation maximisation algorithm. It starts with k arbitrarily chosen means, priors and covariance matrices. It then calculates the membership probabilities for each vector in each of the clusters - this is the 'E' step. The cluster parameters are then updated in the 'M' step using the maximum likelihood estimate from the cluster membership probabilities. This process continues until the likelihood of the data does not significantly increase. They all extend the ClusterI interface which defines common operations available with each clusterer. These operations include. - cluster: clusters a sequence of vectors - classify: assign a vector to a cluster - classification_probdist: give the probability distribution over cluster memberships The current existing classifiers also extend cluster.VectorSpace, an abstract class which allows for singular value decomposition (SVD) and vector normalisation. SVD is used to reduce the dimensionality of the vector space in such a manner as to preserve as much of the variation as possible, by reparameterising the axes in order of variability and discarding all bar the first d dimensions. Normalisation ensures that vectors fall in the unit hypersphere. Usage example (see also demo()):: from nltk import cluster from nltk.cluster import euclidean_distance from numpy import array vectors = [array(f) for f in [[3, 3], [1, 2], [4, 2], [4, 0]]] # initialise the clusterer (will also assign the vectors to clusters) clusterer = cluster.KMeansClusterer(2, euclidean_distance) clusterer.cluster(vectors, True) # classify a new vector print clusterer.classify(array([3, 3])) Note that the vectors must use numpy array-like objects. nltk_contrib.unimelb.tacohn.SparseArrays may be used for efficiency when required. """ from util import * from kmeans import * from gaac import * from em import * __all__ = ['KMeansClusterer', 'GAAClusterer', 'EMClusterer', 'VectorSpaceClusterer', 'Dendrogram'] nltk-2.0~b9/nltk/classify/weka.py0000644000175000017500000003022211354233125016642 0ustar bhavanibhavani# Natural Language Toolkit: Interface to Weka Classsifiers # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # # $Id: naivebayes.py 2063 2004-07-17 21:02:24Z edloper $ """ Classifiers that make use of the external 'Weka' package. """ import time import tempfile import os import os.path import subprocess import re import zipfile from nltk.probability import * from nltk.internals import java, config_java from api import * _weka_classpath = None _weka_search = ['.', '/usr/share/weka', '/usr/local/share/weka', '/usr/lib/weka', '/usr/local/lib/weka',] def config_weka(classpath=None): global _weka_classpath # Make sure java's configured first. config_java() if classpath is not None: _weka_classpath = classpath if _weka_classpath is None: searchpath = _weka_search if 'WEKAHOME' in os.environ: searchpath.insert(0, os.environ['WEKAHOME']) for path in searchpath: if os.path.exists(os.path.join(path, 'weka.jar')): _weka_classpath = os.path.join(path, 'weka.jar') version = _check_weka_version(_weka_classpath) if version: print ('[Found Weka: %s (version %s)]' % (_weka_classpath, version)) else: print '[Found Weka: %s]' % _weka_classpath _check_weka_version(_weka_classpath) if _weka_classpath is None: raise LookupError('Unable to find weka.jar! Use config_weka() ' 'or set the WEKAHOME environment variable. ' 'For more information about Weka, please see ' 'http://www.cs.waikato.ac.nz/ml/weka/') def _check_weka_version(jar): try: zf = zipfile.ZipFile(jar) except SystemExit, KeyboardInterrupt: raise except: return None try: try: return zf.read('weka/core/version.txt') except KeyError: return None finally: zf.close() class WekaClassifier(ClassifierI): def __init__(self, formatter, model_filename): self._formatter = formatter self._model = model_filename def batch_prob_classify(self, featuresets): return self._batch_classify(featuresets, ['-p', '0', '-distribution']) def batch_classify(self, featuresets): return self._batch_classify(featuresets, ['-p', '0']) def _batch_classify(self, featuresets, options): # Make sure we can find java & weka. config_weka() temp_dir = tempfile.mkdtemp() try: # Write the test data file. test_filename = os.path.join(temp_dir, 'test.arff') self._formatter.write(test_filename, featuresets) # Call weka to classify the data. cmd = ['weka.classifiers.bayes.NaiveBayes', '-l', self._model, '-T', test_filename] + options (stdout, stderr) = java(cmd, classpath=_weka_classpath, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Check if something went wrong: if stderr and not stdout: if 'Illegal options: -distribution' in stderr: raise ValueError('The installed verison of weka does ' 'not support probability distribution ' 'output.') else: raise ValueError('Weka failed to generate output:\n%s' % stderr) # Parse weka's output. return self.parse_weka_output(stdout.split('\n')) finally: for f in os.listdir(temp_dir): os.remove(os.path.join(temp_dir, f)) os.rmdir(temp_dir) def parse_weka_distribution(self, s): probs = [float(v) for v in re.split('[*,]+', s) if v.strip()] probs = dict(zip(self._formatter.labels(), probs)) return DictionaryProbDist(probs) def parse_weka_output(self, lines): if lines[0].split() == ['inst#', 'actual', 'predicted', 'error', 'prediction']: return [line.split()[2].split(':')[1] for line in lines[1:] if line.strip()] elif lines[0].split() == ['inst#', 'actual', 'predicted', 'error', 'distribution']: return [self.parse_weka_distribution(line.split()[-1]) for line in lines[1:] if line.strip()] # is this safe:? elif re.match(r'^0 \w+ [01]\.[0-9]* \?\s*$', lines[0]): return [line.split()[1] for line in lines if line.strip()] else: for line in lines[:10]: print line raise ValueError('Unhandled output format -- your version ' 'of weka may not be supported.\n' ' Header: %s' % lines[0]) # [xx] full list of classifiers (some may be abstract?): # ADTree, AODE, BayesNet, ComplementNaiveBayes, ConjunctiveRule, # DecisionStump, DecisionTable, HyperPipes, IB1, IBk, Id3, J48, # JRip, KStar, LBR, LeastMedSq, LinearRegression, LMT, Logistic, # LogisticBase, M5Base, MultilayerPerceptron, # MultipleClassifiersCombiner, NaiveBayes, NaiveBayesMultinomial, # NaiveBayesSimple, NBTree, NNge, OneR, PaceRegression, PART, # PreConstructedLinearModel, Prism, RandomForest, # RandomizableClassifier, RandomTree, RBFNetwork, REPTree, Ridor, # RuleNode, SimpleLinearRegression, SimpleLogistic, # SingleClassifierEnhancer, SMO, SMOreg, UserClassifier, VFI, # VotedPerceptron, Winnow, ZeroR _CLASSIFIER_CLASS = { 'naivebayes': 'weka.classifiers.bayes.NaiveBayes', 'C4.5': 'weka.classifiers.trees.J48', 'log_regression': 'weka.classifiers.functions.Logistic', 'svm': 'weka.classifiers.functions.SMO', 'kstar': 'weka.classifiers.lazy.kstar', 'ripper': 'weka.classifiers.rules.JRip', } @classmethod def train(cls, model_filename, featuresets, classifier='naivebayes', options=[], quiet=True): # Make sure we can find java & weka. config_weka() # Build an ARFF formatter. formatter = ARFF_Formatter.from_train(featuresets) temp_dir = tempfile.mkdtemp() try: # Write the training data file. train_filename = os.path.join(temp_dir, 'train.arff') formatter.write(train_filename, featuresets) if classifier in cls._CLASSIFIER_CLASS: javaclass = cls._CLASSIFIER_CLASS[classifier] elif classifier in cls._CLASSIFIER_CLASS.values(): javaclass = classifier else: raise ValueError('Unknown classifier %s' % classifier) # Train the weka model. cmd = [javaclass, '-d', model_filename, '-t', train_filename] cmd += list(options) if quiet: stdout = subprocess.PIPE else: stdout = None java(cmd, classpath=_weka_classpath, stdout=stdout) # Return the new classifier. return WekaClassifier(formatter, model_filename) finally: for f in os.listdir(temp_dir): os.remove(os.path.join(temp_dir, f)) os.rmdir(temp_dir) class ARFF_Formatter: """ Converts featuresets and labeled featuresets to ARFF-formatted strings, appropriate for input into Weka. Features and classes can be specified manually in the constructor, or may be determined from data using C{from_train}. """ def __init__(self, labels, features): """ @param labels: A list of all class labels that can be generated. @param features: A list of feature specifications, where each feature specification is a tuple (fname, ftype); and ftype is an ARFF type string such as NUMERIC or STRING. """ self._labels = labels self._features = features def format(self, tokens): """Returns a string representation of ARFF output for the given data.""" return self.header_section() + self.data_section(tokens) def labels(self): """Returns the list of classes.""" return list(self._labels) def write(self, outfile, tokens): """Writes ARFF data to a file for the given data.""" if not hasattr(outfile, 'write'): outfile = open(outfile, 'w') outfile.write(self.format(tokens)) outfile.close() @staticmethod def from_train(tokens): """ Constructs an ARFF_Formatter instance with class labels and feature types determined from the given data. Handles boolean, numeric and string (note: not nominal) types. """ # Find the set of all attested labels. labels = set(label for (tok,label) in tokens) # Determine the types of all features. features = {} for tok, label in tokens: for (fname, fval) in tok.items(): if issubclass(type(fval), bool): ftype = '{True, False}' elif issubclass(type(fval), (int, float, long, bool)): ftype = 'NUMERIC' elif issubclass(type(fval), basestring): ftype = 'STRING' elif fval is None: continue # can't tell the type. else: raise ValueError('Unsupported value type %r' % ftype) if features.get(fname, ftype) != ftype: raise ValueError('Inconsistent type for %s' % fname) features[fname] = ftype features = sorted(features.items()) return ARFF_Formatter(labels, features) def header_section(self): """Returns an ARFF header as a string.""" # Header comment. s = ('% Weka ARFF file\n' + '% Generated automatically by NLTK\n' + '%% %s\n\n' % time.ctime()) # Relation name s += '@RELATION rel\n\n' # Input attribute specifications for fname, ftype in self._features: s += '@ATTRIBUTE %-30r %s\n' % (fname, ftype) # Label attribute specification s += '@ATTRIBUTE %-30r {%s}\n' % ('-label-', ','.join(self._labels)) return s def data_section(self, tokens, labeled=None): """ Returns the ARFF data section for the given data. @param tokens: a list of featuresets (dicts) or labelled featuresets which are tuples (featureset, label). @param labeled: Indicates whether the given tokens are labeled or not. If C{None}, then the tokens will be assumed to be labeled if the first token's value is a tuple or list. """ # Check if the tokens are labeled or unlabeled. If unlabeled, # then use 'None' if labeled is None: labeled = tokens and isinstance(tokens[0], (tuple, list)) if not labeled: tokens = [(tok, None) for tok in tokens] # Data section s = '\n@DATA\n' for (tok, label) in tokens: for fname, ftype in self._features: s += '%s,' % self._fmt_arff_val(tok.get(fname)) s += '%s\n' % self._fmt_arff_val(label) return s def _fmt_arff_val(self, fval): if fval is None: return '?' elif isinstance(fval, (bool, int, long)): return '%s' % fval elif isinstance(fval, float): return '%r' % fval else: return '%r' % fval if __name__ == '__main__': from nltk.classify.util import names_demo, binary_names_demo_features def make_classifier(featuresets): return WekaClassifier.train('/tmp/name.model', featuresets, 'C4.5') classifier = names_demo(make_classifier, binary_names_demo_features) nltk-2.0~b9/nltk/classify/util.py0000644000175000017500000002274411363770700016707 0ustar bhavanibhavani# Natural Language Toolkit: Classifier Utility Functions # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird (minor additions) # URL: # For license information, see LICENSE.TXT """ Utility functions and classes for classifiers. """ import math #from nltk.util import Deprecated import nltk.classify.util # for accuracy & log_likelihood from nltk.util import LazyMap ###################################################################### #{ Helper Functions ###################################################################### # alternative name possibility: 'map_featurefunc()'? # alternative name possibility: 'detect_features()'? # alternative name possibility: 'map_featuredetect()'? # or.. just have users use LazyMap directly? def apply_features(feature_func, toks, labeled=None): """ Use the L{LazyMap} class to construct a lazy list-like object that is analogous to C{map(feature_func, toks)}. In particular, if C{labeled=False}, then the returned list-like object's values are equal to:: [feature_func(tok) for tok in toks] If C{labeled=True}, then the returned list-like object's values are equal to:: [(feature_func(tok), label) for (tok, label) in toks] The primary purpose of this function is to avoid the memory overhead involved in storing all the featuresets for every token in a corpus. Instead, these featuresets are constructed lazily, as-needed. The reduction in memory overhead can be especially significant when the underlying list of tokens is itself lazy (as is the case with many corpus readers). @param feature_func: The function that will be applied to each token. It should return a featureset -- i.e., a C{dict} mapping feature names to feature values. @param toks: The list of tokens to which C{feature_func} should be applied. If C{labeled=True}, then the list elements will be passed directly to C{feature_func()}. If C{labeled=False}, then the list elements should be tuples C{(tok,label)}, and C{tok} will be passed to C{feature_func()}. @param labeled: If true, then C{toks} contains labeled tokens -- i.e., tuples of the form C{(tok, label)}. (Default: auto-detect based on types.) """ if labeled is None: labeled = toks and isinstance(toks[0], (tuple, list)) if labeled: def lazy_func(labeled_token): return (feature_func(labeled_token[0]), labeled_token[1]) return LazyMap(lazy_func, toks) else: return LazyMap(feature_func, toks) def attested_labels(tokens): """ @return: A list of all labels that are attested in the given list of tokens. @rtype: C{list} of (immutable) @param tokens: The list of classified tokens from which to extract labels. A classified token has the form C{(token, label)}. @type tokens: C{list} """ return tuple(set([label for (tok,label) in tokens])) def log_likelihood(classifier, gold): results = classifier.batch_prob_classify([fs for (fs,l) in gold]) ll = [pdist.prob(l) for ((fs,l), pdist) in zip(gold, results)] return math.log(float(sum(ll))/len(ll)) def accuracy(classifier, gold): results = classifier.batch_classify([fs for (fs,l) in gold]) correct = [l==r for ((fs,l), r) in zip(gold, results)] return float(sum(correct))/len(correct) class CutoffChecker(object): """ A helper class that implements cutoff checks based on number of iterations and log likelihood. Accuracy cutoffs are also implemented, but they're almost never a good idea to use. """ def __init__(self, cutoffs): self.cutoffs = cutoffs.copy() if 'min_ll' in cutoffs: cutoffs['min_ll'] = -abs(cutoffs['min_ll']) if 'min_lldelta' in cutoffs: cutoffs['min_lldelta'] = abs(cutoffs['min_lldelta']) self.ll = None self.acc = None self.iter = 1 def check(self, classifier, train_toks): cutoffs = self.cutoffs self.iter += 1 if 'max_iter' in cutoffs and self.iter >= cutoffs['max_iter']: return True # iteration cutoff. new_ll = nltk.classify.util.log_likelihood(classifier, train_toks) if math.isnan(new_ll): return True if 'min_ll' in cutoffs or 'min_lldelta' in cutoffs: if 'min_ll' in cutoffs and new_ll >= cutoffs['min_ll']: return True # log likelihood cutoff if ('min_lldelta' in cutoffs and self.ll and ((new_ll - self.ll) <= abs(cutoffs['min_lldelta']))): return True # log likelihood delta cutoff self.ll = new_ll if 'max_acc' in cutoffs or 'min_accdelta' in cutoffs: new_acc = nltk.classify.util.log_likelihood( classifier, train_toks) if 'max_acc' in cutoffs and new_acc >= cutoffs['max_acc']: return True # log likelihood cutoff if ('min_accdelta' in cutoffs and self.acc and ((new_acc - self.acc) <= abs(cutoffs['min_accdelta']))): return True # log likelihood delta cutoff self.acc = new_acc return False # no cutoff reached. ###################################################################### #{ Demos ###################################################################### def names_demo_features(name): features = {} features['alwayson'] = True features['startswith'] = name[0].lower() features['endswith'] = name[-1].lower() for letter in 'abcdefghijklmnopqrstuvwxyz': features['count(%s)' % letter] = name.lower().count(letter) features['has(%s)' % letter] = letter in name.lower() return features def binary_names_demo_features(name): features = {} features['alwayson'] = True features['startswith(vowel)'] = name[0].lower() in 'aeiouy' features['endswith(vowel)'] = name[-1].lower() in 'aeiouy' for letter in 'abcdefghijklmnopqrstuvwxyz': features['count(%s)' % letter] = name.lower().count(letter) features['has(%s)' % letter] = letter in name.lower() features['startswith(%s)' % letter] = (letter==name[0].lower()) features['endswith(%s)' % letter] = (letter==name[-1].lower()) return features def names_demo(trainer, features=names_demo_features): from nltk.corpus import names import random # Construct a list of classified names, using the names corpus. namelist = ([(name, 'male') for name in names.words('male.txt')] + [(name, 'female') for name in names.words('female.txt')]) # Randomly split the names into a test & train set. random.seed(123456) random.shuffle(namelist) train = namelist[:5000] test = namelist[5000:5500] # Train up a classifier. print 'Training classifier...' classifier = trainer( [(features(n), g) for (n,g) in train] ) # Run the classifier on the test data. print 'Testing classifier...' acc = accuracy(classifier, [(features(n),g) for (n,g) in test]) print 'Accuracy: %6.4f' % acc # For classifiers that can find probabilities, show the log # likelihood and some sample probability distributions. try: test_featuresets = [features(n) for (n,g) in test] pdists = classifier.batch_prob_classify(test_featuresets) ll = [pdist.logprob(gold) for ((name, gold), pdist) in zip(test, pdists)] print 'Avg. log likelihood: %6.4f' % (sum(ll)/len(test)) print print 'Unseen Names P(Male) P(Female)\n'+'-'*40 for ((name, gender), pdist) in zip(test, pdists)[:5]: if gender == 'male': fmt = ' %-15s *%6.4f %6.4f' else: fmt = ' %-15s %6.4f *%6.4f' print fmt % (name, pdist.prob('male'), pdist.prob('female')) except NotImplementedError: pass # Return the classifier return classifier _inst_cache = {} def wsd_demo(trainer, word, features, n=1000): from nltk.corpus import senseval import random # Get the instances. print 'Reading data...' global _inst_cache if word not in _inst_cache: _inst_cache[word] = [(i, i.senses[0]) for i in senseval.instances(word)] instances = _inst_cache[word][:] if n> len(instances): n = len(instances) senses = list(set(l for (i,l) in instances)) print ' Senses: ' + ' '.join(senses) # Randomly split the names into a test & train set. print 'Splitting into test & train...' random.seed(123456) random.shuffle(instances) train = instances[:int(.8*n)] test = instances[int(.8*n):n] # Train up a classifier. print 'Training classifier...' classifier = trainer( [(features(i), l) for (i,l) in train] ) # Run the classifier on the test data. print 'Testing classifier...' acc = accuracy(classifier, [(features(i),l) for (i,l) in test]) print 'Accuracy: %6.4f' % acc # For classifiers that can find probabilities, show the log # likelihood and some sample probability distributions. try: test_featuresets = [features(i) for (i,n) in test] pdists = classifier.batch_prob_classify(test_featuresets) ll = [pdist.logprob(gold) for ((name, gold), pdist) in zip(test, pdists)] print 'Avg. log likelihood: %6.4f' % (sum(ll)/len(test)) except NotImplementedError: pass # Return the classifier return classifier nltk-2.0~b9/nltk/classify/tadm.py0000644000175000017500000000655711327451603016661 0ustar bhavanibhavani# Natural Language Toolkit: Interface to TADM Classifier # # Copyright (C) 2001-2010 NLTK Project # Author: Joseph Frazee # URL: # For license information, see LICENSE.TXT import sys import subprocess from nltk.internals import find_binary try: import numpy except ImportError: numpy = None _tadm_bin = None def config_tadm(bin=None): global _tadm_bin _tadm_bin = find_binary( 'tadm', bin, env_vars=['TADM_DIR'], binary_names=['tadm'], url='http://tadm.sf.net') def write_tadm_file(train_toks, encoding, stream): """ Generate an input file for C{tadm} based on the given corpus of classified tokens. @type train_toks: C{list} of C{tuples} of (C{dict}, C{str}) @param train_toks: Training data, represented as a list of pairs, the first member of which is a feature dictionary, and the second of which is a classification label. @type encoding: L{TadmEventMaxentFeatureEncoding} @param encoding: A feature encoding, used to convert featuresets into feature vectors. @type stream: C{stream} @param stream: The stream to which the C{tadm} input file should be written. """ # See the following for a file format description: # # http://sf.net/forum/forum.php?thread_id=1391502&forum_id=473054 # http://sf.net/forum/forum.php?thread_id=1675097&forum_id=473054 labels = encoding.labels() for featureset, label in train_toks: stream.write('%d\n' % len(labels)) for known_label in labels: v = encoding.encode(featureset, known_label) stream.write('%d %d %s\n' % (int(label == known_label), len(v), ' '.join('%d %d' % u for u in v))) def parse_tadm_weights(paramfile): """ Given the stdout output generated by C{tadm} when training a model, return a C{numpy} array containing the corresponding weight vector. """ weights = [] for line in paramfile: weights.append(float(line.strip())) return numpy.array(weights, 'd') def call_tadm(args): """ Call the C{tadm} binary with the given arguments. """ if isinstance(args, basestring): raise TypeError('args should be a list of strings') if _tadm_bin is None: config_tadm() # Call tadm via a subprocess cmd = [_tadm_bin] + args p = subprocess.Popen(cmd, stdout=sys.stdout) (stdout, stderr) = p.communicate() # Check the return code. if p.returncode != 0: print print stderr raise OSError('tadm command failed!') def names_demo(): from nltk.classify.util import names_demo from nltk.classify.maxent import TadmMaxentClassifier classifier = names_demo(TadmMaxentClassifier.train) def encoding_demo(): import sys from nltk.classify.maxent import TadmEventMaxentFeatureEncoding from nltk.classify.tadm import write_tadm_file tokens = [({'f0':1, 'f1':1, 'f3':1}, 'A'), ({'f0':1, 'f2':1, 'f4':1}, 'B'), ({'f0':2, 'f2':1, 'f3':1, 'f4':1}, 'A')] encoding = TadmEventMaxentFeatureEncoding.train(tokens) write_tadm_file(tokens, encoding, sys.stdout) print for i in range(encoding.length()): print '%s --> %d' % (encoding.describe(i), i) print if __name__ == '__main__': encoding_demo() names_demo() nltk-2.0~b9/nltk/classify/rte_classify.py0000644000175000017500000001441111327451603020407 0ustar bhavanibhavani# Natural Language Toolkit: RTE Classifier # # Copyright (C) 2001-2010 NLTK Project # Author: Ewan Klein # URL: # For license information, see LICENSE.TXT """ Simple classifier for RTE corpus. It calculates the overlap in words and named entities between text and hypothesis, and also whether there are words / named entities in the hypothesis which fail to occur in the text, since this is an indicator that the hypothesis is more informative than (i.e not entailed by) the text. TO DO: better Named Entity classification TO DO: add lemmatization """ import nltk from util import accuracy def ne(token): """ This just assumes that words in all caps or titles are named entities. @type token: C{str} """ if token.istitle() or \ token.isupper(): return True return False def lemmatize(word): """ Use morphy from WordNet to find the base form of verbs. """ lemma = nltk.corpus.wordnet.morphy(word, pos='verb') if lemma is not None: return lemma return word class RTEFeatureExtractor(object): """ This builds a bag of words for both the text and the hypothesis after throwing away some stopwords, then calculates overlap and difference. """ def __init__(self, rtepair, stop=True, lemmatize=False): """ @param rtepair: a L{RTEPair} from which features should be extracted @param stop: if C{True}, stopwords are thrown away. @type stop: C{bool} """ self.stop = stop self.stopwords = set(['a', 'the', 'it', 'they', 'of', 'in', 'to', 'have', 'is', 'are', 'were', 'and', 'very', '.',',']) self.negwords = set(['no', 'not', 'never', 'failed' 'rejected', 'denied']) # Try to tokenize so that abbreviations like U.S.and monetary amounts # like "$23.00" are kept as tokens. from nltk.tokenize import RegexpTokenizer tokenizer = RegexpTokenizer('([A-Z]\.)+|\w+|\$[\d\.]+') #Get the set of word types for text and hypothesis self.text_tokens = tokenizer.tokenize(rtepair.text) self.hyp_tokens = tokenizer.tokenize(rtepair.hyp) self.text_words = set(self.text_tokens) self.hyp_words = set(self.hyp_tokens) if lemmatize: self.text_words = set([lemmatize(token) for token in self.text_tokens]) self.hyp_words = set([lemmatize(token) for token in self.hyp_tokens]) if self.stop: self.text_words = self.text_words - self.stopwords self.hyp_words = self.hyp_words - self.stopwords self._overlap = self.hyp_words & self.text_words self._hyp_extra = self.hyp_words - self.text_words self._txt_extra = self.text_words - self.hyp_words def overlap(self, toktype, debug=False): """ Compute the overlap between text and hypothesis. @param toktype: distinguish Named Entities from ordinary words @type toktype: 'ne' or 'word' """ ne_overlap = set([token for token in self._overlap if ne(token)]) if toktype == 'ne': if debug: print "ne overlap", ne_overlap return ne_overlap elif toktype == 'word': if debug: print "word overlap", self._overlap - ne_overlap return self._overlap - ne_overlap else: raise ValueError("Type not recognized:'%s'" % toktype) def hyp_extra(self, toktype, debug=True): """ Compute the extraneous material in the hypothesis. @param toktype: distinguish Named Entities from ordinary words @type toktype: 'ne' or 'word' """ ne_extra = set([token for token in self._hyp_extra if ne(token)]) if toktype == 'ne': return ne_extra elif toktype == 'word': return self._hyp_extra - ne_extra else: raise ValueError("Type not recognized: '%s'" % toktype) def rte_features(rtepair): extractor = RTEFeatureExtractor(rtepair) features = {} features['alwayson'] = True features['word_overlap'] = len(extractor.overlap('word')) features['word_hyp_extra'] = len(extractor.hyp_extra('word')) features['ne_overlap'] = len(extractor.overlap('ne')) features['ne_hyp_extra'] = len(extractor.hyp_extra('ne')) features['neg_txt'] = len(extractor.negwords & extractor.text_words) features['neg_hyp'] = len(extractor.negwords & extractor.hyp_words) return features def rte_classifier(trainer, features=rte_features): """ Classify RTEPairs """ train = [(pair, pair.value) for pair in nltk.corpus.rte.pairs(['rte1_dev.xml', 'rte2_dev.xml', 'rte3_dev.xml'])] test = [(pair, pair.value) for pair in nltk.corpus.rte.pairs(['rte1_test.xml', 'rte2_test.xml', 'rte3_test.xml'])] # Train up a classifier. print 'Training classifier...' classifier = trainer( [(features(pair), label) for (pair,label) in train] ) # Run the classifier on the test data. print 'Testing classifier...' acc = accuracy(classifier, [(features(pair), label) for (pair,label) in test]) print 'Accuracy: %6.4f' % acc # Return the classifier return classifier def demo_features(): pairs = nltk.corpus.rte.pairs(['rte1_dev.xml'])[:6] for pair in pairs: print for key in sorted(rte_features(pair)): print "%-15s => %s" % (key, rte_features(pair)[key]) def demo_feature_extractor(): rtepair = nltk.corpus.rte.pairs(['rte3_dev.xml'])[33] extractor = RTEFeatureExtractor(rtepair) print extractor.hyp_words print extractor.overlap('word') print extractor.overlap('ne') print extractor.hyp_extra('word') def demo(): import nltk try: nltk.config_megam('/usr/local/bin/megam') trainer = lambda x: nltk.MaxentClassifier.train(x, 'megam') except ValueError: try: trainer = lambda x: nltk.MaxentClassifier.train(x, 'BFGS') except ValueError: trainer = nltk.MaxentClassifier.train nltk.classify.rte_classifier(trainer) if __name__ == '__main__': demo_features() demo_feature_extractor() demo() nltk-2.0~b9/nltk/classify/naivebayes.py0000644000175000017500000002304311327451603020047 0ustar bhavanibhavani# Natural Language Toolkit: Naive Bayes Classifiers # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # # $Id: naivebayes.py 2063 2004-07-17 21:02:24Z edloper $ """ A classifier based on the Naive Bayes algorithm. In order to find the probability for a label, this algorithm first uses the Bayes rule to express P(label|features) in terms of P(label) and P(features|label):: P(label) * P(features|label) P(label|features) = ------------------------------ P(features) The algorithm then makes the 'naive' assumption that all features are independent, given the label:: P(label) * P(f1|label) * ... * P(fn|label) P(label|features) = -------------------------------------------- P(features) Rather than computing P(featues) explicitly, the algorithm just calculates the denominator for each label, and normalizes them so they sum to one:: P(label) * P(f1|label) * ... * P(fn|label) P(label|features) = -------------------------------------------- SUM[l]( P(l) * P(f1|l) * ... * P(fn|l) ) """ from nltk.compat import defaultdict from nltk.probability import * from api import * ##////////////////////////////////////////////////////// ## Naive Bayes Classifier ##////////////////////////////////////////////////////// class NaiveBayesClassifier(ClassifierI): """ A Naive Bayes classifier. Naive Bayes classifiers are paramaterized by two probability distributions: - P(label) gives the probability that an input will receive each label, given no information about the input's features. - P(fname=fval|label) gives the probability that a given feature (fname) will receive a given value (fval), given that the label (label). If the classifier encounters an input with a feature that has never been seen with any label, then rather than assigning a probability of 0 to all labels, it will ignore that feature. The feature value 'None' is reserved for unseen feature values; you generally should not use 'None' as a feature value for one of your own features. """ def __init__(self, label_probdist, feature_probdist): """ @param label_probdist: P(label), the probability distribution over labels. It is expressed as a L{ProbDistI} whose samples are labels. I.e., P(label) = C{label_probdist.prob(label)}. @param feature_probdist: P(fname=fval|label), the probability distribution for feature values, given labels. It is expressed as a dictionary whose keys are C{(label,fname)} pairs and whose values are L{ProbDistI}s over feature values. I.e., P(fname=fval|label) = C{feature_probdist[label,fname].prob(fval)}. If a given C{(label,fname)} is not a key in C{feature_probdist}, then it is assumed that the corresponding P(fname=fval|label) is 0 for all values of C{fval}. """ self._label_probdist = label_probdist self._feature_probdist = feature_probdist self._labels = label_probdist.samples() def labels(self): return self._labels def classify(self, featureset): return self.prob_classify(featureset).max() def prob_classify(self, featureset): # Discard any feature names that we've never seen before. # Otherwise, we'll just assign a probability of 0 to # everything. featureset = featureset.copy() for fname in featureset.keys(): for label in self._labels: if (label, fname) in self._feature_probdist: break else: #print 'Ignoring unseen feature %s' % fname del featureset[fname] # Find the log probabilty of each label, given the features. # Start with the log probability of the label itself. logprob = {} for label in self._labels: logprob[label] = self._label_probdist.logprob(label) # Then add in the log probability of features given labels. for label in self._labels: for (fname, fval) in featureset.items(): if (label, fname) in self._feature_probdist: feature_probs = self._feature_probdist[label,fname] logprob[label] += feature_probs.logprob(fval) else: # nb: This case will never come up if the # classifier was created by # NaiveBayesClassifier.train(). logprob[label] += sum_logs([]) # = -INF. return DictionaryProbDist(logprob, normalize=True, log=True) def show_most_informative_features(self, n=10): # Determine the most relevant features, and display them. cpdist = self._feature_probdist print 'Most Informative Features' for (fname, fval) in self.most_informative_features(n): def labelprob(l): return cpdist[l,fname].prob(fval) labels = sorted([l for l in self._labels if fval in cpdist[l,fname].samples()], key=labelprob) if len(labels) == 1: continue l0 = labels[0] l1 = labels[-1] if cpdist[l0,fname].prob(fval) == 0: ratio = 'INF' else: ratio = '%8.1f' % (cpdist[l1,fname].prob(fval) / cpdist[l0,fname].prob(fval)) print ('%24s = %-14r %6s : %-6s = %s : 1.0' % (fname, fval, l1[:6], l0[:6], ratio)) def most_informative_features(self, n=100): """ Return a list of the 'most informative' features used by this classifier. For the purpose of this function, the informativeness of a feature C{(fname,fval)} is equal to the highest value of P(fname=fval|label), for any label, divided by the lowest value of P(fname=fval|label), for any label:: max[ P(fname=fval|label1) / P(fname=fval|label2) ] """ # The set of (fname, fval) pairs used by this classifier. features = set() # The max & min probability associated w/ each (fname, fval) # pair. Maps (fname,fval) -> float. maxprob = defaultdict(lambda: 0.0) minprob = defaultdict(lambda: 1.0) for (label, fname), probdist in self._feature_probdist.items(): for fval in probdist.samples(): feature = (fname, fval) features.add( feature ) p = probdist.prob(fval) maxprob[feature] = max(p, maxprob[feature]) minprob[feature] = min(p, minprob[feature]) if minprob[feature] == 0: features.discard(feature) # Convert features to a list, & sort it by how informative # features are. features = sorted(features, key=lambda feature: minprob[feature]/maxprob[feature]) return features[:n] @staticmethod def train(labeled_featuresets, estimator=ELEProbDist): """ @param labeled_featuresets: A list of classified featuresets, i.e., a list of tuples C{(featureset, label)}. """ label_freqdist = FreqDist() feature_freqdist = defaultdict(FreqDist) feature_values = defaultdict(set) fnames = set() # Count up how many times each feature value occured, given # the label and featurename. for featureset, label in labeled_featuresets: label_freqdist.inc(label) for fname, fval in featureset.items(): # Increment freq(fval|label, fname) feature_freqdist[label, fname].inc(fval) # Record that fname can take the value fval. feature_values[fname].add(fval) # Keep a list of all feature names. fnames.add(fname) # If a feature didn't have a value given for an instance, then # we assume that it gets the implicit value 'None.' This loop # counts up the number of 'missing' feature values for each # (label,fname) pair, and increments the count of the fval # 'None' by that amount. for label in label_freqdist: num_samples = label_freqdist[label] for fname in fnames: count = feature_freqdist[label, fname].N() feature_freqdist[label, fname].inc(None, num_samples-count) feature_values[fname].add(None) # Create the P(label) distribution label_probdist = estimator(label_freqdist) # Create the P(fval|label, fname) distribution feature_probdist = {} for ((label, fname), freqdist) in feature_freqdist.items(): probdist = estimator(freqdist, bins=len(feature_values[fname])) feature_probdist[label,fname] = probdist return NaiveBayesClassifier(label_probdist, feature_probdist) ##////////////////////////////////////////////////////// ## Demo ##////////////////////////////////////////////////////// def demo(): from nltk.classify.util import names_demo classifier = names_demo(NaiveBayesClassifier.train) classifier.show_most_informative_features() if __name__ == '__main__': demo() nltk-2.0~b9/nltk/classify/megam.py0000644000175000017500000001327111374105242017006 0ustar bhavanibhavani# Natural Language Toolkit: Interface to Megam Classifier # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # # $Id: naivebayes.py 2063 2004-07-17 21:02:24Z edloper $ """ A set of functions used to interface with the external U{megam } maxent optimization package. Before C{megam} can be used, you should tell NLTK where it can find the C{megam} binary, using the L{config_megam()} function. Typical usage: >>> import nltk >>> nltk.config_megam('.../path/to/megam') >>> classifier = nltk.MaxentClassifier.train(corpus, 'megam') """ __docformat__ = 'epytext en' import os import os.path import subprocess from nltk.internals import find_binary try: import numpy except ImportError: numpy = None ###################################################################### #{ Configuration ###################################################################### _megam_bin = None def config_megam(bin=None): """ Configure NLTK's interface to the C{megam} maxent optimization package. @param bin: The full path to the C{megam} binary. If not specified, then nltk will search the system for a C{megam} binary; and if one is not found, it will raise a C{LookupError} exception. @type bin: C{string} """ global _megam_bin _megam_bin = find_binary( 'megam', bin, env_vars=['MEGAM', 'MEGAMHOME'], binary_names=['megam.opt', 'megam', 'megam_686', 'megam_i686.opt'], url='http://www.cs.utah.edu/~hal/megam/') ###################################################################### #{ Megam Interface Functions ###################################################################### def write_megam_file(train_toks, encoding, stream, bernoulli=True, explicit=True): """ Generate an input file for C{megam} based on the given corpus of classified tokens. @type train_toks: C{list} of C{tuples} of (C{dict}, C{str}) @param train_toks: Training data, represented as a list of pairs, the first member of which is a feature dictionary, and the second of which is a classification label. @type encoding: L{MaxentFeatureEncodingI} @param encoding: A feature encoding, used to convert featuresets into feature vectors. @type stream: C{stream} @param stream: The stream to which the megam input file should be written. @param bernoulli: If true, then use the 'bernoulli' format. I.e., all joint features have binary values, and are listed iff they are true. Otherwise, list feature values explicitly. If C{bernoulli=False}, then you must call C{megam} with the C{-fvals} option. @param explicit: If true, then use the 'explicit' format. I.e., list the features that would fire for any of the possible labels, for each token. If C{explicit=True}, then you must call C{megam} with the C{-explicit} option. """ # Look up the set of labels. labels = encoding.labels() labelnum = dict([(label, i) for (i, label) in enumerate(labels)]) # Write the file, which contains one line per instance. for featureset, label in train_toks: # First, the instance number. stream.write('%d' % labelnum[label]) # For implicit file formats, just list the features that fire # for this instance's actual label. if not explicit: _write_megam_features(encoding.encode(featureset, label), stream, bernoulli) # For explicit formats, list the features that would fire for # any of the possible labels. else: for l in labels: stream.write(' #') _write_megam_features(encoding.encode(featureset, l), stream, bernoulli) # End of the isntance. stream.write('\n') def parse_megam_weights(s, features_count, explicit=True): """ Given the stdout output generated by C{megam} when training a model, return a C{numpy} array containing the corresponding weight vector. This function does not currently handle bias features. """ if numpy is None: raise ValueError('This function requires that numpy be installed') assert explicit, 'non-explicit not supported yet' lines = s.strip().split('\n') weights = numpy.zeros(features_count, 'd') for line in lines: if line.strip(): fid, weight = line.split() weights[int(fid)] = float(weight) return weights def _write_megam_features(vector, stream, bernoulli): if not vector: raise ValueError('MEGAM classifier requires the use of an ' 'always-on feature.') for (fid, fval) in vector: if bernoulli: if fval == 1: stream.write(' %s' % fid) elif fval != 0: raise ValueError('If bernoulli=True, then all' 'features must be binary.') else: stream.write(' %s %s' % (fid, fval)) def call_megam(args): """ Call the C{megam} binary with the given arguments. """ if isinstance(args, basestring): raise TypeError('args should be a list of strings') if _megam_bin is None: config_megam() # Call megam via a subprocess cmd = [_megam_bin] + args p = subprocess.Popen(cmd, stdout=subprocess.PIPE) (stdout, stderr) = p.communicate() # Check the return code. if p.returncode != 0: print print stderr raise OSError('megam command failed!') return stdout nltk-2.0~b9/nltk/classify/maxent.py0000644000175000017500000020165211374105242017216 0ustar bhavanibhavani# Natural Language Toolkit: Maximum Entropy Classifiers # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Dmitry Chichkov (TypedMaxentFeatureEncoding) # URL: # For license information, see LICENSE.TXT """ A classifier model based on maximum entropy modeling framework. This framework considers all of the probability distributions that are empirically consistant with the training data; and chooses the distribution with the highest entropy. A probability distribution is X{empirically consistant} with a set of training data if its estimated frequency with which a class and a feature vector value co-occur is equal to the actual frequency in the data. Terminology: 'feature' ====================== The term I{feature} is usually used to refer to some property of an unlabeled token. For example, when performing word sense disambiguation, we might define a C{'prevword'} feature whose value is the word preceeding the target word. However, in the context of maxent modeling, the term I{feature} is typically used to refer to a property of a X{labeled} token. In order to prevent confusion, we will introduce two distinct terms to disambiguate these two different concepts: - An X{input-feature} is a property of an unlabeled token. - A X{joint-feature} is a property of a labeled token. In the rest of the C{nltk.classify} module, the term X{features} is used to refer to what we will call X{input-features} in this module. In literature that describes and discusses maximum entropy models, input-features are typically called X{contexts}, and joint-features are simply referred to as X{features}. Converting Input-Features to Joint-Features ------------------------------------------- In maximum entropy models, joint-features are required to have numeric values. Typically, each input-feature C{input_feat} is mapped to a set of joint-features of the form:: joint_feat(token, label) = { 1 if input_feat(token) == feat_val { and label == some_label { { 0 otherwise For all values of C{feat_val} and C{some_label}. This mapping is performed by classes that implement the L{MaxentFeatureEncodingI} interface. """ __docformat__ = 'epytext en' import numpy import time import tempfile import os import gzip from nltk.compat import defaultdict from nltk.util import OrderedDict from nltk.probability import * import nltk.classify.util # for accuracy & log_likelihood from api import * from util import attested_labels, CutoffChecker from megam import call_megam, write_megam_file, parse_megam_weights from tadm import call_tadm, write_tadm_file, parse_tadm_weights ###################################################################### #{ Classifier Model ###################################################################### class MaxentClassifier(ClassifierI): """ A maximum entropy classifier (also known as a X{conditional exponential classifier}). This classifier is parameterized by a set of X{weights}, which are used to combine the joint-features that are generated from a featureset by an X{encoding}. In particular, the encoding maps each C{(featureset, label)} pair to a vector. The probability of each label is then computed using the following equation:: dotprod(weights, encode(fs,label)) prob(fs|label) = --------------------------------------------------- sum(dotprod(weights, encode(fs,l)) for l in labels) Where C{dotprod} is the dot product:: dotprod(a,b) = sum(x*y for (x,y) in zip(a,b)) """ def __init__(self, encoding, weights, logarithmic=True): """ Construct a new maxent classifier model. Typically, new classifier models are created using the L{train()} method. @type encoding: L{MaxentFeatureEncodingI} @param encoding: An encoding that is used to convert the featuresets that are given to the C{classify} method into joint-feature vectors, which are used by the maxent classifier model. @type weights: C{list} of C{float} @param weights: The feature weight vector for this classifier. @type logarithmic: C{bool} @param logarithmic: If false, then use non-logarithmic weights. """ self._encoding = encoding self._weights = weights self._logarithmic = logarithmic #self._logarithmic = False assert encoding.length() == len(weights) def labels(self): return self._encoding.labels() def set_weights(self, new_weights): """ Set the feature weight vector for this classifier. @param new_weights: The new feature weight vector. @type new_weights: C{list} of C{float} """ self._weights = new_weights assert (self._encoding.length() == len(new_weights)) def weights(self): """ @return: The feature weight vector for this classifier. @rtype: C{list} of C{float} """ return self._weights def classify(self, featureset): return self.prob_classify(featureset).max() def prob_classify(self, featureset): prob_dict = {} for label in self._encoding.labels(): feature_vector = self._encoding.encode(featureset, label) if self._logarithmic: total = 0.0 for (f_id, f_val) in feature_vector: total += self._weights[f_id] * f_val prob_dict[label] = total else: prod = 1.0 for (f_id, f_val) in feature_vector: prod *= self._weights[f_id] ** f_val prob_dict[label] = prod # Normalize the dictionary to give a probability distribution return DictionaryProbDist(prob_dict, log=self._logarithmic, normalize=True) def explain(self, featureset, columns=4): """ Print a table showing the effect of each of the features in the given feature set, and how they combine to determine the probabilities of each label for that featureset. """ descr_width = 50 TEMPLATE = ' %-'+str(descr_width-2)+'s%s%8.3f' pdist = self.prob_classify(featureset) labels = sorted(pdist.samples(), key=pdist.prob, reverse=True) labels = labels[:columns] print ' Feature'.ljust(descr_width)+''.join( '%8s' % l[:7] for l in labels) print ' '+'-'*(descr_width-2+8*len(labels)) sums = defaultdict(int) for i, label in enumerate(labels): feature_vector = self._encoding.encode(featureset, label) feature_vector.sort(key=lambda (fid,_): abs(self._weights[fid]), reverse=True) for (f_id, f_val) in feature_vector: if self._logarithmic: score = self._weights[f_id] * f_val else: score = self._weights[fid] ** f_val descr = self._encoding.describe(f_id) descr = descr.split(' and label is ')[0] # hack descr += ' (%s)' % f_val # hack if len(descr) > 47: descr = descr[:44]+'...' print TEMPLATE % (descr, i*8*' ', score) sums[label] += score print ' '+'-'*(descr_width-1+8*len(labels)) print ' TOTAL:'.ljust(descr_width)+''.join( '%8.3f' % sums[l] for l in labels) print ' PROBS:'.ljust(descr_width)+''.join( '%8.3f' % pdist.prob(l) for l in labels) def show_most_informative_features(self, n=10, show='all'): """ @param show: all, neg, or pos (for negative-only or positive-only) """ fids = sorted(range(len(self._weights)), key=lambda fid: abs(self._weights[fid]), reverse=True) if show == 'pos': fids = [fid for fid in fids if self._weights[fid]>0] elif show == 'neg': fids = [fid for fid in fids if self._weights[fid]<0] for fid in fids[:n]: print '%8.3f %s' % (self._weights[fid], self._encoding.describe(fid)) def __repr__(self): return ('' % (len(self._encoding.labels()), self._encoding.length())) #: A list of the algorithm names that are accepted for the #: L{train()} method's C{algorithm} parameter. ALGORITHMS = ['GIS', 'IIS', 'CG', 'BFGS', 'Powell', 'LBFGSB', 'Nelder-Mead', 'MEGAM', 'TADM'] @classmethod def train(cls, train_toks, algorithm=None, trace=3, encoding=None, labels=None, sparse=True, gaussian_prior_sigma=0, **cutoffs): """ Train a new maxent classifier based on the given corpus of training samples. This classifier will have its weights chosen to maximize entropy while remaining empirically consistent with the training corpus. @rtype: L{MaxentClassifier} @return: The new maxent classifier @type train_toks: C{list} @param train_toks: Training data, represented as a list of pairs, the first member of which is a featureset, and the second of which is a classification label. @type algorithm: C{str} @param algorithm: A case-insensitive string, specifying which algorithm should be used to train the classifier. The following algorithms are currently available. - Iterative Scaling Methods - C{'GIS'}: Generalized Iterative Scaling - C{'IIS'}: Improved Iterative Scaling - Optimization Methods (require C{scipy}) - C{'CG'}: Conjugate gradient - C{'BFGS'}: Broyden-Fletcher-Goldfarb-Shanno algorithm - C{'Powell'}: Powell agorithm - C{'LBFGSB'}: A limited-memory variant of the BFGS algorithm - C{'Nelder-Mead'}: The Nelder-Mead algorithm - External Libraries - C{'megam'}: LM-BFGS algorithm, with training performed by an U{megam }. (requires that C{megam} be installed.) The default algorithm is C{'CG'} if C{'scipy'} is installed; and C{'iis'} otherwise. @type trace: C{int} @param trace: The level of diagnostic tracing output to produce. Higher values produce more verbose output. @type encoding: L{MaxentFeatureEncodingI} @param encoding: A feature encoding, used to convert featuresets into feature vectors. If none is specified, then a L{BinaryMaxentFeatureEncoding} will be built based on the features that are attested in the training corpus. @type labels: C{list} of C{str} @param labels: The set of possible labels. If none is given, then the set of all labels attested in the training data will be used instead. @param sparse: If true, then use sparse matrices instead of dense matrices. Currently, this is only supported by the scipy (optimization method) algorithms. For other algorithms, its value is ignored. @param gaussian_prior_sigma: The sigma value for a gaussian prior on model weights. Currently, this is supported by the scipy (optimization method) algorithms and C{megam}. For other algorithms, its value is ignored. @param cutoffs: Arguments specifying various conditions under which the training should be halted. (Some of the cutoff conditions are not supported by some algorithms.) - C{max_iter=v}: Terminate after C{v} iterations. - C{min_ll=v}: Terminate after the negative average log-likelihood drops under C{v}. - C{min_lldelta=v}: Terminate if a single iteration improves log likelihood by less than C{v}. - C{tolerance=v}: Terminate a scipy optimization method when improvement drops below a tolerance level C{v}. The exact meaning of this tolerance depends on the scipy algorithm used. See C{scipy} documentation for more info. Default values: 1e-3 for CG, 1e-5 for LBFGSB, and 1e-4 for other algorithms. I{(C{scipy} only)} """ if algorithm is None: try: import scipy algorithm = 'cg' except ImportError: algorithm = 'iis' for key in cutoffs: if key not in ('max_iter', 'min_ll', 'min_lldelta', 'tolerance', 'max_acc', 'min_accdelta', 'count_cutoff', 'norm', 'explicit', 'bernoulli'): raise TypeError('Unexpected keyword arg %r' % key) algorithm = algorithm.lower() if algorithm == 'iis': return train_maxent_classifier_with_iis( train_toks, trace, encoding, labels, **cutoffs) elif algorithm == 'gis': return train_maxent_classifier_with_gis( train_toks, trace, encoding, labels, **cutoffs) elif algorithm in cls._SCIPY_ALGS: return train_maxent_classifier_with_scipy( train_toks, trace, encoding, labels, cls._SCIPY_ALGS[algorithm], sparse, gaussian_prior_sigma, **cutoffs) elif algorithm == 'megam': return train_maxent_classifier_with_megam( train_toks, trace, encoding, labels, gaussian_prior_sigma, **cutoffs) elif algorithm == 'tadm': kwargs = cutoffs kwargs['trace'] = trace kwargs['encoding'] = encoding kwargs['labels'] = labels kwargs['gaussian_prior_sigma'] = gaussian_prior_sigma return TadmMaxentClassifier.train(train_toks, **kwargs) else: raise ValueError('Unknown algorithm %s' % algorithm) _SCIPY_ALGS = {'cg':'CG', 'bfgs':'BFGS', 'powell':'Powell', 'lbfgsb':'LBFGSB', 'nelder-mead':'Nelder-Mead'} #: Alias for MaxentClassifier. ConditionalExponentialClassifier = MaxentClassifier @deprecated('Use MaxentClassifier.train() instead') def train_maxent_classifier(*args, **kwargs): return MaxentClassifier.train(*args, **kwargs) ###################################################################### #{ Feature Encodings ###################################################################### class MaxentFeatureEncodingI(object): """ A mapping that converts a set of input-feature values to a vector of joint-feature values, given a label. This conversion is necessary to translate featuresets into a format that can be used by maximum entropy models. The set of joint-features used by a given encoding is fixed, and each index in the generated joint-feature vectors corresponds to a single joint-feature. The length of the generated joint-feature vectors is therefore constant (for a given encoding). Because the joint-feature vectors generated by C{MaxentFeatureEncodingI} are typically very sparse, they are represented as a list of C{(index, value)} tuples, specifying the value of each non-zero joint-feature. Feature encodings are generally created using the L{train()} method, which generates an appropriate encoding based on the input-feature values and labels that are present in a given corpus. """ def encode(self, featureset, label): """ Given a (featureset, label) pair, return the corresponding vector of joint-feature values. This vector is represented as a list of C{(index, value)} tuples, specifying the value of each non-zero joint-feature. @type featureset: C{dict} @rtype: C{list} of C{(int, number)} """ raise AssertionError('Not implemented') def length(self): """ @return: The size of the fixed-length joint-feature vectors that are generated by this encoding. @rtype: C{int} """ raise AssertionError('Not implemented') def labels(self): """ @return: A list of the \"known labels\" -- i.e., all labels C{l} such that C{self.encode(fs,l)} can be a nonzero joint-feature vector for some value of C{fs}. @rtype: C{list} """ raise AssertionError('Not implemented') def describe(self, fid): """ @return: A string describing the value of the joint-feature whose index in the generated feature vectors is C{fid}. @rtype: C{str} """ raise AssertionError('Not implemented') def train(cls, train_toks): """ Construct and return new feature encoding, based on a given training corpus C{train_toks}. @type train_toks: C{list} of C{tuples} of (C{dict}, C{str}) @param train_toks: Training data, represented as a list of pairs, the first member of which is a feature dictionary, and the second of which is a classification label. """ raise AssertionError('Not implemented') class FunctionBackedMaxentFeatureEncoding(MaxentFeatureEncodingI): """ A feature encoding that calls a user-supplied function to map a given featureset/label pair to a sparse joint-feature vector. """ def __init__(self, func, length, labels): """ Construct a new feature encoding based on the given function. @type func: (callable) @param func: A function that takes two arguments, a featureset and a label, and returns the sparse joint feature vector that encodes them: >>> func(featureset, label) -> feature_vector This sparse joint feature vector (C{feature_vector}) is a list of C{(index,value)} tuples. @type length: C{int} @param length: The size of the fixed-length joint-feature vectors that are generated by this encoding. @type labels: C{list} @param labels: A list of the \"known labels\" for this encoding -- i.e., all labels C{l} such that C{self.encode(fs,l)} can be a nonzero joint-feature vector for some value of C{fs}. """ self._length = length self._func = func self._labels = labels def encode(self, featureset, label): return self._func(featureset, label) def length(self): return self._length def labels(self): return self._labels def describe(self, fid): return 'no description available' class BinaryMaxentFeatureEncoding(MaxentFeatureEncodingI): """ A feature encoding that generates vectors containing a binary joint-features of the form:: joint_feat(fs, l) = { 1 if (fs[fname] == fval) and (l == label) { { 0 otherwise Where C{fname} is the name of an input-feature, C{fval} is a value for that input-feature, and C{label} is a label. Typically, these features are constructed based on a training corpus, using the L{train()} method. This method will create one feature for each combination of C{fname}, C{fval}, and C{label} that occurs at least once in the training corpus. The C{unseen_features} parameter can be used to add X{unseen-value features}, which are used whenever an input feature has a value that was not encountered in the training corpus. These features have the form:: joint_feat(fs, l) = { 1 if is_unseen(fname, fs[fname]) { and l == label { { 0 otherwise Where C{is_unseen(fname, fval)} is true if the encoding does not contain any joint features that are true when C{fs[fname]==fval}. The C{alwayson_features} parameter can be used to add X{always-on features}, which have the form:: joint_feat(fs, l) = { 1 if (l == label) { { 0 otherwise These always-on features allow the maxent model to directly model the prior probabilities of each label. """ def __init__(self, labels, mapping, unseen_features=False, alwayson_features=False): """ @param labels: A list of the \"known labels\" for this encoding. @param mapping: A dictionary mapping from C{(fname,fval,label)} tuples to corresponding joint-feature indexes. These indexes must be the set of integers from 0...len(mapping). If C{mapping[fname,fval,label]=id}, then C{self.encode({..., fname:fval, ...}, label)[id]} is 1; otherwise, it is 0. @param unseen_features: If true, then include unseen value features in the generated joint-feature vectors. @param alwayson_features: If true, then include always-on features in the generated joint-feature vectors. """ if set(mapping.values()) != set(range(len(mapping))): raise ValueError('Mapping values must be exactly the ' 'set of integers from 0...len(mapping)') self._labels = list(labels) """A list of attested labels.""" self._mapping = mapping """dict mapping from (fname,fval,label) -> fid""" self._length = len(mapping) """The length of generated joint feature vectors.""" self._alwayson = None """dict mapping from label -> fid""" self._unseen = None """dict mapping from fname -> fid""" if alwayson_features: self._alwayson = dict([(label,i+self._length) for (i,label) in enumerate(labels)]) self._length += len(self._alwayson) if unseen_features: fnames = set(fname for (fname, fval, label) in mapping) self._unseen = dict([(fname, i+self._length) for (i, fname) in enumerate(fnames)]) self._length += len(fnames) def encode(self, featureset, label): # Inherit docs. encoding = [] # Convert input-features to joint-features: for fname, fval in featureset.items(): # Known feature name & value: if (fname, fval, label) in self._mapping: encoding.append((self._mapping[fname, fval, label], 1)) # Otherwise, we might want to fire an "unseen-value feature". elif self._unseen: # Have we seen this fname/fval combination with any label? for label2 in self._labels: if (fname, fval, label2) in self._mapping: break # we've seen this fname/fval combo # We haven't -- fire the unseen-value feature else: if fname in self._unseen: encoding.append((self._unseen[fname], 1)) # Add always-on features: if self._alwayson and label in self._alwayson: encoding.append((self._alwayson[label], 1)) return encoding def describe(self, f_id): # Inherit docs. if not isinstance(f_id, (int, long)): raise TypeError('describe() expected an int') try: self._inv_mapping except AttributeError: self._inv_mapping = [-1]*len(self._mapping) for (info, i) in self._mapping.items(): self._inv_mapping[i] = info if f_id < len(self._mapping): (fname, fval, label) = self._inv_mapping[f_id] return '%s==%r and label is %r' % (fname, fval, label) elif self._alwayson and f_id in self._alwayson.values(): for (label, f_id2) in self._alwayson.items(): if f_id==f_id2: return 'label is %r' % label elif self._unseen and f_id in self._unseen.values(): for (fname, f_id2) in self._unseen.items(): if f_id==f_id2: return '%s is unseen' % fname else: raise ValueError('Bad feature id') def labels(self): # Inherit docs. return self._labels def length(self): # Inherit docs. return self._length @classmethod def train(cls, train_toks, count_cutoff=0, labels=None, **options): """ Construct and return new feature encoding, based on a given training corpus C{train_toks}. See the L{class description } for a description of the joint-features that will be included in this encoding. @type train_toks: C{list} of C{tuples} of (C{dict}, C{str}) @param train_toks: Training data, represented as a list of pairs, the first member of which is a feature dictionary, and the second of which is a classification label. @type count_cutoff: C{int} @param count_cutoff: A cutoff value that is used to discard rare joint-features. If a joint-feature's value is 1 fewer than C{count_cutoff} times in the training corpus, then that joint-feature is not included in the generated encoding. @type labels: C{list} @param labels: A list of labels that should be used by the classifier. If not specified, then the set of labels attested in C{train_toks} will be used. @param options: Extra parameters for the constructor, such as C{unseen_features} and C{alwayson_features}. """ mapping = {} # maps (fname, fval, label) -> fid seen_labels = set() # The set of labels we've encountered count = defaultdict(int) # maps (fname, fval) -> count for (tok, label) in train_toks: if labels and label not in labels: raise ValueError('Unexpected label %s' % label) seen_labels.add(label) # Record each of the features. for (fname, fval) in tok.items(): # If a count cutoff is given, then only add a joint # feature once the corresponding (fname, fval, label) # tuple exceeds that cutoff. count[fname,fval] += 1 if count[fname,fval] >= count_cutoff: if (fname, fval, label) not in mapping: mapping[fname, fval, label] = len(mapping) if labels is None: labels = seen_labels return cls(labels, mapping, **options) class GISEncoding(BinaryMaxentFeatureEncoding): """ A binary feature encoding which adds one new joint-feature to the joint-features defined by L{BinaryMaxentFeatureEncoding}: a correction feature, whose value is chosen to ensure that the sparse vector always sums to a constant non-negative number. This new feature is used to ensure two preconditions for the GIS training algorithm: - At least one feature vector index must be nonzero for every token. - The feature vector must sum to a constant non-negative number for every token. """ def __init__(self, labels, mapping, unseen_features=False, alwayson_features=False, C=None): """ @param C: The correction constant. The value of the correction feature is based on this value. In particular, its value is C{C - sum([v for (f,v) in encoding])}. @seealso: L{BinaryMaxentFeatureEncoding.__init__} """ BinaryMaxentFeatureEncoding.__init__( self, labels, mapping, unseen_features, alwayson_features) if C is None: C = len(set([fname for (fname,fval,label) in mapping]))+1 self._C = C """The non-negative constant that all encoded feature vectors will sum to.""" C = property(lambda self: self._C, doc=""" The non-negative constant that all encoded feature vectors will sum to.""") def encode(self, featureset, label): # Get the basic encoding. encoding = BinaryMaxentFeatureEncoding.encode(self, featureset, label) base_length = BinaryMaxentFeatureEncoding.length(self) # Add a correction feature. total = sum([v for (f,v) in encoding]) if total >= self._C: raise ValueError('Correction feature is not high enough!') encoding.append( (base_length, self._C-total) ) # Return the result return encoding def length(self): return BinaryMaxentFeatureEncoding.length(self) + 1 def describe(self, f_id): if f_id == BinaryMaxentFeatureEncoding.length(self): return 'Correction feature (%s)' % self._C else: return BinaryMaxentFeatureEncoding.describe(self, f_id) class TadmEventMaxentFeatureEncoding(BinaryMaxentFeatureEncoding): def __init__(self, labels, mapping, unseen_features=False, alwayson_features=False): self._mapping = OrderedDict(mapping) self._label_mapping = OrderedDict() BinaryMaxentFeatureEncoding.__init__(self, labels, self._mapping, unseen_features, alwayson_features) def encode(self, featureset, label): encoding = [] for feature, value in featureset.items(): if (feature, label) not in self._mapping: self._mapping[(feature, label)] = len(self._mapping) if value not in self._label_mapping: if not isinstance(value, int): self._label_mapping[value] = len(self._label_mapping) else: self._label_mapping[value] = value encoding.append((self._mapping[(feature, label)], self._label_mapping[value])) return encoding def labels(self): return self._labels def describe(self, fid): for (feature, label) in self._mapping: if self._mapping[(feature, label)] == fid: return (feature, label) def length(self): return len(self._mapping) @classmethod def train(cls, train_toks, count_cutoff=0, labels=None, **options): mapping = OrderedDict() if not labels: labels = [] # This gets read twice, so compute the values in case it's lazy. train_toks = list(train_toks) for (featureset, label) in train_toks: if label not in labels: labels.append(label) for (featureset, label) in train_toks: for label in labels: for feature in featureset: if (feature, label) not in mapping: mapping[(feature, label)] = len(mapping) return cls(labels, mapping, **options) class TypedMaxentFeatureEncoding(MaxentFeatureEncodingI): """ A feature encoding that generates vectors containing integer, float and binary joint-features of the form:: Binary (for string and boolean features): joint_feat(fs, l) = { 1 if (fs[fname] == fval) and (l == label) { { 0 otherwise Value (for integer and float features): joint_feat(fs, l) = { fval if (fs[fname] == type(fval)) { and (l == label) { { not encoded otherwise Where C{fname} is the name of an input-feature, C{fval} is a value for that input-feature, and C{label} is a label. Typically, these features are constructed based on a training corpus, using the L{train()} method. For string and boolean features [type(fval) not in (int, float)] this method will create one feature for each combination of C{fname}, C{fval}, and C{label} that occurs at least once in the training corpus. For integer and float features [type(fval) in (int, float)] this method will create one feature for each combination of C{fname} and C{label} that occurs at least once in the training corpus. For binary features the C{unseen_features} parameter can be used to add X{unseen-value features}, which are used whenever an input feature has a value that was not encountered in the training corpus. These features have the form:: joint_feat(fs, l) = { 1 if is_unseen(fname, fs[fname]) { and l == label { { 0 otherwise Where C{is_unseen(fname, fval)} is true if the encoding does not contain any joint features that are true when C{fs[fname]==fval}. The C{alwayson_features} parameter can be used to add X{always-on features}, which have the form:: joint_feat(fs, l) = { 1 if (l == label) { { 0 otherwise These always-on features allow the maxent model to directly model the prior probabilities of each label. """ def __init__(self, labels, mapping, unseen_features=False, alwayson_features=False): """ @param labels: A list of the \"known labels\" for this encoding. @param mapping: A dictionary mapping from C{(fname,fval,label)} tuples to corresponding joint-feature indexes. These indexes must be the set of integers from 0...len(mapping). If C{mapping[fname,fval,label]=id}, then C{self.encode({..., fname:fval, ...}, label)[id]} is 1; otherwise, it is 0. @param unseen_features: If true, then include unseen value features in the generated joint-feature vectors. @param alwayson_features: If true, then include always-on features in the generated joint-feature vectors. """ if set(mapping.values()) != set(range(len(mapping))): raise ValueError('Mapping values must be exactly the ' 'set of integers from 0...len(mapping)') self._labels = list(labels) """A list of attested labels.""" self._mapping = mapping """dict mapping from (fname,fval,label) -> fid""" self._length = len(mapping) """The length of generated joint feature vectors.""" self._alwayson = None """dict mapping from label -> fid""" self._unseen = None """dict mapping from fname -> fid""" if alwayson_features: self._alwayson = dict([(label,i+self._length) for (i,label) in enumerate(labels)]) self._length += len(self._alwayson) if unseen_features: fnames = set(fname for (fname, fval, label) in mapping) self._unseen = dict([(fname, i+self._length) for (i, fname) in enumerate(fnames)]) self._length += len(fnames) def encode(self, featureset, label): # Inherit docs. encoding = [] # Convert input-features to joint-features: for fname, fval in featureset.items(): if(type(fval) in (int, float)): # Known feature name & value: if (fname, type(fval), label) in self._mapping: encoding.append((self._mapping[fname, type(fval), label], fval)) else: # Known feature name & value: if (fname, fval, label) in self._mapping: encoding.append((self._mapping[fname, fval, label], 1)) # Otherwise, we might want to fire an "unseen-value feature". elif self._unseen: # Have we seen this fname/fval combination with any label? for label2 in self._labels: if (fname, fval, label2) in self._mapping: break # we've seen this fname/fval combo # We haven't -- fire the unseen-value feature else: if fname in self._unseen: encoding.append((self._unseen[fname], 1)) # Add always-on features: if self._alwayson and label in self._alwayson: encoding.append((self._alwayson[label], 1)) return encoding def describe(self, f_id): # Inherit docs. if not isinstance(f_id, (int, long)): raise TypeError('describe() expected an int') try: self._inv_mapping except AttributeError: self._inv_mapping = [-1]*len(self._mapping) for (info, i) in self._mapping.items(): self._inv_mapping[i] = info if f_id < len(self._mapping): (fname, fval, label) = self._inv_mapping[f_id] return '%s==%r and label is %r' % (fname, fval, label) elif self._alwayson and f_id in self._alwayson.values(): for (label, f_id2) in self._alwayson.items(): if f_id==f_id2: return 'label is %r' % label elif self._unseen and f_id in self._unseen.values(): for (fname, f_id2) in self._unseen.items(): if f_id==f_id2: return '%s is unseen' % fname else: raise ValueError('Bad feature id') def labels(self): # Inherit docs. return self._labels def length(self): # Inherit docs. return self._length @classmethod def train(cls, train_toks, count_cutoff=0, labels=None, **options): """ Construct and return new feature encoding, based on a given training corpus C{train_toks}. See the L{class description } for a description of the joint-features that will be included in this encoding. Note: recognized feature values types are (int, float), over types are interpreted as regular binary features. @type train_toks: C{list} of C{tuples} of (C{dict}, C{str}) @param train_toks: Training data, represented as a list of pairs, the first member of which is a feature dictionary, and the second of which is a classification label. @type count_cutoff: C{int} @param count_cutoff: A cutoff value that is used to discard rare joint-features. If a joint-feature's value is 1 fewer than C{count_cutoff} times in the training corpus, then that joint-feature is not included in the generated encoding. @type labels: C{list} @param labels: A list of labels that should be used by the classifier. If not specified, then the set of labels attested in C{train_toks} will be used. @param options: Extra parameters for the constructor, such as C{unseen_features} and C{alwayson_features}. """ mapping = {} # maps (fname, fval, label) -> fid seen_labels = set() # The set of labels we've encountered count = defaultdict(int) # maps (fname, fval) -> count for (tok, label) in train_toks: if labels and label not in labels: raise ValueError('Unexpected label %s' % label) seen_labels.add(label) # Record each of the features. for (fname, fval) in tok.items(): if(type(fval) in (int, float)): fval = type(fval) # If a count cutoff is given, then only add a joint # feature once the corresponding (fname, fval, label) # tuple exceeds that cutoff. count[fname,fval] += 1 if count[fname,fval] >= count_cutoff: if (fname, fval, label) not in mapping: mapping[fname, fval, label] = len(mapping) if labels is None: labels = seen_labels return cls(labels, mapping, **options) ###################################################################### #{ Classifier Trainer: Generalized Iterative Scaling ###################################################################### def train_maxent_classifier_with_gis(train_toks, trace=3, encoding=None, labels=None, **cutoffs): """ Train a new C{ConditionalExponentialClassifier}, using the given training samples, using the Generalized Iterative Scaling algorithm. This C{ConditionalExponentialClassifier} will encode the model that maximizes entropy from all the models that are empirically consistent with C{train_toks}. @see: L{train_maxent_classifier()} for parameter descriptions. """ cutoffs.setdefault('max_iter', 100) cutoffchecker = CutoffChecker(cutoffs) # Construct an encoding from the training data. if encoding is None: encoding = GISEncoding.train(train_toks, labels=labels) if not hasattr(encoding, 'C'): raise TypeError('The GIS algorithm requires an encoding that ' 'defines C (e.g., GISEncoding).') # Cinv is the inverse of the sum of each joint feature vector. # This controls the learning rate: higher Cinv (or lower C) gives # faster learning. Cinv = 1.0/encoding.C # Count how many times each feature occurs in the training data. empirical_fcount = calculate_empirical_fcount(train_toks, encoding) # Check for any features that are not attested in train_toks. unattested = set(numpy.nonzero(empirical_fcount==0)[0]) # Build the classifier. Start with weight=0 for each attested # feature, and weight=-infinity for each unattested feature. weights = numpy.zeros(len(empirical_fcount), 'd') for fid in unattested: weights[fid] = numpy.NINF classifier = ConditionalExponentialClassifier(encoding, weights) # Take the log of the empirical fcount. log_empirical_fcount = numpy.log2(empirical_fcount) del empirical_fcount # Old log-likelihood and accuracy; used to check if the change # in log-likelihood or accuracy is sufficient to indicate convergence. ll_old = None acc_old = None if trace > 0: print ' ==> Training (%d iterations)' % cutoffs['max_iter'] if trace > 2: print print ' Iteration Log Likelihood Accuracy' print ' ---------------------------------------' # Train the classifier. try: while True: if trace > 2: ll = cutoffchecker.ll or nltk.classify.util.log_likelihood( classifier, train_toks) acc = cutoffchecker.acc or nltk.classify.util.accuracy( classifier, train_toks) iternum = cutoffchecker.iter print ' %9d %14.5f %9.3f' % (iternum, ll, acc) # Use the model to estimate the number of times each # feature should occur in the training data. estimated_fcount = calculate_estimated_fcount( classifier, train_toks, encoding) # Take the log of estimated fcount (avoid taking log(0).) for fid in unattested: estimated_fcount[fid] += 1 log_estimated_fcount = numpy.log2(estimated_fcount) del estimated_fcount # Update the classifier weights weights = classifier.weights() weights += (log_empirical_fcount - log_estimated_fcount) * Cinv classifier.set_weights(weights) # Check the log-likelihood & accuracy cutoffs. if cutoffchecker.check(classifier, train_toks): break except KeyboardInterrupt: print ' Training stopped: keyboard interrupt' except: raise if trace > 2: ll = nltk.classify.util.log_likelihood(classifier, train_toks) acc = nltk.classify.util.accuracy(classifier, train_toks) print ' Final %14.5f %9.3f' % (ll, acc) # Return the classifier. return classifier def calculate_empirical_fcount(train_toks, encoding): fcount = numpy.zeros(encoding.length(), 'd') for tok, label in train_toks: for (index, val) in encoding.encode(tok, label): fcount[index] += val return fcount def calculate_estimated_fcount(classifier, train_toks, encoding): fcount = numpy.zeros(encoding.length(), 'd') for tok, label in train_toks: pdist = classifier.prob_classify(tok) for label in pdist.samples(): prob = pdist.prob(label) for (fid, fval) in encoding.encode(tok, label): fcount[fid] += prob*fval return fcount ###################################################################### #{ Classifier Trainer: Improved Iterative Scaling ###################################################################### def train_maxent_classifier_with_iis(train_toks, trace=3, encoding=None, labels=None, **cutoffs): """ Train a new C{ConditionalExponentialClassifier}, using the given training samples, using the Improved Iterative Scaling algorithm. This C{ConditionalExponentialClassifier} will encode the model that maximizes entropy from all the models that are empirically consistent with C{train_toks}. @see: L{train_maxent_classifier()} for parameter descriptions. """ cutoffs.setdefault('max_iter', 100) cutoffchecker = CutoffChecker(cutoffs) # Construct an encoding from the training data. if encoding is None: encoding = BinaryMaxentFeatureEncoding.train(train_toks, labels=labels) # Count how many times each feature occurs in the training data. empirical_ffreq = (calculate_empirical_fcount(train_toks, encoding) / len(train_toks)) # Find the nf map, and related variables nfarray and nfident. # nf is the sum of the features for a given labeled text. # nfmap compresses this sparse set of values to a dense list. # nfarray performs the reverse operation. nfident is # nfarray multiplied by an identity matrix. nfmap = calculate_nfmap(train_toks, encoding) nfarray = numpy.array(sorted(nfmap, key=nfmap.__getitem__), 'd') nftranspose = numpy.reshape(nfarray, (len(nfarray), 1)) # Check for any features that are not attested in train_toks. unattested = set(numpy.nonzero(empirical_ffreq==0)[0]) # Build the classifier. Start with weight=0 for each attested # feature, and weight=-infinity for each unattested feature. weights = numpy.zeros(len(empirical_ffreq), 'd') for fid in unattested: weights[fid] = numpy.NINF classifier = ConditionalExponentialClassifier(encoding, weights) if trace > 0: print ' ==> Training (%d iterations)' % cutoffs['max_iter'] if trace > 2: print print ' Iteration Log Likelihood Accuracy' print ' ---------------------------------------' # Old log-likelihood and accuracy; used to check if the change # in log-likelihood or accuracy is sufficient to indicate convergence. ll_old = None acc_old = None # Train the classifier. try: while True: if trace > 2: ll = cutoffchecker.ll or nltk.classify.util.log_likelihood( classifier, train_toks) acc = cutoffchecker.acc or nltk.classify.util.accuracy( classifier, train_toks) iternum = cutoffchecker.iter print ' %9d %14.5f %9.3f' % (iternum, ll, acc) # Calculate the deltas for this iteration, using Newton's method. deltas = calculate_deltas( train_toks, classifier, unattested, empirical_ffreq, nfmap, nfarray, nftranspose, encoding) # Use the deltas to update our weights. weights = classifier.weights() weights += deltas classifier.set_weights(weights) # Check the log-likelihood & accuracy cutoffs. if cutoffchecker.check(classifier, train_toks): break except KeyboardInterrupt: print ' Training stopped: keyboard interrupt' except: raise if trace > 2: ll = nltk.classify.util.log_likelihood(classifier, train_toks) acc = nltk.classify.util.accuracy(classifier, train_toks) print ' Final %14.5f %9.3f' % (ll, acc) # Return the classifier. return classifier def calculate_nfmap(train_toks, encoding): """ Construct a map that can be used to compress C{nf} (which is typically sparse). M{nf(feature_vector)} is the sum of the feature values for M{feature_vector}. This represents the number of features that are active for a given labeled text. This method finds all values of M{nf(t)} that are attested for at least one token in the given list of training tokens; and constructs a dictionary mapping these attested values to a continuous range M{0...N}. For example, if the only values of M{nf()} that were attested were 3, 5, and 7, then C{_nfmap} might return the dictionary {3:0, 5:1, 7:2}. @return: A map that can be used to compress C{nf} to a dense vector. @rtype: C{dictionary} from C{int} to C{int} """ # Map from nf to indices. This allows us to use smaller arrays. nfset = set() for tok, _ in train_toks: for label in encoding.labels(): nfset.add(sum([val for (id,val) in encoding.encode(tok,label)])) return dict([(nf, i) for (i, nf) in enumerate(nfset)]) def calculate_deltas(train_toks, classifier, unattested, ffreq_empirical, nfmap, nfarray, nftranspose, encoding): """ Calculate the update values for the classifier weights for this iteration of IIS. These update weights are the value of C{delta} that solves the equation:: ffreq_empirical[i] = SUM[fs,l] (classifier.prob_classify(fs).prob(l) * feature_vector(fs,l)[i] * exp(delta[i] * nf(feature_vector(fs,l)))) Where: - M{(fs,l)} is a (featureset, label) tuple from C{train_toks} - M{feature_vector(fs,l)} = C{encoding.encode(fs,l)} - M{nf(vector)} = C{sum([val for (id,val) in vector])} This method uses Newton's method to solve this equation for M{delta[i]}. In particular, it starts with a guess of C{delta[i]}=1; and iteratively updates C{delta} with:: delta[i] -= (ffreq_empirical[i] - sum1[i])/(-sum2[i]) until convergence, where M{sum1} and M{sum2} are defined as:: sum1[i](delta) = SUM[fs,l] f[i](fs,l,delta) sum2[i](delta) = SUM[fs,l] (f[i](fs,l,delta) * nf(feature_vector(fs,l))) f[i](fs,l,delta) = (classifier.prob_classify(fs).prob(l) * feature_vector(fs,l)[i] * exp(delta[i] * nf(feature_vector(fs,l)))) Note that M{sum1} and M{sum2} depend on C{delta}; so they need to be re-computed each iteration. The variables C{nfmap}, C{nfarray}, and C{nftranspose} are used to generate a dense encoding for M{nf(ltext)}. This allows C{_deltas} to calculate M{sum1} and M{sum2} using matrices, which yields a signifigant performance improvement. @param train_toks: The set of training tokens. @type train_toks: C{list} of C{tuples} of (C{dict}, C{str}) @param classifier: The current classifier. @type classifier: C{ClassifierI} @param ffreq_empirical: An array containing the empirical frequency for each feature. The M{i}th element of this array is the empirical frequency for feature M{i}. @type ffreq_empirical: C{sequence} of C{float} @param unattested: An array that is 1 for features that are not attested in the training data; and 0 for features that are attested. In other words, C{unattested[i]==0} iff C{ffreq_empirical[i]==0}. @type unattested: C{sequence} of C{int} @param nfmap: A map that can be used to compress C{nf} to a dense vector. @type nfmap: C{dictionary} from C{int} to C{int} @param nfarray: An array that can be used to uncompress C{nf} from a dense vector. @type nfarray: C{array} of C{float} @param nftranspose: C{array} of C{float} @type nftranspose: The transpose of C{nfarray} """ # These parameters control when we decide that we've # converged. It probably should be possible to set these # manually, via keyword arguments to train. NEWTON_CONVERGE = 1e-12 MAX_NEWTON = 300 deltas = numpy.ones(encoding.length(), 'd') # Precompute the A matrix: # A[nf][id] = sum ( p(fs) * p(label|fs) * f(fs,label) ) # over all label,fs s.t. num_features[label,fs]=nf A = numpy.zeros((len(nfmap), encoding.length()), 'd') for tok, label in train_toks: dist = classifier.prob_classify(tok) for label in encoding.labels(): # Generate the feature vector feature_vector = encoding.encode(tok,label) # Find the number of active features nf = sum([val for (id, val) in feature_vector]) # Update the A matrix for (id, val) in feature_vector: A[nfmap[nf], id] += dist.prob(label) * val A /= len(train_toks) # Iteratively solve for delta. Use the following variables: # - nf_delta[x][y] = nfarray[x] * delta[y] # - exp_nf_delta[x][y] = exp(nf[x] * delta[y]) # - nf_exp_nf_delta[x][y] = nf[x] * exp(nf[x] * delta[y]) # - sum1[i][nf] = sum p(fs)p(label|fs)f[i](label,fs) # exp(delta[i]nf) # - sum2[i][nf] = sum p(fs)p(label|fs)f[i](label,fs) # nf exp(delta[i]nf) for rangenum in range(MAX_NEWTON): nf_delta = numpy.outer(nfarray, deltas) exp_nf_delta = 2 ** nf_delta nf_exp_nf_delta = nftranspose * exp_nf_delta sum1 = numpy.sum(exp_nf_delta * A, axis=0) sum2 = numpy.sum(nf_exp_nf_delta * A, axis=0) # Avoid division by zero. for fid in unattested: sum2[fid] += 1 # Update the deltas. deltas -= (ffreq_empirical - sum1) / -sum2 # We can stop once we converge. n_error = (numpy.sum(abs((ffreq_empirical-sum1)))/ numpy.sum(abs(deltas))) if n_error < NEWTON_CONVERGE: return deltas return deltas ###################################################################### #{ Classifier Trainer: scipy algorithms (GC, LBFGSB, etc.) ###################################################################### # [xx] n.b.: it's possible to supply custom trace functions, which # could be used to make trace output consistent with iis/gis. def train_maxent_classifier_with_scipy(train_toks, trace=3, encoding=None, labels=None, algorithm='CG', sparse=True, gaussian_prior_sigma=0, **cutoffs): """ Train a new C{ConditionalExponentialClassifier}, using the given training samples, using the specified C{scipy} optimization algorithm. This C{ConditionalExponentialClassifier} will encode the model that maximizes entropy from all the models that are empirically consistent with C{train_toks}. @see: L{train_maxent_classifier()} for parameter descriptions. @require: The C{scipy} package must be installed. """ try: import scipy except ImportError, e: raise ValueError('The maxent training algorithm %r requires ' 'that the scipy package be installed. See ' 'http://www.scipy.org/' % algorithm) try: # E.g., if libgfortran.2.dylib is not found. import scipy.sparse, scipy.maxentropy except ImportError, e: raise ValueError('Import of scipy package failed: %s' % e) # Construct an encoding from the training data. if encoding is None: encoding = BinaryMaxentFeatureEncoding.train(train_toks, labels=labels) elif labels is not None: raise ValueError('Specify encoding or labels, not both') labels = encoding.labels() labelnum = dict([(label, i) for (i, label) in enumerate(labels)]) num_features = encoding.length() num_toks = len(train_toks) num_labels = len(labels) # Decide whether to use a sparse matrix or a dense one. Very # limited testing has shown that the lil matrix format # (list-of-lists) performs better than csr and csc formats. # Limited testing also suggests that the sparse matrix format # doesn't save much memory over the dense format in practice # (in terms of max memory usage). if sparse: zeros = scipy.sparse.lil_matrix else: zeros = numpy.zeros # Construct the 'F' matrix, which lists the feature values for # each training instance. F[i, j*len(labels)+k] is equal to the # value of the i'th feature for the feature vector corresponding # to (tok[j], label[k]). F = zeros((num_features, num_toks*num_labels)) # Construct the 'N' matrix, which specifies the correct label for # each training instance. N[0, j*len(labels)+k] is equal to one # iff label[k] is the correct label for tok[j]. N = zeros((1, num_toks*num_labels)) # Fill in the 'F' and 'N' matrices (just make one pass through the # training tokens.) for toknum, (featureset, label) in enumerate(train_toks): N[0, toknum*len(labels) + labelnum[label]] += 1 for label2 in labels: for (fid, fval) in encoding.encode(featureset, label2): F[fid, toknum*len(labels) + labelnum[label2]] = fval # Set up the scipy model, based on the matrices F and N. model = scipy.maxentropy.conditionalmodel(F, N, num_toks) # note -- model.setsmooth() is buggy. if gaussian_prior_sigma: model.sigma2 = gaussian_prior_sigma**2 if algorithm == 'LBFGSB': model.log = None if trace >= 3: model.verbose = True if 'max_iter' in cutoffs: model.maxiter = cutoffs['max_iter'] if 'tolerance' in cutoffs: if algorithm == 'CG': model.avegtol = cutoffs['tolerance'] elif algorithm == 'LBFGSB': model.maxgtol = cutoffs['tolerance'] else: model.tol = cutoffs['tolerance'] # Train the model. model.fit(algorithm=algorithm) # Convert the model's weights from base-e to base-2 weights. weights = model.params * numpy.log2(numpy.e) # Build the classifier return MaxentClassifier(encoding, weights) ###################################################################### #{ Classifier Trainer: megam ###################################################################### # [xx] possible extension: add support for using implicit file format; # this would need to put requirements on what encoding is used. But # we may need this for other maxent classifier trainers that require # implicit formats anyway. def train_maxent_classifier_with_megam(train_toks, trace=3, encoding=None, labels=None, gaussian_prior_sigma=0, **kwargs): """ Train a new C{ConditionalExponentialClassifier}, using the given training samples, using the external C{megam} library. This C{ConditionalExponentialClassifier} will encode the model that maximizes entropy from all the models that are empirically consistent with C{train_toks}. @see: L{train_maxent_classifier()} for parameter descriptions. @see: L{nltk.classify.megam} """ explicit = True bernoulli = True if 'explicit' in kwargs: explicit = kwargs['explicit'] if 'bernoulli' in kwargs: bernoulli = kwargs['bernoulli'] # Construct an encoding from the training data. if encoding is None: # Count cutoff can also be controlled by megam with the -minfc # option. Not sure where the best place for it is. count_cutoff = kwargs.get('count_cutoff', 0) encoding = BinaryMaxentFeatureEncoding.train(train_toks, count_cutoff, labels=labels, alwayson_features=True) elif labels is not None: raise ValueError('Specify encoding or labels, not both') # Write a training file for megam. try: fd, trainfile_name = tempfile.mkstemp(prefix='nltk-', suffix='.gz') trainfile = gzip.open(trainfile_name, 'wb') write_megam_file(train_toks, encoding, trainfile, \ explicit=explicit, bernoulli=bernoulli) trainfile.close() except (OSError, IOError, ValueError), e: raise ValueError('Error while creating megam training file: %s' % e) # Run megam on the training file. options = [] options += ['-nobias', '-repeat', '10'] if explicit: options += ['-explicit'] if not bernoulli: options += ['-fvals'] if gaussian_prior_sigma: # Lambda is just the precision of the Gaussian prior, i.e. it's the # inverse variance, so the parameter conversion is 1.0/sigma**2. # See http://www.cs.utah.edu/~hal/docs/daume04cg-bfgs.pdf. inv_variance = 1.0 / gaussian_prior_sigma**2 else: inv_variance = 0 options += ['-lambda', '%.2f' % inv_variance, '-tune'] if trace < 3: options += ['-quiet'] if 'max_iter' in kwargs: options += ['-maxi', '%s' % kwargs['max_iter']] if 'll_delta' in kwargs: # [xx] this is actually a perplexity delta, not a log # likelihood delta options += ['-dpp', '%s' % abs(kwargs['ll_delta'])] options += ['multiclass', trainfile_name] stdout = call_megam(options) # print './megam_i686.opt ', ' '.join(options) # Delete the training file try: os.remove(trainfile_name) except (OSError, IOError), e: print 'Warning: unable to delete %s: %s' % (trainfile_name, e) # Parse the generated weight vector. weights = parse_megam_weights(stdout, encoding.length(), explicit) # Convert from base-e to base-2 weights. weights *= numpy.log2(numpy.e) # Build the classifier return MaxentClassifier(encoding, weights) ###################################################################### #{ Classifier Trainer: tadm ###################################################################### class TadmMaxentClassifier(MaxentClassifier): @classmethod def train(cls, train_toks, **kwargs): algorithm = kwargs.get('algorithm', 'tao_lmvm') trace = kwargs.get('trace', 3) encoding = kwargs.get('encoding', None) labels = kwargs.get('labels', None) sigma = kwargs.get('gaussian_prior_sigma', 0) count_cutoff = kwargs.get('count_cutoff', 0) max_iter = kwargs.get('max_iter') ll_delta = kwargs.get('min_lldelta') # Construct an encoding from the training data. if not encoding: encoding = TadmEventMaxentFeatureEncoding.train(train_toks, count_cutoff, labels=labels) trainfile_fd, trainfile_name = \ tempfile.mkstemp(prefix='nltk-tadm-events-', suffix='.gz') weightfile_fd, weightfile_name = \ tempfile.mkstemp(prefix='nltk-tadm-weights-') trainfile = gzip.open(trainfile_name, 'wb') write_tadm_file(train_toks, encoding, trainfile) trainfile.close() options = [] options.extend(['-monitor']) options.extend(['-method', algorithm]) if sigma: options.extend(['-l2', '%.6f' % sigma**2]) if max_iter: options.extend(['-max_it', '%d' % max_iter]) if ll_delta: options.extend(['-fatol', '%.6f' % abs(ll_delta)]) options.extend(['-events_in', trainfile_name]) options.extend(['-params_out', weightfile_name]) if trace < 3: options.extend(['2>&1']) else: options.extend(['-summary']) call_tadm(options) weightfile = open(weightfile_name, 'rb') weights = parse_tadm_weights(weightfile) weightfile.close() os.remove(trainfile_name) os.remove(weightfile_name) # Convert from base-e to base-2 weights. weights *= numpy.log2(numpy.e) # Build the classifier return cls(encoding, weights) ###################################################################### #{ Demo ###################################################################### def demo(): from nltk.classify.util import names_demo classifier = names_demo(MaxentClassifier.train) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/classify/mallet.py0000644000175000017500000000565011327451603017203 0ustar bhavanibhavani# Natural Language Toolkit: Interface to Mallet Machine Learning Package # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # # $Id: naivebayes.py 2063 2004-07-17 21:02:24Z edloper $ """ A set of functions used to interface with the external U{Mallet } machine learning package. Before C{mallet} can be used, you should tell NLTK where it can find the C{mallet} package, using the L{config_mallet()} function. Typical usage: >>> import nltk >>> nltk.config_mallet('.../path/to/mallet') """ __docformat__ = 'epytext en' import os import os.path from nltk.internals import find_binary, java ###################################################################### #{ Configuration ###################################################################### _mallet_home = None _mallet_classpath = None def config_mallet(mallet_home=None): """ Configure NLTK's interface to the C{mallet} machine learning package. @param mallet_home: The full path to the C{mallet} directory. If not specified, then nltk will search the system for a C{mallet} directory; and if one is not found, it will raise a C{LookupError} exception. @type mallet_home: C{string} """ global _mallet_home, _mallet_classpath # We don't actually care about this binary -- we just use it to # make sure we've found the right directory. mallethon_bin = find_binary( 'mallet', mallet_home, env_vars=['MALLET', 'MALLET_HOME'], binary_names=['mallethon'], url='http://mallet.cs.umass.edu>') # Record the location where mallet lives. bin_dir = os.path.split(mallethon_bin)[0] _mallet_home = os.path.split(bin_dir)[0] # Construct a classpath for using mallet. lib_dir = os.path.join(_mallet_home, 'lib') if not os.path.isdir(lib_dir): raise ValueError('While configuring mallet: directory %r ' 'not found.' % lib_dir) _mallet_classpath = ':'.join([os.path.join(lib_dir, filename) for filename in sorted(os.listdir(lib_dir)) if filename.endswith('.jar')]) def call_mallet(cmd, classpath=None, stdin=None, stdout=None, stderr=None, blocking=True): """ Call L{nltk.internals.java()} with the given command, and with the classpath modified to include both C{nltk.jar} and all the C{.jar} files defined by Mallet. See L{nltk.internals.java()} for parameter and return value descriptions. """ if _mallet_classpath is None: config_mallet() # Set up the classpath if classpath is None: classpath = _mallet_classpath else: classpath += ':' + _mallet_classpath # Delegate to java() return java(cmd, classpath, stdin, stdout, stderr, blocking) nltk-2.0~b9/nltk/classify/decisiontree.py0000644000175000017500000002720211414551264020400 0ustar bhavanibhavani# Natural Language Toolkit: Decision Tree Classifiers # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # # $Id: naivebayes.py 2063 2004-07-17 21:02:24Z edloper $ """ A classifier model that decides which label to assign to a token on the basis of a tree structure, where branches correspond to conditions on feature values, and leaves correspond to label assignments. """ from nltk.probability import * from nltk.compat import defaultdict from api import * class DecisionTreeClassifier(ClassifierI): def __init__(self, label, feature_name=None, decisions=None, default=None): """ @param label: The most likely label for tokens that reach this node in the decision tree. If this decision tree has no children, then this label will be assigned to any token that reaches this decision tree. @param feature_name: The name of the feature that this decision tree selects for. @param decisions: A dictionary mapping from feature values for the feature identified by C{feature_name} to child decision trees. @param default: The child that will be used if the value of feature C{feature_name} does not match any of the keys in C{decisions}. This is used when constructing binary decision trees. """ self._label = label self._fname = feature_name self._decisions = decisions self._default = default def labels(self): labels = [self._label] if self._decisions is not None: for dt in self._decisions.values(): labels.extend(dt.labels()) if self._default is not None: labels.extend(self._default.labels()) return list(set(labels)) def classify(self, featureset): # Decision leaf: if self._fname is None: return self._label # Decision tree: fval = featureset.get(self._fname) if fval in self._decisions: return self._decisions[fval].classify(featureset) elif self._default is not None: return self._default.classify(featureset) else: return self._label def error(self, labeled_featuresets): errors = 0 for featureset, label in labeled_featuresets: if self.classify(featureset) != label: errors += 1 return float(errors)/len(labeled_featuresets) def pp(self, width=70, prefix='', depth=4): """ Return a string containing a pretty-printed version of this decision tree. Each line in this string corresponds to a single decision tree node or leaf, and indentation is used to display the structure of the decision tree. """ # [xx] display default!! if self._fname is None: n = width-len(prefix)-15 return '%s%s %s\n' % (prefix, '.'*n, self._label) s = '' for i, (fval, result) in enumerate(sorted(self._decisions.items())): hdr = '%s%s=%s? ' % (prefix, self._fname, fval) n = width-15-len(hdr) s += '%s%s %s\n' % (hdr, '.'*(n), result._label) if result._fname is not None and depth>1: s += result.pp(width, prefix+' ', depth-1) if self._default is not None: n = width-len(prefix)-21 s += '%selse: %s %s\n' % (prefix, '.'*n, self._default._label) if self._default._fname is not None and depth>1: s += self._default.pp(width, prefix+' ', depth-1) return s def pseudocode(self, prefix='', depth=4): """ Return a string representation of this decision tree that expresses the decisions it makes as a nested set of pseudocode if statements. """ if self._fname is None: return "%sreturn %r\n" % (prefix, self._label) s = '' for (fval, result) in sorted(self._decisions.items()): s += '%sif %s == %r: ' % (prefix, self._fname, fval) if result._fname is not None and depth>1: s += '\n'+result.pseudocode(prefix+' ', depth-1) else: s += 'return %r\n' % result._label if self._default is not None: if len(self._decisions) == 1: s += '%sif %s != %r: '% (prefix, self._fname, self._decisions.keys()[0]) else: s += '%selse: ' % (prefix,) if self._default._fname is not None and depth>1: s += '\n'+self._default.pseudocode(prefix+' ', depth-1) else: s += 'return %r\n' % self._default._label return s def __str__(self): return self.pp() @staticmethod def train(labeled_featuresets, entropy_cutoff=0.05, depth_cutoff=100, support_cutoff=10, binary=False, feature_values=None, verbose=False): """ @param binary: If true, then treat all feature/value pairs a individual binary features, rather than using a single n-way branch for each feature. """ # Collect a list of all feature names. feature_names = set() for featureset, label in labeled_featuresets: for fname in featureset: feature_names.add(fname) # Collect a list of the values each feature can take. if feature_values is None and binary: feature_values = defaultdict(set) for featureset, label in labeled_featuresets: for fname, fval in featureset.items(): feature_values[fname].add(fval) # Start with a stump. if not binary: tree = DecisionTreeClassifier.best_stump( feature_names, labeled_featuresets, verbose) else: tree = DecisionTreeClassifier.best_binary_stump( feature_names, labeled_featuresets, feature_values, verbose) # Refine the stump. tree.refine(labeled_featuresets, entropy_cutoff, depth_cutoff-1, support_cutoff, binary, feature_values, verbose) # Return it return tree @staticmethod def leaf(labeled_featuresets): label = FreqDist([label for (featureset,label) in labeled_featuresets]).max() return DecisionTreeClassifier(label) @staticmethod def stump(feature_name, labeled_featuresets): label = FreqDist([label for (featureset,label) in labeled_featuresets]).max() # Find the best label for each value. freqs = defaultdict(FreqDist) # freq(label|value) for featureset, label in labeled_featuresets: feature_value = featureset[feature_name] freqs[feature_value].inc(label) decisions = dict([(val, DecisionTreeClassifier(freqs[val].max())) for val in freqs]) return DecisionTreeClassifier(label, feature_name, decisions) def refine(self, labeled_featuresets, entropy_cutoff, depth_cutoff, support_cutoff, binary=False, feature_values=None, verbose=False): if len(labeled_featuresets) <= support_cutoff: return if self._fname is None: return if depth_cutoff <= 0: return for fval in self._decisions: fval_featuresets = [(featureset,label) for (featureset,label) in labeled_featuresets if featureset.get(self._fname) == fval] label_freqs = FreqDist([label for (featureset,label) in fval_featuresets]) if entropy(MLEProbDist(label_freqs)) > entropy_cutoff: self._decisions[fval] = DecisionTreeClassifier.train( fval_featuresets, entropy_cutoff, depth_cutoff, support_cutoff, binary, feature_values, verbose) if self._default is not None: default_featuresets = [(featureset, label) for (featureset, label) in labeled_featuresets if featureset.get(self._fname) not in self._decisions.keys()] label_freqs = FreqDist([label for (featureset,label) in default_featuresets]) if entropy(MLEProbDist(label_freqs)) > entropy_cutoff: self._default = DecisionTreeClassifier.train( default_featuresets, entropy_cutoff, depth_cutoff, support_cutoff, binary, feature_values, verbose) @staticmethod def best_stump(feature_names, labeled_featuresets, verbose=False): best_stump = DecisionTreeClassifier.leaf(labeled_featuresets) best_error = best_stump.error(labeled_featuresets) for fname in feature_names: stump = DecisionTreeClassifier.stump(fname, labeled_featuresets) stump_error = stump.error(labeled_featuresets) if stump_error < best_error: best_error = stump_error best_stump = stump if verbose: print ('best stump for %6d toks uses %-20s err=%6.4f' % (len(labeled_featuresets), best_stump._fname, best_error)) return best_stump @staticmethod def binary_stump(feature_name, feature_value, labeled_featuresets): label = FreqDist([label for (featureset,label) in labeled_featuresets]).max() # Find the best label for each value. pos_fdist = FreqDist() neg_fdist = FreqDist() for featureset, label in labeled_featuresets: if featureset.get(feature_name) == feature_value: pos_fdist.inc(label) else: neg_fdist.inc(label) decisions = {feature_value: DecisionTreeClassifier(pos_fdist.max())} default = DecisionTreeClassifier(neg_fdist.max()) return DecisionTreeClassifier(label, feature_name, decisions, default) @staticmethod def best_binary_stump(feature_names, labeled_featuresets, feature_values, verbose=False): best_stump = DecisionTreeClassifier.leaf(labeled_featuresets) best_error = best_stump.error(labeled_featuresets) for fname in feature_names: for fval in feature_values[fname]: stump = DecisionTreeClassifier.binary_stump( fname, fval, labeled_featuresets) stump_error = stump.error(labeled_featuresets) if stump_error < best_error: best_error = stump_error best_stump = stump if best_stump._decisions: descr = '%s=%s' % (best_stump._fname, best_stump._decisions.keys()[0]) else: descr = '(default)' if verbose: print ('best stump for %6d toks uses %-20s err=%6.4f' % (len(labeled_featuresets), descr, best_error)) return best_stump ##////////////////////////////////////////////////////// ## Demo ##////////////////////////////////////////////////////// def f(x): return DecisionTreeClassifier.train(x, binary=True, verbose=True) def demo(): from nltk.classify.util import names_demo, binary_names_demo_features classifier = names_demo(f, #DecisionTreeClassifier.train, binary_names_demo_features) print classifier.pp(depth=7) print classifier.pseudocode(depth=7) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/classify/api.py0000644000175000017500000001641511327451603016477 0ustar bhavanibhavani# Natural Language Toolkit: Classifier Interface # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird (minor additions) # URL: # For license information, see LICENSE.TXT """ Interfaces for labeling tokens with category labels (or X{class labels}). L{ClassifierI} is a standard interface for X{single-category classification}, in which: - The set of categories is known. - The number of categories is finite. - Each text belongs to exactly one category. L{MultiClassifierI} is a standard interface for C{multi-category classification}, in which: - The set of categories is known. - The number of categories is finite. - Each text belongs to zero or more categories. """ from nltk.internals import deprecated, overridden ##////////////////////////////////////////////////////// #{ Classification Interfaces ##////////////////////////////////////////////////////// class ClassifierI(object): """ A processing interface for labeling tokens with a single category label (or X{class}). Labels are typically C{string}s or C{integer}s, but can be any immutable type. The set of labels that the classifier chooses from must be fixed and finite. Subclasses must define: - L{labels()} - either L{classify()} or L{batch_classify()} (or both) Subclasses may define: - either L{prob_classify()} or L{batch_prob_classify()} (or both) """ def labels(self): """ @return: the list of category labels used by this classifier. @rtype: C{list} of (immutable) """ raise NotImplementedError() def classify(self, featureset): """ @return: the most appropriate label for the given featureset. @rtype: label """ if overridden(self.batch_classify): return self.batch_classify([featureset])[0] else: raise NotImplementedError() def prob_classify(self, featureset): """ @return: a probability distribution over labels for the given featureset. @rtype: L{ProbDistI } """ if overridden(self.batch_prob_classify): return self.batch_prob_classify([featureset])[0] else: raise NotImplementedError() def batch_classify(self, featuresets): """ Apply L{self.classify()} to each element of C{featuresets}. I.e.: >>> return [self.classify(fs) for fs in featuresets] @rtype: C{list} of I{label} """ return [self.classify(fs) for fs in featuresets] def batch_prob_classify(self, featuresets): """ Apply L{self.prob_classify()} to each element of C{featuresets}. I.e.: >>> return [self.prob_classify(fs) for fs in featuresets] @rtype: C{list} of L{ProbDistI } """ return [self.prob_classify(fs) for fs in featuresets] #{ Deprecated @deprecated("Use .batch_prob_classify() instead.") def batch_probdist(self, featuresets): return self.batch_prob_classify(featuresets) @deprecated("Use .prob_classify() instead.") def probdist(self, featureset): return self.prob_classify(featureset) #} class MultiClassifierI(object): """ A processing interface for labeling tokens with zero or more category labels (or X{labels}). Labels are typically C{string}s or C{integer}s, but can be any immutable type. The set of labels that the multi-classifier chooses from must be fixed and finite. Subclasses must define: - L{labels()} - either L{classify()} or L{batch_classify()} (or both) Subclasses may define: - either L{prob_classify()} or L{batch_prob_classify()} (or both) """ def labels(self): """ @return: the list of category labels used by this classifier. @rtype: C{list} of (immutable) """ raise NotImplementedError() def classify(self, featureset): """ @return: the most appropriate set of labels for the given featureset. @rtype: C{set} of I{label} """ if overridden(self.batch_classify): return self.batch_classify([featureset])[0] else: raise NotImplementedError() def prob_classify(self, featureset): """ @return: a probability distribution over sets of labels for the given featureset. @rtype: L{ProbDistI } """ if overridden(self.batch_prob_classify): return self.batch_prob_classify([featureset])[0] else: raise NotImplementedError() def batch_classify(self, featuresets): """ Apply L{self.classify()} to each element of C{featuresets}. I.e.: >>> return [self.classify(fs) for fs in featuresets] @rtype: C{list} of (C{set} of I{label}) """ return [self.classify(fs) for fs in featuresets] def batch_prob_classify(self, featuresets): """ Apply L{self.prob_classify()} to each element of C{featuresets}. I.e.: >>> return [self.prob_classify(fs) for fs in featuresets] @rtype: C{list} of L{ProbDistI } """ return [self.prob_classify(fs) for fs in featuresets] #{ Deprecated @deprecated("Use .batch_prob_classify() instead.") def batch_probdist(self, featuresets): return self.batch_prob_classify(featuresets) @deprecated("Use .prob_classify() instead.") def probdist(self, featureset): return self.prob_classify(featureset) #} # # [XX] IN PROGRESS: # class SequenceClassifierI(object): # """ # A processing interface for labeling sequences of tokens with a # single category label (or X{class}). Labels are typically # C{string}s or C{integer}s, but can be any immutable type. The set # of labels that the classifier chooses from must be fixed and # finite. # """ # def labels(self): # """ # @return: the list of category labels used by this classifier. # @rtype: C{list} of (immutable) # """ # raise NotImplementedError() # def prob_classify(self, featureset): # """ # Return a probability distribution over labels for the given # featureset. # If C{featureset} is a list of featuresets, then return a # corresponding list containing the probability distribution # over labels for each of the given featuresets, where the # M{i}th element of this list is the most appropriate label for # the M{i}th element of C{featuresets}. # """ # raise NotImplementedError() # def classify(self, featureset): # """ # Return the most appropriate label for the given featureset. # If C{featureset} is a list of featuresets, then return a # corresponding list containing the most appropriate label for # each of the given featuresets, where the M{i}th element of # this list is the most appropriate label for the M{i}th element # of C{featuresets}. # """ # raise NotImplementedError() nltk-2.0~b9/nltk/classify/__init__.py0000644000175000017500000001061611327451603017462 0ustar bhavanibhavani# Natural Language Toolkit: Classifiers # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT """ Classes and interfaces for labeling tokens with category labels (or X{class labels}). Typically, labels are represented with strings (such as C{'health'} or C{'sports'}). Classifiers can be used to perform a wide range of classification tasks. For example, classifiers can be used... - to classify documents by topic. - to classify ambiguous words by which word sense is intended. - to classify acoustic signals by which phoneme they represent. - to classify sentences by their author. Features ======== In order to decide which category label is appropriate for a given token, classifiers examine one or more 'features' of the token. These X{features} are typically chosen by hand, and indicate which aspects of the token are relevant to the classification decision. For example, a document classifier might use a separate feature for each word, recording how often that word occured in the document. Featuresets =========== The features describing a token are encoded using a X{featureset}, which is a dictionary that maps from X{feature names} to X{feature values}. Feature names are unique strings that indicate what aspect of the token is encoded by the feature. Examples include C{'prevword'}, for a feature whose value is the previous word; and C{'contains-word(library)'} for a feature that is true when a document contains the word C{'library'}. Feature values are typically booleans, numbers, or strings, depending on which feature they describe. Featuresets are typically constructed using a X{feature detector} (also known as a X{feature extractor}). A feature detector is a function that takes a token (and sometimes information about its context) as its input, and returns a featureset describing that token. For example, the following feature detector converts a document (stored as a list of words) to a featureset describing the set of words included in the document: >>> # Define a feature detector function. >>> def document_features(document): ... return dict([('contains-word(%s)' % w, True) for w in document]) Feature detectors are typically applied to each token before it is fed to the classifier: >>> Classify each Gutenberg document. >>> for file in gutenberg.files(): ... doc = gutenberg.tokenized(file) ... print doc_name, classifier.classify(document_features(doc)) The parameters that a feature detector expects will vary, depending on the task and the needs of the feature detector. For example, a feature detector for word sense disambiguation (WSD) might take as its input a sentence, and the index of a word that should be classified, and return a featureset for that word. The following feature detector for WSD includes features describing the left and right contexts of the target word: >>> def wsd_features(sentence, index): ... featureset = {} ... for i in range(max(0, index-3), index): ... featureset['left-context(%s)' % sentence[i]] = True ... for i in range(index, max(index+3, len(sentence))): ... featureset['right-context(%s)' % sentence[i]] = True ... return featureset Training Classifiers ==================== Most classifiers are built by training them on a list of hand-labeled examples, known as the X{training set}. Training sets are represented as lists of C{(featuredict, label)} tuples. """ from weka import * from megam import * from api import * from util import * from naivebayes import * from decisiontree import * from rte_classify import * __all__ = [ # Classifier Interfaces 'ClassifierI', 'MultiClassifierI', # Classifiers 'NaiveBayesClassifier', 'DecisionTreeClassifier', 'WekaClassifier', # Utility functions. Note that accuracy() is intentionally # omitted -- it should be accessed as nltk.classify.accuracy(); # similarly for log_likelihood() and attested_labels(). 'config_weka', 'config_megam', # RTE 'rte_classifier', 'rte_features', 'RTEFeatureExtractor', # Demos -- not included. ] try: import numpy from maxent import * __all__ += ['MaxentClassifier', 'BinaryMaxentFeatureEncoding', 'ConditionalExponentialClassifier', 'train_maxent_classifier'] except ImportError: pass nltk-2.0~b9/nltk/chunk/util.py0000644000175000017500000005003111327451576016177 0ustar bhavanibhavani# Natural Language Toolkit: Chunk format conversions # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird (minor additions) # URL: # For license information, see LICENSE.TXT import re import string from nltk.tree import Tree import nltk.tag.util from api import * ##////////////////////////////////////////////////////// ## EVALUATION ##////////////////////////////////////////////////////// from nltk.metrics import accuracy as _accuracy def accuracy(chunker, gold): """ Score the accuracy of the chunker against the gold standard. Strip the chunk information from the gold standard and rechunk it using the chunker, then compute the accuracy score. @type chunker: C{ChunkParserI} @param chunker: The chunker being evaluated. @type gold: C{tree} @param gold: The chunk structures to score the chunker on. @rtype: C{float} """ gold_tags = [] test_tags = [] for gold_tree in gold: test_tree = chunker.parse(gold_tree.flatten()) gold_tags += tree2conlltags(gold_tree) test_tags += tree2conlltags(test_tree) # print 'GOLD:', gold_tags[:50] # print 'TEST:', test_tags[:50] return _accuracy(gold_tags, test_tags) # Patched for increased performance by Yoav Goldberg , 2006-01-13 # -- statistics are evaluated only on demand, instead of at every sentence evaluation # # SB: use nltk.metrics for precision/recall scoring? # class ChunkScore(object): """ A utility class for scoring chunk parsers. C{ChunkScore} can evaluate a chunk parser's output, based on a number of statistics (precision, recall, f-measure, misssed chunks, incorrect chunks). It can also combine the scores from the parsing of multiple texts; this makes it signifigantly easier to evaluate a chunk parser that operates one sentence at a time. Texts are evaluated with the C{score} method. The results of evaluation can be accessed via a number of accessor methods, such as C{precision} and C{f_measure}. A typical use of the C{ChunkScore} class is:: >>> chunkscore = ChunkScore() >>> for correct in correct_sentences: ... guess = chunkparser.parse(correct.leaves()) ... chunkscore.score(correct, guess) >>> print 'F Measure:', chunkscore.f_measure() F Measure: 0.823 @ivar kwargs: Keyword arguments: - max_tp_examples: The maximum number actual examples of true positives to record. This affects the C{correct} member function: C{correct} will not return more than this number of true positive examples. This does *not* affect any of the numerical metrics (precision, recall, or f-measure) - max_fp_examples: The maximum number actual examples of false positives to record. This affects the C{incorrect} member function and the C{guessed} member function: C{incorrect} will not return more than this number of examples, and C{guessed} will not return more than this number of true positive examples. This does *not* affect any of the numerical metrics (precision, recall, or f-measure) - max_fn_examples: The maximum number actual examples of false negatives to record. This affects the C{missed} member function and the C{correct} member function: C{missed} will not return more than this number of examples, and C{correct} will not return more than this number of true negative examples. This does *not* affect any of the numerical metrics (precision, recall, or f-measure) - chunk_node: A regular expression indicating which chunks should be compared. Defaults to C{'.*'} (i.e., all chunks). @type _tp: C{list} of C{Token} @ivar _tp: List of true positives @type _fp: C{list} of C{Token} @ivar _fp: List of false positives @type _fn: C{list} of C{Token} @ivar _fn: List of false negatives @type _tp_num: C{int} @ivar _tp_num: Number of true positives @type _fp_num: C{int} @ivar _fp_num: Number of false positives @type _fn_num: C{int} @ivar _fn_num: Number of false negatives. """ def __init__(self, **kwargs): self._correct = set() self._guessed = set() self._tp = set() self._fp = set() self._fn = set() self._max_tp = kwargs.get('max_tp_examples', 100) self._max_fp = kwargs.get('max_fp_examples', 100) self._max_fn = kwargs.get('max_fn_examples', 100) self._chunk_node = kwargs.get('chunk_node', '.*') self._tp_num = 0 self._fp_num = 0 self._fn_num = 0 self._count = 0 self._tags_correct = 0.0 self._tags_total = 0.0 self._measuresNeedUpdate = False def _updateMeasures(self): if (self._measuresNeedUpdate): self._tp = self._guessed & self._correct self._fn = self._correct - self._guessed self._fp = self._guessed - self._correct self._tp_num = len(self._tp) self._fp_num = len(self._fp) self._fn_num = len(self._fn) self._measuresNeedUpdate = False def score(self, correct, guessed): """ Given a correctly chunked sentence, score another chunked version of the same sentence. @type correct: chunk structure @param correct: The known-correct ("gold standard") chunked sentence. @type guessed: chunk structure @param guessed: The chunked sentence to be scored. """ self._correct |= _chunksets(correct, self._count, self._chunk_node) self._guessed |= _chunksets(guessed, self._count, self._chunk_node) self._count += 1 self._measuresNeedUpdate = True # Keep track of per-tag accuracy (if possible) try: correct_tags = tree2conlltags(correct) guessed_tags = tree2conlltags(guessed) except ValueError: # This exception case is for nested chunk structures, # where tree2conlltags will fail with a ValueError: "Tree # is too deeply nested to be printed in CoNLL format." correct_tags = guessed_tags = () self._tags_total += len(correct_tags) self._tags_correct += sum(1 for (t,g) in zip(guessed_tags, correct_tags) if t==g) def accuracy(self): """ @return: The overall tag-based accuracy for all text that have been scored by this C{ChunkScore}, using the IOB (conll2000) tag encoding. """ if self._tags_total == 0: return 1 return self._tags_correct/self._tags_total def precision(self): """ @return: the overall precision for all texts that have been scored by this C{ChunkScore}. @rtype: C{float} """ self._updateMeasures() div = self._tp_num + self._fp_num if div == 0: return 0 else: return float(self._tp_num) / div def recall(self): """ @return: the overall recall for all texts that have been scored by this C{ChunkScore}. @rtype: C{float} """ self._updateMeasures() div = self._tp_num + self._fn_num if div == 0: return 0 else: return float(self._tp_num) / div def f_measure(self, alpha=0.5): """ @return: the overall F measure for all texts that have been scored by this C{ChunkScore}. @rtype: C{float} @param alpha: the relative weighting of precision and recall. Larger alpha biases the score towards the precision value, while smaller alpha biases the score towards the recall value. C{alpha} should have a value in the range [0,1]. @type alpha: C{float} """ self._updateMeasures() p = self.precision() r = self.recall() if p == 0 or r == 0: # what if alpha is 0 or 1? return 0 return 1/(alpha/p + (1-alpha)/r) def missed(self): """ @rtype: C{list} of chunks @return: the chunks which were included in the correct chunk structures, but not in the guessed chunk structures, listed in input order. """ self._updateMeasures() chunks = list(self._fn) return [c[1] for c in chunks] # discard position information def incorrect(self): """ @rtype: C{list} of chunks @return: the chunks which were included in the guessed chunk structures, but not in the correct chunk structures, listed in input order. """ self._updateMeasures() chunks = list(self._fp) return [c[1] for c in chunks] # discard position information def correct(self): """ @rtype: C{list} of chunks @return: the chunks which were included in the correct chunk structures, listed in input order. """ chunks = list(self._correct) return [c[1] for c in chunks] # discard position information def guessed(self): """ @rtype: C{list} of chunks @return: the chunks which were included in the guessed chunk structures, listed in input order. """ chunks = list(self._guessed) return [c[1] for c in chunks] # discard position information def __len__(self): self._updateMeasures() return self._tp_num + self._fn_num def __repr__(self): """ @rtype: C{String} @return: a concise representation of this C{ChunkScoring}. """ return '' def __str__(self): """ @rtype: C{String} @return: a verbose representation of this C{ChunkScoring}. This representation includes the precision, recall, and f-measure scores. For other information about the score, use the accessor methods (e.g., C{missed()} and C{incorrect()}). """ return ("ChunkParse score:\n" + (" IOB Accuracy: %5.1f%%\n" % (self.accuracy()*100)) + (" Precision: %5.1f%%\n" % (self.precision()*100)) + (" Recall: %5.1f%%\n" % (self.recall()*100))+ (" F-Measure: %5.1f%%" % (self.f_measure()*100))) # extract chunks, and assign unique id, the absolute position of # the first word of the chunk def _chunksets(t, count, chunk_node): pos = 0 chunks = [] for child in t: if isinstance(child, Tree): if re.match(chunk_node, child.node): chunks.append(((count, pos), child.freeze())) pos += len(child.leaves()) else: pos += 1 return set(chunks) def tagstr2tree(s, chunk_node="NP", top_node="S", sep='/'): """ Divide a string of bracketted tagged text into chunks and unchunked tokens, and produce a C{Tree}. Chunks are marked by square brackets (C{[...]}). Words are delimited by whitespace, and each word should have the form C{I{text}/I{tag}}. Words that do not contain a slash are assigned a C{tag} of C{None}. @return: A tree corresponding to the string representation. @rtype: C{tree} @param s: The string to be converted @type s: C{string} @param chunk_node: The label to use for chunk nodes @type chunk_node: C{string} @param top_node: The label to use for the root of the tree @type top_node: C{string} """ WORD_OR_BRACKET = re.compile(r'\[|\]|[^\[\]\s]+') stack = [Tree(top_node, [])] for match in WORD_OR_BRACKET.finditer(s): text = match.group() if text[0] == '[': if len(stack) != 1: raise ValueError('Unexpected [ at char %d' % match.start()) chunk = Tree(chunk_node, []) stack[-1].append(chunk) stack.append(chunk) elif text[0] == ']': if len(stack) != 2: raise ValueError('Unexpected ] at char %d' % match.start()) stack.pop() else: if sep is None: stack[-1].append(text) else: stack[-1].append(nltk.tag.util.str2tuple(text, sep)) if len(stack) != 1: raise ValueError('Expected ] at char %d' % len(s)) return stack[0] ### CONLL _LINE_RE = re.compile('(\S+)\s+(\S+)\s+([IOB])-?(\S+)?') def conllstr2tree(s, chunk_types=('NP', 'PP', 'VP'), top_node="S"): """ Convert a CoNLL IOB string into a tree. Uses the specified chunk types (defaults to NP, PP and VP), and creates a tree rooted at a node labeled S (by default). @param s: The CoNLL string to be converted. @type s: C{string} @param chunk_types: The chunk types to be converted. @type chunk_types: C{tuple} @param top_node: The node label to use for the root. @type top_node: C{string} @return: A chunk structure for a single sentence encoded in the given CONLL 2000 style string. @rtype: L{Tree} """ stack = [Tree(top_node, [])] for lineno, line in enumerate(s.split('\n')): if not line.strip(): continue # Decode the line. match = _LINE_RE.match(line) if match is None: raise ValueError, 'Error on line %d' % lineno (word, tag, state, chunk_type) = match.groups() # If it's a chunk type we don't care about, treat it as O. if (chunk_types is not None and chunk_type not in chunk_types): state = 'O' # For "Begin"/"Outside", finish any completed chunks - # also do so for "Inside" which don't match the previous token. mismatch_I = state == 'I' and chunk_type != stack[-1].node if state in 'BO' or mismatch_I: if len(stack) == 2: stack.pop() # For "Begin", start a new chunk. if state == 'B' or mismatch_I: chunk = Tree(chunk_type, []) stack[-1].append(chunk) stack.append(chunk) # Add the new word token. stack[-1].append((word, tag)) return stack[0] def tree2conlltags(t): """ Convert a tree to the CoNLL IOB tag format @param t: The tree to be converted. @type t: C{Tree} @return: A list of 3-tuples containing word, tag and IOB tag. @rtype: C{list} of C{tuple} """ tags = [] for child in t: try: category = child.node prefix = "B-" for contents in child: if isinstance(contents, Tree): raise ValueError, "Tree is too deeply nested to be printed in CoNLL format" tags.append((contents[0], contents[1], prefix+category)) prefix = "I-" except AttributeError: tags.append((child[0], child[1], "O")) return tags def conlltags2tree(sentence, chunk_types=('NP','PP','VP'), top_node='S', strict=False): """ Convert the CoNLL IOB format to a tree. """ tree = nltk.Tree(top_node, []) for (word, postag, chunktag) in sentence: if chunktag is None: if strict: raise ValueError("Bad conll tag sequence") else: # Treat as O tree.append((word,postag)) elif chunktag.startswith('B-'): tree.append(nltk.Tree(chunktag[2:], [(word,postag)])) elif chunktag.startswith('I-'): if (len(tree)==0 or not isinstance(tree[-1], nltk.Tree) or tree[-1].node != chunktag[2:]): if strict: raise ValueError("Bad conll tag sequence") else: # Treat as B-* tree.append(nltk.Tree(chunktag[2:], [(word,postag)])) else: tree[-1].append((word,postag)) elif chunktag == 'O': tree.append((word,postag)) else: raise ValueError("Bad conll tag %r" % chunktag) return tree def tree2conllstr(t): """ Convert a tree to the CoNLL IOB string format @param t: The tree to be converted. @type t: C{Tree} @return: A multiline string where each line contains a word, tag and IOB tag. @rtype: C{string} """ lines = [string.join(token) for token in tree2conlltags(t)] return '\n'.join(lines) ### IEER _IEER_DOC_RE = re.compile(r'\s*' r'(\s*(?P.+?)\s*\s*)?' r'(\s*(?P.+?)\s*\s*)?' r'(\s*(?P.+?)\s*\s*)?' r'\s*' r'(\s*(?P.+?)\s*\s*)?' r'(?P.*?)\s*' r'\s*\s*', re.DOTALL) _IEER_TYPE_RE = re.compile(']*?type="(?P\w+)"') def _ieer_read_text(s, top_node): stack = [Tree(top_node, [])] # s will be None if there is no headline in the text # return the empty list in place of a Tree if s is None: return [] for piece_m in re.finditer('<[^>]+>|[^\s<]+', s): piece = piece_m.group() try: if piece.startswith('.... m = _IEER_DOC_RE.match(s) if m: return { 'text': _ieer_read_text(m.group('text'), top_node), 'docno': m.group('docno'), 'doctype': m.group('doctype'), 'date_time': m.group('date_time'), #'headline': m.group('headline') # we want to capture NEs in the headline too! 'headline': _ieer_read_text(m.group('headline'), top_node), } else: return _ieer_read_text(s, top_node) def demo(): s = "[ Pierre/NNP Vinken/NNP ] ,/, [ 61/CD years/NNS ] old/JJ ,/, will/MD join/VB [ the/DT board/NN ] ./." import nltk t = nltk.chunk.tagstr2tree(s, chunk_node='NP') print t.pprint() print s = """ These DT B-NP research NN I-NP protocols NNS I-NP offer VBP B-VP to TO B-PP the DT B-NP patient NN I-NP not RB O only RB O the DT B-NP very RB I-NP best JJS I-NP therapy NN I-NP which WDT B-NP we PRP B-NP have VBP B-VP established VBN I-VP today NN B-NP but CC B-NP also RB I-NP the DT B-NP hope NN I-NP of IN B-PP something NN B-NP still RB B-ADJP better JJR I-ADJP . . O """ conll_tree = conllstr2tree(s, chunk_types=('NP', 'PP')) print conll_tree.pprint() # Demonstrate CoNLL output print "CoNLL output:" print nltk.chunk.tree2conllstr(conll_tree) print if __name__ == '__main__': demo() nltk-2.0~b9/nltk/chunk/regexp.py0000644000175000017500000015310511327451576016522 0ustar bhavanibhavani# Natural Language Toolkit: Regular Expression Chunkers # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird (minor additions) # URL: # For license information, see LICENSE.TXT import re import types from nltk.tree import Tree from nltk.chunk.api import * from nltk.chunk.util import * ##////////////////////////////////////////////////////// ## ChunkString ##////////////////////////////////////////////////////// class ChunkString(object): """ A string-based encoding of a particular chunking of a text. Internally, the C{ChunkString} class uses a single string to encode the chunking of the input text. This string contains a sequence of angle-bracket delimited tags, with chunking indicated by braces. An example of this encoding is:: {

}{
}<.>{
}<.> C{ChunkString} are created from tagged texts (i.e., C{list}s of C{tokens} whose type is C{TaggedType}). Initially, nothing is chunked. The chunking of a C{ChunkString} can be modified with the C{xform} method, which uses a regular expression to transform the string representation. These transformations should only add and remove braces; they should I{not} modify the sequence of angle-bracket delimited tags. @type _str: C{string} @ivar _str: The internal string representation of the text's encoding. This string representation contains a sequence of angle-bracket delimited tags, with chunking indicated by braces. An example of this encoding is:: {
}{
}<.>{
}<.> @type _pieces: C{list} of pieces (tagged tokens and chunks) @ivar _pieces: The tagged tokens and chunks encoded by this C{ChunkString}. @ivar _debug: The debug level. See the constructor docs. @cvar IN_CHUNK_PATTERN: A zero-width regexp pattern string that will only match positions that are in chunks. @cvar IN_CHINK_PATTERN: A zero-width regexp pattern string that will only match positions that are in chinks. """ CHUNK_TAG_CHAR = r'[^\{\}<>]' CHUNK_TAG = r'(<%s+?>)' % CHUNK_TAG_CHAR IN_CHUNK_PATTERN = r'(?=[^\{]*\})' IN_CHINK_PATTERN = r'(?=[^\}]*(\{|$))' # These are used by _verify _CHUNK = r'(\{%s+?\})+?' % CHUNK_TAG _CHINK = r'(%s+?)+?' % CHUNK_TAG _VALID = re.compile(r'^(\{?%s\}?)*?$' % CHUNK_TAG) _BRACKETS = re.compile('[^\{\}]+') _BALANCED_BRACKETS = re.compile(r'(\{\})*$') def __init__(self, chunk_struct, debug_level=1): """ Construct a new C{ChunkString} that encodes the chunking of the text C{tagged_tokens}. @type chunk_struct: C{Tree} @param chunk_struct: The chunk structure to be further chunked. @type debug_level: int @param debug_level: The level of debugging which should be applied to transformations on the C{ChunkString}. The valid levels are: - 0: no checks - 1: full check on to_chunkstruct - 2: full check on to_chunkstruct and cursory check after each transformation. - 3: full check on to_chunkstruct and full check after each transformation. We recommend you use at least level 1. You should probably use level 3 if you use any non-standard subclasses of C{RegexpChunkRule}. """ self._top_node = chunk_struct.node self._pieces = chunk_struct[:] tags = [self._tag(tok) for tok in self._pieces] self._str = '<' + '><'.join(tags) + '>' self._debug = debug_level def _tag(self, tok): if type(tok) == types.TupleType: return tok[1] elif isinstance(tok, Tree): return tok.node else: raise ValueError('chunk structures must contain tagged ' 'tokens or trees') def _verify(self, s, verify_tags): """ Check to make sure that C{s} still corresponds to some chunked version of C{_pieces}. @type verify_tags: C{boolean} @param verify_tags: Whether the individual tags should be checked. If this is false, C{_verify} will check to make sure that C{_str} encodes a chunked version of I{some} list of tokens. If this is true, then C{_verify} will check to make sure that the tags in C{_str} match those in C{_pieces}. @raise ValueError: if this C{ChunkString}'s internal string representation is invalid or not consistent with _pieces. """ # Check overall form if not ChunkString._VALID.match(s): raise ValueError('Transformation generated invalid ' 'chunkstring:\n %s' % s) # Check that parens are balanced. If the string is long, we # have to do this in pieces, to avoid a maximum recursion # depth limit for regular expressions. brackets = ChunkString._BRACKETS.sub('', s) for i in range(1+len(brackets)/5000): substr = brackets[i*5000:i*5000+5000] if not ChunkString._BALANCED_BRACKETS.match(substr): raise ValueError('Transformation generated invalid ' 'chunkstring:\n %s' % s) if verify_tags<=0: return tags1 = (re.split(r'[\{\}<>]+', s))[1:-1] tags2 = [self._tag(piece) for piece in self._pieces] if tags1 != tags2: raise ValueError('Transformation generated invalid ' 'chunkstring: tag changed') def to_chunkstruct(self, chunk_node='CHUNK'): """ @return: the chunk structure encoded by this C{ChunkString}. @rtype: C{Tree} @raise ValueError: If a transformation has generated an invalid chunkstring. """ if self._debug > 0: self._verify(self._str, 1) # Use this alternating list to create the chunkstruct. pieces = [] index = 0 piece_in_chunk = 0 for piece in re.split('[{}]', self._str): # Find the list of tokens contained in this piece. length = piece.count('<') subsequence = self._pieces[index:index+length] # Add this list of tokens to our pieces. if piece_in_chunk: pieces.append(Tree(chunk_node, subsequence)) else: pieces += subsequence # Update index, piece_in_chunk index += length piece_in_chunk = not piece_in_chunk return Tree(self._top_node, pieces) def xform(self, regexp, repl): """ Apply the given transformation to this C{ChunkString}'s string encoding. In particular, find all occurrences that match C{regexp}, and replace them using C{repl} (as done by C{re.sub}). This transformation should only add and remove braces; it should I{not} modify the sequence of angle-bracket delimited tags. Furthermore, this transformation may not result in improper bracketing. Note, in particular, that bracketing may not be nested. @type regexp: C{string} or C{regexp} @param regexp: A regular expression matching the substring that should be replaced. This will typically include a named group, which can be used by C{repl}. @type repl: C{string} @param repl: An expression specifying what should replace the matched substring. Typically, this will include a named replacement group, specified by C{regexp}. @rtype: C{None} @raise ValueError: If this transformation generated an invalid chunkstring. """ # Do the actual substitution s = re.sub(regexp, repl, self._str) # The substitution might have generated "empty chunks" # (substrings of the form "{}"). Remove them, so they don't # interfere with other transformations. s = re.sub('\{\}', '', s) # Make sure that the transformation was legal. if self._debug > 1: self._verify(s, self._debug-2) # Commit the transformation. self._str = s def __repr__(self): """ @rtype: C{string} @return: A string representation of this C{ChunkString}. This string representation has the form:: }{
}'> """ return '' % `self._str` def __str__(self): """ @rtype: C{string} @return: A formatted representation of this C{ChunkString}'s string encoding. This representation will include extra spaces to ensure that tags will line up with the representation of other C{ChunkStrings} for the same text, regardless of the chunking. """ # Add spaces to make everything line up. str = re.sub(r'>(?!\})', r'> ', self._str) str = re.sub(r'([^\{])<', r'\1 <', str) if str[0] == '<': str = ' ' + str return str ##////////////////////////////////////////////////////// ## Chunking Rules ##////////////////////////////////////////////////////// class RegexpChunkRule(object): """ A rule specifying how to modify the chunking in a C{ChunkString}, using a transformational regular expression. The C{RegexpChunkRule} class itself can be used to implement any transformational rule based on regular expressions. There are also a number of subclasses, which can be used to implement simpler types of rules, based on matching regular expressions. Each C{RegexpChunkRule} has a regular expression and a replacement expression. When a C{RegexpChunkRule} is X{applied} to a C{ChunkString}, it searches the C{ChunkString} for any substring that matches the regular expression, and replaces it using the replacement expression. This search/replace operation has the same semantics as C{re.sub}. Each C{RegexpChunkRule} also has a description string, which gives a short (typically less than 75 characters) description of the purpose of the rule. This transformation defined by this C{RegexpChunkRule} should only add and remove braces; it should I{not} modify the sequence of angle-bracket delimited tags. Furthermore, this transformation may not result in nested or mismatched bracketing. """ def __init__(self, regexp, repl, descr): """ Construct a new RegexpChunkRule. @type regexp: C{regexp} or C{string} @param regexp: This C{RegexpChunkRule}'s regular expression. When this rule is applied to a C{ChunkString}, any substring that matches C{regexp} will be replaced using the replacement string C{repl}. Note that this must be a normal regular expression, not a tag pattern. @type repl: C{string} @param repl: This C{RegexpChunkRule}'s replacement expression. When this rule is applied to a C{ChunkString}, any substring that matches C{regexp} will be replaced using C{repl}. @type descr: C{string} @param descr: A short description of the purpose and/or effect of this rule. """ if isinstance(regexp, basestring): regexp = re.compile(regexp) self._repl = repl self._descr = descr self._regexp = regexp def apply(self, chunkstr): # Keep docstring generic so we can inherit it. """ Apply this rule to the given C{ChunkString}. See the class reference documentation for a description of what it means to apply a rule. @type chunkstr: C{ChunkString} @param chunkstr: The chunkstring to which this rule is applied. @rtype: C{None} @raise ValueError: If this transformation generated an invalid chunkstring. """ chunkstr.xform(self._regexp, self._repl) def descr(self): """ @rtype: C{string} @return: a short description of the purpose and/or effect of this rule. """ return self._descr def __repr__(self): """ @rtype: C{string} @return: A string representation of this rule. This string representation has the form:: }'->''> Note that this representation does not include the description string; that string can be accessed separately with the C{descr} method. """ return (''+`self._repl`+'>') @staticmethod def parse(s): """ Create a RegexpChunkRule from a string description. Currently, the following formats are supported:: {regexp} # chunk rule }regexp{ # chink rule regexp}{regexp # split rule regexp{}regexp # merge rule Where C{regexp} is a regular expression for the rule. Any text following the comment marker (C{#}) will be used as the rule's description: >>> RegexpChunkRule.parse('{
?+}') ?+'> """ # Split off the comment (but don't split on '\#') m = re.match(r'(?P(\\.|[^#])*)(?P#.*)?', s) rule = m.group('rule').strip() comment = (m.group('comment') or '')[1:].strip() # Pattern bodies: chunk, chink, split, merge try: if not rule: raise ValueError('Empty chunk pattern') if rule[0] == '{' and rule[-1] == '}': return ChunkRule(rule[1:-1], comment) elif rule[0] == '}' and rule[-1] == '{': return ChinkRule(rule[1:-1], comment) elif '}{' in rule: left, right = rule.split('}{') return SplitRule(left, right, comment) elif '{}' in rule: left, right = rule.split('{}') return MergeRule(left, right, comment) elif re.match('[^{}]*{[^{}]*}[^{}]*', rule): left, chunk, right = re.split('[{}]', rule) return ChunkRuleWithContext(left, chunk, right, comment) else: raise ValueError('Illegal chunk pattern: %s' % rule) except (ValueError, re.error): raise ValueError('Illegal chunk pattern: %s' % rule) class ChunkRule(RegexpChunkRule): """ A rule specifying how to add chunks to a C{ChunkString}, using a matching tag pattern. When applied to a C{ChunkString}, it will find any substring that matches this tag pattern and that is not already part of a chunk, and create a new chunk containing that substring. """ def __init__(self, tag_pattern, descr): """ Construct a new C{ChunkRule}. @type tag_pattern: C{string} @param tag_pattern: This rule's tag pattern. When applied to a C{ChunkString}, this rule will chunk any substring that matches this tag pattern and that is not already part of a chunk. @type descr: C{string} @param descr: A short description of the purpose and/or effect of this rule. """ self._pattern = tag_pattern regexp = re.compile('(?P%s)%s' % (tag_pattern2re_pattern(tag_pattern), ChunkString.IN_CHINK_PATTERN)) RegexpChunkRule.__init__(self, regexp, '{\g}', descr) def __repr__(self): """ @rtype: C{string} @return: A string representation of this rule. This string representation has the form:: '> Note that this representation does not include the description string; that string can be accessed separately with the C{descr} method. """ return '' class ChinkRule(RegexpChunkRule): """ A rule specifying how to remove chinks to a C{ChunkString}, using a matching tag pattern. When applied to a C{ChunkString}, it will find any substring that matches this tag pattern and that is contained in a chunk, and remove it from that chunk, thus creating two new chunks. """ def __init__(self, tag_pattern, descr): """ Construct a new C{ChinkRule}. @type tag_pattern: C{string} @param tag_pattern: This rule's tag pattern. When applied to a C{ChunkString}, this rule will find any substring that matches this tag pattern and that is contained in a chunk, and remove it from that chunk, thus creating two new chunks. @type descr: C{string} @param descr: A short description of the purpose and/or effect of this rule. """ self._pattern = tag_pattern regexp = re.compile('(?P%s)%s' % (tag_pattern2re_pattern(tag_pattern), ChunkString.IN_CHUNK_PATTERN)) RegexpChunkRule.__init__(self, regexp, '}\g{', descr) def __repr__(self): """ @rtype: C{string} @return: A string representation of this rule. This string representation has the form:: '> Note that this representation does not include the description string; that string can be accessed separately with the C{descr} method. """ return '' class UnChunkRule(RegexpChunkRule): """ A rule specifying how to remove chunks to a C{ChunkString}, using a matching tag pattern. When applied to a C{ChunkString}, it will find any complete chunk that matches this tag pattern, and un-chunk it. """ def __init__(self, tag_pattern, descr): """ Construct a new C{UnChunkRule}. @type tag_pattern: C{string} @param tag_pattern: This rule's tag pattern. When applied to a C{ChunkString}, this rule will find any complete chunk that matches this tag pattern, and un-chunk it. @type descr: C{string} @param descr: A short description of the purpose and/or effect of this rule. """ self._pattern = tag_pattern regexp = re.compile('\{(?P%s)\}' % tag_pattern2re_pattern(tag_pattern)) RegexpChunkRule.__init__(self, regexp, '\g', descr) def __repr__(self): """ @rtype: C{string} @return: A string representation of this rule. This string representation has the form:: '> Note that this representation does not include the description string; that string can be accessed separately with the C{descr} method. """ return '' class MergeRule(RegexpChunkRule): """ A rule specifying how to merge chunks in a C{ChunkString}, using two matching tag patterns: a left pattern, and a right pattern. When applied to a C{ChunkString}, it will find any chunk whose end matches left pattern, and immediately followed by a chunk whose beginning matches right pattern. It will then merge those two chunks into a single chunk. """ def __init__(self, left_tag_pattern, right_tag_pattern, descr): """ Construct a new C{MergeRule}. @type right_tag_pattern: C{string} @param right_tag_pattern: This rule's right tag pattern. When applied to a C{ChunkString}, this rule will find any chunk whose end matches C{left_tag_pattern}, and immediately followed by a chunk whose beginning matches this pattern. It will then merge those two chunks into a single chunk. @type left_tag_pattern: C{string} @param left_tag_pattern: This rule's left tag pattern. When applied to a C{ChunkString}, this rule will find any chunk whose end matches this pattern, and immediately followed by a chunk whose beginning matches C{right_tag_pattern}. It will then merge those two chunks into a single chunk. @type descr: C{string} @param descr: A short description of the purpose and/or effect of this rule. """ # Ensure that the individual patterns are coherent. E.g., if # left='(' and right=')', then this will raise an exception: re.compile(tag_pattern2re_pattern(left_tag_pattern)) re.compile(tag_pattern2re_pattern(right_tag_pattern)) self._left_tag_pattern = left_tag_pattern self._right_tag_pattern = right_tag_pattern regexp = re.compile('(?P%s)}{(?=%s)' % (tag_pattern2re_pattern(left_tag_pattern), tag_pattern2re_pattern(right_tag_pattern))) RegexpChunkRule.__init__(self, regexp, '\g', descr) def __repr__(self): """ @rtype: C{string} @return: A string representation of this rule. This string representation has the form:: ', ''> Note that this representation does not include the description string; that string can be accessed separately with the C{descr} method. """ return ('') class SplitRule(RegexpChunkRule): """ A rule specifying how to split chunks in a C{ChunkString}, using two matching tag patterns: a left pattern, and a right pattern. When applied to a C{ChunkString}, it will find any chunk that matches the left pattern followed by the right pattern. It will then split the chunk into two new chunks, at the point between the two pattern matches. """ def __init__(self, left_tag_pattern, right_tag_pattern, descr): """ Construct a new C{SplitRule}. @type right_tag_pattern: C{string} @param right_tag_pattern: This rule's right tag pattern. When applied to a C{ChunkString}, this rule will find any chunk containing a substring that matches C{left_tag_pattern} followed by this pattern. It will then split the chunk into two new chunks at the point between these two matching patterns. @type left_tag_pattern: C{string} @param left_tag_pattern: This rule's left tag pattern. When applied to a C{ChunkString}, this rule will find any chunk containing a substring that matches this pattern followed by C{right_tag_pattern}. It will then split the chunk into two new chunks at the point between these two matching patterns. @type descr: C{string} @param descr: A short description of the purpose and/or effect of this rule. """ # Ensure that the individual patterns are coherent. E.g., if # left='(' and right=')', then this will raise an exception: re.compile(tag_pattern2re_pattern(left_tag_pattern)) re.compile(tag_pattern2re_pattern(right_tag_pattern)) self._left_tag_pattern = left_tag_pattern self._right_tag_pattern = right_tag_pattern regexp = re.compile('(?P%s)(?=%s)' % (tag_pattern2re_pattern(left_tag_pattern), tag_pattern2re_pattern(right_tag_pattern))) RegexpChunkRule.__init__(self, regexp, r'\g}{', descr) def __repr__(self): """ @rtype: C{string} @return: A string representation of this rule. This string representation has the form:: ', '
'> Note that this representation does not include the description string; that string can be accessed separately with the C{descr} method. """ return ('') class ExpandLeftRule(RegexpChunkRule): """ A rule specifying how to expand chunks in a C{ChunkString} to the left, using two matching tag patterns: a left pattern, and a right pattern. When applied to a C{ChunkString}, it will find any chunk whose beginning matches right pattern, and immediately preceded by a chink whose end matches left pattern. It will then expand the chunk to incorporate the new material on the left. """ def __init__(self, left_tag_pattern, right_tag_pattern, descr): """ Construct a new C{ExpandRightRule}. @type right_tag_pattern: C{string} @param right_tag_pattern: This rule's right tag pattern. When applied to a C{ChunkString}, this rule will find any chunk whose beginning matches C{right_tag_pattern}, and immediately preceded by a chink whose end matches this pattern. It will then merge those two chunks into a single chunk. @type left_tag_pattern: C{string} @param left_tag_pattern: This rule's left tag pattern. When applied to a C{ChunkString}, this rule will find any chunk whose beginning matches this pattern, and immediately preceded by a chink whose end matches C{left_tag_pattern}. It will then expand the chunk to incorporate the new material on the left. @type descr: C{string} @param descr: A short description of the purpose and/or effect of this rule. """ # Ensure that the individual patterns are coherent. E.g., if # left='(' and right=')', then this will raise an exception: re.compile(tag_pattern2re_pattern(left_tag_pattern)) re.compile(tag_pattern2re_pattern(right_tag_pattern)) self._left_tag_pattern = left_tag_pattern self._right_tag_pattern = right_tag_pattern regexp = re.compile('(?P%s)\{(?P%s)' % (tag_pattern2re_pattern(left_tag_pattern), tag_pattern2re_pattern(right_tag_pattern))) RegexpChunkRule.__init__(self, regexp, '{\g\g', descr) def __repr__(self): """ @rtype: C{string} @return: A string representation of this rule. This string representation has the form:: ', ''> Note that this representation does not include the description string; that string can be accessed separately with the C{descr} method. """ return ('') class ExpandRightRule(RegexpChunkRule): """ A rule specifying how to expand chunks in a C{ChunkString} to the right, using two matching tag patterns: a left pattern, and a right pattern. When applied to a C{ChunkString}, it will find any chunk whose end matches left pattern, and immediately followed by a chink whose beginning matches right pattern. It will then expand the chunk to incorporate the new material on the right. """ def __init__(self, left_tag_pattern, right_tag_pattern, descr): """ Construct a new C{ExpandRightRule}. @type right_tag_pattern: C{string} @param right_tag_pattern: This rule's right tag pattern. When applied to a C{ChunkString}, this rule will find any chunk whose end matches C{left_tag_pattern}, and immediately followed by a chink whose beginning matches this pattern. It will then merge those two chunks into a single chunk. @type left_tag_pattern: C{string} @param left_tag_pattern: This rule's left tag pattern. When applied to a C{ChunkString}, this rule will find any chunk whose end matches this pattern, and immediately followed by a chink whose beginning matches C{right_tag_pattern}. It will then expand the chunk to incorporate the new material on the right. @type descr: C{string} @param descr: A short description of the purpose and/or effect of this rule. """ # Ensure that the individual patterns are coherent. E.g., if # left='(' and right=')', then this will raise an exception: re.compile(tag_pattern2re_pattern(left_tag_pattern)) re.compile(tag_pattern2re_pattern(right_tag_pattern)) self._left_tag_pattern = left_tag_pattern self._right_tag_pattern = right_tag_pattern regexp = re.compile('(?P%s)\}(?P%s)' % (tag_pattern2re_pattern(left_tag_pattern), tag_pattern2re_pattern(right_tag_pattern))) RegexpChunkRule.__init__(self, regexp, '\g\g}', descr) def __repr__(self): """ @rtype: C{string} @return: A string representation of this rule. This string representation has the form:: ', ''> Note that this representation does not include the description string; that string can be accessed separately with the C{descr} method. """ return ('') class ChunkRuleWithContext(RegexpChunkRule): """ A rule specifying how to add chunks to a C{ChunkString}, using three matching tag patterns: one for the left context, one for the chunk, and one for the right context. When applied to a C{ChunkString}, it will find any substring that matches the chunk tag pattern, is surrounded by substrings that match the two context patterns, and is not already part of a chunk; and create a new chunk containing the substring that matched the chunk tag pattern. Caveat: Both the left and right context are consumed when this rule matches; therefore, if you need to find overlapping matches, you will need to apply your rule more than once. """ def __init__(self, left_context_tag_pattern, chunk_tag_pattern, right_context_tag_pattern, descr): """ Construct a new C{ChunkRuleWithContext}. @type left_context_tag_pattern: C{string} @param left_context_tag_pattern: A tag pattern that must match the left context of C{chunk_tag_pattern} for this rule to apply. @type chunk_tag_pattern: C{string} @param chunk_tag_pattern: A tag pattern that must match for this rule to apply. If the rule does apply, then this pattern also identifies the substring that will be made into a chunk. @type right_context_tag_pattern: C{string} @param right_context_tag_pattern: A tag pattern that must match the right context of C{chunk_tag_pattern} for this rule to apply. @type descr: C{string} @param descr: A short description of the purpose and/or effect of this rule. """ # Ensure that the individual patterns are coherent. E.g., if # left='(' and right=')', then this will raise an exception: re.compile(tag_pattern2re_pattern(left_context_tag_pattern)) re.compile(tag_pattern2re_pattern(chunk_tag_pattern)) re.compile(tag_pattern2re_pattern(right_context_tag_pattern)) self._left_context_tag_pattern = left_context_tag_pattern self._chunk_tag_pattern = chunk_tag_pattern self._right_context_tag_pattern = right_context_tag_pattern regexp = re.compile('(?P%s)(?P%s)(?P%s)%s' % (tag_pattern2re_pattern(left_context_tag_pattern), tag_pattern2re_pattern(chunk_tag_pattern), tag_pattern2re_pattern(right_context_tag_pattern), ChunkString.IN_CHINK_PATTERN)) replacement = r'\g{\g}\g' RegexpChunkRule.__init__(self, regexp, replacement, descr) def __repr__(self): """ @rtype: C{string} @return: A string representation of this rule. This string representation has the form:: ', '', '
'> Note that this representation does not include the description string; that string can be accessed separately with the C{descr} method. """ return '' % ( self._left_context_tag_pattern, self._chunk_tag_pattern, self._right_context_tag_pattern) ##////////////////////////////////////////////////////// ## Tag Pattern Format Conversion ##////////////////////////////////////////////////////// # this should probably be made more strict than it is -- e.g., it # currently accepts 'foo'. CHUNK_TAG_PATTERN = re.compile(r'^((%s|<%s>)*)$' % ('[^\{\}<>]+', '[^\{\}<>]+')) def tag_pattern2re_pattern(tag_pattern): """ Convert a tag pattern to a regular expression pattern. A X{tag pattern} is a modified version of a regular expression, designed for matching sequences of tags. The differences between regular expression patterns and tag patterns are: - In tag patterns, C{'<'} and C{'>'} act as parentheses; so C{'+'} matches one or more repetitions of C{''}, not C{''}. - Whitespace in tag patterns is ignored. So C{'
| '} is equivalant to C{'
|'} - In tag patterns, C{'.'} is equivalant to C{'[^{}<>]'}; so C{''} matches any single tag starting with C{'NN'}. In particular, C{tag_pattern2re_pattern} performs the following transformations on the given pattern: - Replace '.' with '[^<>{}]' - Remove any whitespace - Add extra parens around '<' and '>', to make '<' and '>' act like parentheses. E.g., so that in '+', the '+' has scope over the entire ''; and so that in '', the '|' has scope over 'NN' and 'IN', but not '<' or '>'. - Check to make sure the resulting pattern is valid. @type tag_pattern: C{string} @param tag_pattern: The tag pattern to convert to a regular expression pattern. @raise ValueError: If C{tag_pattern} is not a valid tag pattern. In particular, C{tag_pattern} should not include braces; and it should not contain nested or mismatched angle-brackets. @rtype: C{string} @return: A regular expression pattern corresponding to C{tag_pattern}. """ # Clean up the regular expression tag_pattern = re.sub(r'\s', '', tag_pattern) tag_pattern = re.sub(r'<', '(<(', tag_pattern) tag_pattern = re.sub(r'>', ')>)', tag_pattern) # Check the regular expression if not CHUNK_TAG_PATTERN.match(tag_pattern): raise ValueError('Bad tag pattern: %r' % tag_pattern) # Replace "." with CHUNK_TAG_CHAR. # We have to do this after, since it adds {}[]<>s, which would # confuse CHUNK_TAG_PATTERN. # PRE doesn't have lookback assertions, so reverse twice, and do # the pattern backwards (with lookahead assertions). This can be # made much cleaner once we can switch back to SRE. def reverse_str(str): lst = list(str) lst.reverse() return ''.join(lst) tc_rev = reverse_str(ChunkString.CHUNK_TAG_CHAR) reversed = reverse_str(tag_pattern) reversed = re.sub(r'\.(?!\\(\\\\)*($|[^\\]))', tc_rev, reversed) tag_pattern = reverse_str(reversed) return tag_pattern ##////////////////////////////////////////////////////// ## RegexpChunkParser ##////////////////////////////////////////////////////// class RegexpChunkParser(ChunkParserI): """ A regular expression based chunk parser. C{RegexpChunkParser} uses a sequence of X{rules} to find chunks of a single type within a text. The chunking of the text is encoded using a C{ChunkString}, and each rule acts by modifying the chunking in the C{ChunkString}. The rules are all implemented using regular expression matching and substitution. The C{RegexpChunkRule} class and its subclasses (C{ChunkRule}, C{ChinkRule}, C{UnChunkRule}, C{MergeRule}, and C{SplitRule}) define the rules that are used by C{RegexpChunkParser}. Each rule defines an C{apply} method, which modifies the chunking encoded by a given C{ChunkString}. @type _rules: C{list} of C{RegexpChunkRule} @ivar _rules: The list of rules that should be applied to a text. @type _trace: C{int} @ivar _trace: The default level of tracing. """ def __init__(self, rules, chunk_node='NP', top_node='S', trace=0): """ Construct a new C{RegexpChunkParser}. @type rules: C{list} of C{RegexpChunkRule} @param rules: The sequence of rules that should be used to generate the chunking for a tagged text. @type chunk_node: C{string} @param chunk_node: The node value that should be used for chunk subtrees. This is typically a short string describing the type of information contained by the chunk, such as C{"NP"} for base noun phrases. @type top_node: C{string} @param top_node: The node value that should be used for the top node of the chunk structure. @type trace: C{int} @param trace: The level of tracing that should be used when parsing a text. C{0} will generate no tracing output; C{1} will generate normal tracing output; and C{2} or higher will generate verbose tracing output. """ self._rules = rules self._trace = trace self._chunk_node = chunk_node self._top_node = top_node def _trace_apply(self, chunkstr, verbose): """ Apply each of this C{RegexpChunkParser}'s rules to C{chunkstr}, in turn. Generate trace output between each rule. If C{verbose} is true, then generate verbose output. @type chunkstr: C{ChunkString} @param chunkstr: The chunk string to which each rule should be applied. @type verbose: C{boolean} @param verbose: Whether output should be verbose. @rtype: C{None} """ print '# Input:' print chunkstr for rule in self._rules: rule.apply(chunkstr) if verbose: print '#', rule.descr()+' ('+`rule`+'):' else: print '#', rule.descr()+':' print chunkstr def _notrace_apply(self, chunkstr): """ Apply each of this C{RegexpChunkParser}'s rules to C{chunkstr}, in turn. @param chunkstr: The chunk string to which each rule should be applied. @type chunkstr: C{ChunkString} @rtype: C{None} """ for rule in self._rules: rule.apply(chunkstr) def parse(self, chunk_struct, trace=None): """ @type chunk_struct: C{Tree} @param chunk_struct: the chunk structure to be (further) chunked @type trace: C{int} @param trace: The level of tracing that should be used when parsing a text. C{0} will generate no tracing output; C{1} will generate normal tracing output; and C{2} or highter will generate verbose tracing output. This value overrides the trace level value that was given to the constructor. @rtype: C{Tree} @return: a chunk structure that encodes the chunks in a given tagged sentence. A chunk is a non-overlapping linguistic group, such as a noun phrase. The set of chunks identified in the chunk structure depends on the rules used to define this C{RegexpChunkParser}. """ if len(chunk_struct) == 0: print 'Warning: parsing empty text' return Tree(self._top_node, []) try: chunk_struct.node except AttributeError: chunk_struct = Tree(self._top_node, chunk_struct) # Use the default trace value? if trace == None: trace = self._trace chunkstr = ChunkString(chunk_struct) # Apply the sequence of rules to the chunkstring. if trace: verbose = (trace>1) self._trace_apply(chunkstr, verbose) else: self._notrace_apply(chunkstr) # Use the chunkstring to create a chunk structure. return chunkstr.to_chunkstruct(self._chunk_node) def rules(self): """ @return: the sequence of rules used by C{RegexpChunkParser}. @rtype: C{list} of C{RegexpChunkRule} """ return self._rules def __repr__(self): """ @return: a concise string representation of this C{RegexpChunkParser}. @rtype: C{string} """ return "" % len(self._rules) def __str__(self): """ @return: a verbose string representation of this C{RegexpChunkParser}. @rtype: C{string} """ s = "RegexpChunkParser with %d rules:\n" % len(self._rules) margin = 0 for rule in self._rules: margin = max(margin, len(rule.descr())) if margin < 35: format = " %" + `-(margin+3)` + "s%s\n" else: format = " %s\n %s\n" for rule in self._rules: s += format % (rule.descr(), `rule`) return s[:-1] ##////////////////////////////////////////////////////// ## Chunk Grammar ##////////////////////////////////////////////////////// class RegexpParser(ChunkParserI): """ A grammar based chunk parser. C{chunk.RegexpParser} uses a set of regular expression patterns to specify the behavior of the parser. The chunking of the text is encoded using a C{ChunkString}, and each rule acts by modifying the chunking in the C{ChunkString}. The rules are all implemented using regular expression matching and substitution. A grammar contains one or more clauses in the following form:: NP: {} # chunk determiners and adjectives }<[\.VI].*>+{ # chink any tag beginning with V, I, or . <.*>}{
# split a chunk at a determiner {} # merge chunk ending with det/adj # with one starting with a noun The patterns of a clause are executed in order. An earlier pattern may introduce a chunk boundary that prevents a later pattern from executing. Sometimes an individual pattern will match on multiple, overlapping extents of the input. As with regular expression substitution more generally, the chunker will identify the first match possible, then continue looking for matches after this one has ended. The clauses of a grammar are also executed in order. A cascaded chunk parser is one having more than one clause. The maximum depth of a parse tree created by this chunk parser is the same as the number of clauses in the grammar. When tracing is turned on, the comment portion of a line is displayed each time the corresponding pattern is applied. @type _start: C{string} @ivar _start: The start symbol of the grammar (the root node of resulting trees) @type _stages: C{int} @ivar _stages: The list of parsing stages corresponding to the grammar """ def __init__(self, grammar, top_node='S', loop=1, trace=0): """ Create a new chunk parser, from the given start state and set of chunk patterns. @param grammar: The list of patterns that defines the grammar @type grammar: C{list} of C{string} @param top_node: The top node of the tree being created @type top_node: L{string} or L{Nonterminal} @param loop: The number of times to run through the patterns @type loop: L{int} @type trace: C{int} @param trace: The level of tracing that should be used when parsing a text. C{0} will generate no tracing output; C{1} will generate normal tracing output; and C{2} or higher will generate verbose tracing output. """ self._trace = trace self._stages = [] self._grammar = grammar self._loop = loop if isinstance(grammar, basestring): self._parse_grammar(grammar, top_node, trace) else: # Make sur the grammar looks like it has the right type: type_err = ('Expected string or list of RegexpChunkParsers ' 'for the grammar.') try: grammar = list(grammar) except: raise TypeError(type_err) for elt in grammar: if not isinstance(elt, RegexpChunkParser): raise TypeError(type_err) self._stages = grammar def _parse_grammar(self, grammar, top_node, trace): """ Helper function for __init__: parse the grammar if it is a string. """ rules = [] lhs = None for line in grammar.split('\n'): line = line.strip() # New stage begins if there's an unescaped ':' m = re.match('(?P(\\.|[^:])*)(:(?P.*))', line) if m: # Record the stage that we just completed. self._add_stage(rules, lhs, top_node, trace) # Start a new stage. lhs = m.group('nonterminal').strip() rules = [] line = m.group('rule').strip() # Skip blank & comment-only lines if line=='' or line.startswith('#'): continue # Add the rule rules.append(RegexpChunkRule.parse(line)) # Record the final stage self._add_stage(rules, lhs, top_node, trace) def _add_stage(self, rules, lhs, top_node, trace): """ Helper function for __init__: add a new stage to the parser. """ if rules != []: if not lhs: raise ValueError('Expected stage marker (eg NP:)') parser = RegexpChunkParser(rules, chunk_node=lhs, top_node=top_node, trace=trace) self._stages.append(parser) def parse(self, chunk_struct, trace=None): """ Apply the chunk parser to this input. @type chunk_struct: C{Tree} @param chunk_struct: the chunk structure to be (further) chunked (this tree is modified, and is also returned) @type trace: C{int} @param trace: The level of tracing that should be used when parsing a text. C{0} will generate no tracing output; C{1} will generate normal tracing output; and C{2} or highter will generate verbose tracing output. This value overrides the trace level value that was given to the constructor. @return: the chunked output. @rtype: C{Tree} """ if trace == None: trace = self._trace for i in range(self._loop): for parser in self._stages: chunk_struct = parser.parse(chunk_struct, trace=trace) return chunk_struct def __repr__(self): """ @return: a concise string representation of this C{chunk.RegexpParser}. @rtype: C{string} """ return "" % len(self._stages) def __str__(self): """ @return: a verbose string representation of this C{RegexpChunkParser}. @rtype: C{string} """ s = "chunk.RegexpParser with %d stages:\n" % len(self._stages) margin = 0 for parser in self._stages: s += parser.__str__() + "\n" return s[:-1] ##////////////////////////////////////////////////////// ## Demonstration code ##////////////////////////////////////////////////////// def demo_eval(chunkparser, text): """ Demonstration code for evaluating a chunk parser, using a C{ChunkScore}. This function assumes that C{text} contains one sentence per line, and that each sentence has the form expected by C{tree.chunk}. It runs the given chunk parser on each sentence in the text, and scores the result. It prints the final score (precision, recall, and f-measure); and reports the set of chunks that were missed and the set of chunks that were incorrect. (At most 10 missing chunks and 10 incorrect chunks are reported). @param chunkparser: The chunkparser to be tested @type chunkparser: C{ChunkParserI} @param text: The chunked tagged text that should be used for evaluation. @type text: C{string} """ from nltk import chunk from nltk.tree import Tree # Evaluate our chunk parser. chunkscore = chunk.ChunkScore() for sentence in text.split('\n'): print sentence sentence = sentence.strip() if not sentence: continue gold = chunk.tagstr2tree(sentence) tokens = gold.leaves() test = chunkparser.parse(Tree('S', tokens), trace=1) chunkscore.score(gold, test) print print '/'+('='*75)+'\\' print 'Scoring', chunkparser print ('-'*77) print 'Precision: %5.1f%%' % (chunkscore.precision()*100), ' '*4, print 'Recall: %5.1f%%' % (chunkscore.recall()*100), ' '*6, print 'F-Measure: %5.1f%%' % (chunkscore.f_measure()*100) # Missed chunks. if chunkscore.missed(): print 'Missed:' missed = chunkscore.missed() for chunk in missed[:10]: print ' ', ' '.join(c.__str__() for c in chunk) if len(chunkscore.missed()) > 10: print ' ...' # Incorrect chunks. if chunkscore.incorrect(): print 'Incorrect:' incorrect = chunkscore.incorrect() for chunk in incorrect[:10]: print ' ', ' '.join(c.__str__() for c in chunk) if len(chunkscore.incorrect()) > 10: print ' ...' print '\\'+('='*75)+'/' print def demo(): """ A demonstration for the C{RegexpChunkParser} class. A single text is parsed with four different chunk parsers, using a variety of rules and strategies. """ from nltk import chunk, Tree text = """\ [ the/DT little/JJ cat/NN ] sat/VBD on/IN [ the/DT mat/NN ] ./. [ John/NNP ] saw/VBD [the/DT cats/NNS] [the/DT dog/NN] chased/VBD ./. [ John/NNP ] thinks/VBZ [ Mary/NN ] saw/VBD [ the/DT cat/NN ] sit/VB on/IN [ the/DT mat/NN ]./. """ print '*'*75 print 'Evaluation text:' print text print '*'*75 print grammar = r""" NP: # NP stage {
?*} # chunk determiners, adjectives and nouns {+} # chunk proper nouns """ cp = chunk.RegexpParser(grammar) chunk.demo_eval(cp, text) grammar = r""" NP: {<.*>} # start by chunking each tag }<[\.VI].*>+{ # unchunk any verbs, prepositions or periods {} # merge det/adj with nouns """ cp = chunk.RegexpParser(grammar) chunk.demo_eval(cp, text) grammar = r""" NP: {
?*} # chunk determiners, adjectives and nouns VP: {?} # VP = verb words """ cp = chunk.RegexpParser(grammar) chunk.demo_eval(cp, text) grammar = r""" NP: {<.*>*} # start by chunking everything }<[\.VI].*>+{ # chink any verbs, prepositions or periods <.*>}{
# separate on determiners PP: {} # PP = preposition + noun phrase VP: {*} # VP = verb words + NPs and PPs """ cp = chunk.RegexpParser(grammar) chunk.demo_eval(cp, text) # Evaluation from nltk.corpus import conll2000 print print "Demonstration of empty grammar:" cp = chunk.RegexpParser("") print chunk.accuracy(cp, conll2000.chunked_sents('test.txt', chunk_types=('NP',))) print print "Demonstration of accuracy evaluation using CoNLL tags:" grammar = r""" NP: {<.*>} # start by chunking each tag }<[\.VI].*>+{ # unchunk any verbs, prepositions or periods {} # merge det/adj with nouns """ cp = chunk.RegexpParser(grammar) print chunk.accuracy(cp, conll2000.chunked_sents('test.txt')[:5]) print print "Demonstration of tagged token input" grammar = r""" NP: {<.*>*} # start by chunking everything }<[\.VI].*>+{ # chink any verbs, prepositions or periods <.*>}{
# separate on determiners PP: {} # PP = preposition + noun phrase VP: {*} # VP = verb words + NPs and PPs """ cp = chunk.RegexpParser(grammar) print cp.parse([("the","DT"), ("little","JJ"), ("cat", "NN"), ("sat", "VBD"), ("on", "IN"), ("the", "DT"), ("mat", "NN"), (".", ".")]) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/chunk/named_entity.py0000644000175000017500000002434611327451576017714 0ustar bhavanibhavani# Natural Language Toolkit: Chunk parsing API # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT """ Named entity chunker """ import os, re, pickle from nltk.etree import ElementTree as ET from nltk.chunk.api import * from nltk.chunk.util import * import nltk # This really shouldn't be loaded at import time. But it's used by a # static method. Do a lazy loading? _short_en_wordlist = set(nltk.corpus.words.words('en-basic')) class NEChunkParserTagger(nltk.tag.ClassifierBasedTagger): """ The IOB tagger used by the chunk parser. """ def __init__(self, train): nltk.tag.ClassifierBasedTagger.__init__( self, train=train, classifier_builder=self._classifier_builder) def _classifier_builder(self, train): return nltk.MaxentClassifier.train(train, algorithm='megam', gaussian_prior_sigma=1, trace=2) def _feature_detector(self, tokens, index, history): word = tokens[index][0] pos = simplify_pos(tokens[index][1]) if index == 0: prevword = prevprevword = None prevpos = prevprevpos = None prevtag = prevprevtag = None elif index == 1: prevword = tokens[index-1][0].lower() prevprevword = None prevpos = simplify_pos(tokens[index-1][1]) prevprevpos = None prevtag = history[index-1][0] prevprevtag = None else: prevword = tokens[index-1][0].lower() prevprevword = tokens[index-2][0].lower() prevpos = simplify_pos(tokens[index-1][1]) prevprevpos = simplify_pos(tokens[index-2][1]) prevtag = history[index-1] prevprevtag = history[index-2] if index == len(tokens)-1: nextword = nextnextword = None nextpos = nextnextpos = None elif index == len(tokens)-2: nextword = tokens[index+1][0].lower() nextpos = tokens[index+1][1].lower() nextnextword = None nextnextpos = None else: nextword = tokens[index+1][0].lower() nextpos = tokens[index+1][1].lower() nextnextword = tokens[index+2][0].lower() nextnextpos = tokens[index+2][1].lower() # 89.6 features = { 'bias': True, 'shape': shape(word), 'wordlen': len(word), 'prefix3': word[:3].lower(), 'suffix3': word[-3:].lower(), 'pos': pos, 'word': word, 'en-wordlist': (word in _short_en_wordlist), # xx! 'prevtag': prevtag, 'prevpos': prevpos, 'nextpos': nextpos, 'prevword': prevword, 'nextword': nextword, 'word+nextpos': '%s+%s' % (word.lower(), nextpos), 'pos+prevtag': '%s+%s' % (pos, prevtag), 'shape+prevtag': '%s+%s' % (shape, prevtag), } return features class NEChunkParser(ChunkParserI): """ Expected input: list of pos-tagged words """ def __init__(self, train): self._train(train) def parse(self, tokens): """ Each token should be a pos-tagged word """ tagged = self._tagger.tag(tokens) tree = self._tagged_to_parse(tagged) return tree def _train(self, corpus): # Convert to tagged sequence corpus = [self._parse_to_tagged(s) for s in corpus] self._tagger = NEChunkParserTagger(train=corpus) def _tagged_to_parse(self, tagged_tokens): """ Convert a list of tagged tokens to a chunk-parse tree. """ sent = nltk.Tree('S', []) for (tok,tag) in tagged_tokens: if tag == 'O': sent.append(tok) elif tag.startswith('B-'): sent.append(nltk.Tree(tag[2:], [tok])) elif tag.startswith('I-'): if (sent and isinstance(sent[-1], Tree) and sent[-1].node == tag[2:]): sent[-1].append(tok) else: sent.append(nltk.Tree(tag[2:], [tok])) return sent @staticmethod def _parse_to_tagged(sent): """ Convert a chunk-parse tree to a list of tagged tokens. """ toks = [] for child in sent: if isinstance(child, nltk.Tree): if len(child) == 0: print "Warning -- empty chunk in sentence" continue toks.append((child[0], 'B-%s' % child.node)) for tok in child[1:]: toks.append((tok, 'I-%s' % child.node)) else: toks.append((child, 'O')) return toks def shape(word): if re.match('[0-9]+(\.[0-9]*)?|[0-9]*\.[0-9]+$', word): return 'number' elif re.match('\W+$', word): return 'punct' elif re.match('[A-Z][a-z]+$', word): return 'upcase' elif re.match('[a-z]+$', word): return 'downcase' elif re.match('\w+$', word): return 'mixedcase' else: return 'other' def simplify_pos(s): if s.startswith('V'): return "V" else: return s.split('-')[0] def postag_tree(tree): # Part-of-speech tagging. words = tree.leaves() tag_iter = (pos for (word, pos) in nltk.pos_tag(words)) newtree = Tree('S', []) for child in tree: if isinstance(child, nltk.Tree): newtree.append(Tree(child.node, [])) for subchild in child: newtree[-1].append( (subchild, tag_iter.next()) ) else: newtree.append( (child, tag_iter.next()) ) return newtree def load_ace_data(roots, fmt='binary', skip_bnews=True): for root in roots: for root, dirs, files in os.walk(root): if root.endswith('bnews') and skip_bnews: continue for f in files: if f.endswith('.sgm'): for sent in load_ace_file(os.path.join(root, f), fmt): yield sent def load_ace_file(textfile, fmt): print ' - %s' % os.path.split(textfile)[1] annfile = textfile+'.tmx.rdc.xml' # Read the xml file, and get a list of entities entities = [] xml = ET.parse(open(annfile)).getroot() for entity in xml.findall('document/entity'): typ = entity.find('entity_type').text for mention in entity.findall('entity_mention'): if mention.get('TYPE') != 'NAME': continue # only NEs s = int(mention.find('head/charseq/start').text) e = int(mention.find('head/charseq/end').text)+1 entities.append( (s, e, typ) ) # Read the text file, and mark the entities. text = open(textfile).read() # Strip XML tags, since they don't count towards the indices text = re.sub('<(?!/?TEXT)[^>]+>', '', text) # Blank out anything before/after def subfunc(m): return ' '*(m.end()-m.start()-6) text = re.sub('[\s\S]*', subfunc, text) text = re.sub('[\s\S]*', '', text) # Simplify quotes text = re.sub("``", ' "', text) text = re.sub("''", '" ', text) entity_types = set(typ for (s,e,typ) in entities) # Binary distinction (NE or not NE) if fmt == 'binary': i = 0 toks = nltk.Tree('S', []) for (s,e,typ) in sorted(entities): if s < i: s = i # Overlapping! Deal with this better? if e <= s: continue toks.extend(nltk.word_tokenize(text[i:s])) toks.append(nltk.Tree('NE', text[s:e].split())) i = e toks.extend(nltk.word_tokenize(text[i:])) yield toks # Multiclass distinction (NE type) elif fmt == 'multiclass': i = 0 toks = nltk.Tree('S', []) for (s,e,typ) in sorted(entities): if s < i: s = i # Overlapping! Deal with this better? if e <= s: continue toks.extend(nltk.word_tokenize(text[i:s])) toks.append(nltk.Tree(typ, text[s:e].split())) i = e toks.extend(nltk.word_tokenize(text[i:])) yield toks else: raise ValueError('bad fmt value') # This probably belongs in a more general-purpose location (as does # the parse_to_tagged function). def cmp_chunks(correct, guessed): correct = NEChunkParser._parse_to_tagged(correct) guessed = NEChunkParser._parse_to_tagged(guessed) ellipsis = False for (w, ct), (w, gt) in zip(correct, guessed): if ct == gt == 'O': if not ellipsis: print " %-15s %-15s %s" % (ct, gt, w) print ' %-15s %-15s %s' % ('...', '...', '...') ellipsis = True else: ellipsis = False print " %-15s %-15s %s" % (ct, gt, w) def build_model(fmt='binary'): print 'Loading training data...' train_paths = [nltk.data.find('corpora/ace_data/ace.dev'), nltk.data.find('corpora/ace_data/ace.heldout'), nltk.data.find('corpora/ace_data/bbn.dev'), nltk.data.find('corpora/ace_data/muc.dev')] train_trees = load_ace_data(train_paths, fmt) train_data = [postag_tree(t) for t in train_trees] print 'Training...' cp = NEChunkParser(train_data) del train_data print 'Loading eval data...' eval_paths = [nltk.data.find('corpora/ace_data/ace.eval')] eval_trees = load_ace_data(eval_paths, fmt) eval_data = [postag_tree(t) for t in eval_trees] print 'Evaluating...' chunkscore = ChunkScore() for i, correct in enumerate(eval_data): guess = cp.parse(correct.leaves()) chunkscore.score(correct, guess) if i < 3: cmp_chunks(correct, guess) print chunkscore outfilename = '/tmp/ne_chunker_%s.pickle' % fmt print 'Saving chunker to %s...' % outfilename out = open(outfilename, 'wb') pickle.dump(cp, out, -1) out.close() return cp if __name__ == '__main__': # Make sure that the pickled object has the right class name: from nltk.chunk.named_entity import build_model build_model('binary') build_model('multiclass') nltk-2.0~b9/nltk/chunk/api.py0000644000175000017500000000347611327451576016006 0ustar bhavanibhavani# Natural Language Toolkit: Chunk parsing API # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird (minor additions) # URL: # For license information, see LICENSE.TXT ##////////////////////////////////////////////////////// ## Chunk Parser Interface ##////////////////////////////////////////////////////// from nltk.parse import ParserI import nltk class ChunkParserI(ParserI): """ A processing interface for identifying non-overlapping groups in unrestricted text. Typically, chunk parsers are used to find base syntactic constituants, such as base noun phrases. Unlike L{ParserI}, C{ChunkParserI} guarantees that the C{parse} method will always generate a parse. """ def parse(self, tokens): """ @return: the best chunk structure for the given tokens and return a tree. @param tokens: The list of (word, tag) tokens to be chunked. @type tokens: C{list} of L{tuple} @rtype: L{Tree} """ assert 0, "ChunkParserI is an abstract interface" def evaluate(self, gold): """ Score the accuracy of the chunker against the gold standard. Remove the chunking the gold standard text, rechunk it using the chunker, and return a L{ChunkScore} object reflecting the performance of this chunk peraser. @type gold: C{list} of L{Tree} @param gold: The list of chunked sentences to score the chunker on. @rtype: L{ChunkScore} """ chunkscore = nltk.chunk.util.ChunkScore() for correct in gold: chunkscore.score(correct, self.parse(correct.leaves())) return chunkscore nltk-2.0~b9/nltk/chunk/__init__.py0000644000175000017500000001714311327451576016770 0ustar bhavanibhavani# Natural Language Toolkit: Chunkers # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT # """ Classes and interfaces for identifying non-overlapping linguistic groups (such as base noun phrases) in unrestricted text. This task is called X{chunk parsing} or X{chunking}, and the identified groups are called X{chunks}. The chunked text is represented using a shallow tree called a "chunk structure." A X{chunk structure} is a tree containing tokens and chunks, where each chunk is a subtree containing only tokens. For example, the chunk structure for base noun phrase chunks in the sentence "I saw the big dog on the hill" is:: (SENTENCE: (NP: ) (NP: ) (NP: )) To convert a chunk structure back to a list of tokens, simply use the chunk structure's L{leaves} method. The C{parser.chunk} module defines L{ChunkParserI}, a standard interface for chunking texts; and L{RegexpChunkParser}, a regular-expression based implementation of that interface. It also defines L{ChunkScore}, a utility class for scoring chunk parsers. RegexpChunkParser ================= C{parse.RegexpChunkParser} is an implementation of the chunk parser interface that uses regular-expressions over tags to chunk a text. Its C{parse} method first constructs a C{ChunkString}, which encodes a particular chunking of the input text. Initially, nothing is chunked. C{parse.RegexpChunkParser} then applies a sequence of C{RegexpChunkRule}s to the C{ChunkString}, each of which modifies the chunking that it encodes. Finally, the C{ChunkString} is transformed back into a chunk structure, which is returned. C{RegexpChunkParser} can only be used to chunk a single kind of phrase. For example, you can use an C{RegexpChunkParser} to chunk the noun phrases in a text, or the verb phrases in a text; but you can not use it to simultaneously chunk both noun phrases and verb phrases in the same text. (This is a limitation of C{RegexpChunkParser}, not of chunk parsers in general.) RegexpChunkRules ---------------- C{RegexpChunkRule}s are transformational rules that update the chunking of a text by modifying its C{ChunkString}. Each C{RegexpChunkRule} defines the C{apply} method, which modifies the chunking encoded by a C{ChunkString}. The L{RegexpChunkRule} class itself can be used to implement any transformational rule based on regular expressions. There are also a number of subclasses, which can be used to implement simpler types of rules: - L{ChunkRule} chunks anything that matches a given regular expression. - L{ChinkRule} chinks anything that matches a given regular expression. - L{UnChunkRule} will un-chunk any chunk that matches a given regular expression. - L{MergeRule} can be used to merge two contiguous chunks. - L{SplitRule} can be used to split a single chunk into two smaller chunks. - L{ExpandLeftRule} will expand a chunk to incorporate new unchunked material on the left. - L{ExpandRightRule} will expand a chunk to incorporate new unchunked material on the right. Tag Patterns ~~~~~~~~~~~~ C{RegexpChunkRule}s use a modified version of regular expression patterns, called X{tag patterns}. Tag patterns are used to match sequences of tags. Examples of tag patterns are:: r'(
||)+' r'+' r'' The differences between regular expression patterns and tag patterns are: - In tag patterns, C{'<'} and C{'>'} act as parentheses; so C{'+'} matches one or more repetitions of C{''}, not C{''}. - Whitespace in tag patterns is ignored. So C{'
| '} is equivalant to C{'
|'} - In tag patterns, C{'.'} is equivalant to C{'[^{}<>]'}; so C{''} matches any single tag starting with C{'NN'}. The function L{tag_pattern2re_pattern} can be used to transform a tag pattern to an equivalent regular expression pattern. Efficiency ---------- Preliminary tests indicate that C{RegexpChunkParser} can chunk at a rate of about 300 tokens/second, with a moderately complex rule set. There may be problems if C{RegexpChunkParser} is used with more than 5,000 tokens at a time. In particular, evaluation of some regular expressions may cause the Python regular expression engine to exceed its maximum recursion depth. We have attempted to minimize these problems, but it is impossible to avoid them completely. We therefore recommend that you apply the chunk parser to a single sentence at a time. Emacs Tip --------- If you evaluate the following elisp expression in emacs, it will colorize C{ChunkString}s when you use an interactive python shell with emacs or xemacs ("C-c !"):: (let () (defconst comint-mode-font-lock-keywords '(("<[^>]+>" 0 'font-lock-reference-face) ("[{}]" 0 'font-lock-function-name-face))) (add-hook 'comint-mode-hook (lambda () (turn-on-font-lock)))) You can evaluate this code by copying it to a temporary buffer, placing the cursor after the last close parenthesis, and typing "C{C-x C-e}". You should evaluate it before running the interactive session. The change will last until you close emacs. Unresolved Issues ----------------- If we use the C{re} module for regular expressions, Python's regular expression engine generates "maximum recursion depth exceeded" errors when processing very large texts, even for regular expressions that should not require any recursion. We therefore use the C{pre} module instead. But note that C{pre} does not include Unicode support, so this module will not work with unicode strings. Note also that C{pre} regular expressions are not quite as advanced as C{re} ones (e.g., no leftward zero-length assertions). @type CHUNK_TAG_PATTERN: C{regexp} @var CHUNK_TAG_PATTERN: A regular expression to test whether a tag pattern is valid. """ from api import * from util import * from regexp import * __all__ = [ # ChunkParser interface 'ChunkParserI', # Parsers 'RegexpChunkParser', 'RegexpParser', 'ne_chunk', 'batch_ne_chunk', ] # Standard treebank POS tagger _BINARY_NE_CHUNKER = 'chunkers/maxent_ne_chunker/english_ace_binary.pickle' _MULTICLASS_NE_CHUNKER = 'chunkers/maxent_ne_chunker/english_ace_multiclass.pickle' def ne_chunk(tagged_tokens, binary=False): """ Use NLTK's currently recommended named entity chunker to chunk the given list of tagged tokens. """ if binary: chunker_pickle = _BINARY_NE_CHUNKER else: chunker_pickle = _MULTICLASS_NE_CHUNKER chunker = nltk.data.load(chunker_pickle) return chunker.parse(tagged_tokens) def batch_ne_chunk(tagged_sentences, binary=False): """ Use NLTK's currently recommended named entity chunker to chunk the given list of tagged sentences, each consisting of a list of tagged tokens. """ if binary: chunker_pickle = _BINARY_NE_CHUNKER else: chunker_pickle = _MULTICLASS_NE_CHUNKER chunker = nltk.data.load(chunker_pickle) return chunker.batch_parse(tagged_sentences) ###################################################################### #{ Deprecated ###################################################################### from nltk.internals import Deprecated class ChunkParseI(ChunkParserI, Deprecated): """Use nltk.ChunkParserI instead.""" class RegexpChunk(RegexpChunkParser, Deprecated): """Use nltk.RegexpChunkParser instead.""" class Regexp(RegexpParser, Deprecated): """Use nltk.RegexpParser instead.""" nltk-2.0~b9/nltk/chat/zen.py0000644000175000017500000002335411327451601015622 0ustar bhavanibhavani# Natural Language Toolkit: Zen Chatbot # # Copyright (C) 2001-2010 NLTK Project # Author: Amy Holland # URL: # For license information, see LICENSE.TXT """ Zen Chatbot talks in gems of Zen wisdom. This is a sample conversation with Zen Chatbot: ZC: Welcome, my child. me: Good afternoon. ZC: Ask the question you have come to ask. me: How can I achieve enlightenment? ZC: How do you suppose? me: Through meditation. ZC: Form is emptiness, and emptiness form. me: How can I empty my mind of worldly troubles? ZC: Will an answer to that really help in your search for enlightenment? me: Yes. ZC: It is better to be right than to be certain. me: I seek truth and wisdom. ZC: The search for truth is a long journey. me: Are you sure? ZC: Maybe sure, maybe not sure. The chatbot structure is based on that of chat.eliza. Thus, it uses a translation table to convert from question to response i.e. "I am" --> "you are" Of course, since Zen Chatbot does not understand the meaning of any words, responses are very limited. Zen Chatbot will usually answer very vaguely, or respond to a question by asking a different question, in much the same way as Eliza. """ from util import * # responses are matched top to bottom, so non-specific matches occur later # for each match, a list of possible responses is provided responses = ( # Zen Chatbot opens with the line "Welcome, my child." The usual # response will be a greeting problem: 'good' matches "good morning", # "good day" etc, but also "good grief!" and other sentences starting # with the word 'good' that may not be a greeting (r'(hello(.*))|(good [a-zA-Z]+)', ( "The path to enlightenment is often difficult to see.", "Greetings. I sense your mind is troubled. Tell me of your troubles.", "Ask the question you have come to ask.", "Hello. Do you seek englightenment?")), # "I need" and "I want" can be followed by a thing (eg 'help') # or an action (eg 'to see you') # # This is a problem with this style of response - # person: "I need you" # chatbot: "me can be achieved by hard work and dedication of the mind" # i.e. 'you' is not really a thing that can be mapped this way, so this # interpretation only makes sense for some inputs # (r'i need (.*)', ( "%1 can be achieved by hard work and dedication of the mind.", "%1 is not a need, but a desire of the mind. Clear your mind of such concerns.", "Focus your mind on%1, and you will find what you need.")), (r'i want (.*)', ( "Desires of the heart will distract you from the path to enlightenment.", "Will%1 help you attain enlightenment?", "Is%1 a desire of the mind, or of the heart?")), # why questions are separated into three types: # "why..I" e.g. "why am I here?" "Why do I like cake?" # "why..you" e.g. "why are you here?" "Why won't you tell me?" # "why..." e.g. "Why is the sky blue?" # problems: # person: "Why can't you tell me?" # chatbot: "Are you sure I tell you?" # - this style works for positives (e.g. "why do you like cake?") # but does not work for negatives (e.g. "why don't you like cake?") (r'why (.*) i (.*)\?', ( "You%1%2?", "Perhaps you only think you%1%2")), (r'why (.*) you(.*)\?', ( "Why%1 you%2?", "%2 I%1", "Are you sure I%2?")), (r'why (.*)\?', ( "I cannot tell you why%1.", "Why do you think %1?" )), # e.g. "are you listening?", "are you a duck" (r'are you (.*)\?', ( "Maybe%1, maybe not%1.", "Whether I am%1 or not is God's business.")), # e.g. "am I a duck?", "am I going to die?" (r'am i (.*)\?', ( "Perhaps%1, perhaps not%1.", "Whether you are%1 or not is not for me to say.")), # what questions, e.g. "what time is it?" # problems: # person: "What do you want?" # chatbot: "Seek truth, not what do me want." (r'what (.*)\?', ( "Seek truth, not what%1.", "What%1 should not concern you.")), # how questions, e.g. "how do you do?" (r'how (.*)\?', ( "How do you suppose?", "Will an answer to that really help in your search for enlightenment?", "Ask yourself not how, but why.")), # can questions, e.g. "can you run?", "can you come over here please?" (r'can you (.*)\?', ( "I probably can, but I may not.", "Maybe I can%1, and maybe I cannot.", "I can do all, and I can do nothing.")), # can questions, e.g. "can I have some cake?", "can I know truth?" (r'can i (.*)\?', ( "You can%1 if you believe you can%1, and have a pure spirit.", "Seek truth and you will know if you can%1.")), # e.g. "It is raining" - implies the speaker is certain of a fact (r'it is (.*)', ( "How can you be certain that%1, when you do not even know yourself?", "Whether it is%1 or not does not change the way the world is.")), # e.g. "is there a doctor in the house?" (r'is there (.*)\?', ( "There is%1 if you believe there is.", "It is possible that there is%1.")), # e.g. "is it possible?", "is this true?" (r'is(.*)\?', ( "%1 is not relevant.", "Does this matter?")), # non-specific question (r'(.*)\?', ( "Do you think %1?", "You seek the truth. Does the truth seek you?", "If you intentionally pursue the answers to your questions, the answers become hard to see.", "The answer to your question cannot be told. It must be experienced.")), # expression of hate of form "I hate you" or "Kelly hates cheese" (r'(.*) (hate[s]?)|(dislike[s]?)|(don\'t like)(.*)', ( "Perhaps it is not about hating %2, but about hate from within.", "Weeds only grow when we dislike them", "Hate is a very strong emotion.")), # statement containing the word 'truth' (r'(.*) truth(.*)', ( "Seek truth, and truth will seek you.", "Remember, it is not the spoon which bends - only yourself.", "The search for truth is a long journey.")), # desire to do an action # e.g. "I want to go shopping" (r'i want to (.*)', ( "You may %1 if your heart truely desires to.", "You may have to %1.")), # desire for an object # e.g. "I want a pony" (r'i want (.*)', ( "Does your heart truely desire %1?", "Is this a desire of the heart, or of the mind?")), # e.g. "I can't wait" or "I can't do this" (r'i can\'t (.*)', ( "What we can and can't do is a limitation of the mind.", "There are limitations of the body, and limitations of the mind.", "Have you tried to%1 with a clear mind?")), # "I think.." indicates uncertainty. e.g. "I think so." # problem: exceptions... # e.g. "I think, therefore I am" (r'i think (.*)', ( "Uncertainty in an uncertain world.", "Indeed, how can we be certain of anything in such uncertain times.", "Are you not, in fact, certain that%1?")), # "I feel...emotions/sick/light-headed..." (r'i feel (.*)', ( "Your body and your emotions are both symptoms of your mind." "What do you believe is the root of such feelings?", "Feeling%1 can be a sign of your state-of-mind.")), # exclaimation mark indicating emotion # e.g. "Wow!" or "No!" (r'(.*)!', ( "I sense that you are feeling emotional today.", "You need to calm your emotions.")), # because [statement] # e.g. "because I said so" (r'because (.*)', ( "Does knowning the reasons behind things help you to understand" " the things themselves?", "If%1, what else must be true?")), # yes or no - raise an issue of certainty/correctness (r'(yes)|(no)', ( "Is there certainty in an uncertain world?", "It is better to be right than to be certain.")), # sentence containing word 'love' (r'(.*)love(.*)', ( "Think of the trees: they let the birds perch and fly with no intention to call them when they come, and no longing for their return when they fly away. Let your heart be like the trees.", "Free love!")), # sentence containing word 'understand' - r (r'(.*)understand(.*)', ( "If you understand, things are just as they are;" " if you do not understand, things are just as they are.", "Imagination is more important than knowledge.")), # 'I', 'me', 'my' - person is talking about themself. # this breaks down when words contain these - eg 'Thyme', 'Irish' (r'(.*)(me )|( me)|(my)|(mine)|(i)(.*)', ( "'I', 'me', 'my'... these are selfish expressions.", "Have you ever considered that you might be a selfish person?", "Try to consider others, not just yourself.", "Think not just of yourself, but of others.")), # 'you' starting a sentence # e.g. "you stink!" (r'you (.*)', ( "My path is not of conern to you.", "I am but one, and you but one more.")), # say goodbye with some extra Zen wisdom. (r'exit', ( "Farewell. The obstacle is the path.", "Farewell. Life is a journey, not a destination.", "Good bye. We are cups, constantly and quietly being filled." "\nThe trick is knowning how to tip ourselves over and let the beautiful stuff out.")), # fall through case - # when stumped, respond with generic zen wisdom # (r'(.*)', ( "When you're enlightened, every word is wisdom.", "Random talk is useless.", "The reverse side also has a reverse side.", "Form is emptiness, and emptiness is form.", "I pour out a cup of water. Is the cup empty?")) ) zen_chatbot = Chat(responses, reflections) def zen_chat(): print '*'*75 print "Zen Chatbot!".center(75) print '*'*75 print '"Look beyond mere words and letters - look into your mind"'.center(75) print "* Talk your way to truth with Zen Chatbot." print "* Type 'quit' when you have had enough." print '*'*75 print "Welcome, my child." zen_chatbot.converse() def demo(): zen_chat() if __name__ == "__main__": demo() nltk-2.0~b9/nltk/chat/util.py0000644000175000017500000000710211327451601015774 0ustar bhavanibhavani# Natural Language Toolkit: Chatbot Utilities # # Copyright (C) 2001-2010 NLTK Project # Authors: Steven Bird # URL: # For license information, see LICENSE.TXT # Based on an Eliza implementation by Joe Strout , # Jeff Epler and Jez Higgins . import string import re import random reflections = { "am" : "are", "was" : "were", "i" : "you", "i'd" : "you would", "i've" : "you have", "i'll" : "you will", "my" : "your", "are" : "am", "you've" : "I have", "you'll" : "I will", "your" : "my", "yours" : "mine", "you" : "me", "me" : "you" } class Chat(object): def __init__(self, pairs, reflections={}): """ Initialize the chatbot. Pairs is a list of patterns and responses. Each pattern is a regular expression matching the user's statement or question, e.g. r'I like (.*)'. For each such pattern a list of possible responses is given, e.g. ['Why do you like %1', 'Did you ever dislike %1']. Material which is matched by parenthesized sections of the patterns (e.g. .*) is mapped to the numbered positions in the responses, e.g. %1. @type pairs: C{list} of C{tuple} @param pairs: The patterns and responses @type reflections: C{dict} @param reflections: A mapping between first and second person expressions @rtype: C{None} """ self._pairs = [(re.compile(x, re.IGNORECASE),y) for (x,y) in pairs] self._reflections = reflections # bug: only permits single word expressions to be mapped def _substitute(self, str): """ Substitute words in the string, according to the specified reflections, e.g. "I'm" -> "you are" @type str: C{string} @param str: The string to be mapped @rtype: C{string} """ words = "" for word in string.split(string.lower(str)): if self._reflections.has_key(word): word = self._reflections[word] words += ' ' + word return words def _wildcards(self, response, match): pos = string.find(response,'%') while pos >= 0: num = string.atoi(response[pos+1:pos+2]) response = response[:pos] + \ self._substitute(match.group(num)) + \ response[pos+2:] pos = string.find(response,'%') return response def respond(self, str): """ Generate a response to the user input. @type str: C{string} @param str: The string to be mapped @rtype: C{string} """ # check each pattern for (pattern, response) in self._pairs: match = pattern.match(str) # did the pattern match? if match: resp = random.choice(response) # pick a random response resp = self._wildcards(resp, match) # process wildcards # fix munged punctuation at the end if resp[-2:] == '?.': resp = resp[:-2] + '.' if resp[-2:] == '??': resp = resp[:-2] + '?' return resp # Hold a conversation with a chatbot def converse(self, quit="quit"): input = "" while input != quit: input = quit try: input = raw_input(">") except EOFError: print input if input: while input[-1] in "!.": input = input[:-1] print self.respond(input) nltk-2.0~b9/nltk/chat/suntsu.py0000644000175000017500000001411711327451601016364 0ustar bhavanibhavani# Natural Language Toolkit: Sun Tsu-Bot # # Copyright (C) 2001-2010 NLTK Project # Author: Sam Huston 2007 # URL: # For license information, see LICENSE.TXT from util import * """ Tsu bot responds to all queries with a Sun Tsu sayings Quoted from Sun Tsu's The Art of War Translated by LIONEL GILES, M.A. 1910 Hosted by the Gutenberg Project http://www.gutenberg.org/ """ pairs = ( (r'quit', ( "Good-bye.", "Plan well", "May victory be your future")), (r'[^\?]*\?', ("Please consider whether you can answer your own question.", "Ask me no questions!")), (r'[0-9]+(.*)', ("It is the rule in war, if our forces are ten to the enemy's one, to surround him; if five to one, to attack him; if twice as numerous, to divide our army into two.", "There are five essentials for victory")), (r'[A-Ca-c](.*)', ("The art of war is of vital importance to the State.", "All warfare is based on deception.", "If your opponent is secure at all points, be prepared for him. If he is in superior strength, evade him.", "If the campaign is protracted, the resources of the State will not be equal to the strain.", "Attack him where he is unprepared, appear where you are not expected.", "There is no instance of a country having benefited from prolonged warfare.")), (r'[D-Fd-f](.*)', ("The skillful soldier does not raise a second levy, neither are his supply-wagons loaded more than twice.", "Bring war material with you from home, but forage on the enemy.", "In war, then, let your great object be victory, not lengthy campaigns.", "To fight and conquer in all your battles is not supreme excellence; supreme excellence consists in breaking the enemy's resistance without fighting.")), (r'[G-Ig-i](.*)', ("Heaven signifies night and day, cold and heat, times and seasons.", "It is the rule in war, if our forces are ten to the enemy's one, to surround him; if five to one, to attack him; if twice as numerous, to divide our army into two.", "The good fighters of old first put themselves beyond the possibility of defeat, and then waited for an opportunity of defeating the enemy.", "One may know how to conquer without being able to do it.")), (r'[J-Lj-l](.*)', ("There are three ways in which a ruler can bring misfortune upon his army.", "By commanding the army to advance or to retreat, being ignorant of the fact that it cannot obey. This is called hobbling the army.", "By attempting to govern an army in the same way as he administers a kingdom, being ignorant of the conditions which obtain in an army. This causes restlessness in the soldier's minds.", "By employing the officers of his army without discrimination, through ignorance of the military principle of adaptation to circumstances. This shakes the confidence of the soldiers.", "There are five essentials for victory", "He will win who knows when to fight and when not to fight.", "He will win who knows how to handle both superior and inferior forces.", "He will win whose army is animated by the same spirit throughout all its ranks.", "He will win who, prepared himself, waits to take the enemy unprepared.", "He will win who has military capacity and is not interfered with by the sovereign.")), (r'[M-Om-o](.*)', ("If you know the enemy and know yourself, you need not fear the result of a hundred battles.", "If you know yourself but not the enemy, for every victory gained you will also suffer a defeat.", "If you know neither the enemy nor yourself, you will succumb in every battle.", "The control of a large force is the same principle as the control of a few men: it is merely a question of dividing up their numbers.")), (r'[P-Rp-r](.*)', ("Security against defeat implies defensive tactics; ability to defeat the enemy means taking the offensive.", "Standing on the defensive indicates insufficient strength; attacking, a superabundance of strength.", "He wins his battles by making no mistakes. Making no mistakes is what establishes the certainty of victory, for it means conquering an enemy that is already defeated.", "A victorious army opposed to a routed one, is as a pound's weight placed in the scale against a single grain.", "The onrush of a conquering force is like the bursting of pent-up waters into a chasm a thousand fathoms deep.")), (r'[S-Us-u](.*)', ("What the ancients called a clever fighter is one who not only wins, but excels in winning with ease.", "Hence his victories bring him neither reputation for wisdom nor credit for courage.", "Hence the skillful fighter puts himself into a position which makes defeat impossible, and does not miss the moment for defeating the enemy.", "In war the victorious strategist only seeks battle after the victory has been won, whereas he who is destined to defeat first fights and afterwards looks for victory.", "There are not more than five musical notes, yet the combinations of these five give rise to more melodies than can ever be heard.", "Appear at points which the enemy must hasten to defend; march swiftly to places where you are not expected.")), (r'[V-Zv-z](.*)', ("It is a matter of life and death, a road either to safety or to ruin.", "Hold out baits to entice the enemy. Feign disorder, and crush him.", "All men can see the tactics whereby I conquer, but what none can see is the strategy out of which victory is evolved.", "Do not repeat the tactics which have gained you one victory, but let your methods be regulated by the infinite variety of circumstances.", "So in war, the way is to avoid what is strong and to strike at what is weak.", "Just as water retains no constant shape, so in warfare there are no constant conditions.")), (r'(.*)', ( "Your statement insults me.", "")) ) suntsu_chatbot = Chat(pairs, reflections) def suntsu_chat(): print "Talk to the program by typing in plain English, using normal upper-" print 'and lower-case letters and punctuation. Enter "quit" when done.' print '='*72 print "You seek enlightenment?" suntsu_chatbot.converse() def demo(): suntsu_chat() if __name__ == "__main__": demo() nltk-2.0~b9/nltk/chat/rude.py0000644000175000017500000000516011327451601015760 0ustar bhavanibhavani# Natural Language Toolkit: Zen Chatbot # # Copyright (C) 2001-2010 NLTK Project # Author: Peter Spiller # URL: # For license information, see LICENSE.TXT from util import * pairs = ( (r'We (.*)', ("What do you mean, 'we'?", "Don't include me in that!", "I wouldn't be so sure about that.")), (r'You should (.*)', ("Don't tell me what to do, buddy.", "Really? I should, should I?")), (r'You\'re(.*)', ("More like YOU'RE %1!", "Hah! Look who's talking.", "Come over here and tell me I'm %1.")), (r'You are(.*)', ("More like YOU'RE %1!", "Hah! Look who's talking.", "Come over here and tell me I'm %1.")), (r'I can\'t(.*)', ("You do sound like the type who can't %1.", "Hear that splashing sound? That's my heart bleeding for you.", "Tell somebody who might actually care.")), (r'I think (.*)', ("I wouldn't think too hard if I were you.", "You actually think? I'd never have guessed...")), (r'I (.*)', ("I'm getting a bit tired of hearing about you.", "How about we talk about me instead?", "Me, me, me... Frankly, I don't care.")), (r'How (.*)', ("How do you think?", "Take a wild guess.", "I'm not even going to dignify that with an answer.")), (r'What (.*)', ("Do I look like an encylopedia?", "Figure it out yourself.")), (r'Why (.*)', ("Why not?", "That's so obvious I thought even you'd have already figured it out.")), (r'(.*)shut up(.*)', ("Make me.", "Getting angry at a feeble NLP assignment? Somebody's losing it.", "Say that again, I dare you.")), (r'Shut up(.*)', ("Make me.", "Getting angry at a feeble NLP assignment? Somebody's losing it.", "Say that again, I dare you.")), (r'Hello(.*)', ("Oh good, somebody else to talk to. Joy.", "'Hello'? How original...")), (r'(.*)', ("I'm getting bored here. Become more interesting.", "Either become more thrilling or get lost, buddy.", "Change the subject before I die of fatal boredom.")) ) rude_chatbot = Chat(pairs, reflections) def rude_chat(): print "Talk to the program by typing in plain English, using normal upper-" print 'and lower-case letters and punctuation. Enter "quit" when done.' print '='*72 print "I suppose I should say hello." rude_chatbot.converse() def demo(): rude_chat() if __name__ == "__main__": demo() nltk-2.0~b9/nltk/chat/iesha.py0000644000175000017500000000735011327451601016115 0ustar bhavanibhavani# Natural Language Toolkit: Teen Chatbot # # Copyright (C) 2001-2010 NLTK Project # Author: Selina Dennis # URL: # For license information, see LICENSE.TXT """ This chatbot is a tongue-in-cheek take on the average teen anime junky that frequents YahooMessenger or MSNM. All spelling mistakes and flawed grammar are intentional. """ from util import * reflections = { "am" : "r", "was" : "were", "i" : "u", "i'd" : "u'd", "i've" : "u'v", "ive" : "u'v", "i'll" : "u'll", "my" : "ur", "are" : "am", "you're" : "im", "you've" : "ive", "you'll" : "i'll", "your" : "my", "yours" : "mine", "you" : "me", "u" : "me", "ur" : "my", "urs" : "mine", "me" : "u" } # Note: %1/2/etc are used without spaces prior as the chat bot seems # to add a superfluous space when matching. pairs = ( (r'I\'m (.*)', ( "ur%1?? that's so cool! kekekekeke ^_^ tell me more!", "ur%1? neat!! kekeke >_<")), (r'(.*) don\'t you (.*)', ( "u think I can%2??! really?? kekeke \<_\<", "what do u mean%2??!", "i could if i wanted, don't you think!! kekeke")), (r'ye[as] [iI] (.*)', ( "u%1? cool!! how?", "how come u%1??", "u%1? so do i!!")), (r'do (you|u) (.*)\??', ( "do i%2? only on tuesdays! kekeke *_*", "i dunno! do u%2??")), (r'(.*)\?', ( "man u ask lots of questions!", "booooring! how old r u??", "boooooring!! ur not very fun")), (r'(cos|because) (.*)', ( "hee! i don't believe u! >_<", "nuh-uh! >_<", "ooooh i agree!")), (r'why can\'t [iI] (.*)', ( "i dunno! y u askin me for!", "try harder, silly! hee! ^_^", "i dunno! but when i can't%1 i jump up and down!")), (r'I can\'t (.*)', ( "u can't what??! >_<", "that's ok! i can't%1 either! kekekekeke ^_^", "try harder, silly! hee! ^&^")), (r'(.*) (like|love|watch) anime', ( "omg i love anime!! do u like sailor moon??! ^&^", "anime yay! anime rocks sooooo much!", "oooh anime! i love anime more than anything!", "anime is the bestest evar! evangelion is the best!", "hee anime is the best! do you have ur fav??")), (r'I (like|love|watch|play) (.*)', ( "yay! %2 rocks!", "yay! %2 is neat!", "cool! do u like other stuff?? ^_^")), (r'anime sucks|(.*) (hate|detest) anime', ( "ur a liar! i'm not gonna talk to u nemore if u h8 anime *;*", "no way! anime is the best ever!", "nuh-uh, anime is the best!")), (r'(are|r) (you|u) (.*)', ( "am i%1??! how come u ask that!", "maybe! y shud i tell u?? kekeke >_>")), (r'what (.*)', ( "hee u think im gonna tell u? .v.", "booooooooring! ask me somethin else!")), (r'how (.*)', ( "not tellin!! kekekekekeke ^_^",)), (r'(hi|hello|hey) (.*)', ( "hi!!! how r u!!",)), (r'quit', ( "mom says i have to go eat dinner now :,( bye!!", "awww u have to go?? see u next time!!", "how to see u again soon! ^_^")), (r'(.*)', ( "ur funny! kekeke", "boooooring! talk about something else! tell me wat u like!", "do u like anime??", "do u watch anime? i like sailor moon! ^_^", "i wish i was a kitty!! kekekeke ^_^")) ) iesha_chatbot = Chat(pairs, reflections) def iesha_chat(): print "Iesha the TeenBoT\n---------" print "Talk to the program by typing in plain English, using normal upper-" print 'and lower-case letters and punctuation. Enter "quit" when done.' print '='*72 print "hi!! i'm iesha! who r u??!" iesha_chatbot.converse() def demo(): iesha_chat() if __name__ == "__main__": demo() nltk-2.0~b9/nltk/chat/eliza.py0000644000175000017500000001601611327451601016127 0ustar bhavanibhavani# Natural Language Toolkit: Eliza # # Copyright (C) 2001-2010 NLTK Project # Authors: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT # Based on an Eliza implementation by Joe Strout , # Jeff Epler and Jez Higgins . # a translation table used to convert things you say into things the # computer says back, e.g. "I am" --> "you are" from util import * # a table of response pairs, where each pair consists of a # regular expression, and a list of possible responses, # with group-macros labelled as %1, %2. pairs = ( (r'I need (.*)', ( "Why do you need %1?", "Would it really help you to get %1?", "Are you sure you need %1?")), (r'Why don\'t you (.*)', ( "Do you really think I don't %1?", "Perhaps eventually I will %1.", "Do you really want me to %1?")), (r'Why can\'t I (.*)', ( "Do you think you should be able to %1?", "If you could %1, what would you do?", "I don't know -- why can't you %1?", "Have you really tried?")), (r'I can\'t (.*)', ( "How do you know you can't %1?", "Perhaps you could %1 if you tried.", "What would it take for you to %1?")), (r'I am (.*)', ( "Did you come to me because you are %1?", "How long have you been %1?", "How do you feel about being %1?")), (r'I\'m (.*)', ( "How does being %1 make you feel?", "Do you enjoy being %1?", "Why do you tell me you're %1?", "Why do you think you're %1?")), (r'Are you (.*)', ( "Why does it matter whether I am %1?", "Would you prefer it if I were not %1?", "Perhaps you believe I am %1.", "I may be %1 -- what do you think?")), (r'What (.*)', ( "Why do you ask?", "How would an answer to that help you?", "What do you think?")), (r'How (.*)', ( "How do you suppose?", "Perhaps you can answer your own question.", "What is it you're really asking?")), (r'Because (.*)', ( "Is that the real reason?", "What other reasons come to mind?", "Does that reason apply to anything else?", "If %1, what else must be true?")), (r'(.*) sorry (.*)', ( "There are many times when no apology is needed.", "What feelings do you have when you apologize?")), (r'Hello(.*)', ( "Hello... I'm glad you could drop by today.", "Hi there... how are you today?", "Hello, how are you feeling today?")), (r'I think (.*)', ( "Do you doubt %1?", "Do you really think so?", "But you're not sure %1?")), (r'(.*) friend (.*)', ( "Tell me more about your friends.", "When you think of a friend, what comes to mind?", "Why don't you tell me about a childhood friend?")), (r'Yes', ( "You seem quite sure.", "OK, but can you elaborate a bit?")), (r'(.*) computer(.*)', ( "Are you really talking about me?", "Does it seem strange to talk to a computer?", "How do computers make you feel?", "Do you feel threatened by computers?")), (r'Is it (.*)', ( "Do you think it is %1?", "Perhaps it's %1 -- what do you think?", "If it were %1, what would you do?", "It could well be that %1.")), (r'It is (.*)', ( "You seem very certain.", "If I told you that it probably isn't %1, what would you feel?")), (r'Can you (.*)', ( "What makes you think I can't %1?", "If I could %1, then what?", "Why do you ask if I can %1?")), (r'Can I (.*)', ( "Perhaps you don't want to %1.", "Do you want to be able to %1?", "If you could %1, would you?")), (r'You are (.*)', ( "Why do you think I am %1?", "Does it please you to think that I'm %1?", "Perhaps you would like me to be %1.", "Perhaps you're really talking about yourself?")), (r'You\'re (.*)', ( "Why do you say I am %1?", "Why do you think I am %1?", "Are we talking about you, or me?")), (r'I don\'t (.*)', ( "Don't you really %1?", "Why don't you %1?", "Do you want to %1?")), (r'I feel (.*)', ( "Good, tell me more about these feelings.", "Do you often feel %1?", "When do you usually feel %1?", "When you feel %1, what do you do?")), (r'I have (.*)', ( "Why do you tell me that you've %1?", "Have you really %1?", "Now that you have %1, what will you do next?")), (r'I would (.*)', ( "Could you explain why you would %1?", "Why would you %1?", "Who else knows that you would %1?")), (r'Is there (.*)', ( "Do you think there is %1?", "It's likely that there is %1.", "Would you like there to be %1?")), (r'My (.*)', ( "I see, your %1.", "Why do you say that your %1?", "When your %1, how do you feel?")), (r'You (.*)', ( "We should be discussing you, not me.", "Why do you say that about me?", "Why do you care whether I %1?")), (r'Why (.*)', ( "Why don't you tell me the reason why %1?", "Why do you think %1?" )), (r'I want (.*)', ( "What would it mean to you if you got %1?", "Why do you want %1?", "What would you do if you got %1?", "If you got %1, then what would you do?")), (r'(.*) mother(.*)', ( "Tell me more about your mother.", "What was your relationship with your mother like?", "How do you feel about your mother?", "How does this relate to your feelings today?", "Good family relations are important.")), (r'(.*) father(.*)', ( "Tell me more about your father.", "How did your father make you feel?", "How do you feel about your father?", "Does your relationship with your father relate to your feelings today?", "Do you have trouble showing affection with your family?")), (r'(.*) child(.*)', ( "Did you have close friends as a child?", "What is your favorite childhood memory?", "Do you remember any dreams or nightmares from childhood?", "Did the other children sometimes tease you?", "How do you think your childhood experiences relate to your feelings today?")), (r'(.*)\?', ( "Why do you ask that?", "Please consider whether you can answer your own question.", "Perhaps the answer lies within yourself?", "Why don't you tell me?")), (r'quit', ( "Thank you for talking with me.", "Good-bye.", "Thank you, that will be $150. Have a good day!")), (r'(.*)', ( "Please tell me more.", "Let's change focus a bit... Tell me about your family.", "Can you elaborate on that?", "Why do you say that %1?", "I see.", "Very interesting.", "%1.", "I see. And what does that tell you?", "How does that make you feel?", "How do you feel when you say that?")) ) eliza_chatbot = Chat(pairs, reflections) def eliza_chat(): print "Therapist\n---------" print "Talk to the program by typing in plain English, using normal upper-" print 'and lower-case letters and punctuation. Enter "quit" when done.' print '='*72 print "Hello. How are you feeling today?" eliza_chatbot.converse() def demo(): eliza_chat() if __name__ == "__main__": demo() nltk-2.0~b9/nltk/chat/__init__.py0000644000175000017500000000272011327451601016557 0ustar bhavanibhavani# Natural Language Toolkit: Chatbots # # Copyright (C) 2001-2010 NLTK Project # Authors: Steven Bird # URL: # For license information, see LICENSE.TXT # Based on an Eliza implementation by Joe Strout , # Jeff Epler and Jez Higgins . """ A class for simple chatbots. These perform simple pattern matching on sentences typed by users, and respond with automatically generated sentences. These chatbots may not work using the windows command line or the windows IDLE GUI. """ from util import * from eliza import eliza_chat from iesha import iesha_chat from rude import rude_chat from suntsu import suntsu_chat from zen import zen_chat bots = [ (eliza_chat, 'Eliza (psycho-babble)'), (iesha_chat, 'Iesha (teen anime junky)'), (rude_chat, 'Rude (abusive bot)'), (suntsu_chat, 'Suntsu (Chinese sayings)'), (zen_chat, 'Zen (gems of wisdom)')] def chatbots(): import sys print 'Which chatbot would you like to talk to?' botcount = len(bots) for i in range(botcount): print ' %d: %s' % (i+1, bots[i][1]) while True: print '\nEnter a number in the range 1-%d: ' % botcount, choice = sys.stdin.readline().strip() if choice.isdigit() and (int(choice) - 1) in range(botcount): break else: print ' Error: bad chatbot number' chatbot = bots[int(choice)-1][0] chatbot() nltk-2.0~b9/nltk/ccg/lexicon.py0000644000175000017500000001614611327451575016317 0ustar bhavanibhavani# Natural Language Toolkit: Combinatory Categorial Grammar # # Copyright (C) 2001-2010 NLTK Project # Author: Graeme Gange # URL: # For license information, see LICENSE.TXT import re from nltk import defaultdict from api import * #------------ # Regular expressions used for parsing components of the lexicon #------------ # Parses a primitive category and subscripts rePrim = re.compile(r'''([A-Za-z]+)(\[[A-Za-z,]+\])?''') # Separates the next primitive category from the remainder of the # string reNextPrim = re.compile(r'''([A-Za-z]+(?:\[[A-Za-z,]+\])?)(.*)''') # Separates the next application operator from the remainder reApp = re.compile(r'''([\\/])([.,]?)([.,]?)(.*)''') # Parses the definition of the category of either a word or a family reLex = re.compile(r'''([A-Za-z_]+)\s*(::|[-=]+>)\s*(.+)''') # Strips comments from a line reComm = re.compile('''([^#]*)(?:#.*)?''') #---------- # Lexicons #---------- class CCGLexicon(object): ''' Class representing a lexicon for CCG grammars. primitives - The list of primitive categories for the lexicon families - Families of categories entries - A mapping of words to possible categories ''' def __init__(self,start,primitives,families,entries): self._start = PrimitiveCategory(start) self._primitives = primitives self._families = families self._entries = entries # Returns all the possible categories for a word def categories(self,word): return self._entries[word] # Returns the target category for the parser def start(self): return self._start # String representation of the lexicon # Used for debugging def __str__(self): st = "" first = True for ident in self._entries.keys(): if not first: st = st + "\n" st = st + ident + " => " first = True for cat in self._entries[ident]: if not first: st = st + " | " else: first = False st = st + str(cat) return st #----------- # Parsing lexicons #----------- # Separates the contents matching the first set of brackets # from the rest of the input. def matchBrackets(string): rest = string[1:] inside = "(" while rest != "" and not rest.startswith(')'): if rest.startswith('('): (part,rest) = matchBrackets(rest) inside = inside + part else: inside = inside + rest[0] rest = rest[1:] if rest.startswith(')'): return (inside + ')',rest[1:]) raise AssertionError, 'Unmatched bracket in string \'' + string + '\'' # Separates the string for the next portion of the category # from the rest of the string def nextCategory(string): if string.startswith('('): return matchBrackets(string) return reNextPrim.match(string).groups() # Parses an application operator def parseApplication(app): return Direction(app[0],app[1:]) # Parses the subscripts for a primitive category def parseSubscripts(subscr): if subscr: return subscr[1:-1].split(',') return [] # Parse a primitive category def parsePrimitiveCategory(chunks,primitives,families,var): # If the primitive is the special category 'var', # replace it with the correct CCGVar if chunks[0] == "var": if chunks[1] == None: if var is None: var = CCGVar() return (var,var) catstr = chunks[0] if catstr in families: (cat, cvar) = families[catstr] if var is None: var = cvar else: cat = cat.substitute([(cvar,var)]) return (cat,var) if catstr in primitives: subscrs = parseSubscripts(chunks[1]) return (PrimitiveCategory(catstr,subscrs),var) raise AssertionError, 'String \'' + catstr + '\' is neither a family nor primitive category.' # parseCategory drops the 'var' from the tuple def parseCategory(line,primitives,families): return augParseCategory(line,primitives,families)[0] # Parses a string representing a category, and returns # a tuple with (possibly) the CCG variable for the category def augParseCategory(line,primitives,families,var = None): (str,rest) = nextCategory(line) if str.startswith('('): (res,var) = augParseCategory(str[1:-1],primitives,families,var) else: # print rePrim.match(str).groups() (res,var) = parsePrimitiveCategory(rePrim.match(str).groups(),primitives,families,var) while rest != "": app = reApp.match(rest).groups() dir = parseApplication(app[0:3]) rest = app[3] (str,rest) = nextCategory(rest) if str.startswith('('): (arg,var) = augParseCategory(str[1:-1],primitives,families,var) else: (arg,var) = parsePrimitiveCategory(rePrim.match(str).groups(),primitives,families,var) res = FunctionalCategory(res,arg,dir) return (res,var) # Takes an input string, and converts it into a lexicon for CCGs. def parseLexicon(lex_str): primitives = [] families = {} entries = defaultdict(list) for line in lex_str.splitlines(): # Strip comments and leading/trailing whitespace. line = reComm.match(line).groups()[0].strip() if line == "": continue if line.startswith(':-'): # A line of primitive categories. # The first line is the target category # ie, :- S, N, NP, VP primitives = primitives + [ prim.strip() for prim in line[2:].strip().split(',') ] else: # Either a family definition, or a word definition (ident, sep, catstr) = reLex.match(line).groups() (cat,var) = augParseCategory(catstr,primitives,families) if sep == '::': # Family definition # ie, Det :: NP/N families[ident] = (cat,var) else: # Word definition # ie, which => (N\N)/(S/NP) entries[ident].append(cat) return CCGLexicon(primitives[0],primitives,families,entries) openccg_tinytiny = parseLexicon(''' # Rather minimal lexicon based on the openccg `tinytiny' grammar. # Only incorporates a subset of the morphological subcategories, however. :- S,NP,N # Primitive categories Det :: NP/N # Determiners Pro :: NP IntransVsg :: S\\NP[sg] # Tensed intransitive verbs (singular) IntransVpl :: S\\NP[pl] # Plural TransVsg :: S\\NP[sg]/NP # Tensed transitive verbs (singular) TransVpl :: S\\NP[pl]/NP # Plural the => NP[sg]/N[sg] the => NP[pl]/N[pl] I => Pro me => Pro we => Pro us => Pro book => N[sg] books => N[pl] peach => N[sg] peaches => N[pl] policeman => N[sg] policemen => N[pl] boy => N[sg] boys => N[pl] sleep => IntransVsg sleep => IntransVpl eat => IntransVpl eat => TransVpl eats => IntransVsg eats => TransVsg see => TransVpl sees => TransVsg ''') nltk-2.0~b9/nltk/ccg/combinator.py0000644000175000017500000002433411327451575017011 0ustar bhavanibhavani# Natural Language Toolkit: Combinatory Categorial Grammar # # Copyright (C) 2001-2010 NLTK Project # Author: Graeme Gange # URL: # For license information, see LICENSE.TXT from api import * class UndirectedBinaryCombinator(object): """ Abstract class for representing a binary combinator. Merely defines functions for checking if the function and argument are able to be combined, and what the resulting category is. Note that as no assumptions are made as to direction, the unrestricted combinators can perform all backward, forward and crossed variations of the combinators; these restrictions must be added in the rule class. """ def can_combine(self, function, argument): raise AssertionError, 'UndirectedBinaryCombinator is an abstract interface' def combine (self,function,argument): raise AssertionError, 'UndirectedBinaryCombinator is an abstract interface' class DirectedBinaryCombinator(object): """ Wrapper for the undirected binary combinator. It takes left and right categories, and decides which is to be the function, and which the argument. It then decides whether or not they can be combined. """ def can_combine(self, left, right): raise AssertionError, 'DirectedBinaryCombinator is an abstract interface' def combine(self, left, right): raise AssertionError, 'DirectedBinaryCombinator is an abstract interface' class ForwardCombinator(DirectedBinaryCombinator): ''' Class representing combinators where the primary functor is on the left. Takes an undirected combinator, and a predicate which adds constraints restricting the cases in which it may apply. ''' def __init__(self, combinator, predicate, suffix=''): self._combinator = combinator self._predicate = predicate self._suffix = suffix def can_combine(self, left, right): return (self._combinator.can_combine(left,right) and self._predicate(left,right)) def combine(self, left, right): for cat in self._combinator.combine(left,right): yield cat def __str__(self): return '>' + str(self._combinator) + self._suffix class BackwardCombinator(DirectedBinaryCombinator): ''' The backward equivalent of the ForwardCombinator class. ''' def __init__(self, combinator, predicate, suffix=''): self._combinator = combinator self._predicate = predicate self._suffix = suffix def can_combine(self, left, right): return (self._combinator.can_combine(right, left) and self._predicate(left,right)) def combine(self, left, right): for cat in self._combinator.combine(right, left): yield cat def __str__(self): return '<' + str(self._combinator) + self._suffix class UndirectedFunctionApplication(UndirectedBinaryCombinator): """ Class representing function application. Implements rules of the form: X/Y Y -> X (>) And the corresponding backwards application rule """ def can_combine(self, function, argument): if not function.is_function(): return False return not function.arg().can_unify(argument) is None def combine(self,function,argument): if not function.is_function(): return subs = function.arg().can_unify(argument) if subs is None: return yield function.res().substitute(subs) def __str__(self): return '' # Predicates for function application. # Ensures the left functor takes an argument on the right def forwardOnly(left,right): return left.dir().is_forward() # Ensures the right functor takes an argument on the left def backwardOnly(left,right): return right.dir().is_backward() # Application combinator instances ForwardApplication = ForwardCombinator(UndirectedFunctionApplication(), forwardOnly) BackwardApplication = BackwardCombinator(UndirectedFunctionApplication(), backwardOnly) class UndirectedComposition(UndirectedBinaryCombinator): """ Functional composition (harmonic) combinator. Implements rules of the form X/Y Y/Z -> X/Z (B>) And the corresponding backwards and crossed variations. """ def can_combine(self, function, argument): # Can only combine two functions, and both functions must # allow composition. if not (function.is_function() and argument.is_function()): return False if function.dir().can_compose() and argument.dir().can_compose(): return not function.arg().can_unify(argument.res()) is None return False def combine(self, function, argument): if not (function.is_function() and argument.is_function()): return if function.dir().can_compose() and argument.dir().can_compose(): subs = function.arg().can_unify(argument.res()) if not subs is None: yield FunctionalCategory(function.res().substitute(subs), argument.arg().substitute(subs),argument.dir()) def __str__(self): return 'B' # Predicates for restricting application of straight composition. def bothForward(left,right): return left.dir().is_forward() and right.dir().is_forward() def bothBackward(left,right): return left.dir().is_backward() and right.dir().is_backward() # Predicates for crossed composition def crossedDirs(left,right): return left.dir().is_forward() and right.dir().is_backward() def backwardBxConstraint(left,right): # The functors must be crossed inwards if not crossedDirs(left, right): return False # Permuting combinators must be allowed if not left.dir().can_cross() and right.dir().can_cross(): return False # The resulting argument category is restricted to be primitive return left.arg().is_primitive() # Straight composition combinators ForwardComposition = ForwardCombinator(UndirectedComposition(), forwardOnly) BackwardComposition = BackwardCombinator(UndirectedComposition(), backwardOnly) # Backward crossed composition BackwardBx = BackwardCombinator(UndirectedComposition(),backwardBxConstraint, suffix='x') class UndirectedSubstitution(UndirectedBinaryCombinator): """ Substitution (permutation) combinator. Implements rules of the form Y/Z (X\Y)/Z -> X/Z ( N\N def innermostFunction(categ): while categ.res().is_function(): categ = categ.res() return categ class UndirectedTypeRaise(UndirectedBinaryCombinator): ''' Undirected combinator for type raising. ''' def can_combine(self,function,arg): # The argument must be a function. # The restriction that arg.res() must be a function # merely reduces redundant type-raising; if arg.res() is # primitive, we have: # X Y\X =>((>) Y # which is equivalent to # X Y\X =>(<) Y if not (arg.is_function() and arg.res().is_function()): return False arg = innermostFunction(arg) subs = left.can_unify(arg_categ.arg()) if subs is not None: return True return False def combine(self,function,arg): if not (function.is_primitive() and \ arg.is_function() and arg.res().is_function()): return # Type-raising matches only the innermost application. arg = innermostFunction(arg) subs = function.can_unify(arg.arg()) if subs is not None: xcat = arg.res().substitute(subs) yield FunctionalCategory(xcat, FunctionalCategory(xcat,function,arg.dir()), -(arg.dir())) def __str__(self): return 'T' # Predicates for type-raising # The direction of the innermost category must be towards # the primary functor. # The restriction that the variable must be primitive is not # common to all versions of CCGs; some authors have other restrictions. def forwardTConstraint(left,right): arg = innermostFunction(right) return arg.dir().is_backward() and arg.res().is_primitive() def backwardTConstraint(left,right): arg = innermostFunction(left) return arg.dir().is_forward() and arg.res().is_primitive() # Instances of type-raising combinators ForwardT = ForwardCombinator(UndirectedTypeRaise(), forwardTConstraint) BackwardT = BackwardCombinator(UndirectedTypeRaise(), backwardTConstraint) nltk-2.0~b9/nltk/ccg/chart.py0000644000175000017500000002766011327451575015762 0ustar bhavanibhavani# Natural Language Toolkit: Combinatory Categorial Grammar # # Copyright (C) 2001-2010 NLTK Project # Author: Graeme Gange # URL: # For license information, see LICENSE.TXT """ The lexicon is constructed by calling lexicon.parseLexicon(). In order to construct a parser, you also need a rule set. The standard English rules are provided in chart as chart.DefaultRuleSet The parser can then be constructed by calling, for example: parser = chart.CCGChartParser(, ) Parsing is then performed by running parser.nbest_parse(.split()) While this returns a list of trees, the default representation of the produced trees is not very enlightening, particularly given that it uses the same tree class as the CFG parsers. It is probably better to call: chart.printCCGDerivation() which should print a nice representation of the derivation. This entire process is shown far more clearly in the demonstration: python chart.py """ from nltk.parse.api import * from nltk.parse.chart import AbstractChartRule, EdgeI, Chart from nltk import Tree, defaultdict import lexicon from combinator import * # Based on the EdgeI class from NLTK. # A number of the properties of the EdgeI interface don't # transfer well to CCGs, however. class CCGEdge(EdgeI): def __init__(self,span,categ,rule): self._span = span self._categ = categ self._rule = rule # Accessors def lhs(self): return self._categ def span(self): return self._span def start(self): return self._span[0] def end(self): return self._span[1] def length(self): return self._span[1] - self.span[0] def rhs(self): return () def dot(self): return 0 def is_complete(self): return True def is_incomplete(self): return False def next(self): return None def categ(self): return self._categ def rule(self): return self._rule def __cmp__(self, other): if not isinstance(other, CCGEdge): return -1 return cmp((self._span,self._categ,self._rule), (other.span(),other.categ(),other.rule())) def __hash__(self): return hash((self._span,self._categ,self._rule)) class CCGLeafEdge(EdgeI): ''' Class representing leaf edges in a CCG derivation. ''' def __init__(self,pos,categ,leaf): self._pos = pos self._categ = categ self._leaf = leaf # Accessors def lhs(self): return self._categ def span(self): return (self._pos,self._pos+1) def start(self): return self._pos def end(self): return self._pos + 1 def length(self): return 1 def rhs(self): return self._leaf def dot(self): return 0 def is_complete(self): return True def is_incomplete(self): return False def next(self): return None def categ(self): return self._categ def leaf(self): return self._leaf def __cmp__(self, other): if not isinstance(other, CCGLeafEdge): return -1 return cmp((self._span,self._categ,self._rule), other.span(),other.categ(),other.rule()) def __hash__(self): return hash((self._pos,self._categ,self._leaf)) class BinaryCombinatorRule(AbstractChartRule): ''' Class implementing application of a binary combinator to a chart. Takes the directed combinator to apply. ''' NUMEDGES = 2 def __init__(self,combinator): self._combinator = combinator # Apply a combinator def apply_iter(self, chart, grammar, left_edge, right_edge): # The left & right edges must be touching. if not (left_edge.end() == right_edge.start()): return # Check if the two edges are permitted to combine. # If so, generate the corresponding edge. if self._combinator.can_combine(left_edge.categ(),right_edge.categ()): for res in self._combinator.combine(left_edge.categ(), right_edge.categ()): new_edge = CCGEdge(span=(left_edge.start(), right_edge.end()),categ=res,rule=self._combinator) if chart.insert(new_edge,(left_edge,right_edge)): yield new_edge # The representation of the combinator (for printing derivations) def __str__(self): return str(self._combinator) # Type-raising must be handled slightly differently to the other rules, as the # resulting rules only span a single edge, rather than both edges. class ForwardTypeRaiseRule(AbstractChartRule): ''' Class for applying forward type raising ''' NUMEDGES = 2 def __init__(self): self._combinator = ForwardT def apply_iter(self, chart, grammar, left_edge, right_edge): if not (left_edge.end() == right_edge.start()): return for res in self._combinator.combine(left_edge.categ(), right_edge.categ()): new_edge = CCGEdge(span=left_edge.span(),categ=res,rule=self._combinator) if chart.insert(new_edge,(left_edge,)): yield new_edge def __str__(self): return str(self._combinator) class BackwardTypeRaiseRule(AbstractChartRule): ''' Class for applying backward type raising. ''' NUMEDGES = 2 def __init__(self): self._combinator = BackwardT def apply_iter(self, chart, grammar, left_edge, right_edge): if not (left_edge.end() == right_edge.start()): return for res in self._combinator.combine(left_edge.categ(), right_edge.categ()): new_edge = CCGEdge(span=right_edge.span(),categ=res,rule=self._combinator) if chart.insert(new_edge,(right_edge,)): yield new_edge def __str__(self): return str(self._combinator) # Common sets of combinators used for English derivations. ApplicationRuleSet = [BinaryCombinatorRule(ForwardApplication), \ BinaryCombinatorRule(BackwardApplication)] CompositionRuleSet = [BinaryCombinatorRule(ForwardComposition), \ BinaryCombinatorRule(BackwardComposition), \ BinaryCombinatorRule(BackwardBx)] SubstitutionRuleSet = [BinaryCombinatorRule(ForwardSubstitution), \ BinaryCombinatorRule(BackwardSx)] TypeRaiseRuleSet = [ForwardTypeRaiseRule(), BackwardTypeRaiseRule()] # The standard English rule set. DefaultRuleSet = ApplicationRuleSet + CompositionRuleSet + \ SubstitutionRuleSet + TypeRaiseRuleSet class CCGChartParser(ParserI): ''' Chart parser for CCGs. Based largely on the ChartParser class from NLTK. ''' def __init__(self, lexicon, rules, trace=0): self._lexicon = lexicon self._rules = rules self._trace = trace def lexicon(self): return self._lexicon # Implements the CYK algorithm def nbest_parse(self, tokens, n=None): tokens = list(tokens) chart = CCGChart(list(tokens)) lex = self._lexicon # Initialize leaf edges. for index in range(chart.num_leaves()): for cat in lex.categories(chart.leaf(index)): new_edge = CCGLeafEdge(index, cat, chart.leaf(index)) chart.insert(new_edge, ()) # Select a span for the new edges for span in range(2,chart.num_leaves()+1): for start in range(0,chart.num_leaves()-span+1): # Try all possible pairs of edges that could generate # an edge for that span for part in range(1,span): lstart = start mid = start + part rend = start + span for left in chart.select(span=(lstart,mid)): for right in chart.select(span=(mid,rend)): # Generate all possible combinations of the two edges for rule in self._rules: edges_added_by_rule = 0 for newedge in rule.apply_iter(chart,lex,left,right): edges_added_by_rule += 1 # Output the resulting parses return chart.parses(lex.start())[:n] class CCGChart(Chart): def __init__(self, tokens): Chart.__init__(self, tokens) # Constructs the trees for a given parse. Unfortnunately, the parse trees need to be # constructed slightly differently to those in the default Chart class, so it has to # be reimplemented def _trees(self, edge, complete, memo, tree_class): if edge in memo: return memo[edge] trees = [] memo[edge] = [] if isinstance(edge,CCGLeafEdge): word = tree_class(edge.lhs(),[self._tokens[edge.start()]]) leaf = tree_class((edge.lhs(),"Leaf"),[word]) memo[edge] = leaf return leaf for cpl in self.child_pointer_lists(edge): child_choices = [self._trees(cp, complete, memo, tree_class) for cp in cpl] if len(child_choices) > 0 and type(child_choices[0]) == type(""): child_choices = [child_choices] for children in self._choose_children(child_choices): lhs = (edge.lhs(),str(edge.rule())) trees.append(tree_class(lhs, children)) memo[edge] = trees return trees #-------- # Displaying derivations #-------- def printCCGDerivation(tree): # Get the leaves and initial categories leafcats = tree.pos() leafstr = '' catstr = '' # Construct a string with both the leaf word and corresponding # category aligned. for (leaf, cat) in leafcats: nextlen = 2 + max(len(leaf),len(str(cat))) lcatlen = (nextlen - len(str(cat)))/2 rcatlen = lcatlen + (nextlen - len(str(cat)))%2 catstr += ' '*lcatlen + str(cat) + ' '*rcatlen lleaflen = (nextlen - len(leaf))/2 rleaflen = lleaflen + (nextlen - len(leaf))%2 leafstr += ' '*lleaflen + leaf + ' '*rleaflen print leafstr print catstr # Display the derivation steps printCCGTree(0,tree) # Prints the sequence of derivation steps. def printCCGTree(lwidth,tree): rwidth = lwidth # Is a leaf (word). # Increment the span by the space occupied by the leaf. if not isinstance(tree,Tree): return 2 + lwidth + len(tree) # Find the width of the current derivation step for child in tree: rwidth = max(rwidth,printCCGTree(rwidth,child)) # Is a leaf node. # Don't print anything, but account for the space occupied. if not isinstance(tree.node,tuple): return max(rwidth,2 + lwidth + len(str(tree.node)), 2 + lwidth + len(tree[0])) (res,op) = tree.node # Pad to the left with spaces, followed by a sequence of '-' # and the derivation rule. print lwidth*' ' + (rwidth-lwidth)*'-' + str(op) # Print the resulting category on a new line. respadlen = (rwidth - lwidth - len(str(res)))/2 + lwidth print respadlen*' ' + str(res) return rwidth ### Demonstration code # Construct the lexicon lex = lexicon.parseLexicon(''' :- S, NP, N, VP # Primitive categories, S is the target primitive Det :: NP/N # Family of words Pro :: NP TV :: VP/NP Modal :: (S\\NP)/VP # Backslashes need to be escaped I => Pro # Word -> Category mapping you => Pro the => Det # Variables have the special keyword 'var' # '.' prevents permutation # ',' prevents composition and => var\\.,var/.,var which => (N\\N)/(S/NP) will => Modal # Categories can be either explicit, or families. might => Modal cook => TV eat => TV mushrooms => N parsnips => N bacon => N ''') def demo(): parser = CCGChartParser(lex, DefaultRuleSet) for parse in parser.nbest_parse("I might cook and eat the bacon".split(), 3): printCCGDerivation(parse) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/ccg/api.py0000644000175000017500000002227211327451575015424 0ustar bhavanibhavani# Natural Language Toolkit: CCG Categories # # Copyright (C) 2001-2010 NLTK Project # Author: Graeme Gange # URL: # For license information, see LICENSE.TXT class AbstractCCGCategory(object): ''' Interface for categories in combinatory grammars. ''' # Returns true if the category is primitive def is_primitive(self): raise AssertionError, 'AbstractCCGCategory is an abstract interface' # Returns true if the category is a function application def is_function(self): raise AssertionError, 'AbstractCCGCategory is an abstract interface' # Returns true if the category is a variable def is_var(self): raise AssertionError, 'AbstractCCGCategory is an abstract interface' # Takes a set of (var, category) substitutions, and replaces every # occurrence of the variable with the corresponding category def substitute(self,substitutions): raise AssertionError, 'AbstractCCGCategory is an abstract interface' # Determines whether two categories can be unified. # - Returns None if they cannot be unified # - Returns a list of necessary substitutions if they can.''' def can_unify(self,other): raise AssertionError, 'AbstractCCGCategory is an abstract interface' # Utility functions: comparison, strings and hashing. def __cmp__(self,other): raise AssertionError, 'AbstractCCGCategory is an abstract interface' def __str__(self): raise AssertionError, 'AbstractCCGCategory is an abstract interface' def __hash__(self): raise AssertionError, 'AbstractCCGCategory is an abstract interface' class CCGVar(AbstractCCGCategory): ''' Class representing a variable CCG category. Used for conjunctions (and possibly type-raising, if implemented as a unary rule). ''' _maxID = 0 def __init__(self, prim_only=False): """Initialize a variable (selects a new identifier) @param prim_only: a boolean that determines whether the variable is restricted to primitives @type prim_only: C{boolean} """ self._id = self.new_id() self._prim_only = prim_only # A class method allowing generation of unique variable identifiers. def new_id(cls): cls._maxID = cls._maxID + 1 return cls._maxID - 1 new_id = classmethod(new_id) def is_primitive(self): return False def is_function(self): return False def is_var(self): return True def substitute(self, substitutions): """If there is a substitution corresponding to this variable, return the substituted category. """ for (var,cat) in substitutions: if var == self: return cat return self def can_unify(self, other): """ If the variable can be replaced with other a substitution is returned. """ if other.is_primitive() or not self._prim_only: return [(self,other)] return None def id(self): return self._id def __cmp__(self,other): if not isinstance(other,CCGVar): return -1 return cmp(self._id,other.id()) def __hash__(self): return hash(self._id) def __str__(self): return "_var" + str(self._id) class Direction: ''' Class representing the direction of a function application. Also contains maintains information as to which combinators may be used with the category. ''' def __init__(self,dir,restrictions): self._dir = dir self._restrs = restrictions # Testing the application direction def is_forward(self): return self._dir == '/' def is_backward(self): return self._dir == '\\' def dir(self): return self._dir def restrs(self): """A list of restrictions on the combinators. '.' denotes that permuting operations are disallowed ',' denotes that function composition is disallowed '_' denotes that the direction has variable restrictions. (This is redundant in the current implementation of type-raising) """ return self._restrs def is_variable(self): return self._restrs == '_' # Unification and substitution of variable directions. # Used only if type-raising is implemented as a unary rule, as it # must inherit restrictions from the argument category. def can_unify(self,other): if other.is_variable(): return [('_',self.restrs())] elif self.is_variable(): return [('_',other.restrs())] else: if self.restrs() == other.restrs(): return [] return None def substitute(self,subs): if not self.is_variable(): return self for (var, restrs) in subs: if var is '_': return Direction(self._dir,restrs) return self # Testing permitted combinators def can_compose(self): return not ',' in self._restrs def can_cross(self): return not '.' in self._restrs def __cmp__(self,other): return cmp((self._dir,self._restrs), (other.dir(),other.restrs())) return res def __hash__(self): return hash((self._dir,tuple(self._restrs))) def __str__(self): r_str = "" for r in self._restrs: r_str = r_str + str(r) return str(self._dir) + r_str # The negation operator reverses the direction of the application def __neg__(self): if self._dir == '/': return Direction('\\',self._restrs) else: return Direction('/',self._restrs) class PrimitiveCategory(AbstractCCGCategory): ''' Class representing primitive categories. Takes a string representation of the category, and a list of strings specifying the morphological subcategories. ''' def __init__(self,categ,restrictions=[]): self._categ = categ self._restrs = restrictions def is_primitive(self): return True def is_function(self): return False def is_var(self): return False def restrs(self): return self._restrs def categ(self): return self._categ # Substitution does nothing to a primitive category def substitute(self,subs): return self # A primitive can be unified with a class of the same # base category, given that the other category shares all # of its subclasses, or with a variable. def can_unify(self,other): if not other.is_primitive(): return None if other.is_var(): return [(other,self)] if other.categ() == self.categ(): for restr in self._restrs: if restr not in other.restrs(): return None return [] return None def __cmp__(self,other): if not isinstance(other,PrimitiveCategory): return -1 return cmp((self._categ,self.restrs()), (other.categ(),other.restrs())) def __hash__(self): return hash((self._categ,tuple(self._restrs))) def __str__(self): if self._restrs == []: return str(self._categ) return str(self._categ) + str(self._restrs) class FunctionalCategory(AbstractCCGCategory): ''' Class that represents a function application category. Consists of argument and result categories, together with an application direction. ''' def __init__(self,res,arg,dir,): self._res = res self._arg = arg self._dir = dir def is_primitive(self): return False def is_function(self): return True def is_var(self): return False # Substitution returns the category consisting of the # substitution applied to each of its constituents. def substitute(self,subs): sub_res = self._res.substitute(subs) sub_dir = self._dir.substitute(subs) sub_arg = self._arg.substitute(subs) return FunctionalCategory(sub_res,sub_arg,self._dir) # A function can unify with another function, so long as its # constituents can unify, or with an unrestricted variable. def can_unify(self,other): if other.is_var(): return [(other,self)] if other.is_function(): sa = self._res.can_unify(other.res()) sd = self._dir.can_unify(other.dir()) if sa != None and sd != None: sb = self._arg.substitute(sa).can_unify(other.arg().substitute(sa)) if sb != None: return sa + sb return None # Constituent accessors def arg(self): return self._arg def res(self): return self._res def dir(self): return self._dir def __cmp__(self,other): if not isinstance(other,FunctionalCategory): return -1 return cmp((self._arg,self._dir,self._res), (other.arg(),other.dir(),other.res())) def __hash__(self): return hash((self._arg,self._dir,self._res)) def __str__(self): return "(" + str(self._res) + str(self._dir) + str(self._arg) + ")" nltk-2.0~b9/nltk/ccg/__init__.py0000644000175000017500000000160311327451575016405 0ustar bhavanibhavani# Natural Language Toolkit: Combinatory Categorial Grammar # # Copyright (C) 2001-2010 NLTK Project # Author: Graeme Gange # URL: # For license information, see LICENSE.TXT """ Combinatory Categorial Grammar. For more information see nltk/doc/contrib/ccg/ccg.pdf """ from combinator import * from chart import * from lexicon import * __all__ = [ 'UndirectedBinaryCombinator', 'DirectedBinaryCombinator', 'ForwardCombinator', 'BackwardCombinator', 'UndirectedFunctionApplication', 'ForwardApplication', 'BackwardApplication', 'UndirectedComposition', 'ForwardComposition', 'BackwardComposition', 'BackwardBx', 'UndirectedSubstitution', 'ForwardSubstitution', 'BackwardSx', 'UndirectedTypeRaise', 'ForwardT', 'BackwardT', 'CCGLexicon', 'CCGEdge', 'CCGLeafEdge', 'CCGChartParser', 'CCGChart' ] nltk-2.0~b9/nltk/app/wxwordnet_app.py0000644000175000017500000010515611327451574017602 0ustar bhavanibhavani# Wordnet Interface: Graphical Wordnet Browser # # Copyright (C) 2001-2010 NLTK Project # Author: Jussi Salmela # Paul Bone # URL: # For license information, see LICENSE.TXT """ Wordnet Interface: Graphical Wordnet Browser python wxbrowse.py This has a GUI programmed using wxPython and thus needs wxPython to be installed. Features of wxbrowse.py: - A short help display at the start - Notebook UI: tabsheets with independent search histories - Menu - File functions: page save&open, print&preview - Tabsheet functions: open&close; start word search in a new tabsheet - Page history: previous&next page - Font size adjustment: increase&decrease&normalize - Show Database Info: - counts of words/synsets/relations for every POS - example words as hyperlinks, 1 word for every relation&POS pair - Help - The position and size of the browser window and the font size chosen is remembered between sessions. - Possibility to give several words/collocations to be searched and displayed at the same time by separating them with a comma in search word(s) field - Tries to deduce the stems of the search word(s) given - Showing the parts of speech for a word typed in the search field - Showing the parts of speech for a word clicked in one of the synsets - Showing the relation names for a synset when S: is clicked and hiding them again at the second click. - Showing the synsets for a relation. This covers most of the relations for nouns and some relations for other POS's also. """ import os import os.path import sys from time import sleep import pickle import platform from urllib import quote_plus, unquote_plus from itertools import groupby import wx import wx.html as html import wx.lib.wxpTag from wordnet_app import page_from_word, new_word_and_body, show_page_and_word,\ html_header, html_trailer, get_static_page_by_path,\ explanation, pg # # Comments within this code indicate future work. These comments are # marked with 'XXX' # # XXX: write these global constants in shouting case. # # Additionally, through-out this module docstrings should be entered. # frame_title = 'NLTK Wordnet Browser' help_about = frame_title + __doc__ # This is used to save options in and to be pickled at exit options_dict = {} try: nltk_prefs_dir = os.path.join(os.environ['HOME'], ".nltk") except KeyError: # XXX: This fallback may have problems on windows. nltk_prefs_dir = os.path.curdir pickle_file_name = os.path.join(nltk_prefs_dir, (frame_title + os.path.extsep + 'pkl')) ### XXX: MUST find a standard location for the stats HTML page, ### perhapsw in NLTK_DATA. WORDNET_DB_INFO_FILEPATH = 'NLTK Wordnet Browser Database Info.html' class MyHtmlWindow(html.HtmlWindow): def __init__(self, parent, id): html.HtmlWindow.__init__(self, parent, id, style=wx.NO_FULL_REPAINT_ON_RESIZE) self.parent = parent self.font_size = self.normal_font_size = \ options_dict['font_size'] self.incr_decr_font_size(0) # Keep it as it is def OnLinkClicked(self, linkinfo): href = linkinfo.GetHref() tab_to_return_to = None word = self.parent.parent.search_word.GetValue() if linkinfo.Event.ControlDown(): if linkinfo.Event.ShiftDown(): self.parent.add_html_page(activate=True) else: tab_to_return_to = self.parent.current_page self.parent.add_html_page(activate=True) self.parent.SetPageText(self.parent.current_page, word) self.parent.parent.search_word.SetValue(word) page,word = page_from_word(word, href) if page: if word: self.parent.SetPageText(self.parent.current_page, word) self.parent.parent.show_page_and_word(page, word) else: self.show_msg('The word was not found!') self.parent.parent.show_page_and_word(page) else: self.show_msg('Relation "%s" is not implemented yet!' % rel_name) # XXX: MAY simplfy the if predicate. */ if tab_to_return_to is not None: self.parent.switch_html_page(tab_to_return_to) def OnSetTitle(self, title): "no-op" pass def OnCellMouseHover(self, cell, x, y): "no-op" pass def OnOpeningURL(self, type, url): "no-op" pass def OnCellClicked(self, cell, x, y, evt): linkinfo = cell.GetLink() if linkinfo is not None: pass else: pass super(MyHtmlWindow, self).OnCellClicked(cell, x, y, evt) def incr_decr_font_size(self, change=None): global options_dict page_to_restore = self.GetParser().GetSource() if change: self.font_size += change if self.font_size <= 0: self.font_size = 1 else: self.font_size = self.normal_font_size options_dict['font_size'] = self.font_size self.SetStandardFonts(size=self.font_size) self.SetPage(page_to_restore) def show_msg(self, msg): msg1 = '*'*8 + ' ' + msg + ' ' + '*'*8 msg2 = '*'*100 for i in range(5): for msg in [msg1, msg2]: self.parent.parent.statusbar.SetStatusText(msg) wx.Yield() sleep(0.2) self.parent.parent.statusbar.SetStatusText(' ') wx.Yield() def show_page(self, page): self.SetPage(page) #---------------------------------------------------------------------------- class NB(wx.Notebook): def __init__(self, parent): wx.Notebook.__init__(self, parent, -1, size=(21,21), style=wx.BK_DEFAULT) self.parent = parent self.add_html_page() self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanged) self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGING, self.OnPageChanging) def OnPageChanged(self, event): old = event.GetOldSelection() new = event.GetSelection() sel = self.GetSelection() if old != new: # and self.current_page != new: self.switch_html_page(new) event.Skip() # XXX: MAY remove three statments with no effect. */ def OnPageChanging(self, event): old = event.GetOldSelection() new = event.GetSelection() sel = self.GetSelection() event.Skip() def switch_html_page(self, new_page): self.current_page = new_page self.ChangeSelection(new_page) self.h_w = self.GetPage(new_page) self.parent.prev_btn.Enable(self.h_w.prev_wp_list != []) self.parent.next_btn.Enable(self.h_w.next_wp_list != []) self.parent.search_word.SetValue(self.h_w.current_word) def add_html_page(self, tab_text='', activate=True): h_w = MyHtmlWindow(self, -1) if 'gtk2' in wx.PlatformInfo: h_w.SetStandardFonts() h_w.SetRelatedFrame(self.parent.frame, self.parent.titleBase + ' -- %s') h_w.SetRelatedStatusBar(0) h_w.current_word = '' # Init the word-page list for history browsing h_w.prev_wp_list = [] h_w.next_wp_list = [] self.AddPage(h_w, tab_text, activate) if activate: self.current_page = self.GetSelection() self.h_w = h_w if self.h_w.prev_wp_list == []: self.parent.prev_btn.Enable(False) if self.h_w.next_wp_list == []: self.parent.next_btn.Enable(False) return self.current_page class HtmlPanel(wx.Panel): def __init__(self, frame): wx.Panel.__init__(self, frame, -1, style=wx.NO_FULL_REPAINT_ON_RESIZE) self.frame = frame # XXX: MAY simplify setting self.cwdJust use os.getcwd(). */ self.cwd = os.path.split(sys.argv[0])[0] if not self.cwd: self.cwd = os.getcwd() if frame: self.titleBase = frame.GetTitle() self.statusbar = self.frame.CreateStatusBar() self.printer = html.HtmlEasyPrinting(frame_title) self.box = wx.BoxSizer(wx.VERTICAL) subbox_1 = wx.BoxSizer(wx.HORIZONTAL) self.prev_btn = wx.Button(self, -1, 'Previous Page') self.Bind(wx.EVT_BUTTON, self.on_prev_page, self.prev_btn) subbox_1.Add(self.prev_btn, 5, wx.GROW | wx.ALL, 2) self.next_btn = wx.Button(self, -1, 'Next Page') self.Bind(wx.EVT_BUTTON, self.on_next_page, self.next_btn) subbox_1.Add(self.next_btn, 5, wx.GROW | wx.ALL, 2) btn = wx.Button(self, -1, 'Help') self.Bind(wx.EVT_BUTTON, self.on_help, btn) subbox_1.Add(btn, 5, wx.GROW | wx.ALL, 2) btn = wx.Button(self, -1, 'Search the word(s)') self.Bind(wx.EVT_BUTTON, self.on_word_enter, btn) subbox_1.Add(btn, 5, wx.GROW | wx.ALL, 2) lbl = wx.StaticText(self, -1, 'Word(s): ', style=wx.ALIGN_RIGHT|wx.ALIGN_BOTTOM) subbox_1.Add(lbl, 5, wx.GROW | wx.ALL, 2) self.search_word = wx.TextCtrl(self, -1, '', style=wx.TE_PROCESS_ENTER) self.Bind(wx.EVT_TEXT_ENTER, self.on_word_enter, self.search_word) self.search_word.Bind(wx.EVT_LEFT_UP, self.on_mouse_up) subbox_1.Add(self.search_word, 20, wx.GROW | wx.ALL, 2) self.box.Add(subbox_1, 0, wx.GROW) self.nb = NB(self) self.box.Add(self.nb, 1, wx.GROW) self.SetSizer(self.box) self.SetAutoLayout(True) def on_key_down(self, event): event.Skip() def on_key_up(self, event): event.Skip() def on_char(self, event): event.Skip() def on_mouse_up(self, event): self.search_word.SetSelection(-1, -1) event.Skip() def on_prev_page(self, event): if self.nb.h_w.prev_wp_list: # Save current word&page&viewStart page = self.nb.h_w.GetParser().GetSource() x,y = self.nb.h_w.GetViewStart() entry = (self.nb.h_w.current_word,page,(x,y)) self.nb.h_w.next_wp_list = [entry] + self.nb.h_w.next_wp_list self.next_btn.Enable(True) # Restore previous word&page word,page,(x,y) = self.nb.h_w.prev_wp_list[-1] self.nb.h_w.prev_wp_list = self.nb.h_w.prev_wp_list[:-1] if self.nb.h_w.prev_wp_list == []: self.prev_btn.Enable(False) self.nb.h_w.current_word = word self.nb.SetPageText(self.nb.current_page, word) self.search_word.SetValue(word) self.nb.h_w.SetPage(page) self.nb.h_w.Scroll(x, y) def on_next_page(self, event): if self.nb.h_w.next_wp_list: # Save current word&page&viewStart page = self.nb.h_w.GetParser().GetSource() x,y = self.nb.h_w.GetViewStart() entry = (self.nb.h_w.current_word,page,(x,y)) self.nb.h_w.prev_wp_list.append(entry) self.prev_btn.Enable(True) # Restore next word&page word,page,(x,y) = self.nb.h_w.next_wp_list[0] self.nb.h_w.next_wp_list = self.nb.h_w.next_wp_list[1:] if self.nb.h_w.next_wp_list == []: self.next_btn.Enable(False) self.nb.h_w.current_word = word self.nb.SetPageText(self.nb.current_page, word) self.search_word.SetValue(word) self.nb.h_w.SetPage(page) self.nb.h_w.Scroll(x, y) def on_help(self, event): self.frame.on_help_help(None) def on_word_change(self, event): word = self.search_word.GetValue() if word.isalnum(): return word_2 = ''.join([x for x in word if x.isalnum() or x == ' ' or x == '-']) self.search_word.SetValue(word_2) event.Skip() def on_word_enter(self, event): if not self.nb.GetPageCount(): self.frame.on_ssw_nt(None) return word = self.search_word.GetValue() word = word.strip() if word == '': return word,body = new_word_and_body(word) if word: self.show_page_and_word(pg(word, body), word) self.nb.h_w.current_word = word self.nb.SetPageText(self.nb.current_page, word) else: self.nb.h_w.show_msg('The word was not found!') def show_page_and_word(self, page, word=None): if self.nb.h_w.current_word: # Save current word&page&viewStart curr_page = self.nb.h_w.GetParser().GetSource() x,y = self.nb.h_w.GetViewStart() entry = (self.nb.h_w.current_word,curr_page,(x,y)) self.nb.h_w.prev_wp_list.append(entry) self.prev_btn.Enable(True) # Clear forward history self.nb.h_w.next_wp_list = [] self.next_btn.Enable(False) if not word: x,y = self.nb.h_w.GetViewStart() self.nb.h_w.SetPage(page) if not word: self.nb.h_w.Scroll(x, y) if word: self.search_word.SetValue(word) self.nb.h_w.current_word = word class MyHtmlFrame(wx.Frame): def __init__(self, parent, title): #, pos, size) wx.Frame.__init__(self, parent, -1, title)#, pos, size) menu_bar = wx.MenuBar() menu_1 = wx.Menu() o_f = menu_1.Append(-1, 'Open File...\tCtrl+O') s_a = menu_1.Append(-1, 'Save Page As...\tCtrl+S') menu_1.AppendSeparator() print_ = menu_1.Append(-1, 'Print...\tCtrl+P') preview = menu_1.Append(-1, 'Preview') menu_1.AppendSeparator() ex = menu_1.Append(-1, 'Exit') menu_bar.Append(menu_1, '&File') menu_1_2 = wx.Menu() nt = menu_1_2.Append(-1, 'New tabsheet\tCtrl+T') ct = menu_1_2.Append(-1, 'Close tabsheet\tCtrl+W') menu_1_2.AppendSeparator() ssw_nt = menu_1_2.Append(-1, 'Show search word in new tabsheet\tAlt+Enter') menu_bar.Append(menu_1_2, '&Tabsheets') menu_2 = wx.Menu() prev_p = menu_2.Append(-1, 'Previous\tCtrl+Left Arrow') next_p = menu_2.Append(-1, 'Next\tCtrl+Right Arrow') menu_bar.Append(menu_2, '&Page History') menu_3 = wx.Menu() i_f = menu_3.Append(-1, 'Increase Font Size\tCtrl++ or Ctrl+Numpad+ or Ctrl+UpArrow') d_f = menu_3.Append(-1, 'Decrease Font Size\tCtrl+- or Ctrl+Numpad- or Ctrl+DownArrow') n_f = menu_3.Append(-1, 'Normal Font Size\tCtrl+0') menu_3.AppendSeparator() # The Database Info File is not supported in this version. # db_i = menu_3.Append(-1, 'Show Database Info') # menu_3.AppendSeparator() s_s = menu_3.Append(-1, 'Show HTML Source\tCtrl+U') menu_bar.Append(menu_3, '&View') menu_4 = wx.Menu() h_h = menu_4.Append(-1, 'Help') h_a = menu_4.Append(-1, 'About...') menu_bar.Append(menu_4, '&Help') self.SetMenuBar(menu_bar) self.Bind(wx.EVT_MENU, self.on_open_file, o_f) self.Bind(wx.EVT_MENU, self.on_save_as, s_a) self.Bind(wx.EVT_MENU, self.on_print, print_) self.Bind(wx.EVT_MENU, self.on_preview, preview) self.Bind(wx.EVT_MENU, self.on_exit, ex) self.Bind(wx.EVT_MENU, self.on_new_tab, nt) self.Bind(wx.EVT_MENU, self.on_close_tab, ct) self.Bind(wx.EVT_MENU, self.on_ssw_nt, ssw_nt) self.Bind(wx.EVT_MENU, self.on_prev_page, prev_p) self.Bind(wx.EVT_MENU, self.on_next_page, next_p) self.Bind(wx.EVT_MENU, self.on_incr_font, i_f) self.Bind(wx.EVT_MENU, self.on_decr_font, d_f) self.Bind(wx.EVT_MENU, self.on_norm_font, n_f) # The Database Info File is not supported in this version. # self.Bind(wx.EVT_MENU, self.on_db_info, db_i) self.Bind(wx.EVT_MENU, self.on_show_source, s_s) self.Bind(wx.EVT_MENU, self.on_help_help, h_h) self.Bind(wx.EVT_MENU, self.on_help_about, h_a) acceltbl = wx.AcceleratorTable([ (wx.ACCEL_CTRL,ord('O'),o_f.GetId()), (wx.ACCEL_CTRL,ord('S'),s_a.GetId()), (wx.ACCEL_CTRL,ord('P'),print_.GetId()), (wx.ACCEL_CTRL,wx.WXK_ADD,i_f.GetId()), (wx.ACCEL_CTRL,wx.WXK_NUMPAD_ADD,i_f.GetId()), (wx.ACCEL_CTRL,wx.WXK_UP,i_f.GetId()), (wx.ACCEL_CTRL,wx.WXK_SUBTRACT,d_f.GetId()), (wx.ACCEL_CTRL,wx.WXK_NUMPAD_SUBTRACT,d_f.GetId()), (wx.ACCEL_CTRL,wx.WXK_DOWN,d_f.GetId()), (wx.ACCEL_CTRL,ord('0'),n_f.GetId()), (wx.ACCEL_CTRL,ord('U'),s_s.GetId()), (wx.ACCEL_ALT,wx.WXK_LEFT,prev_p.GetId()), (wx.ACCEL_CTRL,wx.WXK_LEFT,prev_p.GetId()), (wx.ACCEL_ALT,wx.WXK_RIGHT,next_p.GetId()), (wx.ACCEL_CTRL,wx.WXK_RIGHT,next_p.GetId()), (wx.ACCEL_ALT,wx.WXK_RETURN,ssw_nt.GetId()), ]) self.SetAcceleratorTable(acceltbl) self.Bind(wx.EVT_SIZE, self.on_frame_resize) self.Bind(wx.EVT_MOVE, self.on_frame_move) self.Bind(wx.EVT_CLOSE, self.on_frame_close) self.panel = HtmlPanel(self) def on_key_down(self, event): event.Skip() def on_key_up(self, event): event.Skip() def on_frame_close(self, event): pos = self.GetPosition() size = self.GetSize() if pos == (-32000, -32000): # The frame is minimized, ignore pos pos = (0,0) options_dict['frame_pos'] = pos options_dict['frame_size'] = size if not os.access(nltk_prefs_dir, os.F_OK): os.mkdir(nltk_prefs_dir) pkl = open(pickle_file_name, 'wb') pickle.dump(options_dict, pkl, -1) pkl.close() event.Skip() def on_frame_resize(self, event): event.Skip() def on_frame_move(self, event): event.Skip() def on_open_file(self, event): self.load_file() def on_open_URL(self, event): self.load_url() def on_save_as(self, event): self.save_file() def on_print(self, event): self.print_() def on_preview(self, event): self.preview() def on_exit(self, event): self.Close() def on_new_tab(self, event): current_page = self.panel.nb.add_html_page() def on_close_tab(self, event): self.panel.nb.DeletePage(self.panel.nb.current_page) def on_ol_ut(self, event): pass def on_ol_ft(self, event): pass def on_ssw_nt(self, event): word = self.panel.search_word.GetValue() if word == '': return current_page = self.panel.nb.add_html_page() word,body = new_word_and_body(word) if word: self.panel.show_page_and_word(pg(word, body), word) self.panel.nb.h_w.current_word = word self.panel.nb.SetPageText(current_page, word) else: self.panel.nb.h_w.show_msg('The word was not found!') def on_prev_page(self, event): self.panel.on_prev_page(event) def on_next_page(self, event): self.panel.on_next_page(event) def on_incr_font(self, event): self.panel.nb.h_w.incr_decr_font_size(+1) def on_decr_font(self, event): self.panel.nb.h_w.incr_decr_font_size(-1) def on_norm_font(self, event): self.panel.nb.h_w.incr_decr_font_size() def on_db_info(self, event): self.show_db_info() def on_show_source(self, event): self.show_source() def on_help_help(self, event): self.read_file('wx_help.html') def on_help_about(self, event): wx.MessageBox(help_about) def show_db_info(self): word = '* Database Info *' current_page = self.panel.nb.add_html_page() self.panel.nb.SetPageText(current_page,word) try: file = open(WORDNET_DB_INFO_FILEPATH) html = file.read() file.close() except IOError: # TODO: Should give instructions for using dbinfo_html.py html = (html_header % word) + '

The database info file:'\ '

%s' % WORDNET_DB_INFO_FILEPATH + \ '

was not found. Run the dbinfo_html.py ' + \ 'script to produce it.' + html_trailer self.panel.show_page_and_word(html, word) return # These files need to be placed in a known location. def read_file(self, path): try: if not path.endswith('.htm') and not path.endswith('.html'): path += '.html' page = get_static_page_by_path(path) if path == 'NLTK Wordnet Browser Help.html': word = '* Help *' else: txt = '' + frame_title + ' display of: ' ind_0 = page.find(txt) if ind_0 == -1: err_mess = 'This file is not in NLTK Browser format!' self.panel.nb.h_w.show_msg(err_mess) return ind_1 = page.find('of: ') + len('of: ') ind_2 = page.find('') word = page[ind_1:ind_2] page = page[:ind_0] + page[ind_2+len(''):] current_page = self.panel.nb.add_html_page() self.panel.nb.SetPageText(current_page,word) self.panel.show_page_and_word(page, word) return current_page except: excpt = str(sys.exc_info()) self.panel.nb.h_w.show_msg('Unexpected error; File: ' + \ path + ' ; ' + excpt) def load_file(self): dlg = wx.FileDialog(self, wildcard = '*.htm*', style=wx.OPEN|wx.CHANGE_DIR) if dlg.ShowModal(): path = dlg.GetPath() if path == '': return self.read_file(path) dlg.Destroy() def save_file(self): dlg = wx.FileDialog(self, wildcard='*.html', style=wx.SAVE|wx.CHANGE_DIR|wx.OVERWRITE_PROMPT) if dlg.ShowModal(): path = dlg.GetPath() if path == '': self.panel.nb.h_w.show_msg('Empty Filename!') return source = self.panel.nb.h_w.GetParser().GetSource() try: if not path.endswith('.htm') and not path.endswith('.html'): path += '.html' f = open(path, 'w') f.write(source) f.close() except: excpt = str(sys.exc_info()) self.panel.nb.h_w.show_msg('Unexpected error; File: ' + \ path + ' ; ' + excpt) dlg.Destroy() def load_url(self): dlg = wx.TextEntryDialog(self, 'Enter the URL') if dlg.ShowModal(): url = dlg.GetValue() self.panel.nb.h_w.LoadPage(url) dlg.Destroy() def show_source(self): import wx.lib.dialogs source = self.panel.nb.h_w.GetParser().GetSource() dlg = wx.lib.dialogs.ScrolledMessageDialog(self, source, 'HTML Source', size=(1000, 800)) dlg.ShowModal() dlg.Destroy() def print_(self): self.panel.printer.GetPrintData().SetFilename('unnamed') html = self.panel.nb.h_w.GetParser().GetSource() self.panel.printer.PrintText(html) def preview(self): html = self.panel.nb.h_w.GetParser().GetSource() self.panel.printer.PreviewText(html) def _initalize_options(): global options_dict if os.path.exists(pickle_file_name): pkl = open(pickle_file_name, 'rb') options_dict = pickle.load(pkl) pkl.close() else: options_dict = {} if 'font_size' not in options_dict: options_dict['font_size'] = 11 if 'frame_pos' not in options_dict: options_dict['frame_pos'] = (-1,-1) if 'frame_size' not in options_dict: options_dict['frame_size'] = (-1,-1) def _adjust_pos_and_size(frame): # Try to catch the screen dimensions like this because no better # method is known i.e. start maximized, get the dims, adjust if # pickled info requires max_width,max_height = frame.GetSize() x,y = frame.GetPosition() # The following assumes that, as it seems, when wxPython frame # is created maximized, it is symmetrically oversized the same # amount of pixels. In my case (WinXP, wxPython 2.8) x==y==-4 # and the width and height are 8 pixels too large. In the # frame init it is not possible to give negative values except # (-1,-1) which has the special meaning of using the default! if x < 0: max_width += 2 * x x = 0 if y < 0: max_height += 2 * y y = 0 # If no pos_size_info was found pickled, we're OK if options_dict['frame_pos'] == (-1,-1): return # Now let's try to assure we've got sane values x,y = options_dict['frame_pos'] width,height = options_dict['frame_size'] if x < 0: width += x x = 0 if y < 0: height += y y = 0 width = min(width, max_width) height = min(height, max_height) if x + width > max_width: x -= x + width - max_width if x < 0: width += x width = min(width, max_width) x = 0 if y + height > max_height: y -= y + height - max_height if y < 0: height += y height = min(height, max_height) y = 0 frame.Maximize(False) frame.SetSize((width,height)) frame.SetPosition((x,y)) frame.Iconize(False) def get_static_wx_help_page(): """ Return static WX help page. """ return \ """ NLTK Wordnet Browser display of: * Help *

NLTK Wordnet Browser Help

The NLTK Wordnet Browser is a tool to use in browsing the Wordnet database. It tries to behave like the Wordnet project's web browser but the difference is that the NLTK Wordnet Browser uses a local Wordnet database. The NLTK Wordnet Browser has only a part of normal browser functionality and it is not an Internet browser.

For background information on Wordnet, see the Wordnet project home page: http://wordnet.princeton.edu/. For more information on the NLTK project, see the project home: http://nltk.sourceforge.net/. To get an idea of what the Wordnet version used by this browser includes choose Show Database Info from the View submenu.

The User Interface

The user interface is a so called notebook interface. This is familiar nowadays for almost everyone from Internet browsers, for example. It consists of one or more independent pages often (and here also) called tabsheets.

Every tabsheet contains its own search history which can be browsed back and forth at will. The result of a new word search will be shown on the currently active tabsheet if nothing else is wanted. It is also possible to open a new tabsheet for the search word given.

The position and size of the browser window as well as font size can be adjusted and the selections are retained between sessions.

Word search

The word to be searched is typed into the Word(s): field and the search started with Enter or by clicking the Search the word(s) button. There is no uppercase/lowercase distinction: the search word is transformed to lowercase before the search.

The result of a search is a display of one or more synsets for every part of speech in which a form of the search word was found to occur. A synset is a set of words having the same sense or meaning. Each word in a synset that is underlined is a hyperlink which can be clicked to trigger an automatic search for that word.

Every synset has a hyperlink S: at the start of its display line. Clicking that symbol shows you the name of every relation that this synset is part of. Every relation name is a hyperlink that opens up a display for that relation. Clicking it another time closes the display again. Clicking another relation name on a line that has an opened relation closes the open relation and opens the clicked relation.

It is also possible to give two or more words or collocations to be searched at the same time separating them with a comma like this cheer up,clear up, for example. Click the previous link to see what this kind of search looks like and then come back to this page by clicking the Previous Page button. As you could see the search result includes the synsets found in the same order than the forms were given in the search field.

There are also word level (lexical) relations recorded in the Wordnet database. Opening this kind of relation displays lines with a hyperlink W: at their beginning. Clicking this link shows more info on the word in question.

Menu Structure

The browser has a menubar that you can use to invoke a set of different operations. Most of the menu selections also have a corresponding keyboard shortcut.

The File Menu

Using the file menu you can open a previously saved NLTK Wordnet Browser page. Note that only pages saved with this browser can be read.

And as indicated above you can save a search page. The resulting file is a normal HTML mode file which can be viewed, printed etc. as any other HTML file.

You can also print a page and preview a page to be printed. The selected printing settings are remembered during the session.

The Tabsheets Menu

You can open an empty tabsheet and close the currently active tabsheet.

When you enter a new search word in the search word field you can make the search result be shown in a new tabsheet.

Page History

You can browse the page history of the currently active tabsheet either forwards or backwards. Next Page browses towards the newer pages and Previous Page towards the older pages.

The View Menu

You can increase, decrease and normalize the font size. The font size chosen is retained between sessions.

You can choose Show Database Info to see the word, synset and relation counts by POS as well as one example word (as a hyperlink) for every relation&POS pair occuring.

You can view the HTML source of a page if you are curious.

The Help Menu

You can view this help text as you already know. The about selection tells you something about the program.

The Keyboard Shortcuts

The following keyboard shortcuts can be used to quickly launch the desired operation.

Keyboard Shortcut Operation
Ctrl+O Open a file
Ctrl+S Save current page as
Ctrl+P Print current page
Ctrl+T Open a new (empty) tabsheet
Ctrl+W Close the current tabsheet
Ctrl+LinkClick Open the link in a new unfocused tabsheet
Ctrl+Shift+LinkClick Opent the link in a new focused tabsheet
Alt+Enter (1) Show the word in search word field in a new tabsheet
Alt+LeftArrow Previous page in page history
Ctrl+LeftArrow (2) Previous page in page history
Alt+RightArrow Next page in page history
Ctlr+RightArrow (2) Next page in page history
Ctrl++/Ctrl+Numpad+/Ctrl+UpArrow (3) Increase font size
Ctrl+-/Ctrl+Numpad-/Ctrl+DownArrow (3) Decrease font size
Ctrl+0 (4) Normal font size
Ctrl+U Show HTML source
(1)
This works only when the search word field is active i.e. the caret is in it.
(2)
These are nonstandard combinations, the usual ones being Alt+LeftArrow and Alt+RightArrow. These are still functional because there used to be difficulties with the standard ones earlier in the life of this program. Use these if the standard combinations do not work properly for you.
(3)
There are so many of these combinations because the usual i.e. Ctrl++/Ctrl+- combinations did not work on the author's laptop and the Numpad combinations were cumbersome to use. Hopefully the first ones work on the computers of others.
(4)
This combination Ctrl+0 is "Ctrl+zero" not "Ctrl+ou".
""" def app(): global options_dict app = wx.PySimpleApp() _initalize_options() frm = MyHtmlFrame(None, frame_title) #, -1, -1) # Icon handling may not be portable - don't know # This succeeds in Windows, so let's make it conditional if platform.system() == 'Windows': ico = wx.Icon('favicon.ico', wx.BITMAP_TYPE_ICO) frm.SetIcon(ico) word,body = new_word_and_body('green') frm.panel.nb.SetPageText(0,word) frm.panel.nb.h_w.current_word = word frm.panel.search_word.SetValue(word) body = explanation + body frm.panel.nb.h_w.show_page(pg('green', body)) page = frm.panel.nb.h_w.GetParser().GetSource() page = frm.panel.nb.GetPage(0).GetParser().GetSource() frm.Show() frm.Maximize(True) _adjust_pos_and_size(frm) app.MainLoop() if __name__ == '__main__': app() __all__ = ['app'] nltk-2.0~b9/nltk/app/wordnet_app.py0000644000175000017500000010765111327451574017225 0ustar bhavanibhavani# Natural Language Toolkit: WordNet Browser Application # # Copyright (C) 2001-2010 NLTK Project # Author: Jussi Salmela # Paul Bone # URL: # For license information, see LICENSE.TXT """ A WordNet Browser application which launches the default browser (if it is not already running) and opens a new tab with a connection to http://localhost:port/ . It also starts an HTTP server on the specified port and begins serving browser requests. The default port is 8000. (For command-line help, run "python wordnet -h") This application requires that the user's web browser supports Javascript. BrowServer is a server for browsing the NLTK Wordnet database It first launches a browser client to be used for browsing and then starts serving the requests of that and maybe other clients Usage:: browserver.py -h browserver.py [-s] [-p ] Options:: -h or --help Display this help message. -l or --log-file Logs messages to the given file, If this option is not specified messages are silently dropped. -p or --port Run the web server on this TCP port, defaults to 8000. -s or --server-mode Do not start a web browser, and do not allow a user to shotdown the server through the web interface. """ # TODO: throughout this package variable names and docstrings need # modifying to be compliant with NLTK's coding standards. Tests also # need to be develop to ensure this continues to work in the face of # changes to other NLTK packages. # Allow this program to run inside the NLTK source tree. from sys import path import os from sys import argv from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler from urllib import quote_plus, unquote_plus import webbrowser import datetime import re import threading import time import getopt import base64 import cPickle import copy from nltk.compat import defaultdict from nltk.corpus import wordnet as wn from nltk.corpus.reader.wordnet import Synset, Lemma # now included in local file # from util import html_header, html_trailer, \ # get_static_index_page, get_static_page_by_path, \ # page_from_word, page_from_href firstClient = True # True if we're not also running a web browser. The value f server_mode # gets set by demo(). server_mode = None # If set this is a file object for writting log messages. logfile = None class MyServerHandler(BaseHTTPRequestHandler): def do_HEAD(self): self.send_head() def do_GET(self): global firstClient sp = self.path[1:] if unquote_plus(sp) == 'SHUTDOWN THE SERVER': if server_mode: page = "Server must be killed with SIGTERM." type = "text/plain" else: print 'Server shutting down!' os._exit(0) elif sp == 'favicon.ico': type = 'image/x-icon' page = favicon_data() elif sp == '': # First request. type = 'text/html' if not server_mode and firstClient: firstClient = False page = get_static_index_page(True) else: page = get_static_index_page(False) word = 'green' elif sp.endswith('.html'): # Trying to fetch a HTML file TODO: type = 'text/html' usp = unquote_plus(sp) if usp == 'NLTK Wordnet Browser Database Info.html': word = '* Database Info *' if os.path.isfile(usp): page = open(usp).read() else: page = (html_header % word) + \ '

The database info file:'\ '

' + usp + '' + \ '

was not found. Run this:' + \ '

python dbinfo_html.py' + \ '

to produce it.' + html_trailer else: # Handle files here. word = sp page = get_static_page_by_path(usp) elif sp.startswith("search"): # This doesn't seem to work with MWEs. type = 'text/html' parts = (sp.split("?")[1]).split("&") word = [p.split("=")[1].replace("+", " ") for p in parts if p.startswith("nextWord")][0] page, word = page_from_word(word) elif sp.startswith("lookup_"): # TODO add a variation of this that takes a non ecoded word or MWE. type = 'text/html' sp = sp[len("lookup_"):] page, word = page_from_href(sp) elif sp == "start_page": # if this is the first request we should display help # information, and possibly set a default word. type = 'text/html' page, word = page_from_word("wordnet") else: type = 'text/plain' page = "Could not parse request: '%s'" % sp # Send result. self.send_head(type) self.wfile.write(page) def send_head(self, type=None): self.send_response(200) self.send_header('Content-type', type) self.end_headers() def log_message(self, format, *args): global logfile if logfile: logfile.write( "%s - - [%s] %s\n" % (self.address_string(), self.log_date_time_string(), format%args)) # This data was encoded with the following procedure def encode_icon(): f = open("favicon.ico", "rb") s = f.read() f.close() def split(s): if len(s) <= 72: return [s] else: return [s[0:72]] + split(s[72:]) print split(base64.urlsafe_b64encode(s)) FAVICON_BASE64_DATA = \ ['AAABAAEAEBAAAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAAAAAAAAAAAAAAA', 'AAAAAAAAAAD___8A9___ANb3_wDO9_8AjPf_ALXv_wCc7_8AjO__AHvv_wBz7_8Aa-__AKXn', '_wCc5_8AlOf_AITn_wBz5_8Aa-f_AGPn_wBa5_8Ac97_AGve_wBj3v8AWt7_AFLe_wBK3v8A', 'Qt7_AFrW_wBS1v8AStb_AELW_wA51v8AMdb_ACnO_wAhzv8AGM7_ABjG_wD___cA__f3APf3', '9wB73vcAUtb3AErW9wAhxvcAAMb3AFLO7wAYxu8AEMbvACG95wAYvecA9-fWAHPG1gBKvdYA', 'Ob3WACG91gDv3s4Axt7OACm1zgCMtb0ASq29ACGlvQBStbUAUq21ADGttQA5pbUA3satAEqc', 'rQDWvaUAY62lAOfGnADWvZwAtbWcAJStnADGrZQAzq2MAIycjABznIwAa5yMAN61hADWrXsA', 'zq17AMalewCtpXsAa4x7AMaccwC9nHMAtZRzAISUcwBrjHMAzqVrALWUawCtlGsArYxrAHuE', 'awBre2sAY3trAHuEYwBzhGMAc3tjAGt7YwDGlFoAvYxaAGNzWgBSa1oAxpRSAK2MUgDGjEoA', 'vYxKAL2ESgC1hEoArYRKAIRzSgB7a0oAc2tKAGtrSgBaY0oAtYRCAK17QgCle0IApXM5AJxz', 'OQCcazkAjGMxAIRaMQBzWjEAa1oxAIRaKQB7ShAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'AAAAAAAAAAAAAAAGFh4YLAYAAAAAAAAAAAAHGB4gIBYWBgAAAAAAAAAAFhgzQR45MixGMQAA', 'AAAAABQYTF0WbzplWQAAAABFVEgKFExdFG59eywWCwBAdHRJCgpLXRhxe3IvIiIDT2p0VAdh', 'fn5xbzplciAwFFNqanQ3BwoKChYYGB4gICxYanRqalRPWVRZRhMYHiAYTmlqdnZ2dnh5eX1G', 'FhgeFEVjaT1SVithKzg7WhMYGAsATmM9UjgwXDt2eFsIFgcAAAAAFDRDLUo-bnhZAAAAAAAA', 'AAgwRS1cO3Z2WgAAAAAAAAADUTZHbVJ0d0kAAAAAAAAAADFPY2pqZEgAAAAAAAAA__8AAP__', 'AAD__wAAsaEAAE5eAABOXgAA__4AAPv_AAD__wAA__8AAM3-AADw_wAA__8AAML-AAD__wAA', 'xf4='] def favicon_data(): """ Return the data for the favicon image. """ return base64.urlsafe_b64decode(''.join(FAVICON_BASE64_DATA)) def get_unique_counter_from_url(sp): """ Extract the unique counter from the URL if it has one. Otherwise return null. """ pos = sp.rfind('%23') if pos != -1: return int(sp[(pos + 3):]) else: return None def wnb(port=8000, runBrowser=True, logfilename=None): """ Run NLTK Wordnet Browser Server. @param port: The port number for the server to listen on, defaults to 8000 @type port: C{int} @param runBrowser: True to start a web browser and point it at the web server. @type runBrowser: C{boolean} """ # The webbrowser module is unpredictable, typically it blocks if it uses # a console web browser, and doesn't block if it uses a GUI webbrowser, # so we need to force it to have a clear correct behaviour. # # Normally the server should run for as long as the user wants. they # should idealy be able to control this from the UI by closing the # window or tab. Second best would be clicking a button to say # 'Shutdown' that first shutsdown the server and closes the window or # tab, or exits the text-mode browser. Both of these are unfreasable. # # The next best alternative is to start the server, have it close when # it receives SIGTERM (default), and run the browser as well. The user # may have to shutdown both programs. # # Since webbrowser may block, and the webserver will block, we must run # them in seperate threads. # global server_mode, logfile server_mode = not runBrowser # Setup logging. if logfilename: try: logfile = open(logfilename, "a", 1) # 1 means 'line buffering' except IOError, e: sys.stderr.write("Couldn't open %s for writing: %s", logfilename, e) sys.exit(1) else: logfile = None # Compute URL and start web browser url = 'http://localhost:' + str(port) if runBrowser: server_ready = threading.Event() browser_thread = startBrowser(url, server_ready) # Start the server. server = HTTPServer(('', port), MyServerHandler) if logfile: logfile.write( 'NLTK Wordnet browser server running serving: %s\n' % url) if runBrowser: server_ready.set() try: server.serve_forever() except KeyboardInterrupt: pass if runBrowser: browser_thread.join() def startBrowser(url, server_ready): def run(): server_ready.wait() time.sleep(1) # Wait a little bit more, there's still the chance of # a race condition. webbrowser.open(url, new = 2, autoraise = 1) t = threading.Thread(target=run) t.start() return t ##################################################################### # Utilities ##################################################################### """ WordNet Browser Utilities. This provides a backend to both wxbrowse and browserver.py. """ ################################################################################ # # Main logic for wordnet browser. # # This is wrapped inside a function since wn is only available if the # WordNet corpus is installed. def _pos_tuples(): return [ (wn.NOUN,'N','noun'), (wn.VERB,'V','verb'), (wn.ADJ,'J','adj'), (wn.ADV,'R','adv')] def _pos_match(pos_tuple): """ This function returns the complete pos tuple for the partial pos tuple given to it. It attempts to match it against the first non-null component of the given pos tuple. """ if pos_tuple[0] == 's': pos_tuple = ('a', pos_tuple[1], pos_tuple[2]) for n,x in enumerate(pos_tuple): if x is not None: break for pt in _pos_tuples(): if pt[n] == pos_tuple[n]: return pt return None HYPONYM = 0 HYPERNYM = 1 CLASS_REGIONAL = 2 PART_HOLONYM = 3 PART_MERONYM = 4 ATTRIBUTE = 5 SUBSTANCE_HOLONYM = 6 SUBSTANCE_MERONYM = 7 MEMBER_HOLONYM = 8 MEMBER_MERONYM = 9 VERB_GROUP = 10 INSTANCE_HYPONYM = 12 INSTANCE_HYPERNYM = 13 CAUSE = 14 ALSO_SEE = 15 SIMILAR = 16 ENTAILMENT = 17 ANTONYM = 18 FRAMES = 19 PERTAINYM = 20 CLASS_CATEGORY = 21 CLASS_USAGE = 22 CLASS_REGIONAL = 23 CLASS_USAGE = 24 CLASS_CATEGORY = 11 DERIVATIONALLY_RELATED_FORM = 25 INDIRECT_HYPERNYMS = 26 def lemma_property(word, synset, func): def flattern(l): if l == []: return [] else: return l[0] + flattern(l[1:]) return flattern([func(l) for l in synset.lemmas if l.name == word]) def rebuild_tree(orig_tree): node = orig_tree[0] children = orig_tree[1:] return (node, [rebuild_tree(t) for t in children]) def get_relations_data(word, synset): """ Get synset relations data for a synset. Note that this doesn't yet support things such as full hyponym vs direct hyponym. """ if synset.pos == wn.NOUN: return ((HYPONYM, 'Hyponyms', synset.hyponyms()), (INSTANCE_HYPONYM , 'Instance hyponyms', synset.instance_hyponyms()), (HYPERNYM, 'Direct hypernyms', synset.hypernyms()), (INDIRECT_HYPERNYMS, 'Indirect hypernyms', rebuild_tree(synset.tree(lambda x: x.hypernyms()))[1]), # hypernyms', 'Sister terms', (INSTANCE_HYPERNYM , 'Instance hypernyms', synset.instance_hypernyms()), # (CLASS_REGIONAL, ['domain term region'], ), (PART_HOLONYM, 'Part holonyms', synset.part_holonyms()), (PART_MERONYM, 'Part meronyms', synset.part_meronyms()), (SUBSTANCE_HOLONYM, 'Substance holonyms', synset.substance_holonyms()), (SUBSTANCE_MERONYM, 'Substance meronyms', synset.substance_meronyms()), (MEMBER_HOLONYM, 'Member holonyms', synset.member_holonyms()), (MEMBER_MERONYM, 'Member meronyms', synset.member_meronyms()), (ATTRIBUTE, 'Attributes', synset.attributes()), (ANTONYM, "Antonyms", lemma_property(word, synset, lambda l: l.antonyms())), (DERIVATIONALLY_RELATED_FORM, "Derivationally related form", lemma_property(word, synset, lambda l: l.derivationally_related_forms()))) elif synset.pos == wn.VERB: return ((ANTONYM, 'Antonym', lemma_property(word, synset, lambda l: l.antonyms())), (HYPONYM, 'Hyponym', synset.hyponyms()), (HYPERNYM, 'Direct hypernyms', synset.hypernyms()), (INDIRECT_HYPERNYMS, 'Indirect hypernyms', rebuild_tree(synset.tree(lambda x: x.hypernyms()))[1]), (ENTAILMENT, 'Entailments', synset.entailments()), (CAUSE, 'Causes', synset.causes()), (ALSO_SEE, 'Also see', synset.also_sees()), (VERB_GROUP, 'Verb Groups', synset.verb_groups()), (DERIVATIONALLY_RELATED_FORM, "Derivationally related form", lemma_property(word, synset, lambda l: l.derivationally_related_forms()))) elif synset.pos == wn.ADJ or synset.pos == wn.ADJ_SAT: return ((ANTONYM, 'Antonym', lemma_property(word, synset, lambda l: l.antonyms())), (SIMILAR, 'Similar to', synset.similar_tos()), # Participle of verb - not supported by corpus (PERTAINYM, 'Pertainyms', lemma_property(word, synset, lambda l: l.pertainyms())), (ATTRIBUTE, 'Attributes', synset.attributes()), (ALSO_SEE, 'Also see', synset.also_sees())) elif synset.pos == wn.ADV: # This is weird. adverbs such as 'quick' and 'fast' don't seem # to have antonyms returned by the corpus.a return ((ANTONYM, 'Antonym', lemma_property(word, synset, lambda l: l.antonyms())),) # Derived from adjective - not supported by corpus else: raise TypeError("Unhandles synset POS type: " + str(synset.pos)) html_header = ''' NLTK Wordnet Browser display of: %s ''' html_trailer = ''' ''' explanation = '''

Search Help

  • The display below the line is an example of the output the browser shows you when you enter a search word. The search word was green.
  • The search result shows for different parts of speech the synsets i.e. different meanings for the word.
  • All underlined texts are hypertext links. There are two types of links: word links and others. Clicking a word link carries out a search for the word in the Wordnet database.
  • Clicking a link of the other type opens a display section of data attached to that link. Clicking that link a second time closes the section again.
  • Clicking S: opens a section showing the relations for that synset.
  • Clicking on a relation name opens a section that displays the associated synsets.
  • Type a search word in the Word field and start the search by the Enter/Return key or click the Search button.

''' # HTML oriented functions def _bold(txt): return '%s' % txt def _center(txt): return '
%s
' % txt def _hlev(n,txt): return '%s' % (n,txt,n) def _italic(txt): return '%s' % txt def _li(txt): return '
  • %s
  • ' % txt def pg(word, body): ''' Return a HTML page of NLTK Browser format constructed from the word and body @param word: The word that the body corresponds to @type word: str @param body: The HTML body corresponding to the word @type body: str @return: a HTML page for the word-body combination @rtype: str ''' return (html_header % word) + body + html_trailer def _ul(txt): return '
      ' + txt + '
    ' def _abbc(txt): """ abbc = asterisks, breaks, bold, center """ return _center(_bold('
    '*10 + '*'*10 + ' ' + txt + ' ' + '*'*10)) full_hyponym_cont_text = \ _ul(_li(_italic('(has full hyponym continuation)'))) + '\n' def _get_synset(synset_key): """ The synset key is the unique name of the synset, this can be retrived via synset.name """ return wn.synset(synset_key) def _collect_one_synset(word, synset, synset_relations): ''' Returns the HTML string for one synset or word @param word: the current word @type word: str @param synset: a synset @type synset: synset @param synset_relations: information about which synset relations to display. @type synset_relations: dict(synset_key, set(relation_id)) @return: The HTML string built for this synset @rtype: str ''' if isinstance(synset, tuple): # It's a word raise NotImplementedError("word not supported by _collect_one_synset") typ = 'S' pos_tuple = _pos_match((synset.pos, None, None)) assert pos_tuple != None, "pos_tuple is null: synset.pos: %s" % synset.pos descr = pos_tuple[2] ref = copy.deepcopy(Reference(word, synset_relations)) ref.toggle_synset(synset) synset_label = typ + ";" if synset.name in synset_relations.keys(): synset_label = _bold(synset_label) s = '
  • %s (%s) ' % (make_lookup_link(ref, synset_label), descr) def format_lemma(w): w = w.replace('_', ' ') if w.lower() == word: return _bold(w) else: ref = Reference(w) return make_lookup_link(ref, w) s += ', '.join([format_lemma(l.name) for l in synset.lemmas]) gl = " (%s) %s " % \ (synset.definition, "; ".join(["\"%s\"" % e for e in synset.examples])) return s + gl + _synset_relations(word, synset, synset_relations) + '
  • \n' def _collect_all_synsets(word, pos, synset_relations=dict()): """ Return a HTML unordered list of synsets for the given word and part of speach. """ return '
      %s\n
    \n' % \ ''.join((_collect_one_synset(word, synset, synset_relations) for synset in wn.synsets(word, pos))) def _synset_relations(word, synset, synset_relations): ''' Builds the HTML string for the relations of a synset @param word: The current word @type word: str @param synset: The synset for which we're building the relations. @type synset: Synset @param synset_relations: synset keys and relation types for which to display relations. @type synset_relations: dict(synset_key, set(relation_type)) @return: The HTML for a synset's relations @rtype: str ''' if not synset.name in synset_relations.keys(): return "" ref = Reference(word, synset_relations) def relation_html(r): if type(r) == Synset: return make_lookup_link(Reference(r.lemma_names[0]), r.lemma_names[0]) elif type(r) == Lemma: return relation_html(r.synset) elif type(r) == tuple: # It's probably a tuple containing a Synset and a list of # similar tuples. This forms a tree of synsets. return "%s\n
      %s
    \n" % \ (relation_html(r[0]), ''.join('
  • %s
  • \n' % relation_html(sr) for sr in r[1])) else: raise TypeError("r must be a synset, lemma or list, it was: type(r) = %s, r = %s" % (type(r), r)) def make_synset_html((db_name, disp_name, rels)): synset_html = '%s\n' % \ make_lookup_link( copy.deepcopy(ref).toggle_synset_relation(synset, db_name).encode(), disp_name) if db_name in ref.synset_relations[synset.name]: synset_html += '
      %s
    \n' % \ ''.join("
  • %s
  • \n" % relation_html(r) for r in rels) return synset_html html = '
      ' + \ '\n'.join(("
    • %s
    • " % make_synset_html(x) for x in get_relations_data(word, synset) if x[2] != [])) + \ '
    ' return html class Reference(object): """ A reference to a page that may be generated by page_word """ def __init__(self, word, synset_relations=dict()): """ Build a reference to a new page. word is the word or words (seperated by commas) for which to search for synsets of synset_relations is a dictionary of synset keys to sets of synset relation identifaiers to unfold a list of synset relations for. """ self.word = word self.synset_relations = synset_relations def encode(self): """ Encode this reference into a string to be used in a URL. """ # This uses a tuple rather than an object since the python # pickle representation is much smaller and there is no need # to represent the complete object. string = cPickle.dumps((self.word, self.synset_relations), -1) return base64.urlsafe_b64encode(string) def toggle_synset_relation(self, synset, relation): """ Toggle the display of the relations for the given synset and relation type. This function will throw a KeyError if the synset is currently not being displayed. """ if relation in self.synset_relations[synset.name]: self.synset_relations[synset.name].remove(relation) else: self.synset_relations[synset.name].add(relation) return self def toggle_synset(self, synset): """ Toggle displaying of the relation types for the given synset """ if synset.name in self.synset_relations.keys(): del self.synset_relations[synset.name] else: self.synset_relations[synset.name] = set() return self def decode_reference(string): """ Decode a reference encoded with Reference.encode """ string = base64.urlsafe_b64decode(string) word, synset_relations = cPickle.loads(string) return Reference(word, synset_relations) def make_lookup_link(ref, label): return '%s' % (ref.encode(), label) def page_from_word(word): """ Return a HTML page for the given word. @param word: The currently active word @type word: str @return: A tuple (page,word), where page is the new current HTML page to be sent to the browser and word is the new current word @rtype: A tuple (str,str) """ return page_from_reference(Reference(word)) def page_from_href(href): ''' Returns a tuple of the HTML page built and the new current word @param href: The hypertext reference to be solved @type href: str @return: A tuple (page,word), where page is the new current HTML page to be sent to the browser and word is the new current word @rtype: A tuple (str,str) ''' return page_from_reference(decode_reference(href)) def page_from_reference(href): ''' Returns a tuple of the HTML page built and the new current word @param href: The hypertext reference to be solved @type href: str @return: A tuple (page,word), where page is the new current HTML page to be sent to the browser and word is the new current word @rtype: A tuple (str,str) ''' word = href.word pos_forms = defaultdict(list) words = word.split(',') words = [w for w in [w.strip().lower().replace(' ', '_') for w in words] if w != ""] if len(words) == 0: # No words were found. return "", "Please specify a word to search for." # This looks up multiple words at once. This is probably not # necessary and may lead to problems. for pos in [wn.NOUN, wn.VERB, wn.ADJ, wn.ADV]: form = wn.morphy(w, pos) if form and form not in pos_forms[pos]: pos_forms[pos].append(form) body = '' for pos,pos_str,name in _pos_tuples(): if pos in pos_forms: body += _hlev(3, name) + '\n' for w in pos_forms[pos]: # Not all words of exc files are in the database, skip # to the next word if a KeyError is raised. try: body += _collect_all_synsets(w, pos, href.synset_relations) except KeyError: pass if not body: body = "The word or words '%s' where not found in the dictonary." % word return body, word ##################################################################### # Static pages ##################################################################### def get_static_page_by_path(path): """ Return a static HTML page from the path given. """ if path == "index_2.html": return get_static_index_page(False) elif path == "index.html": return get_static_index_page(True) elif path == "NLTK Wordnet Browser Database Info.html": return "Display of Wordnet Database Statistics is not supported" elif path == "upper_2.html": return get_static_upper_page(False) elif path == "upper.html": return get_static_upper_page(True) elif path == "web_help.html": return get_static_web_help_page() elif path == "wx_help.html": return get_static_wx_help_page() else: return "Internal error: Path for static page '%s' is unknown" % path f = open(path) page = f.read() f.close() return page def get_static_web_help_page(): """ Return the static web help page. """ return \ """ NLTK Wordnet Browser display of: * Help *

    NLTK Wordnet Browser Help

    The NLTK Wordnet Browser is a tool to use in browsing the Wordnet database. It tries to behave like the Wordnet project's web browser but the difference is that the NLTK Wordnet Browser uses a local Wordnet database.

    You are using the Javascript client part of the NLTK Wordnet BrowseServer. We assume your browser is in tab sheets enabled mode.

    For background information on Wordnet, see the Wordnet project home page: http://wordnet.princeton.edu/. For more information on the NLTK project, see the project home: http://nltk.sourceforge.net/. To get an idea of what the Wordnet version used by this browser includes choose Show Database Info from the View submenu.

    Word search

    The word to be searched is typed into the New Word field and the search started with Enter or by clicking the Search button. There is no uppercase/lowercase distinction: the search word is transformed to lowercase before the search.

    In addition, the word does not have to be in base form. The browser tries to find the possible base form(s) by making certain morphological substitutions. Typing fLIeS as an obscure example gives one this. Click the previous link to see what this kind of search looks like and then come back to this page by using the Alt+LeftArrow key combination.

    The result of a search is a display of one or more synsets for every part of speech in which a form of the search word was found to occur. A synset is a set of words having the same sense or meaning. Each word in a synset that is underlined is a hyperlink which can be clicked to trigger an automatic search for that word.

    Every synset has a hyperlink S: at the start of its display line. Clicking that symbol shows you the name of every relation that this synset is part of. Every relation name is a hyperlink that opens up a display for that relation. Clicking it another time closes the display again. Clicking another relation name on a line that has an opened relation closes the open relation and opens the clicked relation.

    It is also possible to give two or more words or collocations to be searched at the same time separating them with a comma like this cheer up,clear up, for example. Click the previous link to see what this kind of search looks like and then come back to this page by using the Alt+LeftArrow key combination. As you could see the search result includes the synsets found in the same order than the forms were given in the search field.

    There are also word level (lexical) relations recorded in the Wordnet database. Opening this kind of relation displays lines with a hyperlink W: at their beginning. Clicking this link shows more info on the word in question.

    The Buttons

    The Search and Help buttons need no more explanation.

    The Show Database Info button shows a collection of Wordnet database statistics.

    The Shutdown the Server button is shown for the first client of the BrowServer program i.e. for the client that is automatically launched when the BrowServer is started but not for the succeeding clients in order to protect the server from accidental shutdowns.

    """ def get_static_welcome_message(): """ Get the static welcome page. """ return \ """

    Search Help

    • The display below the line is an example of the output the browser shows you when you enter a search word. The search word was green.
    • The search result shows for different parts of speech the synsets i.e. different meanings for the word.
    • All underlined texts are hypertext links. There are two types of links: word links and others. Clicking a word link carries out a search for the word in the Wordnet database.
    • Clicking a link of the other type opens a display section of data attached to that link. Clicking that link a second time closes the section again.
    • Clicking S: opens a section showing the relations for that synset.
    • Clicking on a relation name opens a section that displays the associated synsets.
    • Type a search word in the Next Word field and start the search by the Enter/Return key or click the Search button.
    """ def get_static_index_page(with_shutdown): """ Get the static index page. """ template = \ """ NLTK Wordnet Browser """ if with_shutdown: upper_link = "upper.html" else: upper_link = "upper_2.html" return template % upper_link def get_static_upper_page(with_shutdown): """ Return the upper frame page, If with_shutdown is True then a 'shutdown' button is also provided to shutdown the server. """ template = \ """ Untitled Document
    Current Word:  Next Word: 
    Help %s """ if with_shutdown: shutdown_link = "Shutdown" else: shutdown_link = "" return template % shutdown_link def usage(): """ Display the command line help message. """ print __doc__ def app(): # Parse and interpret options. (opts, _) = getopt.getopt(argv[1:], "l:p:sh", ["logfile=", "port=", "server-mode", "help"]) port = 8000 server_mode = False help_mode = False logfilename = None for (opt, value) in opts: if (opt == "-l") or (opt == "--logfile"): logfilename = str(value) elif (opt == "-p") or (opt == "--port"): port = int(value) elif (opt == "-s") or (opt == "--server-mode"): server_mode = True elif (opt == "-h") or (opt == "--help"): help_mode = True if help_mode: usage() else: wnb(port, not server_mode, logfilename) if __name__ == '__main__': app() __all__ = ['app'] nltk-2.0~b9/nltk/app/wordfreq_app.py0000644000175000017500000000154111327451574017363 0ustar bhavanibhavani# Natural Language Toolkit: Wordfreq Application # # Copyright (C) 2001-2010 NLTK Project # Author: Sumukh Ghodke # URL: # For license information, see LICENSE.TXT # import pylab import nltk.text from nltk.corpus import gutenberg def plot_word_freq_dist(text): fd = text.vocab() samples = fd.keys()[:50] values = [fd[sample] for sample in samples] values = [sum(values[:i+1]) * 100.0/fd.N() for i in range(len(values))] pylab.title(text.name) pylab.xlabel("Samples") pylab.ylabel("Cumulative Percentage") pylab.plot(values) pylab.xticks(range(len(samples)), [str(s) for s in samples], rotation=90) pylab.show() def app(): t1 = nltk.Text(gutenberg.words('melville-moby_dick.txt')) plot_word_freq_dist(t1) if __name__ == '__main__': app() __all__ = ['app'] nltk-2.0~b9/nltk/app/srparser_app.py0000644000175000017500000010021411327451574017370 0ustar bhavanibhavani# Natural Language Toolkit: Shift-Reduce Parser Application # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # # $Id: srparser_app.py 8479 2010-01-13 05:40:34Z StevenBird1 $ """ A graphical tool for exploring the shift-reduce parser. The shift-reduce parser maintains a stack, which records the structure of the portion of the text that has been parsed. The stack is initially empty. Its contents are shown on the left side of the main canvas. On the right side of the main canvas is the remaining text. This is the portion of the text which has not yet been considered by the parser. The parser builds up a tree structure for the text using two operations: - "shift" moves the first token from the remaining text to the top of the stack. In the demo, the top of the stack is its right-hand side. - "reduce" uses a grammar production to combine the rightmost stack elements into a single tree token. You can control the parser's operation by using the "shift" and "reduce" buttons; or you can use the "step" button to let the parser automatically decide which operation to apply. The parser uses the following rules to decide which operation to apply: - Only shift if no reductions are available. - If multiple reductions are available, then apply the reduction whose CFG production is listed earliest in the grammar. The "reduce" button applies the reduction whose CFG production is listed earliest in the grammar. There are two ways to manually choose which reduction to apply: - Click on a CFG production from the list of available reductions, on the left side of the main window. The reduction based on that production will be applied to the top of the stack. - Click on one of the stack elements. A popup window will appear, containing all available reductions. Select one, and it will be applied to the top of the stack. Note that reductions can only be applied to the top of the stack. Keyboard Shortcuts:: [Space]\t Perform the next shift or reduce operation [s]\t Perform a shift operation [r]\t Perform a reduction operation [Ctrl-z]\t Undo most recent operation [Delete]\t Reset the parser [g]\t Show/hide available production list [Ctrl-a]\t Toggle animations [h]\t Help [Ctrl-p]\t Print [q]\t Quit """ """ Possible future improvements: - button/window to change and/or select text. Just pop up a window with an entry, and let them modify the text; and then retokenize it? Maybe give a warning if it contains tokens whose types are not in the grammar. - button/window to change and/or select grammar. Select from several alternative grammars? Or actually change the grammar? If the later, then I'd want to define nltk.draw.cfg, which would be responsible for that. """ import string import nltk from nltk.util import in_idle from nltk.draw.util import * from nltk.draw.tree import * from nltk.draw.cfg import CFGEditor class ShiftReduceApp(object): """ A graphical tool for exploring the shift-reduce parser. The tool displays the parser's stack and the remaining text, and allows the user to control the parser's operation. In particular, the user can shift tokens onto the stack, and can perform reductions on the top elements of the stack. A "step" button simply steps through the parsing process, performing the operations that C{nltk.parse.ShiftReduceParser} would use. """ def __init__(self, grammar, sent, trace=0): self._sent = sent self._parser = nltk.parse.SteppingShiftReduceParser(grammar, trace) # Set up the main window. self._top = Tk() self._top.title('Shift Reduce Parser Application') # Animations. animating_lock is a lock to prevent the demo # from performing new operations while it's animating. self._animating_lock = 0 self._animate = IntVar(self._top) self._animate.set(10) # = medium # The user can hide the grammar. self._show_grammar = IntVar(self._top) self._show_grammar.set(1) # Initialize fonts. self._init_fonts(self._top) # Set up key bindings. self._init_bindings() # Create the basic frames. self._init_menubar(self._top) self._init_buttons(self._top) self._init_feedback(self._top) self._init_grammar(self._top) self._init_canvas(self._top) # A popup menu for reducing. self._reduce_menu = Menu(self._canvas, tearoff=0) # Reset the demo, and set the feedback frame to empty. self.reset() self._lastoper1['text'] = '' ######################################### ## Initialization Helpers ######################################### def _init_fonts(self, root): # See: self._sysfont = tkFont.Font(font=Button()["font"]) root.option_add("*Font", self._sysfont) # TWhat's our font size (default=same as sysfont) self._size = IntVar(root) self._size.set(self._sysfont.cget('size')) self._boldfont = tkFont.Font(family='helvetica', weight='bold', size=self._size.get()) self._font = tkFont.Font(family='helvetica', size=self._size.get()) def _init_grammar(self, parent): # Grammar view. self._prodframe = listframe = Frame(parent) self._prodframe.pack(fill='both', side='left', padx=2) self._prodlist_label = Label(self._prodframe, font=self._boldfont, text='Available Reductions') self._prodlist_label.pack() self._prodlist = Listbox(self._prodframe, selectmode='single', relief='groove', background='white', foreground='#909090', font=self._font, selectforeground='#004040', selectbackground='#c0f0c0') self._prodlist.pack(side='right', fill='both', expand=1) self._productions = list(self._parser.grammar().productions()) for production in self._productions: self._prodlist.insert('end', (' %s' % production)) self._prodlist.config(height=min(len(self._productions), 25)) # Add a scrollbar if there are more than 25 productions. if 1:#len(self._productions) > 25: listscroll = Scrollbar(self._prodframe, orient='vertical') self._prodlist.config(yscrollcommand = listscroll.set) listscroll.config(command=self._prodlist.yview) listscroll.pack(side='left', fill='y') # If they select a production, apply it. self._prodlist.bind('<>', self._prodlist_select) # When they hover over a production, highlight it. self._hover = -1 self._prodlist.bind('', self._highlight_hover) self._prodlist.bind('', self._clear_hover) def _init_bindings(self): # Quit self._top.bind('', self.destroy) self._top.bind('', self.destroy) self._top.bind('', self.destroy) self._top.bind('', self.destroy) # Ops (step, shift, reduce, undo) self._top.bind('', self.step) self._top.bind('', self.shift) self._top.bind('', self.shift) self._top.bind('', self.shift) self._top.bind('', self.reduce) self._top.bind('', self.reduce) self._top.bind('', self.reduce) self._top.bind('', self.reset) self._top.bind('', self.undo) self._top.bind('', self.undo) self._top.bind('', self.undo) self._top.bind('', self.undo) self._top.bind('', self.undo) # Misc self._top.bind('', self.postscript) self._top.bind('', self.help) self._top.bind('', self.help) self._top.bind('', self.edit_grammar) self._top.bind('', self.edit_sentence) # Animation speed control self._top.bind('-', lambda e,a=self._animate:a.set(20)) self._top.bind('=', lambda e,a=self._animate:a.set(10)) self._top.bind('+', lambda e,a=self._animate:a.set(4)) def _init_buttons(self, parent): # Set up the frames. self._buttonframe = buttonframe = Frame(parent) buttonframe.pack(fill='none', side='bottom') Button(buttonframe, text='Step', background='#90c0d0', foreground='black', command=self.step,).pack(side='left') Button(buttonframe, text='Shift', underline=0, background='#90f090', foreground='black', command=self.shift).pack(side='left') Button(buttonframe, text='Reduce', underline=0, background='#90f090', foreground='black', command=self.reduce).pack(side='left') Button(buttonframe, text='Undo', underline=0, background='#f0a0a0', foreground='black', command=self.undo).pack(side='left') def _init_menubar(self, parent): menubar = Menu(parent) filemenu = Menu(menubar, tearoff=0) filemenu.add_command(label='Reset Parser', underline=0, command=self.reset, accelerator='Del') filemenu.add_command(label='Print to Postscript', underline=0, command=self.postscript, accelerator='Ctrl-p') filemenu.add_command(label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-x') menubar.add_cascade(label='File', underline=0, menu=filemenu) editmenu = Menu(menubar, tearoff=0) editmenu.add_command(label='Edit Grammar', underline=5, command=self.edit_grammar, accelerator='Ctrl-g') editmenu.add_command(label='Edit Text', underline=5, command=self.edit_sentence, accelerator='Ctrl-t') menubar.add_cascade(label='Edit', underline=0, menu=editmenu) rulemenu = Menu(menubar, tearoff=0) rulemenu.add_command(label='Step', underline=1, command=self.step, accelerator='Space') rulemenu.add_separator() rulemenu.add_command(label='Shift', underline=0, command=self.shift, accelerator='Ctrl-s') rulemenu.add_command(label='Reduce', underline=0, command=self.reduce, accelerator='Ctrl-r') rulemenu.add_separator() rulemenu.add_command(label='Undo', underline=0, command=self.undo, accelerator='Ctrl-u') menubar.add_cascade(label='Apply', underline=0, menu=rulemenu) viewmenu = Menu(menubar, tearoff=0) viewmenu.add_checkbutton(label="Show Grammar", underline=0, variable=self._show_grammar, command=self._toggle_grammar) viewmenu.add_separator() viewmenu.add_radiobutton(label='Tiny', variable=self._size, underline=0, value=10, command=self.resize) viewmenu.add_radiobutton(label='Small', variable=self._size, underline=0, value=12, command=self.resize) viewmenu.add_radiobutton(label='Medium', variable=self._size, underline=0, value=14, command=self.resize) viewmenu.add_radiobutton(label='Large', variable=self._size, underline=0, value=18, command=self.resize) viewmenu.add_radiobutton(label='Huge', variable=self._size, underline=0, value=24, command=self.resize) menubar.add_cascade(label='View', underline=0, menu=viewmenu) animatemenu = Menu(menubar, tearoff=0) animatemenu.add_radiobutton(label="No Animation", underline=0, variable=self._animate, value=0) animatemenu.add_radiobutton(label="Slow Animation", underline=0, variable=self._animate, value=20, accelerator='-') animatemenu.add_radiobutton(label="Normal Animation", underline=0, variable=self._animate, value=10, accelerator='=') animatemenu.add_radiobutton(label="Fast Animation", underline=0, variable=self._animate, value=4, accelerator='+') menubar.add_cascade(label="Animate", underline=1, menu=animatemenu) helpmenu = Menu(menubar, tearoff=0) helpmenu.add_command(label='About', underline=0, command=self.about) helpmenu.add_command(label='Instructions', underline=0, command=self.help, accelerator='F1') menubar.add_cascade(label='Help', underline=0, menu=helpmenu) parent.config(menu=menubar) def _init_feedback(self, parent): self._feedbackframe = feedbackframe = Frame(parent) feedbackframe.pack(fill='x', side='bottom', padx=3, pady=3) self._lastoper_label = Label(feedbackframe, text='Last Operation:', font=self._font) self._lastoper_label.pack(side='left') lastoperframe = Frame(feedbackframe, relief='sunken', border=1) lastoperframe.pack(fill='x', side='right', expand=1, padx=5) self._lastoper1 = Label(lastoperframe, foreground='#007070', background='#f0f0f0', font=self._font) self._lastoper2 = Label(lastoperframe, anchor='w', width=30, foreground='#004040', background='#f0f0f0', font=self._font) self._lastoper1.pack(side='left') self._lastoper2.pack(side='left', fill='x', expand=1) def _init_canvas(self, parent): self._cframe = CanvasFrame(parent, background='white', width=525, closeenough=10, border=2, relief='sunken') self._cframe.pack(expand=1, fill='both', side='top', pady=2) canvas = self._canvas = self._cframe.canvas() self._stackwidgets = [] self._rtextwidgets = [] self._titlebar = canvas.create_rectangle(0,0,0,0, fill='#c0f0f0', outline='black') self._exprline = canvas.create_line(0,0,0,0, dash='.') self._stacktop = canvas.create_line(0,0,0,0, fill='#408080') size = self._size.get()+4 self._stacklabel = TextWidget(canvas, 'Stack', color='#004040', font=self._boldfont) self._rtextlabel = TextWidget(canvas, 'Remaining Text', color='#004040', font=self._boldfont) self._cframe.add_widget(self._stacklabel) self._cframe.add_widget(self._rtextlabel) ######################################### ## Main draw procedure ######################################### def _redraw(self): scrollregion = self._canvas['scrollregion'].split() (cx1, cy1, cx2, cy2) = [int(c) for c in scrollregion] # Delete the old stack & rtext widgets. for stackwidget in self._stackwidgets: self._cframe.destroy_widget(stackwidget) self._stackwidgets = [] for rtextwidget in self._rtextwidgets: self._cframe.destroy_widget(rtextwidget) self._rtextwidgets = [] # Position the titlebar & exprline (x1, y1, x2, y2) = self._stacklabel.bbox() y = y2-y1+10 self._canvas.coords(self._titlebar, -5000, 0, 5000, y-4) self._canvas.coords(self._exprline, 0, y*2-10, 5000, y*2-10) # Position the titlebar labels.. (x1, y1, x2, y2) = self._stacklabel.bbox() self._stacklabel.move(5-x1, 3-y1) (x1, y1, x2, y2) = self._rtextlabel.bbox() self._rtextlabel.move(cx2-x2-5, 3-y1) # Draw the stack. stackx = 5 for tok in self._parser.stack(): if isinstance(tok, Tree): attribs = {'tree_color': '#4080a0', 'tree_width': 2, 'node_font': self._boldfont, 'node_color': '#006060', 'leaf_color': '#006060', 'leaf_font':self._font} widget = tree_to_treesegment(self._canvas, tok, **attribs) widget.node()['color'] = '#000000' else: widget = TextWidget(self._canvas, tok, color='#000000', font=self._font) widget.bind_click(self._popup_reduce) self._stackwidgets.append(widget) self._cframe.add_widget(widget, stackx, y) stackx = widget.bbox()[2] + 10 # Draw the remaining text. rtextwidth = 0 for tok in self._parser.remaining_text(): widget = TextWidget(self._canvas, tok, color='#000000', font=self._font) self._rtextwidgets.append(widget) self._cframe.add_widget(widget, rtextwidth, y) rtextwidth = widget.bbox()[2] + 4 # Allow enough room to shift the next token (for animations) if len(self._rtextwidgets) > 0: stackx += self._rtextwidgets[0].width() # Move the remaining text to the correct location (keep it # right-justified, when possible); and move the remaining text # label, if necessary. stackx = max(stackx, self._stacklabel.width()+25) rlabelwidth = self._rtextlabel.width()+10 if stackx >= cx2-max(rtextwidth, rlabelwidth): cx2 = stackx + max(rtextwidth, rlabelwidth) for rtextwidget in self._rtextwidgets: rtextwidget.move(4+cx2-rtextwidth, 0) self._rtextlabel.move(cx2-self._rtextlabel.bbox()[2]-5, 0) midx = (stackx + cx2-max(rtextwidth, rlabelwidth))/2 self._canvas.coords(self._stacktop, midx, 0, midx, 5000) (x1, y1, x2, y2) = self._stacklabel.bbox() # Set up binding to allow them to shift a token by dragging it. if len(self._rtextwidgets) > 0: def drag_shift(widget, midx=midx, self=self): if widget.bbox()[0] < midx: self.shift() else: self._redraw() self._rtextwidgets[0].bind_drag(drag_shift) self._rtextwidgets[0].bind_click(self.shift) # Draw the stack top. self._highlight_productions() def _draw_stack_top(self, widget): # hack.. midx = widget.bbox()[2]+50 self._canvas.coords(self._stacktop, midx, 0, midx, 5000) def _highlight_productions(self): # Highlight the productions that can be reduced. self._prodlist.selection_clear(0, 'end') for prod in self._parser.reducible_productions(): index = self._productions.index(prod) self._prodlist.selection_set(index) ######################################### ## Button Callbacks ######################################### def destroy(self, *e): if self._top is None: return self._top.destroy() self._top = None def reset(self, *e): self._parser.initialize(self._sent) self._lastoper1['text'] = 'Reset App' self._lastoper2['text'] = '' self._redraw() def step(self, *e): if self.reduce(): return 1 elif self.shift(): return 1 else: if len(self._parser.parses()) > 0: self._lastoper1['text'] = 'Finished:' self._lastoper2['text'] = 'Success' else: self._lastoper1['text'] = 'Finished:' self._lastoper2['text'] = 'Failure' def shift(self, *e): if self._animating_lock: return if self._parser.shift(): tok = self._parser.stack()[-1] self._lastoper1['text'] = 'Shift:' self._lastoper2['text'] = '%r' % tok if self._animate.get(): self._animate_shift() else: self._redraw() return 1 return 0 def reduce(self, *e): if self._animating_lock: return production = self._parser.reduce() if production: self._lastoper1['text'] = 'Reduce:' self._lastoper2['text'] = '%s' % production if self._animate.get(): self._animate_reduce() else: self._redraw() return production def undo(self, *e): if self._animating_lock: return if self._parser.undo(): self._redraw() def postscript(self, *e): self._cframe.print_to_file() def mainloop(self, *args, **kwargs): """ Enter the Tkinter mainloop. This function must be called if this demo is created from a non-interactive program (e.g. from a secript); otherwise, the demo will close as soon as the script completes. """ if in_idle(): return self._top.mainloop(*args, **kwargs) ######################################### ## Menubar callbacks ######################################### def resize(self, size=None): if size is not None: self._size.set(size) size = self._size.get() self._font.configure(size=-(abs(size))) self._boldfont.configure(size=-(abs(size))) self._sysfont.configure(size=-(abs(size))) #self._stacklabel['font'] = ('helvetica', -size-4, 'bold') #self._rtextlabel['font'] = ('helvetica', -size-4, 'bold') #self._lastoper_label['font'] = ('helvetica', -size) #self._lastoper1['font'] = ('helvetica', -size) #self._lastoper2['font'] = ('helvetica', -size) #self._prodlist['font'] = ('helvetica', -size) #self._prodlist_label['font'] = ('helvetica', -size-2, 'bold') self._redraw() def help(self, *e): # The default font's not very legible; try using 'fixed' instead. try: ShowText(self._top, 'Help: Shift-Reduce Parser Application', (__doc__).strip(), width=75, font='fixed') except: ShowText(self._top, 'Help: Shift-Reduce Parser Application', (__doc__).strip(), width=75) def about(self, *e): ABOUT = ("NLTK Shift-Reduce Parser Application\n"+ "Written by Edward Loper") TITLE = 'About: Shift-Reduce Parser Application' try: from tkMessageBox import Message Message(message=ABOUT, title=TITLE).show() except: ShowText(self._top, TITLE, ABOUT) def edit_grammar(self, *e): CFGEditor(self._top, self._parser.grammar(), self.set_grammar) def set_grammar(self, grammar): self._parser.set_grammar(grammar) self._productions = list(grammar.productions()) self._prodlist.delete(0, 'end') for production in self._productions: self._prodlist.insert('end', (' %s' % production)) def edit_sentence(self, *e): sentence = string.join(self._sent) title = 'Edit Text' instr = 'Enter a new sentence to parse.' EntryDialog(self._top, sentence, instr, self.set_sentence, title) def set_sentence(self, sent): self._sent = sent.split() #[XX] use tagged? self.reset() ######################################### ## Reduce Production Selection ######################################### def _toggle_grammar(self, *e): if self._show_grammar.get(): self._prodframe.pack(fill='both', side='left', padx=2, after=self._feedbackframe) self._lastoper1['text'] = 'Show Grammar' else: self._prodframe.pack_forget() self._lastoper1['text'] = 'Hide Grammar' self._lastoper2['text'] = '' def _prodlist_select(self, event): selection = self._prodlist.curselection() if len(selection) != 1: return index = int(selection[0]) production = self._parser.reduce(self._productions[index]) if production: self._lastoper1['text'] = 'Reduce:' self._lastoper2['text'] = '%s' % production if self._animate.get(): self._animate_reduce() else: self._redraw() else: # Reset the production selections. self._prodlist.selection_clear(0, 'end') for prod in self._parser.reducible_productions(): index = self._productions.index(prod) self._prodlist.selection_set(index) def _popup_reduce(self, widget): # Remove old commands. productions = self._parser.reducible_productions() if len(productions) == 0: return self._reduce_menu.delete(0, 'end') for production in productions: self._reduce_menu.add_command(label=str(production), command=self.reduce) self._reduce_menu.post(self._canvas.winfo_pointerx(), self._canvas.winfo_pointery()) ######################################### ## Animations ######################################### def _animate_shift(self): # What widget are we shifting? widget = self._rtextwidgets[0] # Where are we shifting from & to? right = widget.bbox()[0] if len(self._stackwidgets) == 0: left = 5 else: left = self._stackwidgets[-1].bbox()[2]+10 # Start animating. dt = self._animate.get() dx = (left-right)*1.0/dt self._animate_shift_frame(dt, widget, dx) def _animate_shift_frame(self, frame, widget, dx): if frame > 0: self._animating_lock = 1 widget.move(dx, 0) self._top.after(10, self._animate_shift_frame, frame-1, widget, dx) else: # but: stacktop?? # Shift the widget to the stack. del self._rtextwidgets[0] self._stackwidgets.append(widget) self._animating_lock = 0 # Display the available productions. self._draw_stack_top(widget) self._highlight_productions() def _animate_reduce(self): # What widgets are we shifting? numwidgets = len(self._parser.stack()[-1]) # number of children widgets = self._stackwidgets[-numwidgets:] # How far are we moving? if isinstance(widgets[0], TreeSegmentWidget): ydist = 15 + widgets[0].node().height() else: ydist = 15 + widgets[0].height() # Start animating. dt = self._animate.get() dy = ydist*2.0/dt self._animate_reduce_frame(dt/2, widgets, dy) def _animate_reduce_frame(self, frame, widgets, dy): if frame > 0: self._animating_lock = 1 for widget in widgets: widget.move(0, dy) self._top.after(10, self._animate_reduce_frame, frame-1, widgets, dy) else: del self._stackwidgets[-len(widgets):] for widget in widgets: self._cframe.remove_widget(widget) tok = self._parser.stack()[-1] if not isinstance(tok, Tree): raise ValueError() label = TextWidget(self._canvas, str(tok.node), color='#006060', font=self._boldfont) widget = TreeSegmentWidget(self._canvas, label, widgets, width=2) (x1, y1, x2, y2) = self._stacklabel.bbox() y = y2-y1+10 if not self._stackwidgets: x = 5 else: x = self._stackwidgets[-1].bbox()[2] + 10 self._cframe.add_widget(widget, x, y) self._stackwidgets.append(widget) # Display the available productions. self._draw_stack_top(widget) self._highlight_productions() # # Delete the old widgets.. # del self._stackwidgets[-len(widgets):] # for widget in widgets: # self._cframe.destroy_widget(widget) # # # Make a new one. # tok = self._parser.stack()[-1] # if isinstance(tok, Tree): # attribs = {'tree_color': '#4080a0', 'tree_width': 2, # 'node_font': bold, 'node_color': '#006060', # 'leaf_color': '#006060', 'leaf_font':self._font} # widget = tree_to_treesegment(self._canvas, tok.type(), # **attribs) # widget.node()['color'] = '#000000' # else: # widget = TextWidget(self._canvas, tok.type(), # color='#000000', font=self._font) # widget.bind_click(self._popup_reduce) # (x1, y1, x2, y2) = self._stacklabel.bbox() # y = y2-y1+10 # if not self._stackwidgets: x = 5 # else: x = self._stackwidgets[-1].bbox()[2] + 10 # self._cframe.add_widget(widget, x, y) # self._stackwidgets.append(widget) #self._redraw() self._animating_lock = 0 ######################################### ## Hovering. ######################################### def _highlight_hover(self, event): # What production are we hovering over? index = self._prodlist.nearest(event.y) if self._hover == index: return # Clear any previous hover highlighting. self._clear_hover() # If the production corresponds to an available reduction, # highlight the stack. selection = [int(s) for s in self._prodlist.curselection()] if index in selection: rhslen = len(self._productions[index].rhs()) for stackwidget in self._stackwidgets[-rhslen:]: if isinstance(stackwidget, TreeSegmentWidget): stackwidget.node()['color'] = '#00a000' else: stackwidget['color'] = '#00a000' # Remember what production we're hovering over. self._hover = index def _clear_hover(self, *event): # Clear any previous hover highlighting. if self._hover == -1: return self._hover = -1 for stackwidget in self._stackwidgets: if isinstance(stackwidget, TreeSegmentWidget): stackwidget.node()['color'] = 'black' else: stackwidget['color'] = 'black' def app(): """ Create a shift reduce parser app, using a simple grammar and text. """ from nltk.grammar import Nonterminal, Production, ContextFreeGrammar nonterminals = 'S VP NP PP P N Name V Det' (S, VP, NP, PP, P, N, Name, V, Det) = [Nonterminal(s) for s in nonterminals.split()] productions = ( # Syntactic Productions Production(S, [NP, VP]), Production(NP, [Det, N]), Production(NP, [NP, PP]), Production(VP, [VP, PP]), Production(VP, [V, NP, PP]), Production(VP, [V, NP]), Production(PP, [P, NP]), # Lexical Productions Production(NP, ['I']), Production(Det, ['the']), Production(Det, ['a']), Production(N, ['man']), Production(V, ['saw']), Production(P, ['in']), Production(P, ['with']), Production(N, ['park']), Production(N, ['dog']), Production(N, ['statue']), Production(Det, ['my']), ) grammar = ContextFreeGrammar(S, productions) # tokenize the sentence sent = 'my dog saw a man in the park with a statue'.split() ShiftReduceApp(grammar, sent).mainloop() if __name__ == '__main__': app() __all__ = ['app'] nltk-2.0~b9/nltk/app/rdparser_app.py0000644000175000017500000010654011327451574017361 0ustar bhavanibhavani# Natural Language Toolkit: Recursive Descent Parser Application # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # # $Id: rdparser_app.py 8479 2010-01-13 05:40:34Z StevenBird1 $ """ A graphical tool for exploring the recursive descent parser. The recursive descent parser maintains a tree, which records the structure of the portion of the text that has been parsed. It uses CFG productions to expand the fringe of the tree, and matches its leaves against the text. Initially, the tree contains the start symbol ("S"). It is shown in the main canvas, to the right of the list of available expansions. The parser builds up a tree structure for the text using three operations: - "expand" uses a CFG production to add children to a node on the fringe of the tree. - "match" compares a leaf in the tree to a text token. - "backtrack" returns the tree to its state before the most recent expand or match operation. The parser maintains a list of tree locations called a "frontier" to remember which nodes have not yet been expanded and which leaves have not yet been matched against the text. The leftmost frontier node is shown in green, and the other frontier nodes are shown in blue. The parser always performs expand and match operations on the leftmost element of the frontier. You can control the parser's operation by using the "expand," "match," and "backtrack" buttons; or you can use the "step" button to let the parser automatically decide which operation to apply. The parser uses the following rules to decide which operation to apply: - If the leftmost frontier element is a token, try matching it. - If the leftmost frontier element is a node, try expanding it with the first untried expansion. - Otherwise, backtrack. The "expand" button applies the untried expansion whose CFG production is listed earliest in the grammar. To manually choose which expansion to apply, click on a CFG production from the list of available expansions, on the left side of the main window. The "autostep" button will let the parser continue applying applications to the tree until it reaches a complete parse. You can cancel an autostep in progress at any time by clicking on the "autostep" button again. Keyboard Shortcuts:: [Space]\t Perform the next expand, match, or backtrack operation [a]\t Step through operations until the next complete parse [e]\t Perform an expand operation [m]\t Perform a match operation [b]\t Perform a backtrack operation [Delete]\t Reset the parser [g]\t Show/hide available expansions list [h]\t Help [Ctrl-p]\t Print [q]\t Quit """ import string import nltk from nltk.tree import Tree from nltk.util import in_idle from nltk.draw.util import * from nltk.draw.tree import * from nltk.draw.cfg import * class RecursiveDescentApp(object): """ A graphical tool for exploring the recursive descent parser. The tool displays the parser's tree and the remaining text, and allows the user to control the parser's operation. In particular, the user can expand subtrees on the frontier, match tokens on the frontier against the text, and backtrack. A "step" button simply steps through the parsing process, performing the operations that C{RecursiveDescentParser} would use. """ def __init__(self, grammar, sent, trace=0): self._sent = sent self._parser = nltk.parse.SteppingRecursiveDescentParser(grammar, trace) # Set up the main window. self._top = Tk() self._top.title('Recursive Descent Parser Application') # Set up key bindings. self._init_bindings() # Initialize the fonts. self._init_fonts(self._top) # Animations. animating_lock is a lock to prevent the demo # from performing new operations while it's animating. self._animation_frames = IntVar(self._top) self._animation_frames.set(5) self._animating_lock = 0 self._autostep = 0 # The user can hide the grammar. self._show_grammar = IntVar(self._top) self._show_grammar.set(1) # Create the basic frames. self._init_menubar(self._top) self._init_buttons(self._top) self._init_feedback(self._top) self._init_grammar(self._top) self._init_canvas(self._top) # Initialize the parser. self._parser.initialize(self._sent) # Resize callback self._canvas.bind('', self._configure) ######################################### ## Initialization Helpers ######################################### def _init_fonts(self, root): # See: self._sysfont = tkFont.Font(font=Button()["font"]) root.option_add("*Font", self._sysfont) # TWhat's our font size (default=same as sysfont) self._size = IntVar(root) self._size.set(self._sysfont.cget('size')) self._boldfont = tkFont.Font(family='helvetica', weight='bold', size=self._size.get()) self._font = tkFont.Font(family='helvetica', size=self._size.get()) if self._size.get() < 0: big = self._size.get()-2 else: big = self._size.get()+2 self._bigfont = tkFont.Font(family='helvetica', weight='bold', size=big) def _init_grammar(self, parent): # Grammar view. self._prodframe = listframe = Frame(parent) self._prodframe.pack(fill='both', side='left', padx=2) self._prodlist_label = Label(self._prodframe, font=self._boldfont, text='Available Expansions') self._prodlist_label.pack() self._prodlist = Listbox(self._prodframe, selectmode='single', relief='groove', background='white', foreground='#909090', font=self._font, selectforeground='#004040', selectbackground='#c0f0c0') self._prodlist.pack(side='right', fill='both', expand=1) self._productions = list(self._parser.grammar().productions()) for production in self._productions: self._prodlist.insert('end', (' %s' % production)) self._prodlist.config(height=min(len(self._productions), 25)) # Add a scrollbar if there are more than 25 productions. if len(self._productions) > 25: listscroll = Scrollbar(self._prodframe, orient='vertical') self._prodlist.config(yscrollcommand = listscroll.set) listscroll.config(command=self._prodlist.yview) listscroll.pack(side='left', fill='y') # If they select a production, apply it. self._prodlist.bind('<>', self._prodlist_select) def _init_bindings(self): # Key bindings are a good thing. self._top.bind('', self.destroy) self._top.bind('', self.destroy) self._top.bind('', self.destroy) self._top.bind('e', self.expand) #self._top.bind('', self.expand) #self._top.bind('', self.expand) self._top.bind('m', self.match) self._top.bind('', self.match) self._top.bind('', self.match) self._top.bind('b', self.backtrack) self._top.bind('', self.backtrack) self._top.bind('', self.backtrack) self._top.bind('', self.backtrack) self._top.bind('', self.backtrack) self._top.bind('a', self.autostep) #self._top.bind('', self.autostep) self._top.bind('', self.autostep) self._top.bind('', self.cancel_autostep) self._top.bind('', self.step) self._top.bind('', self.reset) self._top.bind('', self.postscript) #self._top.bind('', self.help) #self._top.bind('', self.help) self._top.bind('', self.help) self._top.bind('', self.help) #self._top.bind('', self.toggle_grammar) #self._top.bind('', self.toggle_grammar) #self._top.bind('', self.toggle_grammar) self._top.bind('', self.edit_grammar) self._top.bind('', self.edit_sentence) def _init_buttons(self, parent): # Set up the frames. self._buttonframe = buttonframe = Frame(parent) buttonframe.pack(fill='none', side='bottom', padx=3, pady=2) Button(buttonframe, text='Step', background='#90c0d0', foreground='black', command=self.step,).pack(side='left') Button(buttonframe, text='Autostep', background='#90c0d0', foreground='black', command=self.autostep,).pack(side='left') Button(buttonframe, text='Expand', underline=0, background='#90f090', foreground='black', command=self.expand).pack(side='left') Button(buttonframe, text='Match', underline=0, background='#90f090', foreground='black', command=self.match).pack(side='left') Button(buttonframe, text='Backtrack', underline=0, background='#f0a0a0', foreground='black', command=self.backtrack).pack(side='left') # Replace autostep... # self._autostep_button = Button(buttonframe, text='Autostep', # underline=0, command=self.autostep) # self._autostep_button.pack(side='left') def _configure(self, event): self._autostep = 0 (x1, y1, x2, y2) = self._cframe.scrollregion() y2 = event.height - 6 self._canvas['scrollregion'] = '%d %d %d %d' % (x1,y1,x2,y2) self._redraw() def _init_feedback(self, parent): self._feedbackframe = feedbackframe = Frame(parent) feedbackframe.pack(fill='x', side='bottom', padx=3, pady=3) self._lastoper_label = Label(feedbackframe, text='Last Operation:', font=self._font) self._lastoper_label.pack(side='left') lastoperframe = Frame(feedbackframe, relief='sunken', border=1) lastoperframe.pack(fill='x', side='right', expand=1, padx=5) self._lastoper1 = Label(lastoperframe, foreground='#007070', background='#f0f0f0', font=self._font) self._lastoper2 = Label(lastoperframe, anchor='w', width=30, foreground='#004040', background='#f0f0f0', font=self._font) self._lastoper1.pack(side='left') self._lastoper2.pack(side='left', fill='x', expand=1) def _init_canvas(self, parent): self._cframe = CanvasFrame(parent, background='white', #width=525, height=250, closeenough=10, border=2, relief='sunken') self._cframe.pack(expand=1, fill='both', side='top', pady=2) canvas = self._canvas = self._cframe.canvas() # Initially, there's no tree or text self._tree = None self._textwidgets = [] self._textline = None def _init_menubar(self, parent): menubar = Menu(parent) filemenu = Menu(menubar, tearoff=0) filemenu.add_command(label='Reset Parser', underline=0, command=self.reset, accelerator='Del') filemenu.add_command(label='Print to Postscript', underline=0, command=self.postscript, accelerator='Ctrl-p') filemenu.add_command(label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-x') menubar.add_cascade(label='File', underline=0, menu=filemenu) editmenu = Menu(menubar, tearoff=0) editmenu.add_command(label='Edit Grammar', underline=5, command=self.edit_grammar, accelerator='Ctrl-g') editmenu.add_command(label='Edit Text', underline=5, command=self.edit_sentence, accelerator='Ctrl-t') menubar.add_cascade(label='Edit', underline=0, menu=editmenu) rulemenu = Menu(menubar, tearoff=0) rulemenu.add_command(label='Step', underline=1, command=self.step, accelerator='Space') rulemenu.add_separator() rulemenu.add_command(label='Match', underline=0, command=self.match, accelerator='Ctrl-m') rulemenu.add_command(label='Expand', underline=0, command=self.expand, accelerator='Ctrl-e') rulemenu.add_separator() rulemenu.add_command(label='Backtrack', underline=0, command=self.backtrack, accelerator='Ctrl-b') menubar.add_cascade(label='Apply', underline=0, menu=rulemenu) viewmenu = Menu(menubar, tearoff=0) viewmenu.add_checkbutton(label="Show Grammar", underline=0, variable=self._show_grammar, command=self._toggle_grammar) viewmenu.add_separator() viewmenu.add_radiobutton(label='Tiny', variable=self._size, underline=0, value=10, command=self.resize) viewmenu.add_radiobutton(label='Small', variable=self._size, underline=0, value=12, command=self.resize) viewmenu.add_radiobutton(label='Medium', variable=self._size, underline=0, value=14, command=self.resize) viewmenu.add_radiobutton(label='Large', variable=self._size, underline=0, value=18, command=self.resize) viewmenu.add_radiobutton(label='Huge', variable=self._size, underline=0, value=24, command=self.resize) menubar.add_cascade(label='View', underline=0, menu=viewmenu) animatemenu = Menu(menubar, tearoff=0) animatemenu.add_radiobutton(label="No Animation", underline=0, variable=self._animation_frames, value=0) animatemenu.add_radiobutton(label="Slow Animation", underline=0, variable=self._animation_frames, value=10, accelerator='-') animatemenu.add_radiobutton(label="Normal Animation", underline=0, variable=self._animation_frames, value=5, accelerator='=') animatemenu.add_radiobutton(label="Fast Animation", underline=0, variable=self._animation_frames, value=2, accelerator='+') menubar.add_cascade(label="Animate", underline=1, menu=animatemenu) helpmenu = Menu(menubar, tearoff=0) helpmenu.add_command(label='About', underline=0, command=self.about) helpmenu.add_command(label='Instructions', underline=0, command=self.help, accelerator='F1') menubar.add_cascade(label='Help', underline=0, menu=helpmenu) parent.config(menu=menubar) ######################################### ## Helper ######################################### def _get(self, widget, treeloc): for i in treeloc: widget = widget.subtrees()[i] if isinstance(widget, TreeSegmentWidget): widget = widget.node() return widget ######################################### ## Main draw procedure ######################################### def _redraw(self): canvas = self._canvas # Delete the old tree, widgets, etc. if self._tree is not None: self._cframe.destroy_widget(self._tree) for twidget in self._textwidgets: self._cframe.destroy_widget(twidget) if self._textline is not None: self._canvas.delete(self._textline) # Draw the tree. helv = ('helvetica', -self._size.get()) bold = ('helvetica', -self._size.get(), 'bold') attribs = {'tree_color': '#000000', 'tree_width': 2, 'node_font': bold, 'leaf_font': helv,} tree = self._parser.tree() self._tree = tree_to_treesegment(canvas, tree, **attribs) self._cframe.add_widget(self._tree, 30, 5) # Draw the text. helv = ('helvetica', -self._size.get()) bottom = y = self._cframe.scrollregion()[3] self._textwidgets = [TextWidget(canvas, word, font=self._font) for word in self._sent] for twidget in self._textwidgets: self._cframe.add_widget(twidget, 0, 0) twidget.move(0, bottom-twidget.bbox()[3]-5) y = min(y, twidget.bbox()[1]) # Draw a line over the text, to separate it from the tree. self._textline = canvas.create_line(-5000, y-5, 5000, y-5, dash='.') # Highlight appropriate nodes. self._highlight_nodes() self._highlight_prodlist() # Make sure the text lines up. self._position_text() def _redraw_quick(self): # This should be more-or-less sufficient after an animation. self._highlight_nodes() self._highlight_prodlist() self._position_text() def _highlight_nodes(self): # Highlight the list of nodes to be checked. bold = ('helvetica', -self._size.get(), 'bold') for treeloc in self._parser.frontier()[:1]: self._get(self._tree, treeloc)['color'] = '#20a050' self._get(self._tree, treeloc)['font'] = bold for treeloc in self._parser.frontier()[1:]: self._get(self._tree, treeloc)['color'] = '#008080' def _highlight_prodlist(self): # Highlight the productions that can be expanded. # Boy, too bad tkinter doesn't implement Listbox.itemconfig; # that would be pretty useful here. self._prodlist.delete(0, 'end') expandable = self._parser.expandable_productions() untried = self._parser.untried_expandable_productions() productions = self._productions for index in range(len(productions)): if productions[index] in expandable: if productions[index] in untried: self._prodlist.insert(index, ' %s' % productions[index]) else: self._prodlist.insert(index, ' %s (TRIED)' % productions[index]) self._prodlist.selection_set(index) else: self._prodlist.insert(index, ' %s' % productions[index]) def _position_text(self): # Line up the text widgets that are matched against the tree numwords = len(self._sent) num_matched = numwords - len(self._parser.remaining_text()) leaves = self._tree_leaves()[:num_matched] xmax = self._tree.bbox()[0] for i in range(0, len(leaves)): widget = self._textwidgets[i] leaf = leaves[i] widget['color'] = '#006040' leaf['color'] = '#006040' widget.move(leaf.bbox()[0] - widget.bbox()[0], 0) xmax = widget.bbox()[2] + 10 # Line up the text widgets that are not matched against the tree. for i in range(len(leaves), numwords): widget = self._textwidgets[i] widget['color'] = '#a0a0a0' widget.move(xmax - widget.bbox()[0], 0) xmax = widget.bbox()[2] + 10 # If we have a complete parse, make everything green :) if self._parser.currently_complete(): for twidget in self._textwidgets: twidget['color'] = '#00a000' # Move the matched leaves down to the text. for i in range(0, len(leaves)): widget = self._textwidgets[i] leaf = leaves[i] dy = widget.bbox()[1] - leaf.bbox()[3] - 10.0 dy = max(dy, leaf.parent().node().bbox()[3] - leaf.bbox()[3] + 10) leaf.move(0, dy) def _tree_leaves(self, tree=None): if tree is None: tree = self._tree if isinstance(tree, TreeSegmentWidget): leaves = [] for child in tree.subtrees(): leaves += self._tree_leaves(child) return leaves else: return [tree] ######################################### ## Button Callbacks ######################################### def destroy(self, *e): self._autostep = 0 if self._top is None: return self._top.destroy() self._top = None def reset(self, *e): self._autostep = 0 self._parser.initialize(self._sent) self._lastoper1['text'] = 'Reset Application' self._lastoper2['text'] = '' self._redraw() def autostep(self, *e): if self._animation_frames.get() == 0: self._animation_frames.set(2) if self._autostep: self._autostep = 0 else: self._autostep = 1 self._step() def cancel_autostep(self, *e): #self._autostep_button['text'] = 'Autostep' self._autostep = 0 # Make sure to stop auto-stepping if we get any user input. def step(self, *e): self._autostep = 0; self._step() def match(self, *e): self._autostep = 0; self._match() def expand(self, *e): self._autostep = 0; self._expand() def backtrack(self, *e): self._autostep = 0; self._backtrack() def _step(self): if self._animating_lock: return # Try expanding, matching, and backtracking (in that order) if self._expand(): pass elif self._parser.untried_match() and self._match(): pass elif self._backtrack(): pass else: self._lastoper1['text'] = 'Finished' self._lastoper2['text'] = '' self._autostep = 0 # Check if we just completed a parse. if self._parser.currently_complete(): self._autostep = 0 self._lastoper2['text'] += ' [COMPLETE PARSE]' def _expand(self, *e): if self._animating_lock: return old_frontier = self._parser.frontier() rv = self._parser.expand() if rv is not None: self._lastoper1['text'] = 'Expand:' self._lastoper2['text'] = rv self._prodlist.selection_clear(0, 'end') index = self._productions.index(rv) self._prodlist.selection_set(index) self._animate_expand(old_frontier[0]) return 1 else: self._lastoper1['text'] = 'Expand:' self._lastoper2['text'] = '(all expansions tried)' return 0 def _match(self, *e): if self._animating_lock: return old_frontier = self._parser.frontier() rv = self._parser.match() if rv is not None: self._lastoper1['text'] = 'Match:' self._lastoper2['text'] = rv self._animate_match(old_frontier[0]) return 1 else: self._lastoper1['text'] = 'Match:' self._lastoper2['text'] = '(failed)' return 0 def _backtrack(self, *e): if self._animating_lock: return if self._parser.backtrack(): elt = self._parser.tree() for i in self._parser.frontier()[0]: elt = elt[i] self._lastoper1['text'] = 'Backtrack' self._lastoper2['text'] = '' if isinstance(elt, Tree): self._animate_backtrack(self._parser.frontier()[0]) else: self._animate_match_backtrack(self._parser.frontier()[0]) return 1 else: self._autostep = 0 self._lastoper1['text'] = 'Finished' self._lastoper2['text'] = '' return 0 def about(self, *e): ABOUT = ("NLTK Recursive Descent Parser Application\n"+ "Written by Edward Loper") TITLE = 'About: Recursive Descent Parser Application' try: from tkMessageBox import Message Message(message=ABOUT, title=TITLE).show() except: ShowText(self._top, TITLE, ABOUT) def help(self, *e): self._autostep = 0 # The default font's not very legible; try using 'fixed' instead. try: ShowText(self._top, 'Help: Recursive Descent Parser Application', (__doc__).strip(), width=75, font='fixed') except: ShowText(self._top, 'Help: Recursive Descent Parser Application', (__doc__).strip(), width=75) def postscript(self, *e): self._autostep = 0 self._cframe.print_to_file() def mainloop(self, *args, **kwargs): """ Enter the Tkinter mainloop. This function must be called if this demo is created from a non-interactive program (e.g. from a secript); otherwise, the demo will close as soon as the script completes. """ if in_idle(): return self._top.mainloop(*args, **kwargs) def resize(self, size=None): if size is not None: self._size.set(size) size = self._size.get() self._font.configure(size=-(abs(size))) self._boldfont.configure(size=-(abs(size))) self._sysfont.configure(size=-(abs(size))) self._bigfont.configure(size=-(abs(size+2))) self._redraw() ######################################### ## Expand Production Selection ######################################### def _toggle_grammar(self, *e): if self._show_grammar.get(): self._prodframe.pack(fill='both', side='left', padx=2, after=self._feedbackframe) self._lastoper1['text'] = 'Show Grammar' else: self._prodframe.pack_forget() self._lastoper1['text'] = 'Hide Grammar' self._lastoper2['text'] = '' # def toggle_grammar(self, *e): # self._show_grammar = not self._show_grammar # if self._show_grammar: # self._prodframe.pack(fill='both', expand='y', side='left', # after=self._feedbackframe) # self._lastoper1['text'] = 'Show Grammar' # else: # self._prodframe.pack_forget() # self._lastoper1['text'] = 'Hide Grammar' # self._lastoper2['text'] = '' def _prodlist_select(self, event): selection = self._prodlist.curselection() if len(selection) != 1: return index = int(selection[0]) old_frontier = self._parser.frontier() production = self._parser.expand(self._productions[index]) if production: self._lastoper1['text'] = 'Expand:' self._lastoper2['text'] = production self._prodlist.selection_clear(0, 'end') self._prodlist.selection_set(index) self._animate_expand(old_frontier[0]) else: # Reset the production selections. self._prodlist.selection_clear(0, 'end') for prod in self._parser.expandable_productions(): index = self._productions.index(prod) self._prodlist.selection_set(index) ######################################### ## Animation ######################################### def _animate_expand(self, treeloc): oldwidget = self._get(self._tree, treeloc) oldtree = oldwidget.parent() top = not isinstance(oldtree.parent(), TreeSegmentWidget) tree = self._parser.tree() for i in treeloc: tree = tree[i] widget = tree_to_treesegment(self._canvas, tree, node_font=self._boldfont, leaf_color='white', tree_width=2, tree_color='white', node_color='white', leaf_font=self._font) widget.node()['color'] = '#20a050' (oldx, oldy) = oldtree.node().bbox()[:2] (newx, newy) = widget.node().bbox()[:2] widget.move(oldx-newx, oldy-newy) if top: self._cframe.add_widget(widget, 0, 5) widget.move(30-widget.node().bbox()[0], 0) self._tree = widget else: oldtree.parent().replace_child(oldtree, widget) # Move the children over so they don't overlap. # Line the children up in a strange way. if widget.subtrees(): dx = (oldx + widget.node().width()/2 - widget.subtrees()[0].bbox()[0]/2 - widget.subtrees()[0].bbox()[2]/2) for subtree in widget.subtrees(): subtree.move(dx, 0) self._makeroom(widget) if top: self._cframe.destroy_widget(oldtree) else: oldtree.destroy() colors = ['gray%d' % (10*int(10*x/self._animation_frames.get())) for x in range(self._animation_frames.get(),0,-1)] # Move the text string down, if necessary. dy = widget.bbox()[3] + 30 - self._canvas.coords(self._textline)[1] if dy > 0: for twidget in self._textwidgets: twidget.move(0, dy) self._canvas.move(self._textline, 0, dy) self._animate_expand_frame(widget, colors) def _makeroom(self, treeseg): """ Make sure that no sibling tree bbox's overlap. """ parent = treeseg.parent() if not isinstance(parent, TreeSegmentWidget): return index = parent.subtrees().index(treeseg) # Handle siblings to the right rsiblings = parent.subtrees()[index+1:] if rsiblings: dx = treeseg.bbox()[2] - rsiblings[0].bbox()[0] + 10 for sibling in rsiblings: sibling.move(dx, 0) # Handle siblings to the left if index > 0: lsibling = parent.subtrees()[index-1] dx = max(0, lsibling.bbox()[2] - treeseg.bbox()[0] + 10) treeseg.move(dx, 0) # Keep working up the tree. self._makeroom(parent) def _animate_expand_frame(self, widget, colors): if len(colors) > 0: self._animating_lock = 1 widget['color'] = colors[0] for subtree in widget.subtrees(): if isinstance(subtree, TreeSegmentWidget): subtree.node()['color'] = colors[0] else: subtree['color'] = colors[0] self._top.after(50, self._animate_expand_frame, widget, colors[1:]) else: widget['color'] = 'black' for subtree in widget.subtrees(): if isinstance(subtree, TreeSegmentWidget): subtree.node()['color'] = 'black' else: subtree['color'] = 'black' self._redraw_quick() widget.node()['color'] = 'black' self._animating_lock = 0 if self._autostep: self._step() def _animate_backtrack(self, treeloc): # Flash red first, if we're animating. if self._animation_frames.get() == 0: colors = [] else: colors = ['#a00000', '#000000', '#a00000'] colors += ['gray%d' % (10*int(10*x/(self._animation_frames.get()))) for x in range(1, self._animation_frames.get()+1)] widgets = [self._get(self._tree, treeloc).parent()] for subtree in widgets[0].subtrees(): if isinstance(subtree, TreeSegmentWidget): widgets.append(subtree.node()) else: widgets.append(subtree) self._animate_backtrack_frame(widgets, colors) def _animate_backtrack_frame(self, widgets, colors): if len(colors) > 0: self._animating_lock = 1 for widget in widgets: widget['color'] = colors[0] self._top.after(50, self._animate_backtrack_frame, widgets, colors[1:]) else: for widget in widgets[0].subtrees(): widgets[0].remove_child(widget) widget.destroy() self._redraw_quick() self._animating_lock = 0 if self._autostep: self._step() def _animate_match_backtrack(self, treeloc): widget = self._get(self._tree, treeloc) node = widget.parent().node() dy = (1.0 * (node.bbox()[3] - widget.bbox()[1] + 14) / max(1, self._animation_frames.get())) self._animate_match_backtrack_frame(self._animation_frames.get(), widget, dy) def _animate_match(self, treeloc): widget = self._get(self._tree, treeloc) dy = ((self._textwidgets[0].bbox()[1] - widget.bbox()[3] - 10.0) / max(1, self._animation_frames.get())) self._animate_match_frame(self._animation_frames.get(), widget, dy) def _animate_match_frame(self, frame, widget, dy): if frame > 0: self._animating_lock = 1 widget.move(0, dy) self._top.after(10, self._animate_match_frame, frame-1, widget, dy) else: widget['color'] = '#006040' self._redraw_quick() self._animating_lock = 0 if self._autostep: self._step() def _animate_match_backtrack_frame(self, frame, widget, dy): if frame > 0: self._animating_lock = 1 widget.move(0, dy) self._top.after(10, self._animate_match_backtrack_frame, frame-1, widget, dy) else: widget.parent().remove_child(widget) widget.destroy() self._animating_lock = 0 if self._autostep: self._step() def edit_grammar(self, *e): CFGEditor(self._top, self._parser.grammar(), self.set_grammar) def set_grammar(self, grammar): self._parser.set_grammar(grammar) self._productions = list(grammar.productions()) self._prodlist.delete(0, 'end') for production in self._productions: self._prodlist.insert('end', (' %s' % production)) def edit_sentence(self, *e): sentence = string.join(self._sent) title = 'Edit Text' instr = 'Enter a new sentence to parse.' EntryDialog(self._top, sentence, instr, self.set_sentence, title) def set_sentence(self, sentence): self._sent = sentence.split() #[XX] use tagged? self.reset() def app(): """ Create a recursive descent parser demo, using a simple grammar and text. """ from nltk.grammar import parse_cfg grammar = parse_cfg(""" # Grammatical productions. S -> NP VP NP -> Det N PP | Det N VP -> V NP PP | V NP | V PP -> P NP # Lexical productions. NP -> 'I' Det -> 'the' | 'a' N -> 'man' | 'park' | 'dog' | 'telescope' V -> 'ate' | 'saw' P -> 'in' | 'under' | 'with' """) sent = 'the dog saw a man in the park'.split() RecursiveDescentApp(grammar, sent).mainloop() if __name__ == '__main__': app() __all__ = ['app'] nltk-2.0~b9/nltk/app/nemo_app.py0000755000175000017500000002741311303614104016461 0ustar bhavanibhavani# Finding (and Replacing) Nemo, Version 1.1, Aristide Grange 2006/06/06 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496783 """ Finding (and Replacing) Nemo Instant Regular Expressions Created by Aristide Grange """ import Tkinter as tk import re import itertools windowTitle = "Finding (and Replacing) Nemo" initialFind = r"n(.*?)e(.*?)m(.*?)o" initialRepl = r"M\1A\2K\3I" initialText = """\ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. """ images = { "FIND":"R0lGODlhMAAiAPcAMf/////37//35//n1v97Off///f/9/f37/fexvfOvfeEQvd7QvdrQvdrKfdaKfdSMfdSIe/v9+/v7+/v5+/n3u/e1u/Wxu/Gre+1lO+tnO+thO+Ua+97Y+97Oe97Me9rOe9rMe9jOe9jMe9jIe9aMefe5+fe3ufezuece+eEWudzQudaIedSIedKMedKIedCKedCId7e1t7Wzt7Oxt7Gvd69vd69rd61pd6ljN6UjN6Ue96EY95zY95rUt5rQt5jMd5SId5KIdbn59be3tbGztbGvda1rdaEa9Z7a9Z7WtZzQtZzOdZzMdZjMdZaQtZSOdZSMdZKMdZCKdZCGNY5Ic7W1s7Oxs7Gtc69xs69tc69rc6tpc6llM6clM6cjM6Ue86EY85zWs5rSs5SKc5KKc5KGMa1tcatrcalvcalnMaUpcZ7c8ZzMcZrUsZrOcZrMcZaQsZSOcZSMcZKMcZCKcZCGMYxIcYxGL3Gxr21tb21rb2lpb2crb2cjL2UnL2UlL2UhL2Ec717Wr17Ur1zWr1rMb1jUr1KMb1KIb1CIb0xGLWlrbWlpbWcnLWEe7V7c7VzY7VzUrVSKbVKMbVCMbVCIbU5KbUxIbUxEK2lta2lpa2clK2UjK2MnK2MlK2Ea617e61za61rY61rMa1jSq1aUq1aSq1SQq1KKa0xEKWlnKWcnKWUnKWUhKWMjKWEa6Vza6VrWqVjMaVaUqVaKaVSMaVCMaU5KaUxIaUxGJyclJyMe5yElJyEhJx7e5x7c5xrOZxaQpxSOZxKQpw5IZSMhJSEjJR7c5Rre5RrY5RrUpRSQpRSKZRCOZRCKZQxKZQxIYyEhIx7hIxza4xzY4xrc4xjUoxaa4xaUoxSSoxKQoxCMYw5GIR7c4Rzc4Rre4RjY4RjWoRaa4RSWoRSUoRSMYRKQoRCOYQ5KYQxIXtra3taY3taSntKOXtCMXtCKXNCMXM5MXMxIWtSUmtKSmtKQmtCOWs5MWs5KWs5IWNCKWMxIVIxKUIQCDkhGAAAACH+AS4ALAAAAAAwACIAAAj/AAEIHEiwoMGDCBMqXMiwoUOHMqxIeEiRoZVp7cpZ29WrF4WKIAd208dGAQEVbiTVChUjZMU9+pYQmPmBZpxgvVw+nDdKwQICNVcIXQEkTgKdDdUJ+/nggVAXK1xI3TEA6UIr2uJ8iBqka1cXXTlkqGoVYRZ7iLyqBSs0iiEtZQVKiDGxBI1u3NR6lUpGDKg8MSgEQCphU7Z22vhg0dILXRCpYLuSCcYJT4wqXASBQaBzU7klHxC127OHD7ZDJFpERqRt0x5OnwQpmZmCLEhrbgg4WIHO1RY+nbQ9WRGEDJlmnXwJ+9FBgXMCIzYMVijBBgYMFxIMqJBMSc0Ht7qh/+Gjpte2rnYsYeNlasWIBgQ6yCewIoPCCp/cyP/wgUGbXVu0QcADZNBDnh98gHMLGXYQUw02w61QU3wdbNWDbQVVIIhMMwFF1DaZiPLBAy7E04kafrjSizaK3LFNNc0AAYRQDsAHHQlJ2IDQJ2zE1+EKDjiAijShkECCC8Qgw4cr7ZgyzC2WaHPNLWWoNeNWPiRAw0QFWQFMhz8C+QQ20yAiVSrY+MGOJCsccsst2GCzoHFxxEGGC+8hgs0MB2kyCpgzrUDCbs1Es41UdtATHFFkWELMOtsoQsYcgvRRQw5RSDgGOjZMR1AvPQIq6KCo9AKOJWDd48owQlHR4DXEKP9iyRrK+DNNBTu4RwIPFeTAGUG7hAomkA84gEg1m6ADljy9PBKGGJY4ig0xlsTBRSn98FOFDUC8pwQOPkgHbCGAzhTkA850s0c7j6Hjix9+gBIrMXLeAccWXUCyiRBcBEECdEJ98KtAqtBCYQc/OvDENnl4gYpUxISCIjjzylkGGV9okYUVNogRhAOBuuAEhjG08wOgDYzAgA5bCjIoCe5uwUk80RKTTSppPREGGGCIISOQ9AXBg6cC6WIywvCpoMHAocRBwhP4bHLFLujYkV42xNxBRhAyGrc113EgYtRBerDDDHMoDCyQEL5sE083EkgwQyBhxGFHMM206DUixGxmE0wssbQjCQ4JCaFKFwgQTVAVVhQUwAVPIFJKrHfYYRwi6OCDzzuIJIFhXAD0EccPsYRiSyqKSDpFcWSMIcZRoBMkQyA2BGZDIKSYcggih8TRRg4VxM5QABVYYLxgwiev/PLMCxQQADs=", "find":"R0lGODlhMAAiAPQAMf////f39+/v7+fn597e3tbW1s7OzsbGxr29vbW1ta2traWlpZycnJSUlIyMjISEhHt7e3Nzc2tra2NjY1paWlJSUkpKSkJCQjk5OSkpKRgYGAAAAAAAAAAAAAAAAAAAACH+AS4ALAAAAAAwACIAAAX/ICCOZGmeaKquY2AGLiuvMCAUBuHWc48Kh0iFInEYCb4kSQCxPBiMxkMigRQEgJiSFVBYHNGG0RiZOHjblWAiiY4fkDhEYoBp06dAWfyAQyKAgAwDaHgnB0RwgYASgQ0IhDuGJDAIFhMRVFSLEX8QCJJ4AQM5AgQHTZqqjBAOCQQEkWkCDRMUFQsICQ4Vm5maEwwHOAsPDTpKMAsUDlO4CssTcb+2DAp8YGCyNFoCEsZwFQ3QDRTTVBRS0g1QbgsCd5QAAwgIBwYFAwStzQ8UEdCKVchky0yVBw7YuXkAKt4IAg74vXHVagqFBRgXSCAyYWAVCH0SNhDTitCJfSL5/4RbAPKPhQYYjVCYYAvCP0BxEDaD8CheAAHNwqh8MMGPSwgLeJWhwHSjqkYI+xg4MMCEgQjtRvZ7UAYCpghMF7CxONOWJkYR+rCpY4JlVpVxKDwYWEactKW9mhYRtqCTgwgWEMArERSK1j5q//6T8KXonFsShpiJkAECgQYVjykooCVA0JGHEWNiYCHThTFeb3UkoiCCBgwGEKQ1kuAJlhFwhA71h5SukwUM5qqeCSGBgicEWkfNiWSERtBad4JNIBaQBaQah1ToyGZBAnsIuIJs1qnqiAIVjIE2gnAB1T5x0icgzXT79ipgMOOEH6HBbREBMJCeGEY08IoLAkzB1YYFwjxwSUGSNULQJnNUwRYlCcyEkALIxECAP9cNMMABYpRhy3ZsSLDaR70oUAiABGCkAxowCGCAAfDYIQACXoElGRsdXWDBdg2Y90IWktDYGYAB9PWHP0PMdFZaF07SQgAFNDAMAQg0QA1UC8xoZQl22JGFPgWkOUCOL1pZQyhjxinnnCWEAAA7", "REPL":"R0lGODlhMAAjAPcAMf/////3//+lOf+UKf+MEPf///f39/f35/fv7/ecQvecOfecKfeUIfeUGPeUEPeUCPeMAO/37+/v9+/v3u/n3u/n1u+9jO+9c++1hO+ta++tY++tWu+tUu+tSu+lUu+lQu+lMe+UMe+UKe+UGO+UEO+UAO+MCOfv5+fvxufn7+fn5+fnzue9lOe9c+e1jOe1e+e1c+e1a+etWuetUuelQuecOeeUUueUCN7e597e3t7e1t7ezt7evd7Wzt7Oxt7Ovd7Otd7Opd7OnN7Gtd7Gpd69lN61hN6ta96lStbextberdbW3tbWztbWxtbOvdbOrda1hNalUtaECM7W1s7Ozs7Oxs7Otc7Gxs7Gvc69tc69rc69pc61jM6lc8bWlMbOvcbGxsbGpca9tca9pca1nMaMAL3OhL3Gtb21vb21tb2tpb2tnL2tlLW9tbW9pbW9e7W1pbWtjLWcKa21nK2tra2tnK2tlK2lpa2llK2ljK2le6WlnKWljKWUe6WUc6WUY5y1QpyclJycjJychJyUc5yMY5StY5SUe5SMhJSMe5SMc5SMWpSEa5SESoyUe4yMhIyEY4SlKYScWoSMe4SEe4SEa4R7c4R7Y3uMY3uEe3t7e3t7c3tza3tzY3trKXtjIXOcAHOUMXOEY3Nzc3NzWnNrSmulCGuUMWuMGGtzWmtrY2taMWtaGGOUOWOMAGNzUmNjWmNjSmNaUmNaQmNaOWNaIWNSCFqcAFpjUlpSMVpSIVpSEFpKKVKMAFJSUlJSSlJSMVJKMVJKGFJKAFI5CEqUAEqEAEpzQkpKIUpCQkpCGEpCAEo5EEoxAEJjOUJCOUJCAEI5IUIxADl7ADlaITlCOTkxMTkxKTkxEDkhADFzADFrGDE5OTExADEpEClrCCkxKSkpKSkpISkpACkhCCkhACkYACFzACFrACEhCCEYGBhjEBhjABghABgYCBgYABgQEBgQABAQABAIAAhjAAhSAAhKAAgIEAgICABaAABCAAAhAAAQAAAIAAAAAAAAACH+AS4ALAAAAAAwACMAAAj/AAEIHEiwoMGDCBMqXMiwocOHAA4cgEixIIIJO3JMmAjADIqKFU/8MHIkg5EgYXx4iaTkI0iHE6wE2TCggYILQayEAgXIy8uGCKz8sDCAQAMRG3iEcXULlJkJPwli3OFjh9UdYYLE6NBhA04UXHoVA2XoTZgfPKBWlOBDphAWOdfMcfMDLloeO3hIMjbWVCQ5Fn6E2UFxgpsgFjYIEBADrZU6luqEEfqjTqpt54z1uuWqTIcgWAk7PECGzIUQDRosDmxlUrVJkwQJkqVuX71v06YZcyUlROAdbnLAJKPFyAYFAhoMwFlnEh0rWkpz8raPHm7dqKKc/KFFkBUrVn1M/ziBcEIeLUEQI8/AYk0i9Be4sqjsrN66c9/OnbobhpR3HkIUoZ0WVnBE0AGLFKKFD0HAFUQe77HQgQI1hRBDEHMcY0899bBzihZuCPILJD8EccEGGzwAQhFaUHHQH82sUkgeNHISDBk8WCCCcsqFUEQWmOyzjz3sUGNNOO5Y48YOEgowAAQhnBScQV00k82V47jzjy9CXZBcjziFoco//4CDiSOyhPMPLkJZkEBqJmRQxA9uZGEQD8Ncmc044/zzDF2IZQBCCDYE8QMZz/iiCSx0neHGI7BIhhhNn+1gxRpokEcQAp7seWU7/PwTyxqG/iCEEVzQmUombnDRxRExzP9nBR2PCKLFD3UJwcMPa/SRqUGNWJmNOVn+M44ukMRB4KGcWDNLVhuUMEIJAlzwA3DJBHMJIXm4sQYhqyxCRQQGLSIsn1qac2UzysQSyzX/hLMGD0F0IMCODYAQBA9W/PKPOcRiw0wzwxTiokF9dLMnuv/Mo+fCZF7jBr0xbDDCACWEYKgb1vzjDp/jZNOMLX0IZxAKq2TZTjtaOjwOsXyG+s8sZJTIQsUdIGHoJPf8w487QI/TDSt5mGwQFZxc406o8HiDJchk/ltLHpSlJwSvz5DpTjvmuGNOM57koelBOaAhiCaaPBLL0wwbm003peRBnBZqJMJL1ECz/HXYYx/NdAIOOVCxQyLorswymU93o0wuwfAiTDNR/xz0MLXU0XdCE+UwSTRZAq2lsSATu+4wkGvt+TjNzPLrQyegAUku2Hij5cd8LhxyM8QIg4w18HgcdC6BTBFSDmfQqsovttveDcG7lFLHI75cE841sARCxeWsnxC4G9HADPK6ywzDCRqBo0EHHWhMgT1IJzziNci1N7PMKnSYfML96/90AiJKey/0KtbLX1QK0rrNnQ541xugQ7SHhkXBghN0SKACWRc4KlAhBwKcIOYymJCAAAA7", "repl":"R0lGODlhMAAjAPQAMf////f39+/v7+fn597e3tbW1s7OzsbGxr29vbW1ta2traWlpZycnJSUlIyMjISEhHt7e3Nzc2tra2NjY1paWlJSUkpKSkJCQjk5OTExMSkpKSEhIRgYGBAQEAgICAAAACH+AS4ALAAAAAAwACMAAAX/ICCOZGmeaKqubOu+gCDANBkIQ1EMQhAghFptYEAkEgjEwXBo7ISvweGgWCwUysPjwTgEoCafTySYIhYMxgLBjEQgCULvCw0QdAZdoVhUIJUFChISEAxYeQM1N1OMTAp+UwZ5eA4TEhFbDWYFdC4ECVMJjwl5BwsQa0umEhUVlhESDgqlBp0rAn5nVpBMDxeZDRQbHBgWFBSWDgtLBnFjKwRYCI9VqQsPs0YKEcMXFq0UEalFDWx4BAO2IwPjppAKDkrTWKYUGd7fEJJFEZpM00cOzCgh4EE8SaoWxKNixQooBRMyZMBwAYIRBhUgLDGS4MoBJeoANMhAgQsaCRZm/5lqaCUJhA4cNHjDoKEDBlJUHqkBlYBTiQUZNGjYMMxDhY3VWk6R4MEDBoMUak5AqoYBqANIBo4wcGGDUKIeLlzVZmWJggsVIkwAZaQSA3kdZzlKkIiEAAlDvW5oOkEBs488JTw44oeUIwdvVTFTUK7uiAAPgubt8GFDhQepqETAQCFU1UMGzlqAgFhUsAcCS0AO6lUDhw8xNRSbENGDhgWSHjWUe6ACbKITizmopZoBa6KvOwj9uuHDhwxyj3xekgDDhw5EvWKo0IB4iQLCOCC/njc7ZQ8UeGvza+ABZZgcxJNc4FO1gc0cOsCUrHevc8tdIMTIAhc4F198G2Qwwd8CBIQUAwEINABBBJUwR9R5wElgVRLwWODBBx4cGB8GEzDQIAo33CGJA8gh+JoH/clUgQU0YvDhdfmJdwEFC6Sjgg8yEPAABsPkh2F22cl2AQbn6QdTghTQ5eAJAQyQAAQV0MSBB9gRVZ4GE1mw5JZOAmiAVi1UWcAZDrDyZXYTeaOhA/bIVuIBPtKQ4h7ViYekUPdcEAEbzTzCRp5CADmAAwj+ORGPBcgwAAHo9ABGCYtm0ChwFHShlRiXhmHlkAcCiOeUodqQw5W0oXLAiamy4MOkjOyAaqxUymApDCEAADs=", } colors = ["#FF7B39","#80F121"] emphColors = ["#DAFC33","#F42548"] fieldParams = { "height":3, "width":70, "font":("monaco",14), "highlightthickness":0, "borderwidth":0, "background":"white", } textParams = { "bg":"#F7E0D4", "fg":"#2321F1", "highlightthickness":0, "width":1, "height":10, "font":("verdana",16), "wrap":"word", } class Zone: def __init__(self, image, initialField, initialText): frm = tk.Frame(root) frm.config(background="white") self.image = tk.PhotoImage(format='gif',data=images[image.upper()]) self.imageDimmed = tk.PhotoImage(format='gif',data=images[image]) self.img = tk.Label(frm) self.img.config(borderwidth=0) self.img.pack(side = "left") self.fld = tk.Text(frm, **fieldParams) self.initScrollText(frm,self.fld,initialField) frm = tk.Frame(root) self.txt = tk.Text(frm, **textParams) self.initScrollText(frm,self.txt,initialText) for i in range(2): self.txt.tag_config(colors[i], background = colors[i]) self.txt.tag_config("emph"+colors[i], foreground = emphColors[i]) def initScrollText(self,frm,txt,contents): scl = tk.Scrollbar(frm) scl.config(command = txt.yview) scl.pack(side="right",fill="y") txt.pack(side = "left", expand=True, fill="x") txt.config(yscrollcommand = scl.set) txt.insert("1.0",contents) frm.pack(fill = "x") tk.Frame(height=2, bd=1, relief="ridge").pack(fill="x") def refresh(self): self.colorCycle = itertools.cycle(colors) try: self.substitute() self.img.config(image = self.image) except re.error: self.img.config(image = self.imageDimmed) class FindZone(Zone): def addTags(self,m): color = self.colorCycle.next() self.txt.tag_add(color,"1.0+%sc"%m.start(),"1.0+%sc"%m.end()) try: self.txt.tag_add("emph"+color,"1.0+%sc"%m.start("emph"), "1.0+%sc"%m.end("emph")) except: pass def substitute(self,*args): for color in colors: self.txt.tag_remove(color,"1.0","end") self.txt.tag_remove("emph"+color,"1.0","end") self.rex = re.compile("") # default value in case of misformed regexp self.rex = re.compile(self.fld.get("1.0","end")[:-1],re.MULTILINE) try: re.compile("(?P%s)" % self.fld.get(tk.SEL_FIRST, tk.SEL_LAST)) self.rexSel = re.compile("%s(?P%s)%s" % ( self.fld.get("1.0",tk.SEL_FIRST), self.fld.get(tk.SEL_FIRST,tk.SEL_LAST), self.fld.get(tk.SEL_LAST,"end")[:-1], ),re.MULTILINE) except: self.rexSel = self.rex self.rexSel.sub(self.addTags,self.txt.get("1.0","end")) class ReplaceZone(Zone): def addTags(self,m): s = sz.rex.sub(self.repl,m.group()) self.txt.delete("1.0+%sc"%(m.start()+self.diff), "1.0+%sc"%(m.end()+self.diff)) self.txt.insert("1.0+%sc"%(m.start()+self.diff),s, self.colorCycle.next()) self.diff += len(s) - (m.end() - m.start()) def substitute(self): self.txt.delete("1.0","end") self.txt.insert("1.0",sz.txt.get("1.0","end")[:-1]) self.diff = 0 self.repl = rex0.sub(r"\\g<\1>",self.fld.get("1.0","end")[:-1]) sz.rex.sub(self.addTags,sz.txt.get("1.0","end")[:-1]) def launchRefresh(_): sz.fld.after_idle(sz.refresh) rz.fld.after_idle(rz.refresh) def app(): global root, sz, rz, rex0 root = tk.Tk() root.resizable(height=False,width=True) root.title(windowTitle) root.minsize(width=250,height=0) sz = FindZone("find",initialFind,initialText) sz.fld.bind("",launchRefresh) sz.fld.bind("",launchRefresh) sz.fld.bind("",launchRefresh) sz.rexSel = re.compile("") rz = ReplaceZone("repl",initialRepl,"") rex0 = re.compile(r"(?",launchRefresh) launchRefresh(None) root.mainloop() if __name__ == '__main__': app() __all__ = ['app'] nltk-2.0~b9/nltk/app/concordance_app.py0000755000175000017500000005521711327451574020024 0ustar bhavanibhavani# Natural Language Toolkit: Concordance Application # # Copyright (C) 2001-2010 NLTK Project # Author: Sumukh Ghodke # URL: # For license information, see LICENSE.TXT # # $Id: concordance.py 6121 2008-07-11 02:10:33Z stevenbird $ import re import threading import nltk from nltk.util import in_idle from nltk.draw.util import * WORD_OR_TAG = '[^/ ]+' BOUNDARY = r'\b' CORPUS_LOADED_EVENT = '<>' SEARCH_TERMINATED_EVENT = '<>' SEARCH_ERROR_EVENT = '<>' ERROR_LOADING_CORPUS_EVENT = '<>' # NB All corpora must be specified in a lambda expression so as not to be # loaded when the module is imported. _DEFAULT = 'English: Brown Corpus (Humor, simplified)' _CORPORA = { 'Catalan: CESS-CAT Corpus (simplified)': lambda: nltk.corpus.cess_cat.tagged_sents(simplify_tags=True), 'English: Brown Corpus': lambda: nltk.corpus.brown.tagged_sents(), 'English: Brown Corpus (simplified)': lambda: nltk.corpus.brown.tagged_sents(simplify_tags=True), 'English: Brown Corpus (Press, simplified)': lambda: nltk.corpus.brown.tagged_sents(categories=['news', 'editorial', 'reviews'], simplify_tags=True), 'English: Brown Corpus (Religion, simplified)': lambda: nltk.corpus.brown.tagged_sents(categories='religion', simplify_tags=True), 'English: Brown Corpus (Learned, simplified)': lambda: nltk.corpus.brown.tagged_sents(categories='learned', simplify_tags=True), 'English: Brown Corpus (Science Fiction, simplified)': lambda: nltk.corpus.brown.tagged_sents(categories='science_fiction', simplify_tags=True), 'English: Brown Corpus (Romance, simplified)': lambda: nltk.corpus.brown.tagged_sents(categories='romance', simplify_tags=True), 'English: Brown Corpus (Humor, simplified)': lambda: nltk.corpus.brown.tagged_sents(categories='humor', simplify_tags=True), 'English: NPS Chat Corpus': lambda: nltk.corpus.nps_chat.tagged_posts(), 'English: NPS Chat Corpus (simplified)': lambda: nltk.corpus.nps_chat.tagged_posts(simplify_tags=True), 'English: Wall Street Journal Corpus': lambda: nltk.corpus.treebank.tagged_sents(), 'English: Wall Street Journal Corpus (simplified)': lambda: nltk.corpus.treebank.tagged_sents(simplify_tags=True), 'Chinese: Sinica Corpus': lambda: nltk.corpus.sinica_treebank.tagged_sents(), 'Chinese: Sinica Corpus (simplified)': lambda: nltk.corpus.sinica_treebank.tagged_sents(simplify_tags=True), 'Dutch: Alpino Corpus': lambda: nltk.corpus.alpino.tagged_sents(), 'Dutch: Alpino Corpus (simplified)': lambda: nltk.corpus.alpino.tagged_sents(simplify_tags=True), 'Hindi: Indian Languages Corpus': lambda: nltk.corpus.indian.tagged_sents(files='hindi.pos'), 'Hindi: Indian Languages Corpus (simplified)': lambda: nltk.corpus.indian.tagged_sents(files='hindi.pos', simplify_tags=True), 'Portuguese: Floresta Corpus (Portugal)': lambda: nltk.corpus.floresta.tagged_sents(), 'Portuguese: Floresta Corpus (Portugal, simplified)': lambda: nltk.corpus.floresta.tagged_sents(simplify_tags=True), 'Portuguese: MAC-MORPHO Corpus (Brazil)': lambda: nltk.corpus.mac_morpho.tagged_sents(), 'Portuguese: MAC-MORPHO Corpus (Brazil, simplified)': lambda: nltk.corpus.mac_morpho.tagged_sents(simplify_tags=True), 'Spanish: CESS-ESP Corpus (simplified)': lambda: nltk.corpus.cess_esp.tagged_sents(simplify_tags=True), } class ConcordanceSearchView(object): _BACKGROUND_COLOUR='#FFF' #white #Colour of highlighted results _HIGHLIGHT_WORD_COLOUR='#F00' #red _HIGHLIGHT_WORD_TAG='HL_WRD_TAG' _HIGHLIGHT_LABEL_COLOUR='#C0C0C0' # dark grey _HIGHLIGHT_LABEL_TAG='HL_LBL_TAG' #Percentage of text left of the scrollbar position _FRACTION_LEFT_TEXT=0.30 def __init__(self): self.model = ConcordanceSearchModel() self.model.add_listener(self) self.top = Tk() self._init_top(self.top) self._init_menubar() self._init_widgets(self.top) self._bind_event_handlers() self.load_corpus(self.model.DEFAULT_CORPUS) def _init_top(self, top): top.geometry('950x680+50+50') top.title('NLTK Concordance Search') top.bind('', self.destroy) top.minsize(950,680) def _init_widgets(self, parent): self.main_frame = Frame(parent, dict(background=self._BACKGROUND_COLOUR, padx=1, pady=1, border=1)) self._init_corpus_select(self.main_frame) self._init_query_box(self.main_frame) self._init_results_box(self.main_frame) self._init_paging(self.main_frame) self._init_status(self.main_frame) self.main_frame.pack(fill='both', expand=True) def _init_menubar(self): self._result_size = IntVar(self.top) self._cntx_bf_len = IntVar(self.top) self._cntx_af_len = IntVar(self.top) menubar = Menu(self.top) filemenu = Menu(menubar, tearoff=0, borderwidth=0) filemenu.add_command(label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-q') menubar.add_cascade(label='File', underline=0, menu=filemenu) editmenu = Menu(menubar, tearoff=0) rescntmenu = Menu(editmenu, tearoff=0) rescntmenu.add_radiobutton(label='20', variable=self._result_size, underline=0, value=20, command=self.set_result_size) rescntmenu.add_radiobutton(label='50', variable=self._result_size, underline=0, value=50, command=self.set_result_size) rescntmenu.add_radiobutton(label='100', variable=self._result_size, underline=0, value=100, command=self.set_result_size) rescntmenu.invoke(1) editmenu.add_cascade(label='Result Count', underline=0, menu=rescntmenu) cntxmenu = Menu(editmenu, tearoff=0) cntxbfmenu = Menu(cntxmenu, tearoff=0) cntxbfmenu.add_radiobutton(label='60 characters', variable=self._cntx_bf_len, underline=0, value=60, command=self.set_cntx_bf_len) cntxbfmenu.add_radiobutton(label='80 characters', variable=self._cntx_bf_len, underline=0, value=80, command=self.set_cntx_bf_len) cntxbfmenu.add_radiobutton(label='100 characters', variable=self._cntx_bf_len, underline=0, value=100, command=self.set_cntx_bf_len) cntxbfmenu.invoke(1) cntxmenu.add_cascade(label='Before', underline=0, menu=cntxbfmenu) cntxafmenu = Menu(cntxmenu, tearoff=0) cntxafmenu.add_radiobutton(label='70 characters', variable=self._cntx_af_len, underline=0, value=70, command=self.set_cntx_af_len) cntxafmenu.add_radiobutton(label='90 characters', variable=self._cntx_af_len, underline=0, value=90, command=self.set_cntx_af_len) cntxafmenu.add_radiobutton(label='110 characters', variable=self._cntx_af_len, underline=0, value=110, command=self.set_cntx_af_len) cntxafmenu.invoke(1) cntxmenu.add_cascade(label='After', underline=0, menu=cntxafmenu) editmenu.add_cascade(label='Context', underline=0, menu=cntxmenu) menubar.add_cascade(label='Edit', underline=0, menu=editmenu) self.top.config(menu=menubar) def set_result_size(self, **kwargs): self.model.result_count = self._result_size.get() def set_cntx_af_len(self, **kwargs): self._char_after = self._cntx_af_len.get() def set_cntx_bf_len(self, **kwargs): self._char_before = self._cntx_bf_len.get() def _init_corpus_select(self, parent): innerframe = Frame(parent, background=self._BACKGROUND_COLOUR) self.var = StringVar(innerframe) self.var.set(self.model.DEFAULT_CORPUS) Label(innerframe, justify=LEFT, text=' Corpus: ', background=self._BACKGROUND_COLOUR, padx = 2, pady = 1, border = 0).pack(side='left') other_corpora = self.model.CORPORA.keys().remove(self.model.DEFAULT_CORPUS) om = OptionMenu(innerframe, self.var, self.model.DEFAULT_CORPUS, command=self.corpus_selected, *self.model.non_default_corpora()) om['borderwidth'] = 0 om['highlightthickness'] = 1 om.pack(side='left') innerframe.pack(side='top', fill='x', anchor='n') def _init_status(self, parent): self.status = Label(parent, justify=LEFT, relief=SUNKEN, background=self._BACKGROUND_COLOUR, border=0, padx = 1, pady = 0) self.status.pack(side='top', anchor='sw') def _init_query_box(self, parent): innerframe = Frame(parent, background=self._BACKGROUND_COLOUR) another = Frame(innerframe, background=self._BACKGROUND_COLOUR) self.query_box = Entry(another, width=60) self.query_box.pack(side='left', fill='x', pady=25, anchor='center') self.search_button = Button(another, text='Search', command=self.search, borderwidth=1, highlightthickness=1) self.search_button.pack(side='left', fill='x', pady=25, anchor='center') self.query_box.bind('', self.search_enter_keypress_handler) another.pack() innerframe.pack(side='top', fill='x', anchor='n') def search_enter_keypress_handler(self, *event): self.search() def _init_results_box(self, parent): innerframe = Frame(parent) i1 = Frame(innerframe) i2 = Frame(innerframe) vscrollbar = Scrollbar(i1, borderwidth=1) hscrollbar = Scrollbar(i2, borderwidth=1, orient='horiz') self.results_box = Text(i1, font=tkFont.Font(family='courier', size='16'), state='disabled', borderwidth=1, yscrollcommand=vscrollbar.set, xscrollcommand=hscrollbar.set, wrap='none', width='40', height = '20', exportselection=1) self.results_box.pack(side='left', fill='both', expand=True) self.results_box.tag_config(self._HIGHLIGHT_WORD_TAG, foreground=self._HIGHLIGHT_WORD_COLOUR) self.results_box.tag_config(self._HIGHLIGHT_LABEL_TAG, foreground=self._HIGHLIGHT_LABEL_COLOUR) vscrollbar.pack(side='left', fill='y', anchor='e') vscrollbar.config(command=self.results_box.yview) hscrollbar.pack(side='left', fill='x', expand=True, anchor='w') hscrollbar.config(command=self.results_box.xview) #there is no other way of avoiding the overlap of scrollbars while using pack layout manager!!! Label(i2, text=' ', background=self._BACKGROUND_COLOUR).pack(side='left', anchor='e') i1.pack(side='top', fill='both', expand=True, anchor='n') i2.pack(side='bottom', fill='x', anchor='s') innerframe.pack(side='top', fill='both', expand=True) def _init_paging(self, parent): innerframe = Frame(parent, background=self._BACKGROUND_COLOUR) self.prev = prev = Button(innerframe, text='Previous', command=self.previous, width='10', borderwidth=1, highlightthickness=1, state='disabled') prev.pack(side='left', anchor='center') self.next = next = Button(innerframe, text='Next', command=self.next, width='10', borderwidth=1, highlightthickness=1, state='disabled') next.pack(side='right', anchor='center') innerframe.pack(side='top', fill='y') self.current_page = 0 def previous(self): self.clear_results_box() self.freeze_editable() self.model.prev(self.current_page - 1) def next(self): self.clear_results_box() self.freeze_editable() self.model.next(self.current_page + 1) def about(self, *e): ABOUT = ("NLTK Concordance Search Demo\n") TITLE = 'About: NLTK Concordance Search Demo' try: from tkMessageBox import Message Message(message=ABOUT, title=TITLE, parent=self.main_frame).show() except: ShowText(self.top, TITLE, ABOUT) def _bind_event_handlers(self): self.top.bind(CORPUS_LOADED_EVENT, self.handle_corpus_loaded) self.top.bind(SEARCH_TERMINATED_EVENT, self.handle_search_terminated) self.top.bind(SEARCH_ERROR_EVENT, self.handle_search_error) self.top.bind(ERROR_LOADING_CORPUS_EVENT, self.handle_error_loading_corpus) def handle_error_loading_corpus(self, event): self.status['text'] = 'Error in loading ' + self.var.get() self.unfreeze_editable() self.clear_all() self.freeze_editable() def handle_corpus_loaded(self, event): self.status['text'] = self.var.get() + ' is loaded' self.unfreeze_editable() self.clear_all() self.query_box.focus_set() def handle_search_terminated(self, event): #todo: refactor the model such that it is less state sensitive results = self.model.get_results() self.write_results(results) self.status['text'] = '' if len(results) == 0: self.status['text'] = 'No results found for ' + self.model.query else: self.current_page = self.model.last_requested_page self.unfreeze_editable() self.results_box.xview_moveto(self._FRACTION_LEFT_TEXT) def handle_search_error(self, event): self.status['text'] = 'Error in query ' + self.model.query self.unfreeze_editable() def corpus_selected(self, *args): new_selection = self.var.get() self.load_corpus(new_selection) def load_corpus(self, selection): if self.model.selected_corpus != selection: self.status['text'] = 'Loading ' + selection + '...' self.freeze_editable() self.model.load_corpus(selection) def search(self): self.current_page = 0 self.clear_results_box() self.model.reset_results() query = self.query_box.get() if (len(query.strip()) == 0): return self.status['text'] = 'Searching for ' + query self.freeze_editable() self.model.search(query, self.current_page + 1, ) def write_results(self, results): self.results_box['state'] = 'normal' row = 1 for each in results: sent, pos1, pos2 = each[0].strip(), each[1], each[2] if len(sent) != 0: if (pos1 < self._char_before): sent, pos1, pos2 = self.pad(sent, pos1, pos2) sentence = sent[pos1-self._char_before:pos1+self._char_after] if not row == len(results): sentence += '\n' self.results_box.insert(str(row) + '.0', sentence) word_markers, label_markers = self.words_and_labels(sent, pos1, pos2) for marker in word_markers: self.results_box.tag_add(self._HIGHLIGHT_WORD_TAG, str(row) + '.' + str(marker[0]), str(row) + '.' + str(marker[1])) for marker in label_markers: self.results_box.tag_add(self._HIGHLIGHT_LABEL_TAG, str(row) + '.' + str(marker[0]), str(row) + '.' + str(marker[1])) row += 1 self.results_box['state'] = 'disabled' def words_and_labels(self, sentence, pos1, pos2): search_exp = sentence[pos1:pos2] words, labels = [], [] labeled_words = search_exp.split(' ') index = 0 for each in labeled_words: if each == '': index += 1 else: word, label = each.split('/') words.append((self._char_before + index, self._char_before + index + len(word))) index += len(word) + 1 labels.append((self._char_before + index, self._char_before + index + len(label))) index += len(label) index += 1 return words, labels def pad(self, sent, hstart, hend): if hstart >= self._char_before: return sent, hstart, hend d = self._char_before - hstart sent = ''.join([' '] * d) + sent return sent, hstart + d, hend + d def destroy(self, *e): if self.top is None: return self.top.destroy() self.top = None def clear_all(self): self.query_box.delete(0, END) self.model.reset_query() self.clear_results_box() def clear_results_box(self): self.results_box['state'] = 'normal' self.results_box.delete("1.0", END) self.results_box['state'] = 'disabled' def freeze_editable(self): self.query_box['state'] = 'disabled' self.search_button['state'] = 'disabled' self.prev['state'] = 'disabled' self.next['state'] = 'disabled' def unfreeze_editable(self): self.query_box['state'] = 'normal' self.search_button['state'] = 'normal' self.set_paging_button_states() def set_paging_button_states(self): if self.current_page == 0 or self.current_page == 1: self.prev['state'] = 'disabled' else: self.prev['state'] = 'normal' if self.model.has_more_pages(self.current_page): self.next['state'] = 'normal' else: self.next['state'] = 'disabled' def fire_event(self, event): #Firing an event so that rendering of widgets happen in the mainloop thread self.top.event_generate(event, when='tail') def mainloop(self, *args, **kwargs): if in_idle(): return self.top.mainloop(*args, **kwargs) class ConcordanceSearchModel(object): def __init__(self): self.listeners = [] self.CORPORA = _CORPORA self.DEFAULT_CORPUS = _DEFAULT self.selected_corpus = None self.reset_query() self.reset_results() self.result_count = None self.last_sent_searched = 0 def non_default_corpora(self): copy = [] copy.extend(self.CORPORA.keys()) copy.remove(self.DEFAULT_CORPUS) copy.sort() return copy def load_corpus(self, name): self.selected_corpus = name self.tagged_sents = [] runner_thread = self.LoadCorpus(name, self) runner_thread.start() def search(self, query, page): self.query = query self.last_requested_page = page self.SearchCorpus(self, page, self.result_count).start() def next(self, page): self.last_requested_page = page if len(self.results) < page: self.search(self.query, page) else: self.notify_listeners(SEARCH_TERMINATED_EVENT) def prev(self, page): self.last_requested_page = page self.notify_listeners(SEARCH_TERMINATED_EVENT) def add_listener(self, listener): self.listeners.append(listener) def notify_listeners(self, event): for each in self.listeners: each.fire_event(event) def reset_results(self): self.last_sent_searched = 0 self.results = [] self.last_page = None def reset_query(self): self.query = None def set_results(self, page, resultset): self.results.insert(page - 1, resultset) def get_results(self): return self.results[self.last_requested_page - 1] def has_more_pages(self, page): if self.results == [] or self.results[0] == []: return False if self.last_page == None: return True return page < self.last_page class LoadCorpus(threading.Thread): def __init__(self, name, model): threading.Thread.__init__(self) self.model, self.name = model, name def run(self): try: ts = self.model.CORPORA[self.name]() self.model.tagged_sents = [' '.join(w+'/'+t for (w,t) in sent) for sent in ts] self.model.notify_listeners(CORPUS_LOADED_EVENT) except Exception, e: print e self.model.notify_listeners(ERROR_LOADING_CORPUS_EVENT) class SearchCorpus(threading.Thread): def __init__(self, model, page, count): self.model, self.count, self.page = model, count, page threading.Thread.__init__(self) def run(self): q = self.processed_query() sent_pos, i, sent_count = [], 0, 0 for sent in self.model.tagged_sents[self.model.last_sent_searched:]: try: m = re.search(q, sent) except re.error: self.model.reset_results() self.model.notify_listeners(SEARCH_ERROR_EVENT) return if m: sent_pos.append((sent, m.start(), m.end())) i += 1 if i > self.count: self.model.last_sent_searched += sent_count - 1 break sent_count += 1 if (self.count >= len(sent_pos)): self.model.last_sent_searched += sent_count - 1 self.model.last_page = self.page self.model.set_results(self.page, sent_pos) else: self.model.set_results(self.page, sent_pos[:-1]) self.model.notify_listeners(SEARCH_TERMINATED_EVENT) def processed_query(self): new = [] for term in self.model.query.split(): term = re.sub(r'\.', r'[^/ ]', term) if re.match('[A-Z]+$', term): new.append(BOUNDARY + WORD_OR_TAG + '/' + term + BOUNDARY) elif '/' in term: new.append(BOUNDARY + term + BOUNDARY) else: new.append(BOUNDARY + term + '/' + WORD_OR_TAG + BOUNDARY) return ' '.join(new) def app(): d = ConcordanceSearchView() d.mainloop() if __name__ == '__main__': app() __all__ = ['app'] nltk-2.0~b9/nltk/app/collocations_app.py0000644000175000017500000003207011327451574020224 0ustar bhavanibhavani# Natural Language Toolkit: Collocations Application # Much of the GUI code is imported from concordance.py; We intend to merge these tools together # Copyright (C) 2001-2010 NLTK Project # Author: Sumukh Ghodke # URL: # For license information, see LICENSE.TXT # import threading import nltk from nltk.util import in_idle from nltk.probability import FreqDist from nltk.text import Text as TextDomain from nltk.draw.util import * CORPUS_LOADED_EVENT = '<>' ERROR_LOADING_CORPUS_EVENT = '<>' _DEFAULT = 'English: Brown Corpus (Humor)' _CORPORA = { 'Catalan: CESS-CAT Corpus': lambda: nltk.corpus.cess_cat.words(), 'English: Brown Corpus': lambda: nltk.corpus.brown.words(), 'English: Brown Corpus (Press)': lambda: nltk.corpus.brown.words(categories=['news', 'editorial', 'reviews']), 'English: Brown Corpus (Religion)': lambda: nltk.corpus.brown.words(categories='religion'), 'English: Brown Corpus (Learned)': lambda: nltk.corpus.brown.words(categories='learned'), 'English: Brown Corpus (Science Fiction)': lambda: nltk.corpus.brown.words(categories='science_fiction'), 'English: Brown Corpus (Romance)': lambda: nltk.corpus.brown.words(categories='romance'), 'English: Brown Corpus (Humor)': lambda: nltk.corpus.brown.words(categories='humor'), 'English: NPS Chat Corpus': lambda: nltk.corpus.nps_chat.words(), 'English: Wall Street Journal Corpus': lambda: nltk.corpus.treebank.words(), 'Chinese: Sinica Corpus': lambda: nltk.corpus.sinica_treebank.words(), 'Dutch: Alpino Corpus': lambda: nltk.corpus.alpino.words(), 'Hindi: Indian Languages Corpus': lambda: nltk.corpus.indian.words(files='hindi.pos'), 'Portuguese: Floresta Corpus (Portugal)': lambda: nltk.corpus.floresta.words(), 'Portuguese: MAC-MORPHO Corpus (Brazil)': lambda: nltk.corpus.mac_morpho.words(), 'Portuguese: Machado Corpus (Brazil)': lambda: nltk.corpus.machado.words(), 'Spanish: CESS-ESP Corpus': lambda: nltk.corpus.cess_esp.words(), } class CollocationsView: _BACKGROUND_COLOUR='#FFF' #white def __init__(self): self.model = CollocationsModel() self.model.add_listener(self) self.top = Tk() self._init_top(self.top) self._init_menubar() self._init_widgets(self.top) self._bind_event_handlers() self.load_corpus(self.model.DEFAULT_CORPUS) def _init_top(self, top): top.geometry('550x650+50+50') top.title('NLTK Collocations List') top.bind('', self.destroy) top.minsize(550,650) def _init_widgets(self, parent): self.main_frame = Frame(parent, dict(background=self._BACKGROUND_COLOUR, padx=1, pady=1, border=1)) self._init_corpus_select(self.main_frame) self._init_results_box(self.main_frame) self._init_paging(self.main_frame) self._init_status(self.main_frame) self.main_frame.pack(fill='both', expand=True) def _init_corpus_select(self, parent): innerframe = Frame(parent, background=self._BACKGROUND_COLOUR) self.var = StringVar(innerframe) self.var.set(self.model.DEFAULT_CORPUS) Label(innerframe, justify=LEFT, text=' Corpus: ', background=self._BACKGROUND_COLOUR, padx = 2, pady = 1, border = 0).pack(side='left') other_corpora = self.model.CORPORA.keys().remove(self.model.DEFAULT_CORPUS) om = OptionMenu(innerframe, self.var, self.model.DEFAULT_CORPUS, command=self.corpus_selected, *self.model.non_default_corpora()) om['borderwidth'] = 0 om['highlightthickness'] = 1 om.pack(side='left') innerframe.pack(side='top', fill='x', anchor='n') def _init_status(self, parent): self.status = Label(parent, justify=LEFT, relief=SUNKEN, background=self._BACKGROUND_COLOUR, border=0, padx = 1, pady = 0) self.status.pack(side='top', anchor='sw') def _init_menubar(self): self._result_size = IntVar(self.top) menubar = Menu(self.top) filemenu = Menu(menubar, tearoff=0, borderwidth=0) filemenu.add_command(label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-q') menubar.add_cascade(label='File', underline=0, menu=filemenu) editmenu = Menu(menubar, tearoff=0) rescntmenu = Menu(editmenu, tearoff=0) rescntmenu.add_radiobutton(label='20', variable=self._result_size, underline=0, value=20, command=self.set_result_size) rescntmenu.add_radiobutton(label='50', variable=self._result_size, underline=0, value=50, command=self.set_result_size) rescntmenu.add_radiobutton(label='100', variable=self._result_size, underline=0, value=100, command=self.set_result_size) rescntmenu.invoke(1) editmenu.add_cascade(label='Result Count', underline=0, menu=rescntmenu) menubar.add_cascade(label='Edit', underline=0, menu=editmenu) self.top.config(menu=menubar) def set_result_size(self, **kwargs): self.model.result_count = self._result_size.get() def _init_results_box(self, parent): innerframe = Frame(parent) i1 = Frame(innerframe) i2 = Frame(innerframe) vscrollbar = Scrollbar(i1, borderwidth=1) hscrollbar = Scrollbar(i2, borderwidth=1, orient='horiz') self.results_box = Text(i1, font=tkFont.Font(family='courier', size='16'), state='disabled', borderwidth=1, yscrollcommand=vscrollbar.set, xscrollcommand=hscrollbar.set, wrap='none', width='40', height = '20', exportselection=1) self.results_box.pack(side='left', fill='both', expand=True) vscrollbar.pack(side='left', fill='y', anchor='e') vscrollbar.config(command=self.results_box.yview) hscrollbar.pack(side='left', fill='x', expand=True, anchor='w') hscrollbar.config(command=self.results_box.xview) #there is no other way of avoiding the overlap of scrollbars while using pack layout manager!!! Label(i2, text=' ', background=self._BACKGROUND_COLOUR).pack(side='left', anchor='e') i1.pack(side='top', fill='both', expand=True, anchor='n') i2.pack(side='bottom', fill='x', anchor='s') innerframe.pack(side='top', fill='both', expand=True) def _init_paging(self, parent): innerframe = Frame(parent, background=self._BACKGROUND_COLOUR) self.prev = prev = Button(innerframe, text='Previous', command=self.previous, width='10', borderwidth=1, highlightthickness=1, state='disabled') prev.pack(side='left', anchor='center') self.next = next = Button(innerframe, text='Next', command=self.next, width='10', borderwidth=1, highlightthickness=1, state='disabled') next.pack(side='right', anchor='center') innerframe.pack(side='top', fill='y') self.reset_current_page() def reset_current_page(self): self.current_page = -1 def _bind_event_handlers(self): self.top.bind(CORPUS_LOADED_EVENT, self.handle_corpus_loaded) self.top.bind(ERROR_LOADING_CORPUS_EVENT, self.handle_error_loading_corpus) def handle_error_loading_corpus(self, event): self.status['text'] = 'Error in loading ' + self.var.get() self.unfreeze_editable() self.clear_results_box() self.freeze_editable() self.reset_current_page() def handle_corpus_loaded(self, event): self.status['text'] = self.var.get() + ' is loaded' self.unfreeze_editable() self.clear_results_box() self.reset_current_page() #self.next() collocations = self.model.next(self.current_page + 1) self.write_results(collocations) self.current_page += 1 def corpus_selected(self, *args): new_selection = self.var.get() self.load_corpus(new_selection) def previous(self): self.freeze_editable() collocations = self.model.prev(self.current_page - 1) self.current_page= self.current_page - 1 self.clear_results_box() self.write_results(collocations) self.unfreeze_editable() def next(self): self.freeze_editable() collocations = self.model.next(self.current_page + 1) self.clear_results_box() self.write_results(collocations) self.current_page += 1 self.unfreeze_editable() def load_corpus(self, selection): if self.model.selected_corpus != selection: self.status['text'] = 'Loading ' + selection + '...' self.freeze_editable() self.model.load_corpus(selection) def freeze_editable(self): self.prev['state'] = 'disabled' self.next['state'] = 'disabled' def clear_results_box(self): self.results_box['state'] = 'normal' self.results_box.delete("1.0", END) self.results_box['state'] = 'disabled' def fire_event(self, event): #Firing an event so that rendering of widgets happen in the mainloop thread self.top.event_generate(event, when='tail') def destroy(self, *e): if self.top is None: return self.top.destroy() self.top = None def mainloop(self, *args, **kwargs): if in_idle(): return self.top.mainloop(*args, **kwargs) def unfreeze_editable(self): self.set_paging_button_states() def set_paging_button_states(self): if self.current_page == -1 or self.current_page == 0: self.prev['state'] = 'disabled' else: self.prev['state'] = 'normal' if self.model.is_last_page(self.current_page): self.next['state'] = 'disabled' else: self.next['state'] = 'normal' def write_results(self, results): self.results_box['state'] = 'normal' row = 1 for each in results: self.results_box.insert(str(row) + '.0', each[0] + " " + each[1] + "\n") row += 1 self.results_box['state'] = 'disabled' class CollocationsModel: def __init__(self): self.listeners = [] self.result_count = None self.selected_corpus = None self.collocations = None self.CORPORA = _CORPORA self.DEFAULT_CORPUS = _DEFAULT self.reset_results() def reset_results(self): self.result_pages = [] self.results_returned = 0 def add_listener(self, listener): self.listeners.append(listener) def notify_listeners(self, event): for each in self.listeners: each.fire_event(event) def load_corpus(self, name): self.selected_corpus = name self.collocations = None runner_thread = self.LoadCorpus(name, self) runner_thread.start() self.reset_results() def non_default_corpora(self): copy = [] copy.extend(self.CORPORA.keys()) copy.remove(self.DEFAULT_CORPUS) copy.sort() return copy def is_last_page(self, number): if number < len(self.result_pages): return False return self.results_returned + (number - len(self.result_pages)) * self.result_count >= len(self.collocations) def next(self, page): if (len(self.result_pages) - 1) < page: for i in range(page - (len(self.result_pages) - 1)): self.result_pages.append(self.collocations[self.results_returned:self.results_returned+self.result_count]) self.results_returned += self.result_count return self.result_pages[page] def prev(self, page): if page == -1: return [] return self.result_pages[page] class LoadCorpus(threading.Thread): def __init__(self, name, model): threading.Thread.__init__(self) self.model, self.name = model, name def run(self): try: words = self.model.CORPORA[self.name]() from operator import itemgetter text = filter(lambda w: len(w) > 2, words) fd = FreqDist(tuple(text[i:i+2]) for i in range(len(text)-1)) vocab = FreqDist(text) scored = [((w1,w2), fd[(w1,w2)] ** 3 / float(vocab[w1] * vocab[w2])) for w1, w2 in fd] scored.sort(key=itemgetter(1), reverse=True) self.model.collocations = map(itemgetter(0), scored) self.model.notify_listeners(CORPUS_LOADED_EVENT) except Exception, e: print e self.model.notify_listeners(ERROR_LOADING_CORPUS_EVENT) def collocations(): colloc_strings = [w1 + ' ' + w2 for w1, w2 in self._collocations[:num]] def app(): c = CollocationsView() c.mainloop() if __name__ == '__main__': app() __all__ = ['app'] nltk-2.0~b9/nltk/app/chunkparser_app.py0000644000175000017500000015423311327451574020066 0ustar bhavanibhavani# Natural Language Toolkit: Regexp Chunk Parser Application # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT # # $Id: srparser.py 5609 2007-12-31 03:02:41Z stevenbird $ """ A graphical tool for exploring the regular expression based chunk parser (L{RegexpChunkParser}). @todo: Add a way to select the development set from the menubar. This might just need to be a selection box (conll vs treebank etc) plus configuration parameters to select what's being chunked (eg VP vs NP) and what part of the data is being used as the development set. """ import time import textwrap import re import random import nltk from nltk.tree import Tree from nltk.util import in_idle from nltk.draw.util import * class RegexpChunkApp(object): """ A graphical tool for exploring the regular expression based chunk parser (L{RegexpChunkParser}). See L{HELP} for instructional text. """ ##///////////////////////////////////////////////////////////////// ## Help Text ##///////////////////////////////////////////////////////////////// #: A dictionary mapping from part of speech tags to descriptions, #: which is used in the help text. (This should probably live with #: the conll and/or treebank corpus instead.) TAGSET = { 'CC': 'Coordinating conjunction', 'PRP$': 'Possessive pronoun', 'CD': 'Cardinal number', 'RB': 'Adverb', 'DT': 'Determiner', 'RBR': 'Adverb, comparative', 'EX': 'Existential there', 'RBS': 'Adverb, superlative', 'FW': 'Foreign word', 'RP': 'Particle', 'JJ': 'Adjective', 'TO': 'to', 'JJR': 'Adjective, comparative', 'UH': 'Interjection', 'JJS': 'Adjective, superlative', 'VB': 'Verb, base form', 'LS': 'List item marker', 'VBD': 'Verb, past tense', 'MD': 'Modal', 'NNS': 'Noun, plural', 'NN': 'Noun, singular or masps', 'VBN': 'Verb, past participle', 'VBZ': 'Verb,3rd ps. sing. present', 'NNP': 'Proper noun, singular', 'NNPS': 'Proper noun plural', 'WDT': 'wh-determiner', 'PDT': 'Predeterminer', 'WP': 'wh-pronoun', 'POS': 'Possessive ending', 'WP$': 'Possessive wh-pronoun', 'PRP': 'Personal pronoun', 'WRB': 'wh-adverb', '(': 'open parenthesis', ')': 'close parenthesis', '``': 'open quote', ',': 'comma', "''": 'close quote', '.': 'period', '#': 'pound sign (currency marker)', '$': 'dollar sign (currency marker)', 'IN': 'Preposition/subord. conjunction', 'SYM': 'Symbol (mathematical or scientific)', 'VBG': 'Verb, gerund/present participle', 'VBP': 'Verb, non-3rd ps. sing. present', ':': 'colon', } #: Contents for the help box. This is a list of tuples, one for #: each help page, where each tuple has four elements: #: - A title (displayed as a tab) #: - A string description of tabstops (see Tkinter.Text for details) #: - The text contents for the help page. You can use expressions #: like ... to colorize the text; see L{HELP_AUTOTAG} #: for a list of tags you can use for colorizing. HELP = [ ('Help', '20', "Welcome to the regular expression chunk-parser grammar editor. " "You can use this editor to develop and test chunk parser grammars " "based on NLTK's RegexpChunkParser class.\n\n" # Help box. "Use this box ('Help') to learn more about the editor; click on the " "tabs for help on specific topics:" "\n" "Rules: grammar rule types\n" "Regexps: regular expression syntax\n" "Tags: part of speech tags\n\n" # Grammar. "Use the upper-left box ('Grammar') to edit your grammar. " "Each line of your grammar specifies a single 'rule', " "which performs an action such as creating a chunk or merging " "two chunks.\n\n" # Dev set. "The lower-left box ('Development Set') runs your grammar on the " "development set, and displays the results. " "Your grammar's chunks are highlighted, and " "the correct (gold standard) chunks are " "underlined. If they " "match, they are displayed in green; otherwise, " "they are displayed in red. The box displays a single " "sentence from the development set at a time; use the scrollbar or " "the next/previous buttons view additional sentences.\n\n" # Performance "The lower-right box ('Evaluation') tracks the performance of " "your grammar on the development set. The 'precision' axis " "indicates how many of your grammar's chunks are correct; and " "the 'recall' axis indicates how many of the gold standard " "chunks your system generated. Typically, you should try to " "design a grammar that scores high on both metrics. The " "exact precision and recall of the current grammar, as well " "as their geometric average (the 'f-score'), are displayed in " "the status bar at the bottom of the window." ), ('Rules', '10', "

    {...regexp...}

    " "\nChunk rule: creates new chunks from words matching " "regexp.\n\n" "

    }...regexp...{

    " "\nChink rule: removes words matching regexp from existing " "chunks.\n\n" "

    ...regexp1...}{...regexp2...

    " "\nSplit rule: splits chunks that match regexp1 followed by " "regexp2 in two.\n\n" "

    ...regexp...{}...regexp...

    " "\nMerge rule: joins consecutive chunks that match regexp1 " "and regexp2\n" ), ('Regexps', '10 60', #"Regular Expression Syntax Summary:\n\n" "

    Pattern\t\tMatches...

    \n" "" "\t<T>\ta word with tag T " "(where T may be a regexp).\n" "\tx?\tan optional x\n" "\tx+\ta sequence of 1 or more x's\n" "\tx*\ta sequence of 0 or more x's\n" "\tx|y\tx or y\n" "\t.\tmatches any character\n" "\t(x)\tTreats x as a group\n" "\t# x...\tTreats x... " "(to the end of the line) as a comment\n" "\t\\C\tmatches character C " "(useful when C is a special character " "like + or #)\n" "" "\n

    Examples:

    \n" "" '\t\n' '\t\tMatches "cow/NN"\n' '\t\tMatches "green/NN"\n' '\t\n' '\t\tMatches "eating/VBG"\n' '\t\tMatches "ate/VBD"\n' '\t

    In addition, the word does not have to be in base form. The browser tries to find the possible base form(s) by making certain morphological substitutions. Typing fLIeS as an obscure example gives one this. Click the previous link to see what this kind of search looks like and then come back to this page by clicking the Previous Page button.

    \n' '\t\tMatches "on/IN the/DT car/NN"\n' '\t?\n' '\t\tMatches "ran/VBD"\n' '\t\tMatches "slowly/RB ate/VBD"\n' '\t<\#> # This is a comment...\n' '\t\tMatches "#/# 100/CD"\n' "" ), ('Tags', '10 60', "

    Part of Speech Tags:

    \n" + '' + '<>' + # this gets auto-substituted w/ self.TAGSET '\n') ] HELP_AUTOTAG = [ ('red', dict(foreground='#a00')), ('green', dict(foreground='#080')), ('highlight', dict(background='#ddd')), ('underline', dict(underline=True)), ('h1', dict(underline=True)), ('indent', dict(lmargin1=20, lmargin2=20)), ('hangindent', dict(lmargin1=0, lmargin2=60)), ('var', dict(foreground='#88f')), ('regexp', dict(foreground='#ba7')), ('match', dict(foreground='#6a6')), ] ##///////////////////////////////////////////////////////////////// ## Config Parmeters ##///////////////////////////////////////////////////////////////// _EVAL_DELAY = 1 """If the user has not pressed any key for this amount of time (in seconds), and the current grammar has not been evaluated, then the eval demon will evaluate it.""" _EVAL_CHUNK = 15 """The number of sentences that should be evaluated by the eval demon each time it runs.""" _EVAL_FREQ = 0.2 """The frequency (in seconds) at which the eval demon is run""" _EVAL_DEMON_MIN = .02 """The minimum amount of time that the eval demon should take each time it runs -- if it takes less than this time, _EVAL_CHUNK will be modified upwards.""" _EVAL_DEMON_MAX = .04 """The maximum amount of time that the eval demon should take each time it runs -- if it takes more than this time, _EVAL_CHUNK will be modified downwards.""" _GRAMMARBOX_PARAMS = dict( width=40, height=12, background='#efe', highlightbackground='#efe', highlightthickness=1, relief='groove', border=2, wrap='word') _HELPBOX_PARAMS = dict( width=15, height=15, background='#efe', highlightbackground='#efe', foreground='#555', highlightthickness=1, relief='groove', border=2, wrap='word') _DEVSETBOX_PARAMS = dict( width=70, height=10, background='#eef', highlightbackground='#eef', highlightthickness=1, relief='groove', border=2, wrap='word', tabs=(30,)) _STATUS_PARAMS = dict( background='#9bb', relief='groove', border=2) _FONT_PARAMS = dict( family='helvetica', size=-20) _FRAME_PARAMS = dict( background='#777', padx=2, pady=2, border=3) _EVALBOX_PARAMS = dict( background='#eef', highlightbackground='#eef', highlightthickness=1, relief='groove', border=2, width=300, height=280) _BUTTON_PARAMS = dict( background='#777', activebackground='#777', highlightbackground='#777') _HELPTAB_BG_COLOR = '#aba' _HELPTAB_FG_COLOR = '#efe' _HELPTAB_FG_PARAMS = dict(background='#efe') _HELPTAB_BG_PARAMS = dict(background='#aba') _HELPTAB_SPACER = 6 def normalize_grammar(self, grammar): # Strip comments grammar = re.sub(r'((\\.|[^#])*)(#.*)?', r'\1', grammar) # Normalize whitespace grammar = re.sub(' +', ' ', grammar) grammar = re.sub('\n\s+', '\n', grammar) grammar = grammar.strip() # [xx] Hack: automatically backslash $! grammar = re.sub(r'([^\\])\$', r'\1\\$', grammar) return grammar def __init__(self, devset_name='conll2000', devset=None, grammar = '', chunk_node='NP', tagset=None): """ @param devset_name: The name of the development set; used for display & for save files. If either the name 'treebank' or the name 'conll2000' is used, and devset is None, then devset will be set automatically. @param devset: A list of chunked sentences @param grammar: The initial grammar to display. @param tagset: Dictionary from tags to string descriptions, used for the help page. Defaults to C{self.TAGSET}. """ self._chunk_node = chunk_node if tagset is None: tagset = self.TAGSET self.tagset = tagset # Named development sets: if devset is None: if devset_name == 'conll2000': devset = nltk.corpus.conll2000.chunked_sents('train.txt')#[:100] elif devset == 'treebank': devset = nltk.corpus.treebank_chunk.chunked_sents()#[:100] else: raise ValueError('Unknown development set %s' % devset_name) self.chunker = None """The chunker built from the grammar string""" self.grammar = grammar """The unparsed grammar string""" self.normalized_grammar = None """A normalized version of L{self.grammar}.""" self.grammar_changed = 0 """The last time() that the grammar was changed.""" self.devset = devset """The development set -- a list of chunked sentences.""" self.devset_name = devset_name """The name of the development set (for save files).""" self.devset_index = -1 """The index into the development set of the first instance that's currently being viewed.""" self._last_keypress = 0 """The time() when a key was most recently pressed""" self._history = [] """A list of (grammar, precision, recall, fscore) tuples for grammars that the user has already tried.""" self._history_index = 0 """When the user is scrolling through previous grammars, this is used to keep track of which grammar they're looking at.""" self._eval_grammar = None """The grammar that is being currently evaluated by the eval demon.""" self._eval_normalized_grammar = None """A normalized copy of L{_eval_grammar}.""" self._eval_index = 0 """The index of the next sentence in the development set that should be looked at by the eval demon.""" self._eval_score = nltk.chunk.ChunkScore(chunk_node=chunk_node) """The L{ChunkScore } object that's used to keep track of the score of the current grammar on the development set.""" # Set up the main window. top = self.top = Tk() top.geometry('+50+50') top.title('Regexp Chunk Parser App') top.bind('', self.destroy) # Varaible that restricts how much of the devset we look at. self._devset_size = IntVar(top) self._devset_size.set(100) # Set up all the tkinter widgets self._init_fonts(top) self._init_widgets(top) self._init_bindings(top) self._init_menubar(top) self.grammarbox.focus() # If a grammar was given, then display it. if grammar: self.grammarbox.insert('end', grammar+'\n') self.grammarbox.mark_set('insert', '1.0') # Display the first item in the development set self.show_devset(0) self.update() def _init_bindings(self, top): top.bind('', self._devset_next) top.bind('', self._devset_prev) top.bind('', self.toggle_show_trace) top.bind('', self.update) top.bind('', lambda e: self.save_grammar()) top.bind('', lambda e: self.load_grammar()) self.grammarbox.bind('', self.toggle_show_trace) self.grammarbox.bind('', self._devset_next) self.grammarbox.bind('', self._devset_prev) # Redraw the eval graph when the window size changes self.evalbox.bind('', self._eval_plot) def _init_fonts(self, top): # TWhat's our font size (default=same as sysfont) self._size = IntVar(top) self._size.set(20) self._font = tkFont.Font(family='helvetica', size=-self._size.get()) self._smallfont = tkFont.Font(family='helvetica', size=-(self._size.get()*14/20)) def _init_menubar(self, parent): menubar = Menu(parent) filemenu = Menu(menubar, tearoff=0) filemenu.add_command(label='Reset Application', underline=0, command=self.reset) filemenu.add_command(label='Save Current Grammar', underline=0, accelerator='Ctrl-s', command=self.save_grammar) filemenu.add_command(label='Load Grammar', underline=0, accelerator='Ctrl-o', command=self.load_grammar) filemenu.add_command(label='Save Grammar History', underline=13, command=self.save_history) filemenu.add_command(label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-q') menubar.add_cascade(label='File', underline=0, menu=filemenu) viewmenu = Menu(menubar, tearoff=0) viewmenu.add_radiobutton(label='Tiny', variable=self._size, underline=0, value=10, command=self.resize) viewmenu.add_radiobutton(label='Small', variable=self._size, underline=0, value=16, command=self.resize) viewmenu.add_radiobutton(label='Medium', variable=self._size, underline=0, value=20, command=self.resize) viewmenu.add_radiobutton(label='Large', variable=self._size, underline=0, value=24, command=self.resize) viewmenu.add_radiobutton(label='Huge', variable=self._size, underline=0, value=34, command=self.resize) menubar.add_cascade(label='View', underline=0, menu=viewmenu) devsetmenu = Menu(menubar, tearoff=0) devsetmenu.add_radiobutton(label='50 sentences', variable=self._devset_size, value=50, command=self.set_devset_size) devsetmenu.add_radiobutton(label='100 sentences', variable=self._devset_size, value=100, command=self.set_devset_size) devsetmenu.add_radiobutton(label='200 sentences', variable=self._devset_size, value=200, command=self.set_devset_size) devsetmenu.add_radiobutton(label='500 sentences', variable=self._devset_size, value=500, command=self.set_devset_size) menubar.add_cascade(label='Development-Set', underline=0, menu=devsetmenu) helpmenu = Menu(menubar, tearoff=0) helpmenu.add_command(label='About', underline=0, command=self.about) menubar.add_cascade(label='Help', underline=0, menu=helpmenu) parent.config(menu=menubar) def toggle_show_trace(self, *e): if self._showing_trace: self.show_devset() else: self.show_trace() return 'break' _SCALE_N = 5 # center on the last 5 examples. _DRAW_LINES = False def _eval_plot(self, *e, **config): width = config.get('width', self.evalbox.winfo_width()) height = config.get('height', self.evalbox.winfo_height()) # Clear the canvas self.evalbox.delete('all') # Draw the precision & recall labels. tag = self.evalbox.create_text(10, height/2-10, justify='left', anchor='w', text='Precision') left, right = self.evalbox.bbox(tag)[2] + 5, width-10 tag = self.evalbox.create_text(left + (width-left)/2, height-10, anchor='s', text='Recall', justify='center') top, bot = 10, self.evalbox.bbox(tag)[1]-10 # Draw masks for clipping the plot. bg = self._EVALBOX_PARAMS['background'] self.evalbox.lower(self.evalbox.create_rectangle(0, 0, left-1, 5000, fill=bg, outline=bg)) self.evalbox.lower(self.evalbox.create_rectangle(0, bot+1, 5000, 5000, fill=bg, outline=bg)) # Calculate the plot's scale. if self._autoscale.get() and len(self._history) > 1: max_precision = max_recall = 0 min_precision = min_recall = 1 for i in range(1, min(len(self._history), self._SCALE_N+1)): grammar, precision, recall, fmeasure = self._history[-i] min_precision = min(precision, min_precision) min_recall = min(recall, min_recall) max_precision = max(precision, max_precision) max_recall = max(recall, max_recall) # if max_precision-min_precision > max_recall-min_recall: # min_recall -= (max_precision-min_precision)/2 # max_recall += (max_precision-min_precision)/2 # else: # min_precision -= (max_recall-min_recall)/2 # max_precision += (max_recall-min_recall)/2 # if min_recall < 0: # max_recall -= min_recall # min_recall = 0 # if min_precision < 0: # max_precision -= min_precision # min_precision = 0 min_precision = max(min_precision-.01, 0) min_recall = max(min_recall-.01, 0) max_precision = min(max_precision+.01, 1) max_recall = min(max_recall+.01, 1) else: min_precision = min_recall = 0 max_precision = max_recall = 1 # Draw the axis lines & grid lines for i in range(11): x = left + (right-left)*((i/10.-min_recall)/ (max_recall-min_recall)) y = bot - (bot-top)*((i/10.-min_precision)/ (max_precision-min_precision)) if left < x < right: self.evalbox.create_line(x, top, x, bot, fill='#888') if top < y < bot: self.evalbox.create_line(left, y, right, y, fill='#888') self.evalbox.create_line(left, top, left, bot) self.evalbox.create_line(left, bot, right, bot) # Display the plot's scale self.evalbox.create_text( left-3, bot, justify='right', anchor='se', text='%d%%' % (100*min_precision)) self.evalbox.create_text( left-3, top, justify='right', anchor='ne', text='%d%%' % (100*max_precision)) self.evalbox.create_text( left, bot+3, justify='center', anchor='nw', text='%d%%' % (100*min_recall)) self.evalbox.create_text( right, bot+3, justify='center', anchor='ne', text='%d%%' % (100*max_recall)) # Display the scores. prev_x = prev_y = None for i, (_, precision, recall, fscore) in enumerate(self._history): x = left + (right-left) * ((recall-min_recall) / (max_recall-min_recall)) y = bot - (bot-top) * ((precision-min_precision) / (max_precision-min_precision)) if i == self._history_index: self.evalbox.create_oval(x-2,y-2,x+2,y+2, fill='#0f0', outline='#000') self.status['text'] = ( 'Precision: %.2f%%\t' % (precision*100)+ 'Recall: %.2f%%\t' % (recall*100)+ 'F-score: %.2f%%' % (fscore*100)) else: self.evalbox.lower( self.evalbox.create_oval(x-2,y-2,x+2,y+2, fill='#afa', outline='#8c8')) if prev_x is not None and self._eval_lines.get(): self.evalbox.lower( self.evalbox.create_line(prev_x, prev_y, x, y, fill='#8c8')) prev_x, prev_y = x, y _eval_demon_running = False def _eval_demon(self): if self.top is None: return if self.chunker is None: self._eval_demon_running = False return # Note our starting time. t0 = time.time() # If are still typing, then wait for them to finish. if (time.time()-self._last_keypress < self._EVAL_DELAY and self.normalized_grammar != self._eval_normalized_grammar): self._eval_demon_running = True return self.top.after(int(self._EVAL_FREQ*1000), self._eval_demon) # If the grammar changed, restart the evaluation. if self.normalized_grammar != self._eval_normalized_grammar: # Check if we've seen this grammar already. If so, then # just use the old evaluation values. for (g, p, r, f) in self._history: if self.normalized_grammar == self.normalize_grammar(g): self._history.append( (g, p, r, f) ) self._history_index = len(self._history) - 1 self._eval_plot() self._eval_demon_running = False self._eval_normalized_grammar = None return self._eval_index = 0 self._eval_score = nltk.chunk.ChunkScore(chunk_node= self._chunk_node) self._eval_grammar = self.grammar self._eval_normalized_grammar = self.normalized_grammar # If the grammar is empty, the don't bother evaluating it, or # recording it in history -- the score will just be 0. if self.normalized_grammar.strip() == '': #self._eval_index = self._devset_size.get() self._eval_demon_running = False return # Score the next set of examples for gold in self.devset[self._eval_index: min(self._eval_index+self._EVAL_CHUNK, self._devset_size.get())]: guess = self._chunkparse(gold.leaves()) self._eval_score.score(gold, guess) # update our index in the devset. self._eval_index += self._EVAL_CHUNK # Check if we're done if self._eval_index >= self._devset_size.get(): self._history.append( (self._eval_grammar, self._eval_score.precision(), self._eval_score.recall(), self._eval_score.f_measure()) ) self._history_index = len(self._history)-1 self._eval_plot() self._eval_demon_running = False self._eval_normalized_grammar = None else: progress = 100*self._eval_index/self._devset_size.get() self.status['text'] = ('Evaluating on Development Set (%d%%)' % progress) self._eval_demon_running = True self._adaptively_modify_eval_chunk(time.time() - t0) self.top.after(int(self._EVAL_FREQ*1000), self._eval_demon) def _adaptively_modify_eval_chunk(self, t): """ Modify _EVAL_CHUNK to try to keep the amount of time that the eval demon takes between _EVAL_DEMON_MIN and _EVAL_DEMON_MAX. @param t: The amount of time that the eval demon took. """ if t > self._EVAL_DEMON_MAX and self._EVAL_CHUNK > 5: self._EVAL_CHUNK = min(self._EVAL_CHUNK-1, max(int(self._EVAL_CHUNK*(self._EVAL_DEMON_MAX/t)), self._EVAL_CHUNK-10)) elif t < self._EVAL_DEMON_MIN: self._EVAL_CHUNK = max(self._EVAL_CHUNK+1, min(int(self._EVAL_CHUNK*(self._EVAL_DEMON_MIN/t)), self._EVAL_CHUNK+10)) def _init_widgets(self, top): frame0 = Frame(top, **self._FRAME_PARAMS) frame0.grid_columnconfigure(0, weight=4) frame0.grid_columnconfigure(3, weight=2) frame0.grid_rowconfigure(1, weight=1) frame0.grid_rowconfigure(5, weight=1) # The grammar self.grammarbox = Text(frame0, font=self._font, **self._GRAMMARBOX_PARAMS) self.grammarlabel = Label(frame0, font=self._font, text='Grammar:', highlightcolor='black', background=self._GRAMMARBOX_PARAMS['background']) self.grammarlabel.grid(column=0, row=0, sticky='SW') self.grammarbox.grid(column=0, row=1, sticky='NEWS') # Scroll bar for grammar grammar_scrollbar = Scrollbar(frame0, command=self.grammarbox.yview) grammar_scrollbar.grid(column=1, row=1, sticky='NWS') self.grammarbox.config(yscrollcommand=grammar_scrollbar.set) # grammar buttons bg = self._FRAME_PARAMS['background'] frame3 = Frame(frame0, background=bg) frame3.grid(column=0, row=2, sticky='EW') Button(frame3, text='Prev Grammar', command=self._history_prev, **self._BUTTON_PARAMS).pack(side='left') Button(frame3, text='Next Grammar', command=self._history_next, **self._BUTTON_PARAMS).pack(side='left') # Help box self.helpbox = Text(frame0, font=self._smallfont, **self._HELPBOX_PARAMS) self.helpbox.grid(column=3, row=1, sticky='NEWS') self.helptabs = {} bg = self._FRAME_PARAMS['background'] helptab_frame = Frame(frame0, background=bg) helptab_frame.grid(column=3, row=0, sticky='SW') for i, (tab, tabstops, text) in enumerate(self.HELP): label = Label(helptab_frame, text=tab, font=self._smallfont) label.grid(column=i*2, row=0, sticky='S') #help_frame.grid_columnconfigure(i, weight=1) #label.pack(side='left') label.bind('', lambda e, tab=tab: self.show_help(tab)) self.helptabs[tab] = label Frame(helptab_frame, height=1, width=self._HELPTAB_SPACER, background=bg).grid(column=i*2+1, row=0) self.helptabs[self.HELP[0][0]].configure(font=self._font) self.helpbox.tag_config('elide', elide=True) for (tag, params) in self.HELP_AUTOTAG: self.helpbox.tag_config('tag-%s' % tag, **params) self.show_help(self.HELP[0][0]) # Scroll bar for helpbox help_scrollbar = Scrollbar(frame0, command=self.helpbox.yview) self.helpbox.config(yscrollcommand=help_scrollbar.set) help_scrollbar.grid(column=4, row=1, sticky='NWS') # The dev set frame4 = Frame(frame0, background=self._FRAME_PARAMS['background']) self.devsetbox = Text(frame4, font=self._font, **self._DEVSETBOX_PARAMS) self.devsetbox.pack(expand=True, fill='both') self.devsetlabel = Label(frame0, font=self._font, text='Development Set:', justify='right', background=self._DEVSETBOX_PARAMS['background']) self.devsetlabel.grid(column=0, row=4, sticky='SW') frame4.grid(column=0, row=5, sticky='NEWS') # dev set scrollbars self.devset_scroll = Scrollbar(frame0, command=self._devset_scroll) self.devset_scroll.grid(column=1, row=5, sticky='NWS') self.devset_xscroll = Scrollbar(frame4, command=self.devsetbox.xview, orient='horiz') self.devsetbox['xscrollcommand'] = self.devset_xscroll.set self.devset_xscroll.pack(side='bottom', fill='x') # dev set buttons bg = self._FRAME_PARAMS['background'] frame1 = Frame(frame0, background=bg) frame1.grid(column=0, row=7, sticky='EW') Button(frame1, text='Prev Example (Ctrl-p)', command=self._devset_prev, **self._BUTTON_PARAMS).pack(side='left') Button(frame1, text='Next Example (Ctrl-n)', command=self._devset_next, **self._BUTTON_PARAMS).pack(side='left') self.devset_button = Button(frame1, text='Show example', command=self.show_devset, state='disabled', **self._BUTTON_PARAMS) self.devset_button.pack(side='right') self.trace_button = Button(frame1, text='Show trace', command=self.show_trace, **self._BUTTON_PARAMS) self.trace_button.pack(side='right') # evaluation box self.evalbox = Canvas(frame0, **self._EVALBOX_PARAMS) label = Label(frame0, font=self._font, text='Evaluation:', justify='right', background=self._EVALBOX_PARAMS['background']) label.grid(column=3, row=4, sticky='SW') self.evalbox.grid(column=3, row=5, sticky='NEWS', columnspan=2) # evaluation box buttons bg = self._FRAME_PARAMS['background'] frame2 = Frame(frame0, background=bg) frame2.grid(column=3, row=7, sticky='EW') self._autoscale = IntVar(self.top) self._autoscale.set(False) Checkbutton(frame2, variable=self._autoscale, command=self._eval_plot, text='Zoom', **self._BUTTON_PARAMS).pack(side='left') self._eval_lines = IntVar(self.top) self._eval_lines.set(False) Checkbutton(frame2, variable=self._eval_lines, command=self._eval_plot, text='Lines', **self._BUTTON_PARAMS).pack(side='left') Button(frame2, text='History', **self._BUTTON_PARAMS).pack(side='right') # The status label self.status = Label(frame0, font=self._font, **self._STATUS_PARAMS) self.status.grid(column=0, row=9, sticky='NEW', padx=3, pady=2, columnspan=5) # Help box & devset box can't be edited. self.helpbox['state'] = 'disabled' self.devsetbox['state'] = 'disabled' # Spacers bg = self._FRAME_PARAMS['background'] Frame(frame0, height=10, width=0, background=bg).grid(column=0, row=3) Frame(frame0, height=0, width=10, background=bg).grid(column=2, row=0) Frame(frame0, height=6, width=0, background=bg).grid(column=0, row=8) # pack the frame. frame0.pack(fill='both', expand=True) # Set up colors for the devset box self.devsetbox.tag_config('true-pos', background='#afa', underline='True') self.devsetbox.tag_config('false-neg', underline='True', foreground='#800') self.devsetbox.tag_config('false-pos', background='#faa') self.devsetbox.tag_config('trace', foreground='#666', wrap='none') self.devsetbox.tag_config('wrapindent', lmargin2=30, wrap='none') self.devsetbox.tag_config('error', foreground='#800') # And for the grammarbox self.grammarbox.tag_config('error', background='#fec') self.grammarbox.tag_config('comment', foreground='#840') self.grammarbox.tag_config('angle', foreground='#00f') self.grammarbox.tag_config('brace', foreground='#0a0') self.grammarbox.tag_config('hangindent', lmargin1=0, lmargin2=40) _showing_trace = False def show_trace(self, *e): self._showing_trace = True self.trace_button['state'] = 'disabled' self.devset_button['state'] = 'normal' self.devsetbox['state'] = 'normal' #self.devsetbox['wrap'] = 'none' self.devsetbox.delete('1.0', 'end') self.devsetlabel['text']='Development Set (%d/%d)' % ( (self.devset_index+1, self._devset_size.get())) if self.chunker is None: self.devsetbox.insert('1.0', 'Trace: waiting for a valid grammar.') self.devsetbox.tag_add('error', '1.0', 'end') return # can't do anything more gold_tree = self.devset[self.devset_index] rules = self.chunker.rules() # Calculate the tag sequence tagseq = '\t' charnum = [1] for wordnum, (word, pos) in enumerate(gold_tree.leaves()): tagseq += '%s ' % pos charnum.append(len(tagseq)) self.charnum = dict(((i, j), charnum[j]) for i in range(len(rules)+1) for j in range(len(charnum))) self.linenum = dict((i,i*2+2) for i in range(len(rules)+1)) for i in range(len(rules)+1): if i == 0: self.devsetbox.insert('end', 'Start:\n') self.devsetbox.tag_add('trace', 'end -2c linestart', 'end -2c') else: self.devsetbox.insert('end', 'Apply %s:\n' % rules[i-1]) self.devsetbox.tag_add('trace', 'end -2c linestart', 'end -2c') # Display the tag sequence. self.devsetbox.insert('end', tagseq+'\n') self.devsetbox.tag_add('wrapindent','end -2c linestart','end -2c') # Run a partial parser, and extract gold & test chunks chunker = nltk.chunk.RegexpChunkParser(rules[:i]) test_tree = self._chunkparse(gold_tree.leaves()) gold_chunks = self._chunks(gold_tree) test_chunks = self._chunks(test_tree) # Compare them. for chunk in gold_chunks.intersection(test_chunks): self._color_chunk(i, chunk, 'true-pos') for chunk in gold_chunks - test_chunks: self._color_chunk(i, chunk, 'false-neg') for chunk in test_chunks - gold_chunks: self._color_chunk(i, chunk, 'false-pos') self.devsetbox.insert('end', 'Finished.\n') self.devsetbox.tag_add('trace', 'end -2c linestart', 'end -2c') # This is a hack, because the x-scrollbar isn't updating its # position right -- I'm not sure what the underlying cause is # though. (This is on OS X w/ python 2.5) self.top.after(100, self.devset_xscroll.set, 0, .3) def show_help(self, tab): self.helpbox['state'] = 'normal' self.helpbox.delete('1.0', 'end') for (name, tabstops, text) in self.HELP: if name == tab: text = text.replace('<>', '\n'.join( ('\t%s\t%s' % item for item in sorted(self.tagset.items(), key=lambda (t,w):re.match('\w+',t) and (0,t) or (1,t))))) self.helptabs[name].config(**self._HELPTAB_FG_PARAMS) self.helpbox.config(tabs=tabstops) self.helpbox.insert('1.0', text+'\n'*20) C = '1.0 + %d chars' for (tag, params) in self.HELP_AUTOTAG: pattern = '(?s)(<%s>)(.*?)()' % (tag, tag) for m in re.finditer(pattern, text): self.helpbox.tag_add('elide', C % m.start(1), C % m.end(1)) self.helpbox.tag_add('tag-%s' % tag, C % m.start(2), C % m.end(2)) self.helpbox.tag_add('elide', C % m.start(3), C % m.end(3)) else: self.helptabs[name].config(**self._HELPTAB_BG_PARAMS) self.helpbox['state'] = 'disabled' def _history_prev(self, *e): self._view_history(self._history_index-1) return 'break' def _history_next(self, *e): self._view_history(self._history_index+1) return 'break' def _view_history(self, index): # Bounds & sanity checking: index = max(0, min(len(self._history)-1, index)) if not self._history: return # Already viewing the requested history item? if index == self._history_index: return # Show the requested grammar. It will get added to _history # only if they edit it (causing self.update() to get run.) self.grammarbox['state'] = 'normal' self.grammarbox.delete('1.0', 'end') self.grammarbox.insert('end', self._history[index][0]) self.grammarbox.mark_set('insert', '1.0') self._history_index = index self._syntax_highlight_grammar(self._history[index][0]) # Record the normalized grammar & regenerate the chunker. self.normalized_grammar = self.normalize_grammar( self._history[index][0]) if self.normalized_grammar: rules = [nltk.chunk.regexp.RegexpChunkRule.parse(line) for line in self.normalized_grammar.split('\n')] else: rules = [] self.chunker = nltk.chunk.RegexpChunkParser(rules) # Show the score. self._eval_plot() # Update the devset box self._highlight_devset() if self._showing_trace: self.show_trace() # Update the grammar label if self._history_index < len(self._history)-1: self.grammarlabel['text'] = 'Grammar %s/%s:' % ( self._history_index+1, len(self._history)) else: self.grammarlabel['text'] = 'Grammar:' def _devset_next(self, *e): self._devset_scroll('scroll', 1, 'page') return 'break' def _devset_prev(self, *e): self._devset_scroll('scroll', -1, 'page') return 'break' def destroy(self, *e): if self.top is None: return self.top.destroy() self.top = None def _devset_scroll(self, command, *args): N = 1 # size of a page -- one sentence. showing_trace = self._showing_trace if command == 'scroll' and args[1].startswith('unit'): self.show_devset(self.devset_index+int(args[0])) elif command == 'scroll' and args[1].startswith('page'): self.show_devset(self.devset_index+N*int(args[0])) elif command == 'moveto': self.show_devset(int(float(args[0])*self._devset_size.get())) else: assert 0, 'bad scroll command %s %s' % (command, args) if showing_trace: self.show_trace() def show_devset(self, index=None): if index is None: index = self.devset_index # Bounds checking index = min(max(0, index), self._devset_size.get()-1) if index == self.devset_index and not self._showing_trace: return self.devset_index = index self._showing_trace = False self.trace_button['state'] = 'normal' self.devset_button['state'] = 'disabled' # Clear the text box. self.devsetbox['state'] = 'normal' self.devsetbox['wrap'] = 'word' self.devsetbox.delete('1.0', 'end') self.devsetlabel['text']='Development Set (%d/%d)' % ( (self.devset_index+1, self._devset_size.get())) # Add the sentences sample = self.devset[self.devset_index:self.devset_index+1] self.charnum = {} self.linenum = {0:1} for sentnum, sent in enumerate(sample): linestr = '' for wordnum, (word, pos) in enumerate(sent.leaves()): self.charnum[sentnum, wordnum] = len(linestr) linestr += '%s/%s ' % (word, pos) self.charnum[sentnum, wordnum+1] = len(linestr) self.devsetbox.insert('end', linestr[:-1]+'\n\n') # Highlight chunks in the dev set if self.chunker is not None: self._highlight_devset() self.devsetbox['state'] = 'disabled' # Update the scrollbar first = float(self.devset_index)/self._devset_size.get() last = float(self.devset_index+2)/self._devset_size.get() self.devset_scroll.set(first, last) def _chunks(self, tree): chunks = set() wordnum = 0 for child in tree: if isinstance(child, Tree): if child.node == self._chunk_node: chunks.add( (wordnum, wordnum+len(child)) ) wordnum += len(child) else: wordnum += 1 return chunks def _syntax_highlight_grammar(self, grammar): if self.top is None: return self.grammarbox.tag_remove('comment', '1.0', 'end') self.grammarbox.tag_remove('angle', '1.0', 'end') self.grammarbox.tag_remove('brace', '1.0', 'end') self.grammarbox.tag_add('hangindent', '1.0', 'end') for lineno, line in enumerate(grammar.split('\n')): if not line.strip(): continue m = re.match(r'(\\.|[^#])*(#.*)?', line) comment_start = None if m.group(2): comment_start = m.start(2) s = '%d.%d' % (lineno+1, m.start(2)) e = '%d.%d' % (lineno+1, m.end(2)) self.grammarbox.tag_add('comment', s, e) for m in re.finditer('[<>{}]', line): if comment_start is not None and m.start() >= comment_start: break s = '%d.%d' % (lineno+1, m.start()) e = '%d.%d' % (lineno+1, m.end()) if m.group() in '<>': self.grammarbox.tag_add('angle', s, e) else: self.grammarbox.tag_add('brace', s, e) def _grammarcheck(self, grammar): if self.top is None: return self.grammarbox.tag_remove('error', '1.0', 'end') self._grammarcheck_errs = [] for lineno, line in enumerate(grammar.split('\n')): line = re.sub(r'((\\.|[^#])*)(#.*)?', r'\1', line) line = line.strip() if line: try: nltk.chunk.regexp.RegexpChunkRule.parse(line) except ValueError, e: self.grammarbox.tag_add('error', '%s.0' % (lineno+1), '%s.0 lineend' % (lineno+1)) self.status['text'] = '' def update(self, *event): # Record when update was called (for grammarcheck) if event: self._last_keypress = time.time() # Read the grammar from the Text box. self.grammar = grammar = self.grammarbox.get('1.0', 'end') # If the grammar hasn't changed, do nothing: normalized_grammar = self.normalize_grammar(grammar) if normalized_grammar == self.normalized_grammar: return else: self.normalized_grammar = normalized_grammar # If the grammar has changed, and we're looking at history, # then stop looking at history. if self._history_index < len(self._history)-1: self.grammarlabel['text'] = 'Grammar:' self._syntax_highlight_grammar(grammar) # The grammar has changed; try parsing it. If it doesn't # parse, do nothing. (flag error location?) try: # Note: the normalized grammar has no blank lines. if normalized_grammar: rules = [nltk.chunk.regexp.RegexpChunkRule.parse(line) for line in normalized_grammar.split('\n')] else: rules = [] except ValueError, e: # Use the un-normalized grammar for error highlighting. self._grammarcheck(grammar) self.chunker = None return self.chunker = nltk.chunk.RegexpChunkParser(rules) self.grammarbox.tag_remove('error', '1.0', 'end') self.grammar_changed = time.time() # Display the results if self._showing_trace: self.show_trace() else: self._highlight_devset() # Start the eval demon if not self._eval_demon_running: self._eval_demon() def _highlight_devset(self, sample=None): if sample is None: sample = self.devset[self.devset_index:self.devset_index+1] self.devsetbox.tag_remove('true-pos', '1.0', 'end') self.devsetbox.tag_remove('false-neg', '1.0', 'end') self.devsetbox.tag_remove('false-pos', '1.0', 'end') # Run the grammar on the test cases. for sentnum, gold_tree in enumerate(sample): # Run the chunk parser test_tree = self._chunkparse(gold_tree.leaves()) # Extract gold & test chunks gold_chunks = self._chunks(gold_tree) test_chunks = self._chunks(test_tree) # Compare them. for chunk in gold_chunks.intersection(test_chunks): self._color_chunk(sentnum, chunk, 'true-pos') for chunk in gold_chunks - test_chunks: self._color_chunk(sentnum, chunk, 'false-neg') for chunk in test_chunks - gold_chunks: self._color_chunk(sentnum, chunk, 'false-pos') def _chunkparse(self, words): try: return self.chunker.parse(words) except (ValueError, IndexError), e: # There's an error somewhere in the grammar, but we're not sure # exactly where, so just mark the whole grammar as bad. # E.g., this is caused by: "({})" self.grammarbox.tag_add('error', '1.0', 'end') # Treat it as tagging nothing: return words def _color_chunk(self, sentnum, chunk, tag): start, end = chunk self.devsetbox.tag_add(tag, '%s.%s' % (self.linenum[sentnum], self.charnum[sentnum, start]), '%s.%s' % (self.linenum[sentnum], self.charnum[sentnum, end]-1)) def reset(self): # Clear various variables self.chunker = None self.grammar = None self.normalized_grammar = None self.grammar_changed = 0 self._history = [] self._history_index = 0 # Update the on-screen display. self.grammarbox.delete('1.0', 'end') self.show_devset(0) self.update() #self._eval_plot() SAVE_GRAMMAR_TEMPLATE = ( '# Regexp Chunk Parsing Grammar\n' '# Saved %(date)s\n' '#\n' '# Development set: %(devset)s\n' '# Precision: %(precision)s\n' '# Recall: %(recall)s\n' '# F-score: %(fscore)s\n\n' '%(grammar)s\n') def save_grammar(self, filename=None): if not filename: ftypes = [('Chunk Gramamr', '.chunk'), ('All files', '*')] filename = tkFileDialog.asksaveasfilename(filetypes=ftypes, defaultextension='.chunk') if not filename: return if (self._history and self.normalized_grammar == self.normalize_grammar(self._history[-1][0])): precision, recall, fscore = ['%.2f%%' % (100*v) for v in self._history[-1][1:]] elif self.chunker is None: precision = recall = fscore = 'Grammar not well formed' else: precision = recall = fscore = 'Not finished evaluation yet' out = open(filename, 'w') out.write(self.SAVE_GRAMMAR_TEMPLATE % dict( date=time.ctime(), devset=self.devset_name, precision=precision, recall=recall, fscore=fscore, grammar=self.grammar.strip())) out.close() def load_grammar(self, filename=None): if not filename: ftypes = [('Chunk Gramamr', '.chunk'), ('All files', '*')] filename = tkFileDialog.askopenfilename(filetypes=ftypes, defaultextension='.chunk') if not filename: return self.grammarbox.delete('1.0', 'end') self.update() grammar = open(filename).read() grammar = re.sub('^\# Regexp Chunk Parsing Grammar[\s\S]*' 'F-score:.*\n', '', grammar).lstrip() self.grammarbox.insert('1.0', grammar) self.update() def save_history(self, filename=None): if not filename: ftypes = [('Chunk Gramamr History', '.txt'), ('All files', '*')] filename = tkFileDialog.asksaveasfilename(filetypes=ftypes, defaultextension='.txt') if not filename: return out = open(filename, 'w') out.write('# Regexp Chunk Parsing Grammar History\n') out.write('# Saved %s\n' % time.ctime()) out.write('# Development set: %s\n' % self.devset_name) for i, (g, p, r, f) in enumerate(self._history): hdr = ('Grammar %d/%d (precision=%.2f%%, recall=%.2f%%, ' 'fscore=%.2f%%)' % (i+1, len(self._history), p*100, r*100, f*100)) out.write('\n%s\n' % hdr) out.write(''.join(' %s\n' % line for line in g.strip().split())) if not (self._history and self.normalized_grammar == self.normalize_grammar(self._history[-1][0])): if self.chunker is None: out.write('\nCurrent Grammar (not well-formed)\n') else: out.write('\nCurrent Grammar (not evaluated)\n') out.write(''.join(' %s\n' % line for line in self.grammar.strip().split())) out.close() def about(self, *e): ABOUT = ("NLTK RegExp Chunk Parser Application\n"+ "Written by Edward Loper") TITLE = 'About: Regular Expression Chunk Parser Application' try: from tkMessageBox import Message Message(message=ABOUT, title=TITLE).show() except: ShowText(self.top, TITLE, ABOUT) def set_devset_size(self, size=None): if size is not None: self._devset_size.set(size) self._devset_size.set(min(len(self.devset), self._devset_size.get())) self.show_devset(1) self.show_devset(0) # what about history? Evaluated at diff dev set sizes! def resize(self, size=None): if size is not None: self._size.set(size) size = self._size.get() self._font.configure(size=-(abs(size))) self._smallfont.configure(size=min(-10, -(abs(size))*14/20)) def mainloop(self, *args, **kwargs): """ Enter the Tkinter mainloop. This function must be called if this demo is created from a non-interactive program (e.g. from a secript); otherwise, the demo will close as soon as the script completes. """ if in_idle(): return self.top.mainloop(*args, **kwargs) def app(): RegexpChunkApp().mainloop() if __name__ == '__main__': app() __all__ = ['app'] nltk-2.0~b9/nltk/app/chartparser_app.py0000644000175000017500000024775311327451574020071 0ustar bhavanibhavani# Natural Language Toolkit: Chart Parser Application # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Jean Mark Gawron # Steven Bird # URL: # For license information, see LICENSE.TXT # # $Id: chartparser_app.py 8479 2010-01-13 05:40:34Z StevenBird1 $ """ A graphical tool for exploring chart parsing. Chart parsing is a flexible parsing algorithm that uses a data structure called a "chart" to record hypotheses about syntactic constituents. Each hypothesis is represented by a single "edge" on the chart. A set of "chart rules" determine when new edges can be added to the chart. This set of rules controls the overall behavior of the parser (e.g., whether it parses top-down or bottom-up). The chart parsing tool demonstrates the process of parsing a single sentence, with a given grammar and lexicon. Its display is divided into three sections: the bottom section displays the chart; the middle section displays the sentence; and the top section displays the partial syntax tree corresponding to the selected edge. Buttons along the bottom of the window are used to control the execution of the algorithm. The chart parsing tool allows for flexible control of the parsing algorithm. At each step of the algorithm, you can select which rule or strategy you wish to apply. This allows you to experiment with mixing different strategies (e.g., top-down and bottom-up). You can exercise fine-grained control over the algorithm by selecting which edge you wish to apply a rule to. """ # At some point, we should rewrite this tool to use the new canvas # widget system. import pickle from tkFileDialog import asksaveasfilename, askopenfilename import Tkinter import math import string import os.path from nltk.parse.chart import * from nltk.tree import Tree from nltk.grammar import Nonterminal, parse_cfg from nltk.util import in_idle from nltk.draw.util import * from nltk.draw.cfg import CFGEditor from nltk.draw.tree import tree_to_treesegment, TreeSegmentWidget # Known bug: ChartView doesn't handle edges generated by epsilon # productions (e.g., [Production: PP -> ]) very well. ####################################################################### # Edge List ####################################################################### class EdgeList(ColorizedList): ARROW = SymbolWidget.SYMBOLS['rightarrow'] def _init_colortags(self, textwidget, options): textwidget.tag_config('terminal', foreground='#006000') textwidget.tag_config('arrow', font='symbol', underline='0') textwidget.tag_config('dot', foreground = '#000000') textwidget.tag_config('nonterminal', foreground='blue', font=('helvetica', -12, 'bold')) def _item_repr(self, item): contents = [] contents.append(('%s\t' % item.lhs(), 'nonterminal')) contents.append((self.ARROW, 'arrow')) for i, elt in enumerate(item.rhs()): if i == item.dot(): contents.append((' *', 'dot')) if isinstance(elt, Nonterminal): contents.append((' %s' % elt.symbol(), 'nonterminal')) else: contents.append((' %r' % elt, 'terminal')) if item.is_complete(): contents.append((' *', 'dot')) return contents ####################################################################### # Chart Matrix View ####################################################################### class ChartMatrixView(object): """ A view of a chart that displays the contents of the corresponding matrix. """ def __init__(self, parent, chart, toplevel=True, title='Chart Matrix', show_numedges=False): self._chart = chart self._cells = [] self._marks = [] self._selected_cell = None if toplevel: self._root = Tkinter.Toplevel(parent) self._root.title(title) self._root.bind('', self.destroy) self._init_quit(self._root) else: self._root = Tkinter.Frame(parent) self._init_matrix(self._root) self._init_list(self._root) if show_numedges: self._init_numedges(self._root) else: self._numedges_label = None self._callbacks = {} self._num_edges = 0 self.draw() def _init_quit(self, root): quit = Tkinter.Button(root, text='Quit', command=self.destroy) quit.pack(side='bottom', expand=0, fill='none') def _init_matrix(self, root): cframe = Tkinter.Frame(root, border=2, relief='sunken') cframe.pack(expand=0, fill='none', padx=1, pady=3, side='top') self._canvas = Tkinter.Canvas(cframe, width=200, height=200, background='white') self._canvas.pack(expand=0, fill='none') def _init_numedges(self, root): self._numedges_label = Tkinter.Label(root, text='0 edges') self._numedges_label.pack(expand=0, fill='none', side='top') def _init_list(self, root): self._list = EdgeList(root, [], width=20, height=5) self._list.pack(side='top', expand=1, fill='both', pady=3) def cb(edge, self=self): self._fire_callbacks('select', edge) self._list.add_callback('select', cb) self._list.focus() def destroy(self, *e): if self._root is None: return try: self._root.destroy() except: pass self._root = None def set_chart(self, chart): if chart is not self._chart: self._chart = chart self._num_edges = 0 self.draw() def update(self): if self._root is None: return # Count the edges in each cell N = len(self._cells) cell_edges = [[0 for i in range(N)] for j in range(N)] for edge in self._chart: cell_edges[edge.start()][edge.end()] += 1 # Color the cells correspondingly. for i in range(N): for j in range(i, N): if cell_edges[i][j] == 0: color = 'gray20' else: color = ('#00%02x%02x' % (min(255, 50+128*cell_edges[i][j]/10), max(0, 128-128*cell_edges[i][j]/10))) cell_tag = self._cells[i][j] self._canvas.itemconfig(cell_tag, fill=color) if (i,j) == self._selected_cell: self._canvas.itemconfig(cell_tag, outline='#00ffff', width=3) self._canvas.tag_raise(cell_tag) else: self._canvas.itemconfig(cell_tag, outline='black', width=1) # Update the edge list. edges = list(self._chart.select(span=self._selected_cell)) self._list.set(edges) # Update our edge count. self._num_edges = self._chart.num_edges() if self._numedges_label is not None: self._numedges_label['text'] = '%d edges' % self._num_edges def activate(self): self._canvas.itemconfig('inactivebox', state='hidden') self.update() def inactivate(self): self._canvas.itemconfig('inactivebox', state='normal') self.update() def add_callback(self, event, func): self._callbacks.setdefault(event,{})[func] = 1 def remove_callback(self, event, func=None): if func is None: del self._callbacks[event] else: try: del self._callbacks[event][func] except: pass def _fire_callbacks(self, event, *args): if not self._callbacks.has_key(event): return for cb_func in self._callbacks[event].keys(): cb_func(*args) def select_cell(self, i, j): if self._root is None: return # If the cell is already selected (and the chart contents # haven't changed), then do nothing. if ((i,j) == self._selected_cell and self._chart.num_edges() == self._num_edges): return self._selected_cell = (i,j) self.update() # Fire the callback. self._fire_callbacks('select_cell', i, j) def deselect_cell(self): if self._root is None: return self._selected_cell = None self._list.set([]) self.update() def _click_cell(self, i, j): if self._selected_cell == (i,j): self.deselect_cell() else: self.select_cell(i, j) def view_edge(self, edge): self.select_cell(*edge.span()) self._list.view(edge) def mark_edge(self, edge): if self._root is None: return self.select_cell(*edge.span()) self._list.mark(edge) def unmark_edge(self, edge=None): if self._root is None: return self._list.unmark(edge) def markonly_edge(self, edge): if self._root is None: return self.select_cell(*edge.span()) self._list.markonly(edge) def draw(self): if self._root is None: return LEFT_MARGIN = BOT_MARGIN = 15 TOP_MARGIN = 5 c = self._canvas c.delete('all') N = self._chart.num_leaves()+1 dx = (int(c['width'])-LEFT_MARGIN)/N dy = (int(c['height'])-TOP_MARGIN-BOT_MARGIN)/N c.delete('all') # Labels and dotted lines for i in range(N): c.create_text(LEFT_MARGIN-2, i*dy+dy/2+TOP_MARGIN, text=`i`, anchor='e') c.create_text(i*dx+dx/2+LEFT_MARGIN, N*dy+TOP_MARGIN+1, text=`i`, anchor='n') c.create_line(LEFT_MARGIN, dy*(i+1)+TOP_MARGIN, dx*N+LEFT_MARGIN, dy*(i+1)+TOP_MARGIN, dash='.') c.create_line(dx*i+LEFT_MARGIN, TOP_MARGIN, dx*i+LEFT_MARGIN, dy*N+TOP_MARGIN, dash='.') # A box around the whole thing c.create_rectangle(LEFT_MARGIN, TOP_MARGIN, LEFT_MARGIN+dx*N, dy*N+TOP_MARGIN, width=2) # Cells self._cells = [[None for i in range(N)] for j in range(N)] for i in range(N): for j in range(i, N): t = c.create_rectangle(j*dx+LEFT_MARGIN, i*dy+TOP_MARGIN, (j+1)*dx+LEFT_MARGIN, (i+1)*dy+TOP_MARGIN, fill='gray20') self._cells[i][j] = t def cb(event, self=self, i=i, j=j): self._click_cell(i,j) c.tag_bind(t, '', cb) # Inactive box xmax, ymax = int(c['width']), int(c['height']) t = c.create_rectangle(-100, -100, xmax+100, ymax+100, fill='gray50', state='hidden', tag='inactivebox') c.tag_lower(t) # Update the cells. self.update() def pack(self, *args, **kwargs): self._root.pack(*args, **kwargs) ####################################################################### # Chart Results View ####################################################################### class ChartResultsView(object): def __init__(self, parent, chart, grammar, toplevel=True): self._chart = chart self._grammar = grammar self._trees = [] self._y = 10 self._treewidgets = [] self._selection = None self._selectbox = None if toplevel: self._root = Tkinter.Toplevel(parent) self._root.title('Chart Parser Application: Results') self._root.bind('', self.destroy) else: self._root = Tkinter.Frame(parent) # Buttons if toplevel: buttons = Tkinter.Frame(self._root) buttons.pack(side='bottom', expand=0, fill='x') Tkinter.Button(buttons, text='Quit', command=self.destroy).pack(side='right') Tkinter.Button(buttons, text='Print All', command=self.print_all).pack(side='left') Tkinter.Button(buttons, text='Print Selection', command=self.print_selection).pack(side='left') # Canvas frame. self._cframe = CanvasFrame(self._root, closeenough=20) self._cframe.pack(side='top', expand=1, fill='both') # Initial update self.update() def update(self, edge=None): if self._root is None: return # If the edge isn't a parse edge, do nothing. if edge is not None: if edge.lhs() != self._grammar.start(): return if edge.span() != (0, self._chart.num_leaves()): return for parse in self._chart.parses(self._grammar.start()): if parse not in self._trees: self._add(parse) def _add(self, parse): # Add it to self._trees. self._trees.append(parse) # Create a widget for it. c = self._cframe.canvas() treewidget = tree_to_treesegment(c, parse) # Add it to the canvas frame. self._treewidgets.append(treewidget) self._cframe.add_widget(treewidget, 10, self._y) # Register callbacks. treewidget.bind_click(self._click) # Update y. self._y = treewidget.bbox()[3] + 10 def _click(self, widget): c = self._cframe.canvas() if self._selection is not None: c.delete(self._selectbox) self._selection = widget (x1, y1, x2, y2) = widget.bbox() self._selectbox = c.create_rectangle(x1, y1, x2, y2, width=2, outline='#088') def _color(self, treewidget, color): treewidget.node()['color'] = color for child in treewidget.subtrees(): if isinstance(child, TreeSegmentWidget): self._color(child, color) else: child['color'] = color def print_all(self, *e): if self._root is None: return self._cframe.print_to_file() def print_selection(self, *e): if self._root is None: return if self._selection is None: tkMessageBox.showerror('Print Error', 'No tree selected') else: c = self._cframe.canvas() for widget in self._treewidgets: if widget is not self._selection: self._cframe.destroy_widget(widget) c.delete(self._selectbox) (x1,y1,x2,y2) = self._selection.bbox() self._selection.move(10-x1,10-y1) c['scrollregion'] = '0 0 %s %s' % (x2-x1+20, y2-y1+20) self._cframe.print_to_file() # Restore our state. self._treewidgets = [self._selection] self.clear() self.update() def clear(self): if self._root is None: return for treewidget in self._treewidgets: self._cframe.destroy_widget(treewidget) self._trees = [] self._treewidgets = [] if self._selection is not None: self._cframe.canvas().delete(self._selectbox) self._selection = None self._y = 10 def set_chart(self, chart): self.clear() self._chart = chart self.update() def set_grammar(self, grammar): self.clear() self._grammar = grammar self.update() def destroy(self, *e): if self._root is None: return try: self._root.destroy() except: pass self._root = None def pack(self, *args, **kwargs): self._root.pack(*args, **kwargs) ####################################################################### # Chart Comparer ####################################################################### class ChartComparer(object): """ @ivar _root: The root window @ivar _charts: A dictionary mapping names to charts. When charts are loaded, they are added to this dictionary. @ivar _left_chart: The left L{Chart}. @ivar _left_name: The name C{_left_chart} (derived from filename) @ivar _left_matrix: The L{ChartMatrixView} for C{_left_chart} @ivar _left_selector: The drop-down C{MutableOptionsMenu} used to select C{_left_chart}. @ivar _right_chart: The right L{Chart}. @ivar _right_name: The name C{_right_chart} (derived from filename) @ivar _right_matrix: The L{ChartMatrixView} for C{_right_chart} @ivar _right_selector: The drop-down C{MutableOptionsMenu} used to select C{_right_chart}. @ivar _out_chart: The out L{Chart}. @ivar _out_name: The name C{_out_chart} (derived from filename) @ivar _out_matrix: The L{ChartMatrixView} for C{_out_chart} @ivar _out_label: The label for C{_out_chart}. @ivar _op_label: A Label containing the most recent operation. """ _OPSYMBOL = {'-': '-', 'and': SymbolWidget.SYMBOLS['intersection'], 'or': SymbolWidget.SYMBOLS['union']} def __init__(self, *chart_filenames): # This chart is displayed when we don't have a value (eg # before any chart is loaded). faketok = [''] * 8 self._emptychart = Chart(faketok) # The left & right charts start out empty. self._left_name = 'None' self._right_name = 'None' self._left_chart = self._emptychart self._right_chart = self._emptychart # The charts that have been loaded. self._charts = {'None': self._emptychart} # The output chart. self._out_chart = self._emptychart # The most recent operation self._operator = None # Set up the root window. self._root = Tkinter.Tk() self._root.title('Chart Comparison') self._root.bind('', self.destroy) self._root.bind('', self.destroy) # Initialize all widgets, etc. self._init_menubar(self._root) self._init_chartviews(self._root) self._init_divider(self._root) self._init_buttons(self._root) self._init_bindings(self._root) # Load any specified charts. for filename in chart_filenames: self.load_chart(filename) def destroy(self, *e): if self._root is None: return try: self._root.destroy() except: pass self._root = None def mainloop(self, *args, **kwargs): return self._root.mainloop(*args, **kwargs) #//////////////////////////////////////////////////////////// # Initialization #//////////////////////////////////////////////////////////// def _init_menubar(self, root): menubar = Tkinter.Menu(root) # File menu filemenu = Tkinter.Menu(menubar, tearoff=0) filemenu.add_command(label='Load Chart', accelerator='Ctrl-o', underline=0, command=self.load_chart_dialog) filemenu.add_command(label='Save Output', accelerator='Ctrl-s', underline=0, command=self.save_chart_dialog) filemenu.add_separator() filemenu.add_command(label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-x') menubar.add_cascade(label='File', underline=0, menu=filemenu) # Compare menu opmenu = Tkinter.Menu(menubar, tearoff=0) opmenu.add_command(label='Intersection', command=self._intersection, accelerator='+') opmenu.add_command(label='Union', command=self._union, accelerator='*') opmenu.add_command(label='Difference', command=self._difference, accelerator='-') opmenu.add_separator() opmenu.add_command(label='Swap Charts', command=self._swapcharts) menubar.add_cascade(label='Compare', underline=0, menu=opmenu) # Add the menu self._root.config(menu=menubar) def _init_divider(self, root): divider = Tkinter.Frame(root, border=2, relief='sunken') divider.pack(side='top', fill='x', ipady=2) def _init_chartviews(self, root): opfont=('symbol', -36) # Font for operator. eqfont=('helvetica', -36) # Font for equals sign. frame = Tkinter.Frame(root, background='#c0c0c0') frame.pack(side='top', expand=1, fill='both') # The left matrix. cv1_frame = Tkinter.Frame(frame, border=3, relief='groove') cv1_frame.pack(side='left', padx=8, pady=7, expand=1, fill='both') self._left_selector = MutableOptionMenu( cv1_frame, self._charts.keys(), command=self._select_left) self._left_selector.pack(side='top', pady=5, fill='x') self._left_matrix = ChartMatrixView(cv1_frame, self._emptychart, toplevel=False, show_numedges=True) self._left_matrix.pack(side='bottom', padx=5, pady=5, expand=1, fill='both') self._left_matrix.add_callback('select', self.select_edge) self._left_matrix.add_callback('select_cell', self.select_cell) self._left_matrix.inactivate() # The operator. self._op_label = Tkinter.Label(frame, text=' ', width=3, background='#c0c0c0', font=opfont) self._op_label.pack(side='left', padx=5, pady=5) # The right matrix. cv2_frame = Tkinter.Frame(frame, border=3, relief='groove') cv2_frame.pack(side='left', padx=8, pady=7, expand=1, fill='both') self._right_selector = MutableOptionMenu( cv2_frame, self._charts.keys(), command=self._select_right) self._right_selector.pack(side='top', pady=5, fill='x') self._right_matrix = ChartMatrixView(cv2_frame, self._emptychart, toplevel=False, show_numedges=True) self._right_matrix.pack(side='bottom', padx=5, pady=5, expand=1, fill='both') self._right_matrix.add_callback('select', self.select_edge) self._right_matrix.add_callback('select_cell', self.select_cell) self._right_matrix.inactivate() # The equals sign Tkinter.Label(frame, text='=', width=3, background='#c0c0c0', font=eqfont).pack(side='left', padx=5, pady=5) # The output matrix. out_frame = Tkinter.Frame(frame, border=3, relief='groove') out_frame.pack(side='left', padx=8, pady=7, expand=1, fill='both') self._out_label = Tkinter.Label(out_frame, text='Output') self._out_label.pack(side='top', pady=9) self._out_matrix = ChartMatrixView(out_frame, self._emptychart, toplevel=False, show_numedges=True) self._out_matrix.pack(side='bottom', padx=5, pady=5, expand=1, fill='both') self._out_matrix.add_callback('select', self.select_edge) self._out_matrix.add_callback('select_cell', self.select_cell) self._out_matrix.inactivate() def _init_buttons(self, root): buttons = Tkinter.Frame(root) buttons.pack(side='bottom', pady=5, fill='x', expand=0) Tkinter.Button(buttons, text='Intersection', command=self._intersection).pack(side='left') Tkinter.Button(buttons, text='Union', command=self._union).pack(side='left') Tkinter.Button(buttons, text='Difference', command=self._difference).pack(side='left') Tkinter.Frame(buttons, width=20).pack(side='left') Tkinter.Button(buttons, text='Swap Charts', command=self._swapcharts).pack(side='left') Tkinter.Button(buttons, text='Detatch Output', command=self._detatch_out).pack(side='right') def _init_bindings(self, root): #root.bind('', self.save_chart) root.bind('', self.load_chart_dialog) #root.bind('', self.reset) #//////////////////////////////////////////////////////////// # Input Handling #//////////////////////////////////////////////////////////// def _select_left(self, name): self._left_name = name self._left_chart = self._charts[name] self._left_matrix.set_chart(self._left_chart) if name == 'None': self._left_matrix.inactivate() self._apply_op() def _select_right(self, name): self._right_name = name self._right_chart = self._charts[name] self._right_matrix.set_chart(self._right_chart) if name == 'None': self._right_matrix.inactivate() self._apply_op() def _apply_op(self): if self._operator == '-': self._difference() elif self._operator == 'or': self._union() elif self._operator == 'and': self._intersection() #//////////////////////////////////////////////////////////// # File #//////////////////////////////////////////////////////////// CHART_FILE_TYPES = [('Pickle file', '.pickle'), ('All files', '*')] def save_chart_dialog(self, *args): filename = asksaveasfilename(filetypes=self.CHART_FILE_TYPES, defaultextension='.pickle') if not filename: return try: pickle.dump((self._out_chart), open(filename, 'w')) except Exception, e: tkMessageBox.showerror('Error Saving Chart', 'Unable to open file: %r\n%s' % (filename, e)) def load_chart_dialog(self, *args): filename = askopenfilename(filetypes=self.CHART_FILE_TYPES, defaultextension='.pickle') if not filename: return try: self.load_chart(filename) except Exception, e: tkMessageBox.showerror('Error Loading Chart', 'Unable to open file: %r\n%s' % (filename, e)) def load_chart(self, filename): chart = pickle.load(open(filename, 'r')) name = os.path.basename(filename) if name.endswith('.pickle'): name = name[:-7] if name.endswith('.chart'): name = name[:-6] self._charts[name] = chart self._left_selector.add(name) self._right_selector.add(name) # If either left_matrix or right_matrix is empty, then # display the new chart. if self._left_chart is self._emptychart: self._left_selector.set(name) elif self._right_chart is self._emptychart: self._right_selector.set(name) def _update_chartviews(self): self._left_matrix.update() self._right_matrix.update() self._out_matrix.update() #//////////////////////////////////////////////////////////// # Selection #//////////////////////////////////////////////////////////// def select_edge(self, edge): if edge in self._left_chart: self._left_matrix.markonly_edge(edge) else: self._left_matrix.unmark_edge() if edge in self._right_chart: self._right_matrix.markonly_edge(edge) else: self._right_matrix.unmark_edge() if edge in self._out_chart: self._out_matrix.markonly_edge(edge) else: self._out_matrix.unmark_edge() def select_cell(self, i, j): self._left_matrix.select_cell(i, j) self._right_matrix.select_cell(i, j) self._out_matrix.select_cell(i, j) #//////////////////////////////////////////////////////////// # Operations #//////////////////////////////////////////////////////////// def _difference(self): if not self._checkcompat(): return out_chart = Chart(self._left_chart.tokens()) for edge in self._left_chart: if edge not in self._right_chart: out_chart.insert(edge, []) self._update('-', out_chart) def _intersection(self): if not self._checkcompat(): return out_chart = Chart(self._left_chart.tokens()) for edge in self._left_chart: if edge in self._right_chart: out_chart.insert(edge, []) self._update('and', out_chart) def _union(self): if not self._checkcompat(): return out_chart = Chart(self._left_chart.tokens()) for edge in self._left_chart: out_chart.insert(edge, []) for edge in self._right_chart: out_chart.insert(edge, []) self._update('or', out_chart) def _swapcharts(self): left, right = self._left_name, self._right_name self._left_selector.set(right) self._right_selector.set(left) def _checkcompat(self): if (self._left_chart.tokens() != self._right_chart.tokens() or self._left_chart.property_names() != self._right_chart.property_names() or self._left_chart == self._emptychart or self._right_chart == self._emptychart): # Clear & inactivate the output chart. self._out_chart = self._emptychart self._out_matrix.set_chart(self._out_chart) self._out_matrix.inactivate() self._out_label['text'] = 'Output' # Issue some other warning? return False else: return True def _update(self, operator, out_chart): self._operator = operator self._op_label['text'] = self._OPSYMBOL[operator] self._out_chart = out_chart self._out_matrix.set_chart(out_chart) self._out_label['text'] = '%s %s %s' % (self._left_name, self._operator, self._right_name) def _clear_out_chart(self): self._out_chart = self._emptychart self._out_matrix.set_chart(self._out_chart) self._op_label['text'] = ' ' self._out_matrix.inactivate() def _detatch_out(self): ChartMatrixView(self._root, self._out_chart, title=self._out_label['text']) ####################################################################### # Chart View ####################################################################### class ChartView(object): """ A component for viewing charts. This is used by C{ChartParserApp} to allow students to interactively experiment with various chart parsing techniques. It is also used by C{Chart.draw()}. @ivar _chart: The chart that we are giving a view of. This chart may be modified; after it is modified, you should call C{update}. @ivar _sentence: The list of tokens that the chart spans. @ivar _root: The root window. @ivar _chart_canvas: The canvas we're using to display the chart itself. @ivar _tree_canvas: The canvas we're using to display the tree that each edge spans. May be None, if we're not displaying trees. @ivar _sentence_canvas: The canvas we're using to display the sentence text. May be None, if we're not displaying the sentence text. @ivar _edgetags: A dictionary mapping from edges to the tags of the canvas elements (lines, etc) used to display that edge. The values of this dictionary have the form C{(linetag, rhstag1, dottag, rhstag2, lhstag)}. @ivar _treetags: A list of all the tags that make up the tree; used to erase the tree (without erasing the loclines). @ivar _chart_height: The height of the chart canvas. @ivar _sentence_height: The height of the sentence canvas. @ivar _tree_height: The height of the tree @ivar _text_height: The height of a text string (in the normal font). @ivar _edgelevels: A list of edges at each level of the chart (the top level is the 0th element). This list is used to remember where edges should be drawn; and to make sure that no edges are overlapping on the chart view. @ivar _unitsize: Pixel size of one unit (from the location). This is determined by the span of the chart's location, and the width of the chart display canvas. @ivar _fontsize: The current font size @ivar _marks: A dictionary from edges to marks. Marks are strings, specifying colors (e.g. 'green'). """ _LEAF_SPACING = 10 _MARGIN = 10 _TREE_LEVEL_SIZE = 12 _CHART_LEVEL_SIZE = 40 def __init__(self, chart, root=None, **kw): """ Construct a new C{Chart} display. """ # Process keyword args. draw_tree = kw.get('draw_tree', 0) draw_sentence = kw.get('draw_sentence', 1) self._fontsize = kw.get('fontsize', -12) # The chart! self._chart = chart # Callback functions self._callbacks = {} # Keep track of drawn edges self._edgelevels = [] self._edgetags = {} # Keep track of which edges are marked. self._marks = {} # These are used to keep track of the set of tree tokens # currently displayed in the tree canvas. self._treetoks = [] self._treetoks_edge = None self._treetoks_index = 0 # Keep track of the tags used to draw the tree self._tree_tags = [] # Put multiple edges on each level? self._compact = 0 # If they didn't provide a main window, then set one up. if root is None: top = Tkinter.Tk() top.title('Chart View') def destroy1(e, top=top): top.destroy() def destroy2(top=top): top.destroy() top.bind('q', destroy1) b = Tkinter.Button(top, text='Done', command=destroy2) b.pack(side='bottom') self._root = top else: self._root = root # Create some fonts. self._init_fonts(root) # Create the chart canvas. (self._chart_sb, self._chart_canvas) = self._sb_canvas(self._root) self._chart_canvas['height'] = 300 self._chart_canvas['closeenough'] = 15 # Create the sentence canvas. if draw_sentence: cframe = Tkinter.Frame(self._root, relief='sunk', border=2) cframe.pack(fill='both', side='bottom') self._sentence_canvas = Tkinter.Canvas(cframe, height=50) self._sentence_canvas['background'] = '#e0e0e0' self._sentence_canvas.pack(fill='both') #self._sentence_canvas['height'] = self._sentence_height else: self._sentence_canvas = None # Create the tree canvas. if draw_tree: (sb, canvas) = self._sb_canvas(self._root, 'n', 'x') (self._tree_sb, self._tree_canvas) = (sb, canvas) self._tree_canvas['height'] = 200 else: self._tree_canvas = None # Do some analysis to figure out how big the window should be self._analyze() self.draw() self._resize() self._grow() # Set up the configure callback, which will be called whenever # the window is resized. self._chart_canvas.bind('', self._configure) def _init_fonts(self, root): self._boldfont = tkFont.Font(family='helvetica', weight='bold', size=self._fontsize) self._font = tkFont.Font(family='helvetica', size=self._fontsize) # See: self._sysfont = tkFont.Font(font=Tkinter.Button()["font"]) root.option_add("*Font", self._sysfont) def _sb_canvas(self, root, expand='y', fill='both', side='bottom'): """ Helper for __init__: construct a canvas with a scrollbar. """ cframe =Tkinter.Frame(root, relief='sunk', border=2) cframe.pack(fill=fill, expand=expand, side=side) canvas = Tkinter.Canvas(cframe, background='#e0e0e0') # Give the canvas a scrollbar. sb = Tkinter.Scrollbar(cframe, orient='vertical') sb.pack(side='right', fill='y') canvas.pack(side='left', fill=fill, expand='yes') # Connect the scrollbars to the canvas. sb['command']= canvas.yview canvas['yscrollcommand'] = sb.set return (sb, canvas) def scroll_up(self, *e): self._chart_canvas.yview('scroll', -1, 'units') def scroll_down(self, *e): self._chart_canvas.yview('scroll', 1, 'units') def page_up(self, *e): self._chart_canvas.yview('scroll', -1, 'pages') def page_down(self, *e): self._chart_canvas.yview('scroll', 1, 'pages') def _grow(self): """ Grow the window, if necessary """ # Grow, if need-be N = self._chart.num_leaves() width = max(int(self._chart_canvas['width']), N * self._unitsize + ChartView._MARGIN * 2 ) # It won't resize without the second (height) line, but I # don't understand why not. self._chart_canvas.configure(width=width) self._chart_canvas.configure(height=self._chart_canvas['height']) self._unitsize = (width - 2*ChartView._MARGIN) / N # Reset the height for the sentence window. if self._sentence_canvas is not None: self._sentence_canvas['height'] = self._sentence_height def set_font_size(self, size): self._font.configure(size=-abs(size)) self._boldfont.configure(size=-abs(size)) self._sysfont.configure(size=-abs(size)) self._analyze() self._grow() self.draw() def get_font_size(self): return abs(self._fontsize) def _configure(self, e): """ The configure callback. This is called whenever the window is resized. It is also called when the window is first mapped. It figures out the unit size, and redraws the contents of each canvas. """ N = self._chart.num_leaves() self._unitsize = (e.width - 2*ChartView._MARGIN) / N self.draw() def update(self, chart=None): """ Draw any edges that have not been drawn. This is typically called when a after modifies the canvas that a CanvasView is displaying. C{update} will cause any edges that have been added to the chart to be drawn. If update is given a C{chart} argument, then it will replace the current chart with the given chart. """ if chart is not None: self._chart = chart self._edgelevels = [] self._marks = {} self._analyze() self._grow() self.draw() self.erase_tree() self._resize() else: for edge in self._chart: if not self._edgetags.has_key(edge): self._add_edge(edge) self._resize() def _edge_conflict(self, edge, lvl): """ Return 1 if the given edge overlaps with any edge on the given level. This is used by _add_edge to figure out what level a new edge should be added to. """ (s1, e1) = edge.span() for otheredge in self._edgelevels[lvl]: (s2, e2) = otheredge.span() if (s1 <= s2 < e1) or (s2 <= s1 < e2) or (s1==s2==e1==e2): return 1 return 0 def _analyze_edge(self, edge): """ Given a new edge, recalculate: - _text_height - _unitsize (if the edge text is too big for the current _unitsize, then increase _unitsize) """ c = self._chart_canvas if isinstance(edge, TreeEdge): lhs = edge.lhs() rhselts = [] for elt in edge.rhs(): if isinstance(elt, Nonterminal): rhselts.append(str(elt.symbol())) else: rhselts.append(repr(elt)) rhs = string.join(rhselts) else: lhs = edge.lhs() rhs = '' for s in (lhs, rhs): tag = c.create_text(0,0, text=s, font=self._boldfont, anchor='nw', justify='left') bbox = c.bbox(tag) c.delete(tag) width = bbox[2] #+ ChartView._LEAF_SPACING edgelen = max(edge.length(), 1) self._unitsize = max(self._unitsize, width/edgelen) self._text_height = max(self._text_height, bbox[3] - bbox[1]) def _add_edge(self, edge, minlvl=0): """ Add a single edge to the ChartView: - Call analyze_edge to recalculate display parameters - Find an available level - Call _draw_edge """ # Do NOT show leaf edges in the chart. if isinstance(edge, LeafEdge): return if self._edgetags.has_key(edge): return self._analyze_edge(edge) self._grow() if not self._compact: self._edgelevels.append([edge]) lvl = len(self._edgelevels)-1 self._draw_edge(edge, lvl) self._resize() return # Figure out what level to draw the edge on. lvl = 0 while 1: # If this level doesn't exist yet, create it. while lvl >= len(self._edgelevels): self._edgelevels.append([]) self._resize() # Check if we can fit the edge in this level. if lvl>=minlvl and not self._edge_conflict(edge, lvl): # Go ahead and draw it. self._edgelevels[lvl].append(edge) break # Try the next level. lvl += 1 self._draw_edge(edge, lvl) def view_edge(self, edge): level = None for i in range(len(self._edgelevels)): if edge in self._edgelevels[i]: level = i break if level == None: return # Try to view the new edge.. y = (level+1) * self._chart_level_size dy = self._text_height + 10 self._chart_canvas.yview('moveto', 1.0) if self._chart_height != 0: self._chart_canvas.yview('moveto', float(y-dy)/self._chart_height) def _draw_edge(self, edge, lvl): """ Draw a single edge on the ChartView. """ c = self._chart_canvas # Draw the arrow. x1 = (edge.start() * self._unitsize + ChartView._MARGIN) x2 = (edge.end() * self._unitsize + ChartView._MARGIN) if x2 == x1: x2 += max(4, self._unitsize/5) y = (lvl+1) * self._chart_level_size linetag = c.create_line(x1, y, x2, y, arrow='last', width=3) # Draw a label for the edge. if isinstance(edge, TreeEdge): rhs = [] for elt in edge.rhs(): if isinstance(elt, Nonterminal): rhs.append(str(elt.symbol())) else: rhs.append(repr(elt)) pos = edge.dot() else: rhs = [] pos = 0 rhs1 = string.join(rhs[:pos]) rhs2 = string.join(rhs[pos:]) rhstag1 = c.create_text(x1+3, y, text=rhs1, font=self._font, anchor='nw') dotx = c.bbox(rhstag1)[2] + 6 doty = (c.bbox(rhstag1)[1]+c.bbox(rhstag1)[3])/2 dottag = c.create_oval(dotx-2, doty-2, dotx+2, doty+2) rhstag2 = c.create_text(dotx+6, y, text=rhs2, font=self._font, anchor='nw') lhstag = c.create_text((x1+x2)/2, y, text=str(edge.lhs()), anchor='s', font=self._boldfont) # Keep track of the edge's tags. self._edgetags[edge] = (linetag, rhstag1, dottag, rhstag2, lhstag) # Register a callback for clicking on the edge. def cb(event, self=self, edge=edge): self._fire_callbacks('select', edge) c.tag_bind(rhstag1, '', cb) c.tag_bind(rhstag2, '', cb) c.tag_bind(linetag, '', cb) c.tag_bind(dottag, '', cb) c.tag_bind(lhstag, '', cb) self._color_edge(edge) def _color_edge(self, edge, linecolor=None, textcolor=None): """ Color in an edge with the given colors. If no colors are specified, use intelligent defaults (dependant on selection, etc.) """ if not self._edgetags.has_key(edge): return c = self._chart_canvas if linecolor is not None and textcolor is not None: if self._marks.has_key(edge): linecolor = self._marks[edge] tags = self._edgetags[edge] c.itemconfig(tags[0], fill=linecolor) c.itemconfig(tags[1], fill=textcolor) c.itemconfig(tags[2], fill=textcolor, outline=textcolor) c.itemconfig(tags[3], fill=textcolor) c.itemconfig(tags[4], fill=textcolor) return else: N = self._chart.num_leaves() if self._marks.has_key(edge): self._color_edge(self._marks[edge]) if (edge.is_complete() and edge.span() == (0, N)): self._color_edge(edge, '#084', '#042') elif isinstance(edge, LeafEdge): self._color_edge(edge, '#48c', '#246') else: self._color_edge(edge, '#00f', '#008') def mark_edge(self, edge, mark='#0df'): """ Mark an edge """ self._marks[edge] = mark self._color_edge(edge) def unmark_edge(self, edge=None): """ Unmark an edge (or all edges) """ if edge == None: old_marked_edges = self._marks.keys() self._marks = {} for edge in old_marked_edges: self._color_edge(edge) else: del self._marks[edge] self._color_edge(edge) def markonly_edge(self, edge, mark='#0df'): self.unmark_edge() self.mark_edge(edge, mark) def _analyze(self): """ Analyze the sentence string, to figure out how big a unit needs to be, How big the tree should be, etc. """ # Figure out the text height and the unit size. unitsize = 70 # min unitsize text_height = 0 c = self._chart_canvas # Check against all tokens for leaf in self._chart.leaves(): tag = c.create_text(0,0, text=repr(leaf), font=self._font, anchor='nw', justify='left') bbox = c.bbox(tag) c.delete(tag) width = bbox[2] + ChartView._LEAF_SPACING unitsize = max(width, unitsize) text_height = max(text_height, bbox[3] - bbox[1]) self._unitsize = unitsize self._text_height = text_height self._sentence_height = (self._text_height + 2*ChartView._MARGIN) # Check against edges. for edge in self._chart.edges(): self._analyze_edge(edge) # Size of chart levels self._chart_level_size = self._text_height * 2 # Default tree size.. self._tree_height = (3 * (ChartView._TREE_LEVEL_SIZE + self._text_height)) # Resize the scrollregions. self._resize() def _resize(self): """ Update the scroll-regions for each canvas. This ensures that everything is within a scroll-region, so the user can use the scrollbars to view the entire display. This does I{not} resize the window. """ c = self._chart_canvas # Reset the chart scroll region width = ( self._chart.num_leaves() * self._unitsize + ChartView._MARGIN * 2 ) levels = len(self._edgelevels) self._chart_height = (levels+2)*self._chart_level_size c['scrollregion']=(0,0,width,self._chart_height) # Reset the tree scroll region if self._tree_canvas: self._tree_canvas['scrollregion'] = (0, 0, width, self._tree_height) def _draw_loclines(self): """ Draw location lines. These are vertical gridlines used to show where each location unit is. """ BOTTOM = 50000 c1 = self._tree_canvas c2 = self._sentence_canvas c3 = self._chart_canvas margin = ChartView._MARGIN self._loclines = [] for i in range(0, self._chart.num_leaves()+1): x = i*self._unitsize + margin if c1: t1=c1.create_line(x, 0, x, BOTTOM) c1.tag_lower(t1) if c2: t2=c2.create_line(x, 0, x, self._sentence_height) c2.tag_lower(t2) t3=c3.create_line(x, 0, x, BOTTOM) c3.tag_lower(t3) t4=c3.create_text(x+2, 0, text=`i`, anchor='nw', font=self._font) c3.tag_lower(t4) #if i % 4 == 0: # if c1: c1.itemconfig(t1, width=2, fill='gray60') # if c2: c2.itemconfig(t2, width=2, fill='gray60') # c3.itemconfig(t3, width=2, fill='gray60') if i % 2 == 0: if c1: c1.itemconfig(t1, fill='gray60') if c2: c2.itemconfig(t2, fill='gray60') c3.itemconfig(t3, fill='gray60') else: if c1: c1.itemconfig(t1, fill='gray80') if c2: c2.itemconfig(t2, fill='gray80') c3.itemconfig(t3, fill='gray80') def _draw_sentence(self): """Draw the sentence string.""" if self._chart.num_leaves() == 0: return c = self._sentence_canvas margin = ChartView._MARGIN y = ChartView._MARGIN for i, leaf in enumerate(self._chart.leaves()): x1 = i * self._unitsize + margin x2 = x1 + self._unitsize x = (x1+x2)/2 tag = c.create_text(x, y, text=repr(leaf), font=self._font, anchor='n', justify='left') bbox = c.bbox(tag) rt=c.create_rectangle(x1+2, bbox[1]-(ChartView._LEAF_SPACING/2), x2-2, bbox[3]+(ChartView._LEAF_SPACING/2), fill='#f0f0f0', outline='#f0f0f0') c.tag_lower(rt) def erase_tree(self): for tag in self._tree_tags: self._tree_canvas.delete(tag) self._treetoks = [] self._treetoks_edge = None self._treetoks_index = 0 def draw_tree(self, edge=None): if edge is None and self._treetoks_edge is None: return if edge is None: edge = self._treetoks_edge # If it's a new edge, then get a new list of treetoks. if self._treetoks_edge != edge: self._treetoks = [t for t in self._chart.trees(edge) if isinstance(t, Tree)] self._treetoks_edge = edge self._treetoks_index = 0 # Make sure there's something to draw. if len(self._treetoks) == 0: return # Erase the old tree. for tag in self._tree_tags: self._tree_canvas.delete(tag) # Draw the new tree. tree = self._treetoks[self._treetoks_index] self._draw_treetok(tree, edge.start()) # Show how many trees are available for the edge. self._draw_treecycle() # Update the scroll region. w = self._chart.num_leaves()*self._unitsize+2*ChartView._MARGIN h = tree.height() * (ChartView._TREE_LEVEL_SIZE+self._text_height) self._tree_canvas['scrollregion'] = (0, 0, w, h) def cycle_tree(self): self._treetoks_index = (self._treetoks_index+1)%len(self._treetoks) self.draw_tree(self._treetoks_edge) def _draw_treecycle(self): if len(self._treetoks) <= 1: return # Draw the label. label = '%d Trees' % len(self._treetoks) c = self._tree_canvas margin = ChartView._MARGIN right = self._chart.num_leaves()*self._unitsize+margin-2 tag = c.create_text(right, 2, anchor='ne', text=label, font=self._boldfont) self._tree_tags.append(tag) _, _, _, y = c.bbox(tag) # Draw the triangles. for i in range(len(self._treetoks)): x = right - 20*(len(self._treetoks)-i-1) if i == self._treetoks_index: fill = '#084' else: fill = '#fff' tag = c.create_polygon(x, y+10, x-5, y, x-10, y+10, fill=fill, outline='black') self._tree_tags.append(tag) # Set up a callback: show the tree if they click on its # triangle. def cb(event, self=self, i=i): self._treetoks_index = i self.draw_tree() c.tag_bind(tag, '', cb) def _draw_treetok(self, treetok, index, depth=0): """ @param index: The index of the first leaf in the tree. @return: The index of the first leaf after the tree. """ c = self._tree_canvas margin = ChartView._MARGIN # Draw the children child_xs = [] for child in treetok: if isinstance(child, Tree): child_x, index = self._draw_treetok(child, index, depth+1) child_xs.append(child_x) else: child_xs.append((2*index+1)*self._unitsize/2 + margin) index += 1 # If we have children, then get the node's x by averaging their # node x's. Otherwise, make room for ourselves. if child_xs: nodex = sum(child_xs)/len(child_xs) else: # [XX] breaks for null productions. nodex = (2*index+1)*self._unitsize/2 + margin index += 1 # Draw the node nodey = depth * (ChartView._TREE_LEVEL_SIZE + self._text_height) tag = c.create_text(nodex, nodey, anchor='n', justify='center', text=str(treetok.node), fill='#042', font=self._boldfont) self._tree_tags.append(tag) # Draw lines to the children. childy = nodey + ChartView._TREE_LEVEL_SIZE + self._text_height for childx, child in zip(child_xs, treetok): if isinstance(child, Tree) and child: # A "real" tree token: tag = c.create_line(nodex, nodey + self._text_height, childx, childy, width=2, fill='#084') self._tree_tags.append(tag) if isinstance(child, Tree) and not child: # An unexpanded tree token: tag = c.create_line(nodex, nodey + self._text_height, childx, childy, width=2, fill='#048', dash='2 3') self._tree_tags.append(tag) if not isinstance(child, Tree): # A leaf: tag = c.create_line(nodex, nodey + self._text_height, childx, 10000, width=2, fill='#084') self._tree_tags.append(tag) return nodex, index def draw(self): """ Draw everything (from scratch). """ if self._tree_canvas: self._tree_canvas.delete('all') self.draw_tree() if self._sentence_canvas: self._sentence_canvas.delete('all') self._draw_sentence() self._chart_canvas.delete('all') self._edgetags = {} # Redraw any edges we erased. for lvl in range(len(self._edgelevels)): for edge in self._edgelevels[lvl]: self._draw_edge(edge, lvl) for edge in self._chart: self._add_edge(edge) self._draw_loclines() def add_callback(self, event, func): self._callbacks.setdefault(event,{})[func] = 1 def remove_callback(self, event, func=None): if func is None: del self._callbacks[event] else: try: del self._callbacks[event][func] except: pass def _fire_callbacks(self, event, *args): if not self._callbacks.has_key(event): return for cb_func in self._callbacks[event].keys(): cb_func(*args) ####################################################################### # Edge Rules ####################################################################### # These version of the chart rules only apply to a specific edge. # This lets the user select an edge, and then apply a rule. class EdgeRule(object): """ To create an edge rule, make an empty base class that uses EdgeRule as the first base class, and the basic rule as the second base class. (Order matters!) """ def __init__(self, edge): super = self.__class__.__bases__[1] self._edge = edge self.NUM_EDGES = super.NUM_EDGES-1 def apply_iter(self, chart, grammar, *edges): super = self.__class__.__bases__[1] edges += (self._edge,) for e in super.apply_iter(self, chart, grammar, *edges): yield e def __str__(self): super = self.__class__.__bases__[1] return super.__str__(self) class TopDownPredictEdgeRule(EdgeRule, TopDownPredictRule): pass class BottomUpEdgeRule(EdgeRule, BottomUpPredictRule): pass class BottomUpLeftCornerEdgeRule(EdgeRule, BottomUpPredictCombineRule): pass class FundamentalEdgeRule(EdgeRule, SingleEdgeFundamentalRule): pass ####################################################################### # Chart Parser Application ####################################################################### class ChartParserApp(object): def __init__(self, grammar, tokens, title='Chart Parser Application'): # Initialize the parser self._init_parser(grammar, tokens) self._root = None try: # Create the root window. self._root = Tkinter.Tk() self._root.title(title) self._root.bind('', self.destroy) # Set up some frames. frame3 = Tkinter.Frame(self._root) frame2 = Tkinter.Frame(self._root) frame1 = Tkinter.Frame(self._root) frame3.pack(side='bottom', fill='none') frame2.pack(side='bottom', fill='x') frame1.pack(side='bottom', fill='both', expand=1) self._init_fonts(self._root) self._init_animation() self._init_chartview(frame1) self._init_rulelabel(frame2) self._init_buttons(frame3) self._init_menubar() self._matrix = None self._results = None # Set up keyboard bindings. self._init_bindings() except: print 'Error creating Tree View' self.destroy() raise def destroy(self, *args): if self._root is None: return self._root.destroy() self._root = None def mainloop(self, *args, **kwargs): """ Enter the Tkinter mainloop. This function must be called if this demo is created from a non-interactive program (e.g. from a secript); otherwise, the demo will close as soon as the script completes. """ if in_idle(): return self._root.mainloop(*args, **kwargs) #//////////////////////////////////////////////////////////// # Initialization Helpers #//////////////////////////////////////////////////////////// def _init_parser(self, grammar, tokens): self._grammar = grammar self._tokens = tokens self._reset_parser() def _reset_parser(self): self._cp = SteppingChartParser(self._grammar) self._cp.initialize(self._tokens) self._chart = self._cp.chart() # Insert LeafEdges before the parsing starts. LeafInitRule().apply(self._chart, self._grammar) # The step iterator -- use this to generate new edges self._cpstep = self._cp.step() # The currently selected edge self._selection = None def _init_fonts(self, root): # See: self._sysfont = tkFont.Font(font=Tkinter.Button()["font"]) root.option_add("*Font", self._sysfont) # TWhat's our font size (default=same as sysfont) self._size = Tkinter.IntVar(root) self._size.set(self._sysfont.cget('size')) self._boldfont = tkFont.Font(family='helvetica', weight='bold', size=self._size.get()) self._font = tkFont.Font(family='helvetica', size=self._size.get()) def _init_animation(self): # Are we stepping? (default=yes) self._step = Tkinter.IntVar(self._root) self._step.set(1) # What's our animation speed (default=fast) self._animate = Tkinter.IntVar(self._root) self._animate.set(3) # Default speed = fast # Are we currently animating? self._animating = 0 def _init_chartview(self, parent): self._cv = ChartView(self._chart, parent, draw_tree=1, draw_sentence=1) self._cv.add_callback('select', self._click_cv_edge) def _init_rulelabel(self, parent): ruletxt = 'Last edge generated by:' self._rulelabel1 = Tkinter.Label(parent,text=ruletxt, font=self._boldfont) self._rulelabel2 = Tkinter.Label(parent, width=40, relief='groove', anchor='w', font=self._boldfont) self._rulelabel1.pack(side='left') self._rulelabel2.pack(side='left') step = Tkinter.Checkbutton(parent, variable=self._step, text='Step') step.pack(side='right') def _init_buttons(self, parent): frame1 = Tkinter.Frame(parent) frame2 = Tkinter.Frame(parent) frame1.pack(side='bottom', fill='x') frame2.pack(side='top', fill='none') Tkinter.Button(frame1, text='Reset\nParser', background='#90c0d0', foreground='black', command=self.reset).pack(side='right') #Tkinter.Button(frame1, text='Pause', # background='#90c0d0', foreground='black', # command=self.pause).pack(side='left') Tkinter.Button(frame1, text='Top Down\nStrategy', background='#90c0d0', foreground='black', command=self.top_down_strategy).pack(side='left') Tkinter.Button(frame1, text='Bottom Up\nStrategy', background='#90c0d0', foreground='black', command=self.bottom_up_strategy).pack(side='left') Tkinter.Button(frame1, text='Bottom Up\nLeft-Corner Strategy', background='#90c0d0', foreground='black', command=self.bottom_up_leftcorner_strategy).pack(side='left') Tkinter.Button(frame2, text='Top Down Init\nRule', background='#90f090', foreground='black', command=self.top_down_init).pack(side='left') Tkinter.Button(frame2, text='Top Down Predict\nRule', background='#90f090', foreground='black', command=self.top_down_predict).pack(side='left') Tkinter.Frame(frame2, width=20).pack(side='left') Tkinter.Button(frame2, text='Bottom Up Predict\nRule', background='#90f090', foreground='black', command=self.bottom_up).pack(side='left') Tkinter.Frame(frame2, width=20).pack(side='left') Tkinter.Button(frame2, text='Bottom Up Left-Corner\nPredict Rule', background='#90f090', foreground='black', command=self.bottom_up_leftcorner).pack(side='left') Tkinter.Frame(frame2, width=20).pack(side='left') Tkinter.Button(frame2, text='Fundamental\nRule', background='#90f090', foreground='black', command=self.fundamental).pack(side='left') def _init_bindings(self): self._root.bind('', self._cv.scroll_up) self._root.bind('', self._cv.scroll_down) self._root.bind('', self._cv.page_up) self._root.bind('', self._cv.page_down) self._root.bind('', self.destroy) self._root.bind('', self.destroy) self._root.bind('', self.help) self._root.bind('', self.save_chart) self._root.bind('', self.load_chart) self._root.bind('', self.reset) self._root.bind('t', self.top_down_strategy) self._root.bind('b', self.bottom_up_strategy) self._root.bind('c', self.bottom_up_leftcorner_strategy) self._root.bind('', self._stop_animation) self._root.bind('', self.edit_grammar) self._root.bind('', self.edit_sentence) # Animation speed control self._root.bind('-', lambda e,a=self._animate:a.set(1)) self._root.bind('=', lambda e,a=self._animate:a.set(2)) self._root.bind('+', lambda e,a=self._animate:a.set(3)) # Step control self._root.bind('s', lambda e,s=self._step:s.set(not s.get())) def _init_menubar(self): menubar = Tkinter.Menu(self._root) filemenu = Tkinter.Menu(menubar, tearoff=0) filemenu.add_command(label='Save Chart', underline=0, command=self.save_chart, accelerator='Ctrl-s') filemenu.add_command(label='Load Chart', underline=0, command=self.load_chart, accelerator='Ctrl-o') filemenu.add_command(label='Reset Chart', underline=0, command=self.reset, accelerator='Ctrl-r') filemenu.add_separator() filemenu.add_command(label='Save Grammar', command=self.save_grammar) filemenu.add_command(label='Load Grammar', command=self.load_grammar) filemenu.add_separator() filemenu.add_command(label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-x') menubar.add_cascade(label='File', underline=0, menu=filemenu) editmenu = Tkinter.Menu(menubar, tearoff=0) editmenu.add_command(label='Edit Grammar', underline=5, command=self.edit_grammar, accelerator='Ctrl-g') editmenu.add_command(label='Edit Text', underline=5, command=self.edit_sentence, accelerator='Ctrl-t') menubar.add_cascade(label='Edit', underline=0, menu=editmenu) viewmenu = Tkinter.Menu(menubar, tearoff=0) viewmenu.add_command(label='Chart Matrix', underline=6, command=self.view_matrix) viewmenu.add_command(label='Results', underline=0, command=self.view_results) menubar.add_cascade(label='View', underline=0, menu=viewmenu) rulemenu = Tkinter.Menu(menubar, tearoff=0) rulemenu.add_command(label='Top Down Strategy', underline=0, command=self.top_down_strategy, accelerator='t') rulemenu.add_command(label='Bottom Up Strategy', underline=0, command=self.bottom_up_strategy, accelerator='b') rulemenu.add_command(label='Bottom Up Left-Corner Strategy', underline=0, command=self.bottom_up_leftcorner_strategy, accelerator='c') rulemenu.add_separator() rulemenu.add_command(label='Bottom Up Rule', command=self.bottom_up) rulemenu.add_command(label='Bottom Up Left-Corner Rule', command=self.bottom_up_leftcorner) rulemenu.add_command(label='Top Down Init Rule', command=self.top_down_init) rulemenu.add_command(label='Top Down Predict Rule', command=self.top_down_predict) rulemenu.add_command(label='Fundamental Rule', command=self.fundamental) menubar.add_cascade(label='Apply', underline=0, menu=rulemenu) animatemenu = Tkinter.Menu(menubar, tearoff=0) animatemenu.add_checkbutton(label="Step", underline=0, variable=self._step, accelerator='s') animatemenu.add_separator() animatemenu.add_radiobutton(label="No Animation", underline=0, variable=self._animate, value=0) animatemenu.add_radiobutton(label="Slow Animation", underline=0, variable=self._animate, value=1, accelerator='-') animatemenu.add_radiobutton(label="Normal Animation", underline=0, variable=self._animate, value=2, accelerator='=') animatemenu.add_radiobutton(label="Fast Animation", underline=0, variable=self._animate, value=3, accelerator='+') menubar.add_cascade(label="Animate", underline=1, menu=animatemenu) zoommenu = Tkinter.Menu(menubar, tearoff=0) zoommenu.add_radiobutton(label='Tiny', variable=self._size, underline=0, value=10, command=self.resize) zoommenu.add_radiobutton(label='Small', variable=self._size, underline=0, value=12, command=self.resize) zoommenu.add_radiobutton(label='Medium', variable=self._size, underline=0, value=14, command=self.resize) zoommenu.add_radiobutton(label='Large', variable=self._size, underline=0, value=18, command=self.resize) zoommenu.add_radiobutton(label='Huge', variable=self._size, underline=0, value=24, command=self.resize) menubar.add_cascade(label='Zoom', underline=0, menu=zoommenu) helpmenu = Tkinter.Menu(menubar, tearoff=0) helpmenu.add_command(label='About', underline=0, command=self.about) helpmenu.add_command(label='Instructions', underline=0, command=self.help, accelerator='F1') menubar.add_cascade(label='Help', underline=0, menu=helpmenu) self._root.config(menu=menubar) #//////////////////////////////////////////////////////////// # Selection Handling #//////////////////////////////////////////////////////////// def _click_cv_edge(self, edge): if edge != self._selection: # Clicking on a new edge selects it. self._select_edge(edge) else: # Repeated clicks on one edge cycle its trees. self._cv.cycle_tree() # [XX] this can get confused if animation is running # faster than the callbacks... def _select_matrix_edge(self, edge): self._select_edge(edge) self._cv.view_edge(edge) def _select_edge(self, edge): self._selection = edge # Update the chart view. self._cv.markonly_edge(edge, '#f00') self._cv.draw_tree(edge) # Update the matrix view. if self._matrix: self._matrix.markonly_edge(edge) if self._matrix: self._matrix.view_edge(edge) def _deselect_edge(self): self._selection = None # Update the chart view. self._cv.unmark_edge() self._cv.erase_tree() # Update the matrix view if self._matrix: self._matrix.unmark_edge() def _show_new_edge(self, edge): self._display_rule(self._cp.current_chartrule()) # Update the chart view. self._cv.update() self._cv.draw_tree(edge) self._cv.markonly_edge(edge, '#0df') self._cv.view_edge(edge) # Update the matrix view. if self._matrix: self._matrix.update() if self._matrix: self._matrix.markonly_edge(edge) if self._matrix: self._matrix.view_edge(edge) # Update the results view. if self._results: self._results.update(edge) #//////////////////////////////////////////////////////////// # Help/usage #//////////////////////////////////////////////////////////// def help(self, *e): self._animating = 0 # The default font's not very legible; try using 'fixed' instead. try: ShowText(self._root, 'Help: Chart Parser Application', (__doc__).strip(), width=75, font='fixed') except: ShowText(self._root, 'Help: Chart Parser Application', (__doc__).strip(), width=75) def about(self, *e): ABOUT = ("NLTK Chart Parser Application\n"+ "Written by Edward Loper") tkMessageBox.showinfo('About: Chart Parser Application', ABOUT) #//////////////////////////////////////////////////////////// # File Menu #//////////////////////////////////////////////////////////// CHART_FILE_TYPES = [('Pickle file', '.pickle'), ('All files', '*')] GRAMMAR_FILE_TYPES = [('Plaintext grammar file', '.cfg'), ('Pickle file', '.pickle'), ('All files', '*')] def load_chart(self, *args): "Load a chart from a pickle file" filename = askopenfilename(filetypes=self.CHART_FILE_TYPES, defaultextension='.pickle') if not filename: return try: chart = pickle.load(open(filename, 'r')) self._chart = chart self._cv.update(chart) if self._matrix: self._matrix.set_chart(chart) if self._matrix: self._matrix.deselect_cell() if self._results: self._results.set_chart(chart) self._cp.set_chart(chart) except Exception, e: raise tkMessageBox.showerror('Error Loading Chart', 'Unable to open file: %r' % filename) def save_chart(self, *args): "Save a chart to a pickle file" filename = asksaveasfilename(filetypes=self.CHART_FILE_TYPES, defaultextension='.pickle') if not filename: return try: pickle.dump(self._chart, open(filename, 'w')) except Exception, e: raise tkMessageBox.showerror('Error Saving Chart', 'Unable to open file: %r' % filename) def load_grammar(self, *args): "Load a grammar from a pickle file" filename = askopenfilename(filetypes=self.GRAMMAR_FILE_TYPES, defaultextension='.cfg') if not filename: return try: if filename.endswith('.pickle'): grammar = pickle.load(open(filename, 'r')) else: grammar = parse_cfg(open(filename, 'r').read()) self.set_grammar(grammar) except Exception, e: tkMessageBox.showerror('Error Loading Grammar', 'Unable to open file: %r' % filename) def save_grammar(self, *args): filename = asksaveasfilename(filetypes=self.GRAMMAR_FILE_TYPES, defaultextension='.cfg') if not filename: return try: if filename.endswith('.pickle'): pickle.dump((self._chart, self._tokens), open(filename, 'w')) else: file = open(filename, 'w') prods = self._grammar.productions() start = [p for p in prods if p.lhs() == self._grammar.start()] rest = [p for p in prods if p.lhs() != self._grammar.start()] for prod in start: file.write('%s\n' % prod) for prod in rest: file.write('%s\n' % prod) file.close() except Exception, e: tkMessageBox.showerror('Error Saving Grammar', 'Unable to open file: %r' % filename) def reset(self, *args): self._animating = 0 self._reset_parser() self._cv.update(self._chart) if self._matrix: self._matrix.set_chart(self._chart) if self._matrix: self._matrix.deselect_cell() if self._results: self._results.set_chart(self._chart) #//////////////////////////////////////////////////////////// # Edit #//////////////////////////////////////////////////////////// def edit_grammar(self, *e): CFGEditor(self._root, self._grammar, self.set_grammar) def set_grammar(self, grammar): self._grammar = grammar self._cp.set_grammar(grammar) if self._results: self._results.set_grammar(grammar) def edit_sentence(self, *e): sentence = string.join(self._tokens) title = 'Edit Text' instr = 'Enter a new sentence to parse.' EntryDialog(self._root, sentence, instr, self.set_sentence, title) def set_sentence(self, sentence): self._tokens = list(sentence.split()) self.reset() #//////////////////////////////////////////////////////////// # View Menu #//////////////////////////////////////////////////////////// def view_matrix(self, *e): if self._matrix is not None: self._matrix.destroy() self._matrix = ChartMatrixView(self._root, self._chart) self._matrix.add_callback('select', self._select_matrix_edge) def view_results(self, *e): if self._results is not None: self._results.destroy() self._results = ChartResultsView(self._root, self._chart, self._grammar) #//////////////////////////////////////////////////////////// # Zoom Menu #//////////////////////////////////////////////////////////// def resize(self): self._animating = 0 self.set_font_size(self._size.get()) def set_font_size(self, size): self._cv.set_font_size(size) self._font.configure(size=-abs(size)) self._boldfont.configure(size=-abs(size)) self._sysfont.configure(size=-abs(size)) def get_font_size(self): return abs(self._size.get()) #//////////////////////////////////////////////////////////// # Parsing #//////////////////////////////////////////////////////////// def apply_strategy(self, strategy, edge_strategy=None): # If we're animating, then stop. if self._animating: self._animating = 0 return # Clear the rule display & mark. self._display_rule(None) #self._cv.unmark_edge() if self._step.get(): selection = self._selection if (selection is not None) and (edge_strategy is not None): # Apply the given strategy to the selected edge. self._cp.set_strategy([edge_strategy(selection)]) newedge = self._apply_strategy() # If it failed, then clear the selection. if newedge is None: self._cv.unmark_edge() self._selection = None else: self._cp.set_strategy(strategy) self._apply_strategy() else: self._cp.set_strategy(strategy) if self._animate.get(): self._animating = 1 self._animate_strategy() else: for edge in self._cpstep: if edge is None: break self._cv.update() if self._matrix: self._matrix.update() if self._results: self._results.update() def _stop_animation(self, *e): self._animating = 0 def _animate_strategy(self, speed=1): if self._animating == 0: return if self._apply_strategy() is not None: if self._animate.get() == 0 or self._step.get() == 1: return if self._animate.get() == 1: self._root.after(3000, self._animate_strategy) elif self._animate.get() == 2: self._root.after(1000, self._animate_strategy) else: self._root.after(20, self._animate_strategy) def _apply_strategy(self): new_edge = self._cpstep.next() if new_edge is not None: self._show_new_edge(new_edge) return new_edge def _display_rule(self, rule): if rule == None: self._rulelabel2['text'] = '' else: name = str(rule) self._rulelabel2['text'] = name size = self._cv.get_font_size() #//////////////////////////////////////////////////////////// # Parsing Strategies #//////////////////////////////////////////////////////////// # Basic rules: _TD_INIT = [TopDownInitRule()] _TD_PREDICT = [TopDownPredictRule()] _BU_RULE = [BottomUpPredictRule()] _BU_LC_RULE = [BottomUpPredictCombineRule()] _FUNDAMENTAL = [SingleEdgeFundamentalRule()] # Complete strategies: _TD_STRATEGY = _TD_INIT + _TD_PREDICT + _FUNDAMENTAL _BU_STRATEGY = _BU_RULE + _FUNDAMENTAL _BU_LC_STRATEGY = _BU_LC_RULE + _FUNDAMENTAL # Button callback functions: def top_down_init(self, *e): self.apply_strategy(self._TD_INIT, None) def top_down_predict(self, *e): self.apply_strategy(self._TD_PREDICT, TopDownPredictEdgeRule) def bottom_up(self, *e): self.apply_strategy(self._BU_RULE, BottomUpEdgeRule) def bottom_up_leftcorner(self, *e): self.apply_strategy(self._BU_LC_RULE, BottomUpLeftCornerEdgeRule) def fundamental(self, *e): self.apply_strategy(self._FUNDAMENTAL, FundamentalEdgeRule) def bottom_up_strategy(self, *e): self.apply_strategy(self._BU_STRATEGY, BottomUpEdgeRule) def bottom_up_leftcorner_strategy(self, *e): self.apply_strategy(self._BU_LC_STRATEGY, BottomUpLeftCornerEdgeRule) def top_down_strategy(self, *e): self.apply_strategy(self._TD_STRATEGY, TopDownPredictEdgeRule) def app(): grammar = parse_cfg(""" # Grammatical productions. S -> NP VP VP -> VP PP | V NP | V NP -> Det N | NP PP PP -> P NP # Lexical productions. NP -> 'John' | 'I' Det -> 'the' | 'my' | 'a' N -> 'dog' | 'cookie' | 'table' | 'cake' | 'fork' V -> 'ate' | 'saw' P -> 'on' | 'under' | 'with' """) sent = 'John ate the cake on the table with a fork' sent = 'John ate the cake on the table' tokens = list(sent.split()) print 'grammar= (' for rule in grammar.productions(): print ' ', repr(rule)+',' print ')' print 'tokens = %r' % tokens print 'Calling "ChartParserApp(grammar, tokens)"...' ChartParserApp(grammar, tokens).mainloop() if __name__ == '__main__': app() # Chart comparer: #charts = ['/tmp/earley.pickle', # '/tmp/topdown.pickle', # '/tmp/bottomup.pickle'] #ChartComparer(*charts).mainloop() #import profile #profile.run('demo2()', '/tmp/profile.out') #import pstats #p = pstats.Stats('/tmp/profile.out') #p.strip_dirs().sort_stats('time', 'cum').print_stats(60) #p.strip_dirs().sort_stats('cum', 'time').print_stats(60) __all__ = ['app'] nltk-2.0~b9/nltk/app/__init__.py0000644000175000017500000000341111327451574016427 0ustar bhavanibhavani# Natural Language Toolkit: Applications package # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT # # $Id: __init__.py 7460 2009-01-29 01:06:02Z StevenBird1 $ """ Interactive NLTK Applications: chartparser: Chart Parser chunkparser: Regular-Expression Chunk Parser collocations: Find collocations in text concordance: Part-of-speech concordancer nemo: Finding (and Replacing) Nemo regular expression tool rdparser: Recursive Descent Parser srparser: Shift-Reduce Parser wordnet: WordNet Browser """ # Import Tkinter-based modules if Tkinter is installed try: import Tkinter except ImportError: import warnings warnings.warn("nltk.app package not loaded " "(please install Tkinter library).") else: from chartparser_app import app as chartparser from chunkparser_app import app as chunkparser from collocations_app import app as collocations from concordance_app import app as concordance from nemo_app import app as nemo from rdparser_app import app as rdparser from srparser_app import app as srparser from wordnet_app import app as wordnet try: import pylab except ImportError: import warnings warnings.warn("nltk.app.wordfreq not loaded " "(requires the pylab library).") else: from wordfreq_app import app as wordfreq # Import wx-based modules if wx is installed try: import wx except ImportError: pass # fail silently else: try: from wxwordnet_app import app as wxwordnet except ImportError: pass # fail silently -- but this is BROKEN! nltk-2.0~b9/nltk-2.0b9.pkg/Contents/Resources/postflight0000755000175000017500000000133111332742411022712 0ustar bhavanibhavani#!/bin/bash # $0 = script path # $1 = package path # $2 = default location # $3 = target volume TMP=/tmp/nltk-installer/ cd $TMP MINPYVERMAJOR=2 MINPYVERMINOR=4 PYVER=`python -V 2>&1 | sed 's/Python \([0-9]\.[0-9]\).*/\1/'` PYMAJOR=`echo $PYVER | sed 's/\([0-9]\)\.\([0-9]\)/\1/'` PYMINOR=`echo $PYVER | sed 's/\([0-9]\)\.\([0-9]\)/\2/'` # if [[ ( "$PYMAJOR" -ge "$MINPYVERMAJOR" ) \ permits Python 3 if [[ ( "$PYMAJOR" -eq "$MINPYVERMAJOR" && "$PYMINOR" -ge "$MINPYVERMINOR" ) ]] then /usr/bin/sudo python ./setup.py install else exit -1 fi # Clean up after ourselves by deleting /tmp/nltk-installer? # rm -rf $TMP export NLTK_DATA=/usr/share/nltk echo export NLTK_DATA=/usr/share/nltk >> /private/etc/bashrc nltk-2.0~b9/nltk-2.0b9.pkg/Contents/Resources/package_version0000644000175000017500000000002111423120503023650 0ustar bhavanibhavanimajor: 2 minor: 0nltk-2.0~b9/nltk-2.0b9.pkg/Contents/Resources/._package_version0000644000175000017500000000024711423120503024077 0ustar bhavanibhavaniMac OS X  2u§ATTRè U§œ œ com.apple.TextEncodingMACINTOSH;0nltk-2.0~b9/nltk/corpus/reader/ycoe.py0000644000175000017500000002622511363770677017644 0ustar bhavanibhavani# -*- coding: iso-8859-1 -*- # Natural Language Toolkit: York-Toronto-Helsinki Parsed Corpus of Old English Prose (YCOE) # # Copyright (C) 2001-2010 NLTK Project # Author: Selina Dennis # URL: # For license information, see LICENSE.TXT """ Corpus reader for the York-Toronto-Helsinki Parsed Corpus of Old English Prose (YCOE), a 1.5 million word syntactically-annotated corpus of Old English prose texts. The corpus is distributed by the Oxford Text Archive: http://www.ota.ahds.ac.uk/ It is not included with NLTK. The YCOE corpus is divided into 100 files, each representing an Old English prose text. Tags used within each text complies to the YCOE standard: http://www-users.york.ac.uk/~lang22/YCOE/YcoeHome.htm """ import os import re from nltk.tokenize import RegexpTokenizer from nltk.corpus.reader.bracket_parse import BracketParseCorpusReader from nltk.corpus.reader.tagged import TaggedCorpusReader from string import split from nltk.internals import deprecated from util import * from api import * class YCOECorpusReader(CorpusReader): """ Corpus reader for the York-Toronto-Helsinki Parsed Corpus of Old English Prose (YCOE), a 1.5 million word syntactically-annotated corpus of Old English prose texts. """ def __init__(self, root, encoding=None): CorpusReader.__init__(self, root, [], encoding) self._psd_reader = YCOEParseCorpusReader( self.root.join('psd'), '.*', '.psd', encoding=encoding) self._pos_reader = YCOETaggedCorpusReader( self.root.join('pos'), '.*', '.pos') # Make sure we have a consistent set of items: documents = set(f[:-4] for f in self._psd_reader.fileids()) if set(f[:-4] for f in self._pos_reader.fileids()) != documents: raise ValueError('Items in "psd" and "pos" ' 'subdirectories do not match.') fileids = sorted(['%s.psd' % doc for doc in documents] + ['%s.pos' % doc for doc in documents]) CorpusReader.__init__(self, root, fileids, encoding) self._documents = sorted(documents) def documents(self, fileids=None): """ Return a list of document identifiers for all documents in this corpus, or for the documents with the given file(s) if specified. """ if fileids is None: return self._documents if isinstance(fileids, basestring): fileids = [fileids] for f in fileids: if f not in self._fileids: raise KeyError('File id %s not found' % fileids) # Strip off the '.pos' and '.psd' extensions. return sorted(set(f[:-4] for f in fileids)) def fileids(self, documents=None): """ Return a list of file identifiers for the files that make up this corpus, or that store the given document(s) if specified. """ if documents is None: return self._fileids elif isinstance(documents, basestring): documents = [documents] return sorted(set(['%s.pos' % doc for doc in documents] + ['%s.psd' % doc for doc in documents])) def _getfileids(self, documents, subcorpus): """ Helper that selects the appropriate fileids for a given set of documents from a given subcorpus (pos or psd). """ if documents is None: documents = self._documents else: if isinstance(documents, basestring): documents = [documents] for document in documents: if document not in self._documents: if document[-4:] in ('.pos', '.psd'): raise ValueError( 'Expected a document identifier, not a file ' 'identifier. (Use corpus.documents() to get ' 'a list of document identifiers.') else: raise ValueError('Document identifier %s not found' % document) return ['%s.%s' % (d, subcorpus) for d in documents] # Delegate to one of our two sub-readers: def words(self, documents=None): return self._pos_reader.words(self._getfileids(documents, 'pos')) def sents(self, documents=None): return self._pos_reader.sents(self._getfileids(documents, 'pos')) def paras(self, documents=None): return self._pos_reader.paras(self._getfileids(documents, 'pos')) def tagged_words(self, documents=None): return self._pos_reader.tagged_words(self._getfileids(documents, 'pos')) def tagged_sents(self, documents=None): return self._pos_reader.tagged_sents(self._getfileids(documents, 'pos')) def tagged_paras(self, documents=None): return self._pos_reader.tagged_paras(self._getfileids(documents, 'pos')) def parsed_sents(self, documents=None): return self._psd_reader.parsed_sents(self._getfileids(documents, 'psd')) #{ Deprecated since 0.8 @deprecated("Use .raw() or .words() or .tagged_words() or " ".parsed_sents() instead.") def read(self, items=None, format='parsed'): if format == 'parsed': return self.parsed_sents(items) if format == 'raw': return self.raw(items) if format == 'tokenized': return self.words(items) if format == 'tagged': return self.tagged_words(items) if format == 'chunked': raise ValueError('no longer supported') raise ValueError('bad format %r' % format) @deprecated("Use .parsed_sents() instead.") def parsed(self, items=None): return self.parsed_sents(items) @deprecated("Use .words() instead.") def tokenized(self, items=None): return self.words(items) @deprecated("Use .tagged_words() instead.") def tagged(self, items=None): return self.tagged_words(items) @deprecated("Operation no longer supported.") def chunked(self, items=None): raise ValueError('format "chunked" no longer supported') #} class YCOEParseCorpusReader(BracketParseCorpusReader): """Specialized version of the standard bracket parse corpus reader that strips out (CODE ...) and (ID ...) nodes.""" def _parse(self, t): t = re.sub(r'(?u)\((CODE|ID)[^\)]*\)', '', t) if re.match(r'\s*\(\s*\)\s*$', t): return None return BracketParseCorpusReader._parse(self, t) class YCOETaggedCorpusReader(TaggedCorpusReader): def __init__(self, root, items, encoding=None): gaps_re = r'(?u)(?<=/\.)\s+|\s*\S*_CODE\s*|\s*\S*_ID\s*' sent_tokenizer = RegexpTokenizer(gaps_re, gaps=True) TaggedCorpusReader.__init__(self, root, items, sep='_', sent_tokenizer=sent_tokenizer) #: A list of all documents and their titles in ycoe. documents = { 'coadrian.o34': 'Adrian and Ritheus', 'coaelhom.o3': 'Ælfric, Supplemental Homilies', 'coaelive.o3': 'Ælfric\'s Lives of Saints', 'coalcuin': 'Alcuin De virtutibus et vitiis', 'coalex.o23': 'Alexander\'s Letter to Aristotle', 'coapollo.o3': 'Apollonius of Tyre', 'coaugust': 'Augustine', 'cobede.o2': 'Bede\'s History of the English Church', 'cobenrul.o3': 'Benedictine Rule', 'coblick.o23': 'Blickling Homilies', 'coboeth.o2': 'Boethius\' Consolation of Philosophy', 'cobyrhtf.o3': 'Byrhtferth\'s Manual', 'cocanedgD': 'Canons of Edgar (D)', 'cocanedgX': 'Canons of Edgar (X)', 'cocathom1.o3': 'Ælfric\'s Catholic Homilies I', 'cocathom2.o3': 'Ælfric\'s Catholic Homilies II', 'cochad.o24': 'Saint Chad', 'cochdrul': 'Chrodegang of Metz, Rule', 'cochristoph': 'Saint Christopher', 'cochronA.o23': 'Anglo-Saxon Chronicle A', 'cochronC': 'Anglo-Saxon Chronicle C', 'cochronD': 'Anglo-Saxon Chronicle D', 'cochronE.o34': 'Anglo-Saxon Chronicle E', 'cocura.o2': 'Cura Pastoralis', 'cocuraC': 'Cura Pastoralis (Cotton)', 'codicts.o34': 'Dicts of Cato', 'codocu1.o1': 'Documents 1 (O1)', 'codocu2.o12': 'Documents 2 (O1/O2)', 'codocu2.o2': 'Documents 2 (O2)', 'codocu3.o23': 'Documents 3 (O2/O3)', 'codocu3.o3': 'Documents 3 (O3)', 'codocu4.o24': 'Documents 4 (O2/O4)', 'coeluc1': 'Honorius of Autun, Elucidarium 1', 'coeluc2': 'Honorius of Autun, Elucidarium 1', 'coepigen.o3': 'Ælfric\'s Epilogue to Genesis', 'coeuphr': 'Saint Euphrosyne', 'coeust': 'Saint Eustace and his companions', 'coexodusP': 'Exodus (P)', 'cogenesiC': 'Genesis (C)', 'cogregdC.o24': 'Gregory\'s Dialogues (C)', 'cogregdH.o23': 'Gregory\'s Dialogues (H)', 'coherbar': 'Pseudo-Apuleius, Herbarium', 'coinspolD.o34': 'Wulfstan\'s Institute of Polity (D)', 'coinspolX': 'Wulfstan\'s Institute of Polity (X)', 'cojames': 'Saint James', 'colacnu.o23': 'Lacnunga', 'colaece.o2': 'Leechdoms', 'colaw1cn.o3': 'Laws, Cnut I', 'colaw2cn.o3': 'Laws, Cnut II', 'colaw5atr.o3': 'Laws, Æthelred V', 'colaw6atr.o3': 'Laws, Æthelred VI', 'colawaf.o2': 'Laws, Alfred', 'colawafint.o2': 'Alfred\'s Introduction to Laws', 'colawger.o34': 'Laws, Gerefa', 'colawine.ox2': 'Laws, Ine', 'colawnorthu.o3': 'Northumbra Preosta Lagu', 'colawwllad.o4': 'Laws, William I, Lad', 'coleofri.o4': 'Leofric', 'colsigef.o3': 'Ælfric\'s Letter to Sigefyrth', 'colsigewB': 'Ælfric\'s Letter to Sigeweard (B)', 'colsigewZ.o34': 'Ælfric\'s Letter to Sigeweard (Z)', 'colwgeat': 'Ælfric\'s Letter to Wulfgeat', 'colwsigeT': 'Ælfric\'s Letter to Wulfsige (T)', 'colwsigeXa.o34': 'Ælfric\'s Letter to Wulfsige (Xa)', 'colwstan1.o3': 'Ælfric\'s Letter to Wulfstan I', 'colwstan2.o3': 'Ælfric\'s Letter to Wulfstan II', 'comargaC.o34': 'Saint Margaret (C)', 'comargaT': 'Saint Margaret (T)', 'comart1': 'Martyrology, I', 'comart2': 'Martyrology, II', 'comart3.o23': 'Martyrology, III', 'comarvel.o23': 'Marvels of the East', 'comary': 'Mary of Egypt', 'coneot': 'Saint Neot', 'conicodA': 'Gospel of Nicodemus (A)', 'conicodC': 'Gospel of Nicodemus (C)', 'conicodD': 'Gospel of Nicodemus (D)', 'conicodE': 'Gospel of Nicodemus (E)', 'coorosiu.o2': 'Orosius', 'cootest.o3': 'Heptateuch', 'coprefcath1.o3': 'Ælfric\'s Preface to Catholic Homilies I', 'coprefcath2.o3': 'Ælfric\'s Preface to Catholic Homilies II', 'coprefcura.o2': 'Preface to the Cura Pastoralis', 'coprefgen.o3': 'Ælfric\'s Preface to Genesis', 'copreflives.o3': 'Ælfric\'s Preface to Lives of Saints', 'coprefsolilo': 'Preface to Augustine\'s Soliloquies', 'coquadru.o23': 'Pseudo-Apuleius, Medicina de quadrupedibus', 'corood': 'History of the Holy Rood-Tree', 'cosevensl': 'Seven Sleepers', 'cosolilo': 'St. Augustine\'s Soliloquies', 'cosolsat1.o4': 'Solomon and Saturn I', 'cosolsat2': 'Solomon and Saturn II', 'cotempo.o3': 'Ælfric\'s De Temporibus Anni', 'coverhom': 'Vercelli Homilies', 'coverhomE': 'Vercelli Homilies (E)', 'coverhomL': 'Vercelli Homilies (L)', 'covinceB': 'Saint Vincent (Bodley 343)', 'covinsal': 'Vindicta Salvatoris', 'cowsgosp.o3': 'West-Saxon Gospels', 'cowulf.o34': 'Wulfstan\'s Homilies' } nltk-2.0~b9/nltk/corpus/reader/xmldocs.py0000644000175000017500000003742711327451600020342 0ustar bhavanibhavani# Natural Language Toolkit: XML Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT """ Corpus reader for corpora whose documents are xml files. (note -- not named 'xml' to avoid conflicting w/ standard xml package) """ import codecs # Use the c version of ElementTree, which is faster, if possible: try: from xml.etree import cElementTree as ElementTree except ImportError: from nltk.etree import ElementTree from nltk.data import SeekableUnicodeStreamReader from nltk.tokenize import WordPunctTokenizer from nltk.internals import deprecated, ElementWrapper from nltk.corpus.reader.api import CorpusReader from nltk.corpus.reader.util import * class XMLCorpusReader(CorpusReader): """ Corpus reader for corpora whose documents are xml files. Note that the C{XMLCorpusReader} constructor does not take an C{encoding} argument, because the unicode encoding is specified by the XML files themselves. See the XML specs for more info. """ def __init__(self, root, fileids, wrap_etree=False): self._wrap_etree = wrap_etree CorpusReader.__init__(self, root, fileids) def xml(self, fileid=None): # Make sure we have exactly one file -- no concatenating XML. if fileid is None and len(self._fileids) == 1: fileid = self._fileids[0] if not isinstance(fileid, basestring): raise TypeError('Expected a single file identifier string') # Read the XML in using ElementTree. elt = ElementTree.parse(self.abspath(fileid).open()).getroot() # If requested, wrap it. if self._wrap_etree: elt = ElementWrapper(elt) # Return the ElementTree element. return elt def words(self, fileid=None): """ Returns all of the words and punctuation symbols in the specified file that were in text nodes -- ie, tags are ignored. Like the xml() method, fileid can only specify one file. @return: the given file's text nodes as a list of words and punctuation symbols @rtype: C{list} of C{str} """ elt = self.xml(fileid) word_tokenizer=WordPunctTokenizer() iterator = elt.getiterator() out = [] for node in iterator: text = node.text if text is not None: toks = word_tokenizer.tokenize(text) out.extend(toks) return out def raw(self, fileids=None): if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) #{ Deprecated since 0.8 @deprecated("Use .raw() or .xml() instead.") def read(self, items=None, format='xml'): if format == 'raw': return self.raw(items) if format == 'xml': return self.xml(items) raise ValueError('bad format %r' % format) #} class XMLCorpusView(StreamBackedCorpusView): """ A corpus view that selects out specified elements from an XML file, and provides a flat list-like interface for accessing them. (Note: C{XMLCorpusView} is not used by L{XMLCorpusReader} itself, but may be used by subclasses of L{XMLCorpusReader}.) Every XML corpus view has a X{tag specification}, indicating what XML elements should be included in the view; and each (non-nested) element that matches this specification corresponds to one item in the view. Tag specifications are regular expressions over tag paths, where a tag path is a list of element tag names, separated by '/', indicating the ancestry of the element. Some examples: - C{'foo'}: A top-level element whose tag is C{foo}. - C{'foo/bar'}: An element whose tag is C{bar} and whose parent is a top-level element whose tag is C{foo}. - C{'.*/foo'}: An element whose tag is C{foo}, appearing anywhere in the xml tree. - C{'.*/(foo|bar)'}: An wlement whose tag is C{foo} or C{bar}, appearing anywhere in the xml tree. The view items are generated from the selected XML elements via the method L{handle_elt()}. By default, this method returns the element as-is (i.e., as an ElementTree object); but it can be overridden, either via subclassing or via the C{elt_handler} constructor parameter. """ #: If true, then display debugging output to stdout when reading #: blocks. _DEBUG = False #: The number of characters read at a time by this corpus reader. _BLOCK_SIZE = 1024 def __init__(self, fileid, tagspec, elt_handler=None): """ Create a new corpus view based on a specified XML file. Note that the C{XMLCorpusView} constructor does not take an C{encoding} argument, because the unicode encoding is specified by the XML files themselves. @type tagspec: C{str} @param tagspec: A tag specification, indicating what XML elements should be included in the view. Each non-nested element that matches this specification corresponds to one item in the view. @param elt_handler: A function used to transform each element to a value for the view. If no handler is specified, then L{self.handle_elt()} is called, which returns the element as an ElementTree object. The signature of elt_handler is:: elt_handler(elt, tagspec) -> value """ if elt_handler: self.handle_elt = elt_handler self._tagspec = re.compile(tagspec+r'\Z') """The tag specification for this corpus view.""" self._tag_context = {0: ()} """A dictionary mapping from file positions (as returned by C{stream.seek()} to XML contexts. An XML context is a tuple of XML tag names, indicating which tags have not yet been closed.""" encoding = self._detect_encoding(fileid) StreamBackedCorpusView.__init__(self, fileid, encoding=encoding) def _detect_encoding(self, fileid): if isinstance(fileid, PathPointer): s = fileid.open().readline() else: s = open(fileid, 'rb').readline() if s.startswith(codecs.BOM_UTF16_BE): return 'utf-16-be' if s.startswith(codecs.BOM_UTF16_LE): return 'utf-16-le' if s.startswith(codecs.BOM_UTF32_BE): return 'utf-32-be' if s.startswith(codecs.BOM_UTF32_LE): return 'utf-32-le' if s.startswith(codecs.BOM_UTF8): return 'utf-8' m = re.match(r'\s*) | # comment () | # doctype decl (<[^>]*>)) # tag or PI [^<]*)* \Z""", re.DOTALL|re.VERBOSE) #: A regular expression used to extract the tag name from a start tag, #: end tag, or empty-elt tag string. _XML_TAG_NAME = re.compile('<\s*/?\s*([^\s>]+)') #: A regular expression used to find all start-tags, end-tags, and #: emtpy-elt tags in an XML file. This regexp is more lenient than #: the XML spec -- e.g., it allows spaces in some places where the #: spec does not. _XML_PIECE = re.compile(r""" # Include these so we can skip them: (?P )| (?P )| (?P <\?.*?\?> )| (?P )| # These are the ones we actually care about: (?P <\s*[^>/\?!\s][^>]*/\s*> )| (?P <\s*[^>/\?!\s][^>]*> )| (?P <\s*/[^>/\?!\s][^>]*> )""", re.DOTALL|re.VERBOSE) def _read_xml_fragment(self, stream): """ Read a string from the given stream that does not contain any un-closed tags. In particular, this function first reads a block from the stream of size L{self._BLOCK_SIZE}. It then checks if that block contains an un-closed tag. If it does, then this function either backtracks to the last '<', or reads another block. """ fragment = '' while True: if isinstance(stream, SeekableUnicodeStreamReader): startpos = stream.tell() # Read a block and add it to the fragment. xml_block = stream.read(self._BLOCK_SIZE) fragment += xml_block # Do we have a well-formed xml fragment? if self._VALID_XML_RE.match(fragment): return fragment # Do we have a fragment that will never be well-formed? if re.search('[<>]', fragment).group(0) == '>': pos = stream.tell() - ( len(fragment)-re.search('[<>]', fragment).end()) raise ValueError('Unexpected ">" near char %s' % pos) # End of file? if not xml_block: raise ValueError('Unexpected end of file: tag not closed') # If not, then we must be in the middle of a <..tag..>. # If appropriate, backtrack to the most recent '<' # character. last_open_bracket = fragment.rfind('<') if last_open_bracket > 0: if self._VALID_XML_RE.match(fragment[:last_open_bracket]): if isinstance(stream, SeekableUnicodeStreamReader): stream.seek(startpos) stream.char_seek_forward(last_open_bracket) else: stream.seek(-(len(fragment)-last_open_bracket), 1) return fragment[:last_open_bracket] # Otherwise, read another block. (i.e., return to the # top of the loop.) def read_block(self, stream, tagspec=None, elt_handler=None): """ Read from C{stream} until we find at least one element that matches C{tagspec}, and return the result of applying C{elt_handler} to each element found. """ if tagspec is None: tagspec = self._tagspec if elt_handler is None: elt_handler = self.handle_elt # Use a stack of strings to keep track of our context: context = list(self._tag_context.get(stream.tell())) assert context is not None # check this -- could it ever happen? elts = [] elt_start = None # where does the elt start elt_depth = None # what context depth elt_text = '' while elts==[] or elt_start is not None: if isinstance(stream, SeekableUnicodeStreamReader): startpos = stream.tell() xml_fragment = self._read_xml_fragment(stream) # End of file. if not xml_fragment: if elt_start is None: break else: raise ValueError('Unexpected end of file') # Process each in the xml fragment. for piece in self._XML_PIECE.finditer(xml_fragment): if self._DEBUG: print '%25s %s' % ('/'.join(context)[-20:], piece.group()) if piece.group('START_TAG'): name = self._XML_TAG_NAME.match(piece.group()).group(1) # Keep context up-to-date. context.append(name) # Is this one of the elts we're looking for? if elt_start is None: if re.match(tagspec, '/'.join(context)): elt_start = piece.start() elt_depth = len(context) elif piece.group('END_TAG'): name = self._XML_TAG_NAME.match(piece.group()).group(1) # sanity checks: if not context: raise ValueError('Unmatched tag ' % name) if name != context[-1]: raise ValueError('Unmatched tag <%s>...' % (context[-1], name)) # Is this the end of an element? if elt_start is not None and elt_depth == len(context): elt_text += xml_fragment[elt_start:piece.end()] elts.append( (elt_text, '/'.join(context)) ) elt_start = elt_depth = None elt_text = '' # Keep context up-to-date context.pop() elif piece.group('EMPTY_ELT_TAG'): name = self._XML_TAG_NAME.match(piece.group()).group(1) if elt_start is None: if re.match(tagspec, '/'.join(context)+'/'+name): elts.append((piece.group(), '/'.join(context)+'/'+name)) if elt_start is not None: # If we haven't found any elements yet, then keep # looping until we do. if elts == []: elt_text += xml_fragment[elt_start:] elt_start = 0 # If we've found at least one element, then try # backtracking to the start of the element that we're # inside of. else: # take back the last start-tag, and return what # we've gotten so far (elts is non-empty). if self._DEBUG: print ' '*36+'(backtrack)' if isinstance(stream, SeekableUnicodeStreamReader): stream.seek(startpos) stream.char_seek_forward(elt_start) else: stream.seek(-(len(xml_fragment)-elt_start), 1) context = context[:elt_depth-1] elt_start = elt_depth = None elt_text = '' # Update the _tag_context dict. pos = stream.tell() if pos in self._tag_context: assert tuple(context) == self._tag_context[pos] else: self._tag_context[pos] = tuple(context) return [elt_handler(ElementTree.fromstring( elt.encode('ascii', 'xmlcharrefreplace')), context) for (elt, context) in elts] nltk-2.0~b9/nltk/corpus/reader/wordnet.py0000644000175000017500000017065111363770677020372 0ustar bhavanibhavani# Natural Language Toolkit: WordNet # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bethard # Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT import math import re from itertools import islice, chain from nltk.compat import defaultdict from nltk.corpus.reader import CorpusReader from nltk.util import binary_search_file as _binary_search_file from nltk.probability import FreqDist ###################################################################### ## Table of Contents ###################################################################### ## - Constants ## - Data Classes ## - WordNetError ## - Lemma ## - Synset ## - WordNet Corpus Reader ## - WordNet Information Content Corpus Reader ## - Similarity Metrics ## - Demo ###################################################################### ## Constants ###################################################################### #: Positive infinity (for similarity functions) _INF = 1e300 #{ Part-of-speech constants ADJ, ADJ_SAT, ADV, NOUN, VERB = 'a', 's', 'r', 'n', 'v' #} POS_LIST = [NOUN, VERB, ADJ, ADV] #: A table of strings that are used to express verb frames. VERB_FRAME_STRINGS = ( None, "Something %s", "Somebody %s", "It is %sing", "Something is %sing PP", "Something %s something Adjective/Noun", "Something %s Adjective/Noun", "Somebody %s Adjective", "Somebody %s something", "Somebody %s somebody", "Something %s somebody", "Something %s something", "Something %s to somebody", "Somebody %s on something", "Somebody %s somebody something", "Somebody %s something to somebody", "Somebody %s something from somebody", "Somebody %s somebody with something", "Somebody %s somebody of something", "Somebody %s something on somebody", "Somebody %s somebody PP", "Somebody %s something PP", "Somebody %s PP", "Somebody's (body part) %s", "Somebody %s somebody to INFINITIVE", "Somebody %s somebody INFINITIVE", "Somebody %s that CLAUSE", "Somebody %s to somebody", "Somebody %s to INFINITIVE", "Somebody %s whether INFINITIVE", "Somebody %s somebody into V-ing something", "Somebody %s something with something", "Somebody %s INFINITIVE", "Somebody %s VERB-ing", "It %s that CLAUSE", "Something %s INFINITIVE") ###################################################################### ## Data Classes ###################################################################### class WordNetError(Exception): """An exception class for wordnet-related errors.""" class _WordNetObject(object): """A common base class for lemmas and synsets.""" def hypernyms(self): return self._related('@') def instance_hypernyms(self): return self._related('@i') def hyponyms(self): return self._related('~') def instance_hyponyms(self): return self._related('~i') def member_holonyms(self): return self._related('#m') def substance_holonyms(self): return self._related('#s') def part_holonyms(self): return self._related('#p') def member_meronyms(self): return self._related('%m') def substance_meronyms(self): return self._related('%s') def part_meronyms(self): return self._related('%p') def attributes(self): return self._related('=') def entailments(self): return self._related('*') def causes(self): return self._related('>') def also_sees(self): return self._related('^') def verb_groups(self): return self._related('$') def similar_tos(self): return self._related('&') def __hash__(self): return hash(self.name) def __eq__(self, other): return self.name == other.name def __ne__(self, other): return self.name != other.name class Lemma(_WordNetObject): """ The lexical entry for a single morphological form of a sense-disambiguated word. Create a Lemma from a "..." string where: is the morphological stem identifying the synset is one of the module attributes ADJ, ADJ_SAT, ADV, NOUN or VERB is the sense number, counting from 0. is the morphological form of interest Note that and can be different, e.g. the Synset 'salt.n.03' has the Lemmas 'salt.n.03.salt', 'salt.n.03.saltiness' and 'salt.n.03.salinity'. Lemma attributes ---------------- name - The canonical name of this lemma. synset - The synset that this lemma belongs to. syntactic_marker - For adjectives, the WordNet string identifying the syntactic position relative modified noun. See: http://wordnet.princeton.edu/man/wninput.5WN.html#sect10 For all other parts of speech, this attribute is None. Lemma methods ------------- Lemmas have the following methods for retrieving related Lemmas. They correspond to the names for the pointer symbols defined here: http://wordnet.princeton.edu/man/wninput.5WN.html#sect3 These methods all return lists of Lemmas. antonyms hypernyms instance_hypernyms hyponyms instance_hyponyms member_holonyms substance_holonyms part_holonyms member_meronyms substance_meronyms part_meronyms attributes derivationally_related_forms entailments causes also_sees verb_groups similar_tos pertainyms """ # formerly _from_synset_info def __init__(self, wordnet_corpus_reader, synset, name, lexname_index, lex_id, syntactic_marker): self._wordnet_corpus_reader = wordnet_corpus_reader self.name = name self.syntactic_marker = syntactic_marker self.synset = synset self.frame_strings = [] self.frame_ids = [] self._lexname_index = lexname_index self._lex_id = lex_id self.key = None # gets set later. def __repr__(self): tup = type(self).__name__, self.synset.name, self.name return "%s('%s.%s')" % tup def _related(self, relation_symbol): get_synset = self._wordnet_corpus_reader._synset_from_pos_and_offset return [get_synset(pos, offset).lemmas[lemma_index] for pos, offset, lemma_index in self.synset._lemma_pointers[self.name, relation_symbol]] def count(self): """Return the frequency count for this Lemma""" return self._wordnet_corpus_reader.lemma_count(self) def antonyms(self): return self._related('!') def derivationally_related_forms(self): return self._related('+') def pertainyms(self): return self._related('\\') class Synset(_WordNetObject): """Create a Synset from a ".." string where: is the word's morphological stem is one of the module attributes ADJ, ADJ_SAT, ADV, NOUN or VERB is the sense number, counting from 0. Synset attributes ----------------- name - The canonical name of this synset, formed using the first lemma of this synset. Note that this may be different from the name passed to the constructor if that string used a different lemma to identify the synset. pos - The synset's part of speech, matching one of the module level attributes ADJ, ADJ_SAT, ADV, NOUN or VERB. lemmas - A list of the Lemma objects for this synset. definition - The definition for this synset. examples - A list of example strings for this synset. offset - The offset in the WordNet dict file of this synset. #lexname - The name of the lexicographer file containing this synset. Synset methods -------------- Synsets have the following methods for retrieving related Synsets. They correspond to the names for the pointer symbols defined here: http://wordnet.princeton.edu/man/wninput.5WN.html#sect3 These methods all return lists of Synsets. hypernyms instance_hypernyms hyponyms instance_hyponyms member_holonyms substance_holonyms part_holonyms member_meronyms substance_meronyms part_meronyms attributes entailments causes also_sees verb_groups similar_tos Additionally, Synsets support the following methods specific to the hypernym relation: root_hypernyms common_hypernyms lowest_common_hypernyms Note that Synsets do not support the following relations because these are defined by WordNet as lexical relations: antonyms derivationally_related_forms pertainyms """ def __init__(self, wordnet_corpus_reader): self._wordnet_corpus_reader = wordnet_corpus_reader # All of these attributes get initialized by # WordNetCorpusReader._synset_from_pos_and_line() self.pos = None self.offset = None self.name = None self.frame_ids = [] self.lemmas = [] self.lemma_names = [] self.lemma_infos = [] # never used? self.definition = None self.examples = [] self.lexname = None # lexicographer name self._pointers = defaultdict(set) self._lemma_pointers = defaultdict(set) def root_hypernyms(self): """Get the topmost hypernyms of this synset in WordNet.""" result = [] seen = set() todo = [self] while todo: next_synset = todo.pop() if next_synset not in seen: seen.add(next_synset) next_hypernyms = next_synset.hypernyms() + \ next_synset.instance_hypernyms() if not next_hypernyms: result.append(next_synset) else: todo.extend(next_hypernyms) return result # Simpler implementation which makes incorrect assumption that # hypernym hierarcy is acyclic: # # if not self.hypernyms(): # return [self] # else: # return list(set(root for h in self.hypernyms() # for root in h.root_hypernyms())) def max_depth(self): """ @return: The length of the longest hypernym path from this synset to the root. """ if "_max_depth" not in self.__dict__: hypernyms = self.hypernyms() + self.instance_hypernyms() if not hypernyms: self._max_depth = 0 else: self._max_depth = 1 + max(h.max_depth() for h in hypernyms) return self._max_depth def min_depth(self): """ @return: The length of the shortest hypernym path from this synset to the root. """ if "_min_depth" not in self.__dict__: hypernyms = self.hypernyms() + self.instance_hypernyms() if not hypernyms: self._min_depth = 0 else: self._min_depth = 1 + min(h.min_depth() for h in hypernyms) return self._min_depth def closure(self, rel, depth=-1): """Return the transitive closure of source under the rel relationship, breadth-first >>> from nltk.corpus import wordnet as wn >>> dog = wn.synset('dog.n.01') >>> hyp = lambda s:s.hypernyms() >>> list(dog.closure(hyp)) [Synset('domestic_animal.n.01'), Synset('canine.n.02'), Synset('animal.n.01'), Synset('carnivore.n.01'), Synset('organism.n.01'), Synset('placental.n.01'), Synset('living_thing.n.01'), Synset('mammal.n.01'), Synset('whole.n.02'), Synset('vertebrate.n.01'), Synset('object.n.01'), Synset('chordate.n.01'), Synset('physical_entity.n.01'), Synset('entity.n.01')] """ from nltk.util import breadth_first synset_offsets = [] for synset in breadth_first(self, rel, depth): if synset.offset != self.offset: if synset.offset not in synset_offsets: synset_offsets.append(synset.offset) yield synset def hypernym_paths(self): """ Get the path(s) from this synset to the root, where each path is a list of the synset nodes traversed on the way to the root. @return: A list of lists, where each list gives the node sequence connecting the initial L{Synset} node and a root node. """ paths = [] hypernyms = self.hypernyms() if len(hypernyms) == 0: paths = [[self]] for hypernym in hypernyms: for ancestor_list in hypernym.hypernym_paths(): ancestor_list.append(self) paths.append(ancestor_list) return paths def common_hypernyms(self, other): """ Find all synsets that are hypernyms of this synset and the other synset. @type other: L{Synset} @param other: other input synset. @return: The synsets that are hypernyms of both synsets. """ self_synsets = set(self_synset for self_synsets in self._iter_hypernym_lists() for self_synset in self_synsets) other_synsets = set(other_synset for other_synsets in other._iter_hypernym_lists() for other_synset in other_synsets) return list(self_synsets.intersection(other_synsets)) def lowest_common_hypernyms(self, other): """Get the lowest synset that both synsets have as a hypernym.""" self_hypernyms = self._iter_hypernym_lists() other_hypernyms = other._iter_hypernym_lists() synsets = set(s for synsets in self_hypernyms for s in synsets) others = set(s for synsets in other_hypernyms for s in synsets) synsets.intersection_update(others) try: max_depth = max(s.min_depth() for s in synsets) return [s for s in synsets if s.min_depth() == max_depth] except ValueError: return [] def hypernym_distances(self, distance=0): """ Get the path(s) from this synset to the root, counting the distance of each node from the initial node on the way. A set of (synset, distance) tuples is returned. @type distance: C{int} @param distance: the distance (number of edges) from this hypernym to the original hypernym L{Synset} on which this method was called. @return: A set of (L{Synset}, int) tuples where each L{Synset} is a hypernym of the first L{Synset}. """ distances = set([(self, distance)]) for hypernym in self.hypernyms() + self.instance_hypernyms(): distances |= hypernym.hypernym_distances(distance+1) return distances def shortest_path_distance(self, other): """ Returns the distance of the shortest path linking the two synsets (if one exists). For each synset, all the ancestor nodes and their distances are recorded and compared. The ancestor node common to both synsets that can be reached with the minimum number of traversals is used. If no ancestor nodes are common, None is returned. If a node is compared with itself 0 is returned. @type other: L{Synset} @param other: The Synset to which the shortest path will be found. @return: The number of edges in the shortest path connecting the two nodes, or None if no path exists. """ if self == other: return 0 path_distance = None dist_list1 = self.hypernym_distances() dist_dict1 = {} dist_list2 = other.hypernym_distances() dist_dict2 = {} # Transform each distance list into a dictionary. In cases where # there are duplicate nodes in the list (due to there being multiple # paths to the root) the duplicate with the shortest distance from # the original node is entered. for (l, d) in [(dist_list1, dist_dict1), (dist_list2, dist_dict2)]: for (key, value) in l: if key in d: if value < d[key]: d[key] = value else: d[key] = value # For each ancestor synset common to both subject synsets, find the # connecting path length. Return the shortest of these. for synset1 in dist_dict1.keys(): for synset2 in dist_dict2.keys(): if synset1 == synset2: new_distance = dist_dict1[synset1] + dist_dict2[synset2] if path_distance < 0 or new_distance < path_distance: path_distance = new_distance return path_distance def tree(self, rel, depth=-1, cut_mark=None): """ >>> from nltk.corpus import wordnet as wn >>> dog = wn.synset('dog.n.01') >>> hyp = lambda s:s.hypernyms() >>> from pprint import pprint >>> pprint(dog.tree(hyp)) [Synset('dog.n.01'), [Synset('domestic_animal.n.01'), [Synset('animal.n.01'), [Synset('organism.n.01'), [Synset('living_thing.n.01'), [Synset('whole.n.02'), [Synset('object.n.01'), [Synset('physical_entity.n.01'), [Synset('entity.n.01')]]]]]]]], [Synset('canine.n.02'), [Synset('carnivore.n.01'), [Synset('placental.n.01'), [Synset('mammal.n.01'), [Synset('vertebrate.n.01'), [Synset('chordate.n.01'), [Synset('animal.n.01'), [Synset('organism.n.01'), [Synset('living_thing.n.01'), [Synset('whole.n.02'), [Synset('object.n.01'), [Synset('physical_entity.n.01'), [Synset('entity.n.01')]]]]]]]]]]]]]] """ tree = [self] if depth != 0: tree += [x.tree(rel, depth-1, cut_mark) for x in rel(self)] elif cut_mark: tree += [cut_mark] return tree # interface to similarity methods def path_similarity(self, other, verbose=False): """ Path Distance Similarity: Return a score denoting how similar two word senses are, based on the shortest path that connects the senses in the is-a (hypernym/hypnoym) taxonomy. The score is in the range 0 to 1, except in those cases where a path cannot be found (will only be true for verbs as there are many distinct verb taxonomies), in which case None is returned. A score of 1 represents identity i.e. comparing a sense with itself will return 1. @type other: L{Synset} @param other: The L{Synset} that this L{Synset} is being compared to. @return: A score denoting the similarity of the two L{Synset}s, normally between 0 and 1. None is returned if no connecting path could be found. 1 is returned if a L{Synset} is compared with itself. """ distance = self.shortest_path_distance(other) if distance >= 0: return 1.0 / (distance + 1) else: return None def lch_similarity(self, other, verbose=False): """ Leacock Chodorow Similarity: Return a score denoting how similar two word senses are, based on the shortest path that connects the senses (as above) and the maximum depth of the taxonomy in which the senses occur. The relationship is given as -log(p/2d) where p is the shortest path length and d is the taxonomy depth. @type other: L{Synset} @param other: The L{Synset} that this L{Synset} is being compared to. @return: A score denoting the similarity of the two L{Synset}s, normally greater than 0. None is returned if no connecting path could be found. If a L{Synset} is compared with itself, the maximum score is returned, which varies depending on the taxonomy depth. """ if self.pos != other.pos: raise WordNetError('Computing the lch similarity requires ' + \ '%s and %s to have the same part of speech.' % \ (self, other)) if self.pos not in self._wordnet_corpus_reader._max_depth: self._wordnet_corpus_reader._compute_max_depth(self.pos) depth = self._wordnet_corpus_reader._max_depth[self.pos] distance = self.shortest_path_distance(other) if distance >= 0: return -math.log((distance + 1) / (2.0 * depth)) else: return None def wup_similarity(self, other, verbose=False): """ Wu-Palmer Similarity: Return a score denoting how similar two word senses are, based on the depth of the two senses in the taxonomy and that of their Least Common Subsumer (most specific ancestor node). Note that at this time the scores given do _not_ always agree with those given by Pedersen's Perl implementation of WordNet Similarity. The LCS does not necessarily feature in the shortest path connecting the two senses, as it is by definition the common ancestor deepest in the taxonomy, not closest to the two senses. Typically, however, it will so feature. Where multiple candidates for the LCS exist, that whose shortest path to the root node is the longest will be selected. Where the LCS has multiple paths to the root, the longer path is used for the purposes of the calculation. @type other: L{Synset} @param other: The L{Synset} that this L{Synset} is being compared to. @return: A float score denoting the similarity of the two L{Synset}s, normally greater than zero. If no connecting path between the two senses can be found, None is returned. """ subsumers = self.lowest_common_hypernyms(other) # If no LCS was found return None if len(subsumers) == 0: return None subsumer = subsumers[0] # Get the longest path from the LCS to the root, # including two corrections: # - add one because the calculations include both the start and end # nodes # - add one to non-nouns since they have an imaginary root node depth = subsumer.max_depth() + 1 if subsumer.pos != NOUN: depth += 1 # Get the shortest path from the LCS to each of the synsets it is # subsuming. Add this to the LCS path length to get the path # length from each synset to the root. len1 = self.shortest_path_distance(subsumer) len2 = other.shortest_path_distance(subsumer) if len1 is None or len2 is None: return None len1 += depth len2 += depth return (2.0 * depth) / (len1 + len2) def res_similarity(self, other, ic, verbose=False): """ Resnik Similarity: Return a score denoting how similar two word senses are, based on the Information Content (IC) of the Least Common Subsumer (most specific ancestor node). @type other: L{Synset} @param other: The L{Synset} that this L{Synset} is being compared to. @type ic: C{dict} @param ic: an information content object (as returned by L{load_ic()}). @return: A float score denoting the similarity of the two L{Synset}s. Synsets whose LCS is the root node of the taxonomy will have a score of 0 (e.g. N['dog'][0] and N['table'][0]). """ ic1, ic2, lcs_ic = _lcs_ic(self, other, ic) return lcs_ic def jcn_similarity(self, other, ic, verbose=False): """ Jiang-Conrath Similarity: Return a score denoting how similar two word senses are, based on the Information Content (IC) of the Least Common Subsumer (most specific ancestor node) and that of the two input Synsets. The relationship is given by the equation 1 / (IC(s1) + IC(s2) - 2 * IC(lcs)). @type other: L{Synset} @param other: The L{Synset} that this L{Synset} is being compared to. @type ic: C{dict} @param ic: an information content object (as returned by L{load_ic()}). @return: A float score denoting the similarity of the two L{Synset}s. """ if self == other: return _INF ic1, ic2, lcs_ic = _lcs_ic(self, other, ic) # If either of the input synsets are the root synset, or have a # frequency of 0 (sparse data problem), return 0. if ic1 == 0 or ic2 == 0: return 0 ic_difference = ic1 + ic2 - 2 * lcs_ic if ic_difference == 0: return _INF return 1 / ic_difference def lin_similarity(self, other, ic, verbose=False): """ Lin Similarity: Return a score denoting how similar two word senses are, based on the Information Content (IC) of the Least Common Subsumer (most specific ancestor node) and that of the two input Synsets. The relationship is given by the equation 2 * IC(lcs) / (IC(s1) + IC(s2)). @type other: L{Synset} @param other: The L{Synset} that this L{Synset} is being compared to. @type ic: C{dict} @param ic: an information content object (as returned by L{load_ic()}). @return: A float score denoting the similarity of the two L{Synset}s, in the range 0 to 1. """ ic1, ic2, lcs_ic = _lcs_ic(self, other, ic) return (2.0 * lcs_ic) / (ic1 + ic2) def _iter_hypernym_lists(self): """ @return: An iterator over L{Synset}s that are either proper hypernyms or instance of hypernyms of the synset. """ todo = [self] seen = set() while todo: for synset in todo: seen.add(synset) yield todo todo = [hypernym for synset in todo for hypernym in (synset.hypernyms() + \ synset.instance_hypernyms()) if hypernym not in seen] def __repr__(self): return '%s(%r)' % (type(self).__name__, self.name) def _related(self, relation_symbol): get_synset = self._wordnet_corpus_reader._synset_from_pos_and_offset pointer_tuples = self._pointers[relation_symbol] return [get_synset(pos, offset) for pos, offset in pointer_tuples] ###################################################################### ## WordNet Corpus Reader ###################################################################### class WordNetCorpusReader(CorpusReader): """ A corpus reader used to access wordnet or its variants. """ _ENCODING = None # what encoding should we be using, if any? #{ Part-of-speech constants ADJ, ADJ_SAT, ADV, NOUN, VERB = 'a', 's', 'r', 'n', 'v' #} #{ Filename constants _FILEMAP = {ADJ: 'adj', ADV: 'adv', NOUN: 'noun', VERB: 'verb'} #} #{ Part of speech constants _pos_numbers = {NOUN: 1, VERB: 2, ADJ: 3, ADV: 4, ADJ_SAT: 5} _pos_names = dict(tup[::-1] for tup in _pos_numbers.items()) #} #: A list of file identifiers for all the fileids used by this #: corpus reader. _FILES = ('cntlist.rev', 'lexnames', 'index.sense', 'index.adj', 'index.adv', 'index.noun', 'index.verb', 'data.adj', 'data.adv', 'data.noun', 'data.verb', 'adj.exc', 'adv.exc', 'noun.exc', 'verb.exc', ) def __init__(self, root): """ Construct a new wordnet corpus reader, with the given root directory. """ CorpusReader.__init__(self, root, self._FILES, encoding=self._ENCODING) self._lemma_pos_offset_map = defaultdict(dict) """A index that provides the file offset Map from lemma -> pos -> synset_index -> offset""" self._synset_offset_cache = defaultdict(dict) """A cache so we don't have to reconstuct synsets Map from pos -> offset -> synset""" self._max_depth = defaultdict(dict) """A lookup for the maximum depth of each part of speech. Useful for the lch similarity metric. """ self._data_file_map = {} self._exception_map = {} self._lexnames = [] self._key_count_file = None self._key_synset_file = None # Load the lexnames for i, line in enumerate(self.open('lexnames')): index, lexname, _ = line.split() assert int(index) == i self._lexnames.append(lexname) # Load the indices for lemmas and synset offsets self._load_lemma_pos_offset_map() # load the exception file data into memory self._load_exception_map() def _load_lemma_pos_offset_map(self): for suffix in self._FILEMAP.values(): # parse each line of the file (ignoring comment lines) for i, line in enumerate(self.open('index.%s' % suffix)): if line.startswith(' '): continue next = iter(line.split()).next try: # get the lemma and part-of-speech lemma = next() pos = next() # get the number of synsets for this lemma n_synsets = int(next()) assert n_synsets > 0 # get the pointer symbols for all synsets of this lemma n_pointers = int(next()) _ = [next() for _ in xrange(n_pointers)] # same as number of synsets n_senses = int(next()) assert n_synsets == n_senses # get number of senses ranked according to frequency _ = int(next()) # get synset offsets synset_offsets = [int(next()) for _ in xrange(n_synsets)] # raise more informative error with file name and line number except (AssertionError, ValueError), e: tup = ('index.%s' % suffix), (i + 1), e raise WordNetError('file %s, line %i: %s' % tup) # map lemmas and parts of speech to synsets self._lemma_pos_offset_map[lemma][pos] = synset_offsets if pos == ADJ: self._lemma_pos_offset_map[lemma][ADJ_SAT] = synset_offsets def _load_exception_map(self): # load the exception file data into memory for pos, suffix in self._FILEMAP.items(): self._exception_map[pos] = {} for line in self.open('%s.exc' % suffix): terms = line.split() self._exception_map[pos][terms[0]] = terms[1:] self._exception_map[ADJ_SAT] = self._exception_map[ADJ] def _compute_max_depth(self, pos): """ Compute the max depth for the given part of speech. This is used by the lch similarity metric. """ depth = 0 for ii in self.all_synsets(pos): try: depth = max(depth, ii.max_depth()) except RuntimeError: print ii self._max_depth[pos] = depth #//////////////////////////////////////////////////////////// # Loading Lemmas #//////////////////////////////////////////////////////////// def lemma(self, name): synset_name, lemma_name = name.rsplit('.', 1) synset = self.synset(synset_name) for lemma in synset.lemmas: if lemma.name == lemma_name: return lemma raise WordNetError('no lemma %r in %r' % (lemma_name, synset_name)) def lemma_from_key(self, key): # Keys are case sensitive and always lower-case key = key.lower() lemma_name, lex_sense = key.split('%') pos_number, lexname_index, lex_id, _, _ = lex_sense.split(':') pos = self._pos_names[int(pos_number)] # open the key -> synset file if necessary if self._key_synset_file is None: self._key_synset_file = self.open('index.sense') # Find the synset for the lemma. synset_line = _binary_search_file(self._key_synset_file, key) if not synset_line: raise WordNetError("No synset found for key %r" % key) offset = int(synset_line.split()[1]) synset = self._synset_from_pos_and_offset(pos, offset) # return the corresponding lemma for lemma in synset.lemmas: if lemma.key == key: return lemma raise WordNetError("No lemma found for for key %r" % key) #//////////////////////////////////////////////////////////// # Loading Synsets #//////////////////////////////////////////////////////////// def synset(self, name): # split name into lemma, part of speech and synset number lemma, pos, synset_index_str = name.lower().rsplit('.', 2) synset_index = int(synset_index_str) - 1 # get the offset for this synset try: offset = self._lemma_pos_offset_map[lemma][pos][synset_index] except KeyError: message = 'no lemma %r with part of speech %r' raise WordNetError(message % (lemma, pos)) except IndexError: n_senses = len(self._lemma_pos_offset_map[lemma][pos]) message = "lemma %r with part of speech %r has only %i %s" if n_senses == 1: tup = lemma, pos, n_senses, "sense" else: tup = lemma, pos, n_senses, "senses" raise WordNetError(message % tup) # load synset information from the appropriate file synset = self._synset_from_pos_and_offset(pos, offset) # some basic sanity checks on loaded attributes if pos == 's' and synset.pos == 'a': message = ('adjective satellite requested but only plain ' 'adjective found for lemma %r') raise WordNetError(message % lemma) assert synset.pos == pos or (pos == 'a' and synset.pos == 's') # Return the synset object. return synset def _data_file(self, pos): """ Return an open file pointer for the data file for the given part of speech. """ if pos == ADJ_SAT: pos = ADJ if self._data_file_map.get(pos) is None: fileid = 'data.%s' % self._FILEMAP[pos] self._data_file_map[pos] = self.open(fileid) return self._data_file_map[pos] def _synset_from_pos_and_offset(self, pos, offset): # Check to see if the synset is in the cache if offset in self._synset_offset_cache[pos]: return self._synset_offset_cache[pos][offset] data_file = self._data_file(pos) data_file.seek(offset) data_file_line = data_file.readline() synset = self._synset_from_pos_and_line(pos, data_file_line) assert synset.offset == offset self._synset_offset_cache[pos][offset] = synset return synset def _synset_from_pos_and_line(self, pos, data_file_line): # Construct a new (empty) synset. synset = Synset(self) # parse the entry for this synset try: # parse out the definitions and examples from the gloss columns_str, gloss = data_file_line.split('|') gloss = gloss.strip() definitions = [] for gloss_part in gloss.split(';'): gloss_part = gloss_part.strip() if gloss_part.startswith('"'): synset.examples.append(gloss_part.strip('"')) else: definitions.append(gloss_part) synset.definition = '; '.join(definitions) # split the other info into fields next = iter(columns_str.split()).next # get the offset synset.offset = int(next()) # determine the lexicographer file name lexname_index = int(next()) synset.lexname = self._lexnames[lexname_index] # get the part of speech synset.pos = next() # create Lemma objects for each lemma n_lemmas = int(next(), 16) for _ in xrange(n_lemmas): # get the lemma name lemma_name = next() # get the lex_id (used for sense_keys) lex_id = int(next(), 16) # If the lemma has a syntactic marker, extract it. m = re.match(r'(.*?)(\(.*\))?$', lemma_name) lemma_name, syn_mark = m.groups() # create the lemma object lemma = Lemma(self, synset, lemma_name, lexname_index, lex_id, syn_mark) synset.lemmas.append(lemma) synset.lemma_names.append(lemma.name) # collect the pointer tuples n_pointers = int(next()) for _ in xrange(n_pointers): symbol = next() offset = int(next()) pos = next() lemma_ids_str = next() if lemma_ids_str == '0000': synset._pointers[symbol].add((pos, offset)) else: source_index = int(lemma_ids_str[:2], 16) - 1 target_index = int(lemma_ids_str[2:], 16) - 1 source_lemma_name = synset.lemmas[source_index].name lemma_pointers = synset._lemma_pointers tups = lemma_pointers[source_lemma_name, symbol] tups.add((pos, offset, target_index)) # read the verb frames try: frame_count = int(next()) except StopIteration: pass else: for _ in xrange(frame_count): # read the plus sign assert next() == '+' # read the frame and lemma number frame_number = int(next()) frame_string_fmt = VERB_FRAME_STRINGS[frame_number] lemma_number = int(next(), 16) # lemma number of 00 means all words in the synset if lemma_number == 0: synset.frame_ids.append(frame_number) for lemma in synset.lemmas: lemma.frame_ids.append(frame_number) lemma.frame_strings.append(frame_string_fmt % lemma.name) # only a specific word in the synset else: lemma = synset.lemmas[lemma_number - 1] lemma.frame_ids.append(frame_number) lemma.frame_strings.append(frame_string_fmt % lemma.name) # raise a more informative error with line text except ValueError, e: raise WordNetError('line %r: %s' % (data_file_line, e)) # set sense keys for Lemma objects - note that this has to be # done afterwards so that the relations are available for lemma in synset.lemmas: if synset.pos is ADJ_SAT: head_lemma = synset.similar_tos()[0].lemmas[0] head_name = head_lemma.name head_id = '%02d' % head_lemma._lex_id else: head_name = head_id = '' tup = (lemma.name, WordNetCorpusReader._pos_numbers[synset.pos], lemma._lexname_index, lemma._lex_id, head_name, head_id) lemma.key = ('%s%%%d:%02d:%02d:%s:%s' % tup).lower() # the canonical name is based on the first lemma lemma_name = synset.lemmas[0].name.lower() offsets = self._lemma_pos_offset_map[lemma_name][synset.pos] sense_index = offsets.index(synset.offset) tup = lemma_name, synset.pos, sense_index + 1 synset.name = '%s.%s.%02i' % tup return synset #//////////////////////////////////////////////////////////// # Retrieve synsets and lemmas. #//////////////////////////////////////////////////////////// def synsets(self, lemma, pos=None): """Load all synsets with a given lemma and part of speech tag. If no pos is specified, all synsets for all parts of speech will be loaded. """ lemma = lemma.lower() get_synset = self._synset_from_pos_and_offset index = self._lemma_pos_offset_map if pos is None: pos = POS_LIST return [get_synset(p, offset) for p in pos for form in self._morphy(lemma, p) for offset in index[form].get(p, [])] def lemmas(self, lemma, pos=None): return [lemma_obj for synset in self.synsets(lemma, pos) for lemma_obj in synset.lemmas if lemma_obj.name == lemma] def words(self, pos=None): return [lemma.name for lemma in self.lemmas(pos)] def all_lemma_names(self, pos=None): """Return all lemma names for all synsets for the given part of speech tag. If not pos is specified, all synsets for all parts of speech will be used. """ if pos is None: return iter(self._lemma_pos_offset_map) else: return (lemma for lemma in self._lemma_pos_offset_map if pos in self._lemma_pos_offset_map[lemma]) def all_synsets(self, pos=None): """Iterate over all synsets with a given part of speech tag. If no pos is specified, all synsets for all parts of speech will be loaded. """ if pos is None: pos_tags = self._FILEMAP.keys() else: pos_tags = [pos] cache = self._synset_offset_cache from_pos_and_line = self._synset_from_pos_and_line # generate all synsets for each part of speech for pos_tag in pos_tags: # Open the file for reading. Note that we can not re-use # the file poitners from self._data_file_map here, because # we're defining an iterator, and those file pointers might # be moved while we're not looking. if pos_tag == ADJ_SAT: pos_tag = ADJ fileid = 'data.%s' % self._FILEMAP[pos_tag] data_file = self.open(fileid) try: # generate synsets for each line in the POS file offset = data_file.tell() line = data_file.readline() while line: if not line[0].isspace(): if offset in cache[pos_tag]: # See if the synset is cached synset = cache[pos_tag][offset] else: # Otherwise, parse the line synset = from_pos_and_line(pos_tag, line) cache[pos_tag][offset] = synset # adjective satellites are in the same file as # adjectives so only yield the synset if it's actually # a satellite if pos_tag == ADJ_SAT: if synset.pos == pos_tag: yield synset # for all other POS tags, yield all synsets (this means # that adjectives also include adjective satellites) else: yield synset offset = data_file.tell() line = data_file.readline() # close the extra file handle we opened except: data_file.close() raise else: data_file.close() #//////////////////////////////////////////////////////////// # Misc #//////////////////////////////////////////////////////////// def lemma_count(self, lemma): """Return the frequency count for this Lemma""" # open the count file if we haven't already if self._key_count_file is None: self._key_count_file = self.open('cntlist.rev') # find the key in the counts file and return the count line = _binary_search_file(self._key_count_file, lemma.key) if line: return int(line.rsplit(' ', 1)[-1]) else: return 0 def path_similarity(self, synset1, synset2, verbose=False): return synset1.path_similarity(synset2, verbose) path_similarity.__doc__ = Synset.path_similarity.__doc__ def lch_similarity(self, synset1, synset2, verbose=False): return synset1.lch_similarity(synset2, verbose) lch_similarity.__doc__ = Synset.lch_similarity.__doc__ def wup_similarity(self, synset1, synset2, verbose=False): return synset1.wup_similarity(synset2, verbose) wup_similarity.__doc__ = Synset.wup_similarity.__doc__ def res_similarity(self, synset1, synset2, ic, verbose=False): return synset1.res_similarity(synset2, ic, verbose) res_similarity.__doc__ = Synset.res_similarity.__doc__ def jcn_similarity(self, synset1, synset2, ic, verbose=False): return synset1.jcn_similarity(synset2, ic, verbose) jcn_similarity.__doc__ = Synset.jcn_similarity.__doc__ def lin_similarity(self, synset1, synset2, ic, verbose=False): return synset1.lin_similarity(synset2, ic, verbose) lin_similarity.__doc__ = Synset.lin_similarity.__doc__ #//////////////////////////////////////////////////////////// # Morphy #//////////////////////////////////////////////////////////// # Morphy, adapted from Oliver Steele's pywordnet def morphy(self, form, pos=None): """ Find a possible base form for the given form, with the given part of speech, by checking WordNet's list of exceptional forms, and by recursively stripping affixes for this part of speech until a form in WordNet is found. >>> from nltk.corpus import wordnet as wn >>> wn.morphy('dogs') 'dog' >>> wn.morphy('churches') 'church' >>> wn.morphy('aardwolves') 'aardwolf' >>> wn.morphy('abaci') 'abacus' >>> wn.morphy('hardrock', wn.ADV) >>> wn.morphy('book', wn.NOUN) 'book' >>> wn.morphy('book', wn.ADJ) """ if pos is None: morphy = self._morphy analyses = chain(a for p in POS_LIST for a in morphy(form, p)) else: analyses = self._morphy(form, pos) # get the first one we find first = list(islice(analyses, 1)) if len(first) == 1: return first[0] else: return None MORPHOLOGICAL_SUBSTITUTIONS = { NOUN: [('s', ''), ('ses', 's'), ('ves', 'f'), ('xes', 'x'), ('zes', 'z'), ('ches', 'ch'), ('shes', 'sh'), ('men', 'man'), ('ies', 'y')], VERB: [('s', ''), ('ies', 'y'), ('es', 'e'), ('es', ''), ('ed', 'e'), ('ed', ''), ('ing', 'e'), ('ing', '')], ADJ: [('er', ''), ('est', ''), ('er', 'e'), ('est', 'e')], ADV: []} def _morphy(self, form, pos): # from jordanbg: # Given an original string x # 1. Apply rules once to the input to get y1, y2, y3, etc. # 2. Return all that are in the database # 3. If there are no matches, keep applying rules until you either # find a match or you can't go any further exceptions = self._exception_map[pos] substitutions = self.MORPHOLOGICAL_SUBSTITUTIONS[pos] def apply_rules(forms): return [form[:-len(old)] + new for form in forms for old, new in substitutions if form.endswith(old)] def filter_forms(forms): result = [] seen = set() for form in forms: if form in self._lemma_pos_offset_map: if pos in self._lemma_pos_offset_map[form]: if form not in seen: result.append(form) seen.add(form) return result # 0. Check the exception lists if form in exceptions: return filter_forms([form] + exceptions[form]) # 1. Apply rules once to the input to get y1, y2, y3, etc. forms = apply_rules([form]) # 2. Return all that are in the database (and check the original too) results = filter_forms([form] + forms) if results: return results # 3. If there are no matches, keep applying rules until we find a match while forms: forms = apply_rules(forms) results = filter_forms(forms) if results: return results # Return an empty list if we can't find anything return [] #//////////////////////////////////////////////////////////// # Create information content from corpus #//////////////////////////////////////////////////////////// def ic(self, corpus, weight_senses_equally = False, smoothing = 1.0): """ Creates an information content lookup dictionary from a corpus. @type corpus: L{CorpusReader} @param corpus: The corpus from which we create an information content dictionary. @type weight_senses_equally: L{bool} @param weight_senses_equally: If this is True, gives all possible senses equal weight rather than dividing by the number of possible senses. (If a word has 3 synses, each sense gets 0.3333 per appearance when this is False, 1.0 when it is true.) @param smoothing: How much do we smooth synset counts (default is 1.0) @type smoothing: L{float} @return: An information content dictionary """ counts = FreqDist() for ww in corpus.words(): counts.inc(ww) ic = {} for pp in POS_LIST: ic[pp] = defaultdict(float) # Initialize the counts with the smoothing value if smoothing > 0.0: for ss in self.all_synsets(): pos = ss.pos if pos == ADJ_SAT: pos = ADJ ic[pos][ss.offset] = smoothing for ww in counts: possible_synsets = self.synsets(ww) if len(possible_synsets) == 0: continue # Distribute weight among possible synsets weight = float(counts[ww]) if not weight_senses_equally: weight /= float(len(possible_synsets)) for ss in possible_synsets: pos = ss.pos if pos == ADJ_SAT: pos = ADJ for level in ss._iter_hypernym_lists(): for hh in level: ic[pos][hh.offset] += weight # Add the weight to the root ic[pos][0] += weight return ic ###################################################################### ## WordNet Information Content Corpus Reader ###################################################################### class WordNetICCorpusReader(CorpusReader): """ A corpus reader for the WordNet information content corpus. """ def __init__(self, root, fileids): CorpusReader.__init__(self, root, fileids) # this load function would be more efficient if the data was pickled # Note that we can't use NLTK's frequency distributions because # synsets are overlapping (each instance of a synset also counts # as an instance of its hypernyms) def ic(self, icfile): """ Load an information content file from the wordnet_ic corpus and return a dictionary. This dictionary has just two keys, NOUN and VERB, whose values are dictionaries that map from synsets to information content values. @type icfile: L{str} @param icfile: The name of the wordnet_ic file (e.g. "ic-brown.dat") @return: An information content dictionary """ ic = {} ic[NOUN] = defaultdict(float) ic[VERB] = defaultdict(float) for num, line in enumerate(self.open(icfile)): if num == 0: # skip the header continue fields = line.split() offset = int(fields[0][:-1]) value = float(fields[1]) pos = _get_pos(fields[0]) if len(fields) == 3 and fields[2] == "ROOT": # Store root count. ic[pos][0] += value if value != 0: ic[pos][offset] = value return ic ###################################################################### # Similarity metrics ###################################################################### # TODO: Add in the option to manually add a new root node; this will be # useful for verb similarity as there exist multiple verb taxonomies. # More information about the metrics is available at # http://marimba.d.umn.edu/similarity/measures.html def path_similarity(synset1, synset2, verbose=False): return synset1.path_similarity(synset2, verbose) path_similarity.__doc__ = Synset.path_similarity.__doc__ def lch_similarity(synset1, synset2, verbose=False): return synset1.lch_similarity(synset2, verbose) lch_similarity.__doc__ = Synset.lch_similarity.__doc__ def wup_similarity(synset1, synset2, verbose=False): return synset1.wup_similarity(synset2, verbose) wup_similarity.__doc__ = Synset.wup_similarity.__doc__ def res_similarity(synset1, synset2, ic, verbose=False): return synset1.res_similarity(synset2, verbose) res_similarity.__doc__ = Synset.res_similarity.__doc__ def jcn_similarity(synset1, synset2, ic, verbose=False): return synset1.jcn_similarity(synset2, verbose) jcn_similarity.__doc__ = Synset.jcn_similarity.__doc__ def lin_similarity(synset1, synset2, ic, verbose=False): return synset1.lin_similarity(synset2, verbose) lin_similarity.__doc__ = Synset.lin_similarity.__doc__ def _lcs_by_depth(synset1, synset2, verbose=False): """ Finds the least common subsumer of two synsets in a WordNet taxonomy, where the least common subsumer is defined as the ancestor node common to both input synsets whose shortest path to the root node is the longest. @type synset1: L{Synset} @param synset1: First input synset. @type synset2: L{Synset} @param synset2: Second input synset. @return: The ancestor synset common to both input synsets which is also the LCS. """ subsumer = None max_min_path_length = -1 subsumers = synset1.common_hypernyms(synset2) if verbose: print "> Subsumers1:", subsumers # Eliminate those synsets which are ancestors of other synsets in the # set of subsumers. eliminated = set() hypernym_relation = lambda s: s.hypernyms() for s1 in subsumers: for s2 in subsumers: if s2 in s1.closure(hypernym_relation): eliminated.add(s2) if verbose: print "> Eliminated:", eliminated subsumers = [s for s in subsumers if s not in eliminated] if verbose: print "> Subsumers2:", subsumers # Calculate the length of the shortest path to the root for each # subsumer. Select the subsumer with the longest of these. for candidate in subsumers: paths_to_root = candidate.hypernym_paths() min_path_length = -1 for path in paths_to_root: if min_path_length < 0 or len(path) < min_path_length: min_path_length = len(path) if min_path_length > max_min_path_length: max_min_path_length = min_path_length subsumer = candidate if verbose: print "> LCS Subsumer by depth:", subsumer return subsumer def _lcs_ic(synset1, synset2, ic, verbose=False): """ Get the information content of the least common subsumer that has the highest information content value. If two nodes have no explicit common subsumer, assume that they share an artificial root node that is the hypernym of all explicit roots. @type synset1: L{Synset} @param synset1: First input synset. @type synset2: L{Synset} @param synset2: Second input synset. Must be the same part of speech as the first synset. @type ic: C{dict} @param ic: an information content object (as returned by L{load_ic()}). @return: The information content of the two synsets and their most informative subsumer """ if synset1.pos != synset2.pos: raise WordNetError('Computing the least common subsumer requires ' + \ '%s and %s to have the same part of speech.' % \ (synset1, synset2)) ic1 = information_content(synset1, ic) ic2 = information_content(synset2, ic) subsumers = synset1.common_hypernyms(synset2) if len(subsumers) == 0: subsumer_ic = 0 else: subsumer_ic = max(information_content(s, ic) for s in subsumers) if verbose: print "> LCS Subsumer by content:", subsumer_ic return ic1, ic2, subsumer_ic # Utility functions def information_content(synset, ic): try: icpos = ic[synset.pos] except KeyError: msg = 'Information content file has no entries for part-of-speech: %s' raise WordNetError(msg % synset.pos) counts = icpos[synset.offset] if counts == 0: return _INF else: return -math.log(counts / icpos[0]) # get the part of speech (NOUN or VERB) from the information content record # (each identifier has a 'n' or 'v' suffix) def _get_pos(field): if field[-1] == 'n': return NOUN elif field[-1] == 'v': return VERB else: msg = "Unidentified part of speech in WordNet Information Content file" raise ValueError(msg) ###################################################################### # Demo ###################################################################### def demo(): import nltk print 'loading wordnet' wn = WordNetCorpusReader(nltk.data.find('corpora/wordnet')) print 'done loading' S = wn.synset L = wn.lemma print 'getting a synset for go' move_synset = S('go.v.21') print move_synset.name, move_synset.pos, move_synset.lexname print move_synset.lemma_names print move_synset.definition print move_synset.examples zap_n = ['zap.n.01'] zap_v = ['zap.v.01', 'zap.v.02', 'nuke.v.01', 'microwave.v.01'] def _get_synsets(synset_strings): return [S(synset) for synset in synset_strings] zap_n_synsets = _get_synsets(zap_n) zap_v_synsets = _get_synsets(zap_v) zap_synsets = set(zap_n_synsets + zap_v_synsets) print zap_n_synsets print zap_v_synsets print "Navigations:" print S('travel.v.01').hypernyms() print S('travel.v.02').hypernyms() print S('travel.v.03').hypernyms() print L('zap.v.03.nuke').derivationally_related_forms() print L('zap.v.03.atomize').derivationally_related_forms() print L('zap.v.03.atomise').derivationally_related_forms() print L('zap.v.03.zap').derivationally_related_forms() print S('dog.n.01').member_holonyms() print S('dog.n.01').part_meronyms() print S('breakfast.n.1').hypernyms() print S('meal.n.1').hyponyms() print S('Austen.n.1').instance_hypernyms() print S('composer.n.1').instance_hyponyms() print S('faculty.n.2').member_meronyms() print S('copilot.n.1').member_holonyms() print S('table.n.2').part_meronyms() print S('course.n.7').part_holonyms() print S('water.n.1').substance_meronyms() print S('gin.n.1').substance_holonyms() print L('leader.n.1.leader').antonyms() print L('increase.v.1.increase').antonyms() print S('snore.v.1').entailments() print S('heavy.a.1').similar_tos() print S('light.a.1').attributes() print S('heavy.a.1').attributes() print L('English.a.1.English').pertainyms() print S('person.n.01').root_hypernyms() print S('sail.v.01').root_hypernyms() print S('fall.v.12').root_hypernyms() print S('person.n.01').lowest_common_hypernyms(S('dog.n.01')) print S('dog.n.01').path_similarity(S('cat.n.01')) print S('dog.n.01').lch_similarity(S('cat.n.01')) print S('dog.n.01').wup_similarity(S('cat.n.01')) wnic = WordNetICCorpusReader(nltk.data.find('corpora/wordnet_ic'), '.*\.dat') ic = wnic.ic('ic-brown.dat') print S('dog.n.01').jcn_similarity(S('cat.n.01'), ic) ic = wnic.ic('ic-semcor.dat') print S('dog.n.01').lin_similarity(S('cat.n.01'), ic) if __name__ == '__main__': demo() nltk-2.0~b9/nltk/corpus/reader/wordlist.py0000644000175000017500000000276711327451600020537 0ustar bhavanibhavani# Natural Language Toolkit: Word List Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT from nltk.internals import deprecated from nltk.tokenize import line_tokenize from util import * from api import * class WordListCorpusReader(CorpusReader): """ List of words, one per line. Blank lines are ignored. """ def words(self, fileids=None): return line_tokenize(self.raw(fileids)) def raw(self, fileids=None): if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) #{ Deprecated since 0.8 @deprecated("Use .raw() or .words() instead.") def read(self, items=None, format='listed'): if format == 'raw': return self.raw(items) if format == 'listed': return self.words(items) raise ValueError('bad format %r' % format) @deprecated("Use .words() instead.") def listed(self, items=None): return self.words(items) #} class SwadeshCorpusReader(WordListCorpusReader): def entries(self, fileids=None): """ @return: a tuple of words for the specified fileids. """ if not fileids: fileids = self.fileids() wordlists = [self.words(f) for f in fileids] return zip(*wordlists) nltk-2.0~b9/nltk/corpus/reader/verbnet.py0000644000175000017500000003734211327451600020332 0ustar bhavanibhavani# Natural Language Toolkit: Verbnet Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT import re import textwrap from nltk.compat import * from nltk.internals import deprecated from util import * from api import * from xmldocs import * class VerbnetCorpusReader(XMLCorpusReader): # No unicode encoding param, since the data files are all XML. def __init__(self, root, fileids, wrap_etree=False): XMLCorpusReader.__init__(self, root, fileids, wrap_etree) self._lemma_to_class = defaultdict(list) """A dictionary mapping from verb lemma strings to lists of verbnet class identifiers.""" self._wordnet_to_class = defaultdict(list) """A dictionary mapping from wordnet identifier strings to lists of verbnet class identifiers.""" self._class_to_fileid = {} """A dictionary mapping from class identifiers to corresponding file identifiers. The keys of this dictionary provide a complete list of all classes and subclasses.""" self._shortid_to_longid = {} # Initialize the dictionaries. Use the quick (regexp-based) # method instead of the slow (xml-based) method, because it # runs 2-30 times faster. self._quick_index() _LONGID_RE = re.compile(r'([^\-\.]*)-([\d+.\-]+)$') """Regular expression that matches (and decomposes) longids""" _SHORTID_RE = re.compile(r'[\d+.\-]+$') """Regular expression that matches shortids""" _INDEX_RE = re.compile(r']+>|' r'') """Regular expression used by L{_index()} to quickly scan the corpus for basic information.""" def lemmas(self, classid=None): """ Return a list of all verb lemmas that appear in any class, or in the C{classid} if specified. """ if classid is None: return sorted(self._lemma_to_class.keys()) else: # [xx] should this include subclass members? vnclass = self.vnclass(classid) return [member.get('name') for member in vnclass.findall('MEMBERS/MEMBER')] def wordnetids(self, classid=None): """ Return a list of all wordnet identifiers that appear in any class, or in C{classid} if specified. """ if classid is None: return sorted(self._wordnet_to_class.keys()) else: # [xx] should this include subclass members? vnclass = self.vnclass(classid) return sum([member.get('wn','').split() for member in vnclass.findall('MEMBERS/MEMBER')], []) def classids(self, lemma=None, wordnetid=None, fileid=None, classid=None): """ Return a list of the verbnet class identifiers. If a file identifier is specified, then return only the verbnet class identifiers for classes (and subclasses) defined by that file. If a lemma is specified, then return only verbnet class identifiers for classes that contain that lemma as a member. If a wordnetid is specified, then return only identifiers for classes that contain that wordnetid as a member. If a classid is specified, then return only identifiers for subclasses of the specified verbnet class. """ if len([x for x in [lemma, wordnetid, fileid, classid] if x is not None]) > 1: raise ValueError('Specify at most one of: fileid, wordnetid, ' 'fileid, classid') if fileid is not None: return [c for (c,f) in self._class_to_fileid.items() if f == fileid] elif lemma is not None: return self._lemma_to_class[lemma] elif wordnetid is not None: return self._wordnet_to_class[wordnetid] elif classid is not None: xmltree = self.vnclass(classid) return [subclass.get('ID') for subclass in xmltree.findall('SUBCLASSES/VNSUBCLASS')] else: return sorted(self._class_to_fileid.keys()) def vnclass(self, fileid_or_classid): """ Return an ElementTree containing the xml for the specified verbnet class. @param fileid_or_classid: An identifier specifying which class should be returned. Can be a file identifier (such as C{'put-9.1.xml'}), or a verbnet class identifier (such as C{'put-9.1'}) or a short verbnet class identifier (such as C{'9.1'}). """ # File identifier: just return the xml. if fileid_or_classid in self._fileids: return self.xml(fileid_or_classid) # Class identifier: get the xml, and find the right elt. classid = self.longid(fileid_or_classid) if classid in self._class_to_fileid: fileid = self._class_to_fileid[self.longid(classid)] tree = self.xml(fileid) if classid == tree.get('ID'): return tree else: for subclass in tree.findall('.//VNSUBCLASS'): if classid == subclass.get('ID'): return subclass else: assert False # we saw it during _index()! else: raise ValueError('Unknown identifier %s' % fileid_or_classid) def fileids(self, vnclass_ids=None): """ Return a list of fileids that make up this corpus. If C{vnclass_ids} is specified, then return the fileids that make up the specified verbnet class(es). """ if vnclass_ids is None: return self._fileids elif isinstance(vnclass_ids, basestring): return [self._class_to_fileid[self.longid(vnclass_ids)]] else: return [self._class_to_fileid[self.longid(vnclass_id)] for vnclass_id in vnclass_ids] #{ Deprecated since 0.9.7 @deprecated("Use corpus.fileids() instead") def files(self, vnclass_ids=None): return self.fileids(vnclass_ids) #} ###################################################################### #{ Index Initialization ###################################################################### def _index(self): """ Initialize the indexes L{_lemma_to_class}, L{_wordnet_to_class}, and L{_class_to_fileid} by scanning through the corpus fileids. This is fast with cElementTree (<0.1 secs), but quite slow (>10 secs) with the python implementation of ElementTree. """ for fileid in self._fileids: self._index_helper(self.xml(fileid), fileid) def _index_helper(self, xmltree, fileid): """Helper for L{_index()}""" vnclass = xmltree.get('ID') self._class_to_fileid[vnclass] = fileid self._shortid_to_longid[self.shortid(vnclass)] = vnclass for member in xmltree.findall('MEMBERS/MEMBER'): self._lemma_to_class[member.get('name')].append(vnclass) for wn in member.get('wn', '').split(): self._wordnet_to_class[wn].append(vnclass) for subclass in xmltree.findall('SUBCLASSES/VNSUBCLASS'): self._index_helper(subclass, fileid) def _quick_index(self): """ Initialize the indexes L{_lemma_to_class}, L{_wordnet_to_class}, and L{_class_to_fileid} by scanning through the corpus fileids. This doesn't do proper xml parsing, but is good enough to find everything in the standard verbnet corpus -- and it runs about 30 times faster than xml parsing (with the python ElementTree; only 2-3 times faster with cElementTree). """ # nb: if we got rid of wordnet_to_class, this would run 2-3 # times faster. for fileid in self._fileids: vnclass = fileid[:-4] # strip the '.xml' self._class_to_fileid[vnclass] = fileid self._shortid_to_longid[self.shortid(vnclass)] = vnclass for m in self._INDEX_RE.finditer(self.open(fileid).read()): groups = m.groups() if groups[0] is not None: self._lemma_to_class[groups[0]].append(vnclass) for wn in groups[1].split(): self._wordnet_to_class[wn].append(vnclass) elif groups[2] is not None: self._class_to_fileid[groups[2]] = fileid vnclass = groups[2] # for elts. self._shortid_to_longid[self.shortid(vnclass)] = vnclass else: assert False, 'unexpected match condition' ###################################################################### #{ Identifier conversion ###################################################################### def longid(self, shortid): """Given a short verbnet class identifier (eg '37.10'), map it to a long id (eg 'confess-37.10'). If C{shortid} is already a long id, then return it as-is""" if self._LONGID_RE.match(shortid): return shortid # it's already a longid. elif not self._SHORTID_RE.match(shortid): raise ValueError('vnclass identifier %r not found' % shortid) try: return self._shortid_to_longid[shortid] except KeyError: raise ValueError('vnclass identifier %r not found' % shortid) def shortid(self, longid): """Given a long verbnet class identifier (eg 'confess-37.10'), map it to a short id (eg '37.10'). If C{longid} is already a short id, then return it as-is.""" if self._SHORTID_RE.match(longid): return longid # it's already a shortid. m = self._LONGID_RE.match(longid) if m: return m.group(2) else: raise ValueError('vnclass identifier %r not found' % longid) ###################################################################### #{ Pretty Printing ###################################################################### def pprint(self, vnclass): """ Return a string containing a pretty-printed representation of the given verbnet class. @param vnclass: A verbnet class identifier; or an ElementTree containing the xml contents of a verbnet class. """ if isinstance(vnclass, basestring): vnclass = self.vnclass(vnclass) s = vnclass.get('ID') + '\n' s += self.pprint_subclasses(vnclass, indent=' ') + '\n' s += self.pprint_members(vnclass, indent=' ') + '\n' s += ' Thematic roles:\n' s += self.pprint_themroles(vnclass, indent=' ') + '\n' s += ' Frames:\n' s += '\n'.join(self.pprint_frame(vnframe, indent=' ') for vnframe in vnclass.findall('FRAMES/FRAME')) return s def pprint_subclasses(self, vnclass, indent=''): """ Return a string containing a pretty-printed representation of the given verbnet class's subclasses. @param vnclass: A verbnet class identifier; or an ElementTree containing the xml contents of a verbnet class. """ if isinstance(vnclass, basestring): vnclass = self.vnclass(vnclass) subclasses = [subclass.get('ID') for subclass in vnclass.findall('SUBCLASSES/VNSUBCLASS')] if not subclasses: subclasses = ['(none)'] s = 'Subclasses: ' + ' '.join(subclasses) return textwrap.fill(s, 70, initial_indent=indent, subsequent_indent=indent+' ') def pprint_members(self, vnclass, indent=''): """ Return a string containing a pretty-printed representation of the given verbnet class's member verbs. @param vnclass: A verbnet class identifier; or an ElementTree containing the xml contents of a verbnet class. """ if isinstance(vnclass, basestring): vnclass = self.vnclass(vnclass) members = [member.get('name') for member in vnclass.findall('MEMBERS/MEMBER')] if not members: members = ['(none)'] s = 'Members: ' + ' '.join(members) return textwrap.fill(s, 70, initial_indent=indent, subsequent_indent=indent+' ') def pprint_themroles(self, vnclass, indent=''): """ Return a string containing a pretty-printed representation of the given verbnet class's thematic roles. @param vnclass: A verbnet class identifier; or an ElementTree containing the xml contents of a verbnet class. """ if isinstance(vnclass, basestring): vnclass = self.vnclass(vnclass) pieces = [] for themrole in vnclass.findall('THEMROLES/THEMROLE'): piece = indent + '* ' + themrole.get('type') modifiers = ['%(Value)s%(type)s' % restr.attrib for restr in themrole.findall('SELRESTRS/SELRESTR')] if modifiers: piece += '[%s]' % ' '.join(modifiers) pieces.append(piece) return '\n'.join(pieces) def pprint_frame(self, vnframe, indent=''): """ Return a string containing a pretty-printed representation of the given verbnet frame. @param vnframe: An ElementTree containing the xml contents of a verbnet frame. """ s = self.pprint_description(vnframe, indent) + '\n' s += self.pprint_syntax(vnframe, indent+' Syntax: ') + '\n' s += indent + ' Semantics:\n' s += self.pprint_semantics(vnframe, indent+' ') return s def pprint_description(self, vnframe, indent=''): """ Return a string containing a pretty-printed representation of the given verbnet frame description. @param vnframe: An ElementTree containing the xml contents of a verbnet frame. """ descr = vnframe.find('DESCRIPTION') s = indent + descr.attrib['primary'] if descr.get('secondary', ''): s += ' (%s)' % descr.get('secondary') return s def pprint_syntax(self, vnframe, indent=''): """ Return a string containing a pretty-printed representation of the given verbnet frame syntax. @param vnframe: An ElementTree containing the xml contents of a verbnet frame. """ pieces = [] for elt in vnframe.find('SYNTAX'): piece = elt.tag modifiers = [] if 'value' in elt.attrib: modifiers.append(elt.get('value')) modifiers += ['%(Value)s%(type)s' % restr.attrib for restr in (elt.findall('SELRESTRS/SELRESTR') + elt.findall('SYNRESTRS/SYNRESTR'))] if modifiers: piece += '[%s]' % ' '.join(modifiers) pieces.append(piece) return indent + ' '.join(pieces) def pprint_semantics(self, vnframe, indent=''): """ Return a string containing a pretty-printed representation of the given verbnet frame semantics. @param vnframe: An ElementTree containing the xml contents of a verbnet frame. """ pieces = [] for pred in vnframe.findall('SEMANTICS/PRED'): args = [arg.get('value') for arg in pred.findall('ARGS/ARG')] pieces.append('%s(%s)' % (pred.get('value'), ', '.join(args))) return '\n'.join(['%s* %s' % (indent, piece) for piece in pieces]) nltk-2.0~b9/nltk/corpus/reader/util.py0000644000175000017500000007360611374105241017644 0ustar bhavanibhavani# Natural Language Toolkit: Corpus Reader Utilities # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT import os import sys import bisect import re import tempfile try: import cPickle as pickle except ImportError: import pickle from itertools import islice # Use the c version of ElementTree, which is faster, if possible: try: from xml.etree import cElementTree as ElementTree except ImportError: from nltk.etree import ElementTree from nltk.tokenize import wordpunct_tokenize from nltk.internals import deprecated, slice_bounds from nltk.data import PathPointer, FileSystemPathPointer, ZipFilePathPointer from nltk.data import SeekableUnicodeStreamReader from nltk.sourcedstring import SourcedStringStream from nltk.util import AbstractLazySequence, LazySubsequence, LazyConcatenation ###################################################################### #{ Corpus View ###################################################################### class StreamBackedCorpusView(AbstractLazySequence): """ A 'view' of a corpus file, which acts like a sequence of tokens: it can be accessed by index, iterated over, etc. However, the tokens are only constructed as-needed -- the entire corpus is never stored in memory at once. The constructor to C{StreamBackedCorpusView} takes two arguments: a corpus fileid (specified as a string or as a L{PathPointer}); and a block reader. A X{block reader} is a function that reads zero or more tokens from a stream, and returns them as a list. A very simple example of a block reader is: >>> def simple_block_reader(stream): ... return stream.readline().split() This simple block reader reads a single line at a time, and returns a single token (consisting of a string) for each whitespace-separated substring on the line. When deciding how to define the block reader for a given corpus, careful consideration should be given to the size of blocks handled by the block reader. Smaller block sizes will increase the memory requirements of the corpus view's internal data structures (by 2 integers per block). On the other hand, larger block sizes may decrease performance for random access to the corpus. (But note that larger block sizes will I{not} decrease performance for iteration.) Internally, C{CorpusView} maintains a partial mapping from token index to file position, with one entry per block. When a token with a given index M{i} is requested, the C{CorpusView} constructs it as follows: 1. First, it searches the toknum/filepos mapping for the token index closest to (but less than or equal to) M{i}. 2. Then, starting at the file position corresponding to that index, it reads one block at a time using the block reader until it reaches the requested token. The toknum/filepos mapping is created lazily: it is initially empty, but every time a new block is read, the block's initial token is added to the mapping. (Thus, the toknum/filepos map has one entry per block.) In order to increase efficiency for random access patterns that have high degrees of locality, the corpus view may cache one or more blocks. @note: Each C{CorpusView} object internally maintains an open file object for its underlying corpus file. This file should be automatically closed when the C{CorpusView} is garbage collected, but if you wish to close it manually, use the L{close()} method. If you access a C{CorpusView}'s items after it has been closed, the file object will be automatically re-opened. @warning: If the contents of the file are modified during the lifetime of the C{CorpusView}, then the C{CorpusView}'s behavior is undefined. @warning: If a unicode encoding is specified when constructing a C{CorpusView}, then the block reader may only call C{stream.seek()} with offsets that have been returned by C{stream.tell()}; in particular, calling C{stream.seek()} with relative offsets, or with offsets based on string lengths, may lead to incorrect behavior. @ivar _block_reader: The function used to read a single block from the underlying file stream. @ivar _toknum: A list containing the token index of each block that has been processed. In particular, C{_toknum[i]} is the token index of the first token in block C{i}. Together with L{_filepos}, this forms a partial mapping between token indices and file positions. @ivar _filepos: A list containing the file position of each block that has been processed. In particular, C{_toknum[i]} is the file position of the first character in block C{i}. Together with L{_toknum}, this forms a partial mapping between token indices and file positions. @ivar _stream: The stream used to access the underlying corpus file. @ivar _len: The total number of tokens in the corpus, if known; or C{None}, if the number of tokens is not yet known. @ivar _eofpos: The character position of the last character in the file. This is calculated when the corpus view is initialized, and is used to decide when the end of file has been reached. @ivar _cache: A cache of the most recently read block. It is encoded as a tuple (start_toknum, end_toknum, tokens), where start_toknum is the token index of the first token in the block; end_toknum is the token index of the first token not in the block; and tokens is a list of the tokens in the block. """ def __init__(self, fileid, block_reader=None, startpos=0, encoding=None, source=None): """ Create a new corpus view, based on the file C{fileid}, and read with C{block_reader}. See the class documentation for more information. @param fileid: The path to the file that is read by this corpus view. C{fileid} can either be a string or a L{PathPointer}. @param startpos: The file position at which the view will start reading. This can be used to skip over preface sections. @param encoding: The unicode encoding that should be used to read the file's contents. If no encoding is specified, then the file's contents will be read as a non-unicode string (i.e., a C{str}). @param source: If specified, then use an L{SourcedStringStream} to annotate all strings read from the file with information about their start offset, end ofset, and docid. The value of ``source`` will be used as the docid. """ if block_reader: self.read_block = block_reader # Initialize our toknum/filepos mapping. self._toknum = [0] self._filepos = [startpos] self._encoding = encoding self._source = source # We don't know our length (number of tokens) yet. self._len = None self._fileid = fileid self._stream = None self._current_toknum = None """This variable is set to the index of the next token that will be read, immediately before L{self.read_block()} is called. This is provided for the benefit of the block reader, which under rare circumstances may need to know the current token number.""" self._current_blocknum = None """This variable is set to the index of the next block that will be read, immediately before L{self.read_block()} is called. This is provided for the benefit of the block reader, which under rare circumstances may need to know the current block number.""" # Find the length of the file. try: if isinstance(self._fileid, PathPointer): self._eofpos = self._fileid.file_size() else: self._eofpos = os.stat(self._fileid).st_size except Exception, exc: raise ValueError('Unable to open or access %r -- %s' % (fileid, exc)) # Maintain a cache of the most recently read block, to # increase efficiency of random access. self._cache = (-1, -1, None) fileid = property(lambda self: self._fileid, doc=""" The fileid of the file that is accessed by this view. @type: C{str} or L{PathPointer}""") def read_block(self, stream): """ Read a block from the input stream. @return: a block of tokens from the input stream @rtype: list of any @param stream: an input stream @type stream: stream """ raise NotImplementedError('Abstract Method') def _open(self): """ Open the file stream associated with this corpus view. This will be called performed if any value is read from the view while its file stream is closed. """ if isinstance(self._fileid, PathPointer): self._stream = self._fileid.open(self._encoding) elif self._encoding: self._stream = SeekableUnicodeStreamReader( open(self._fileid, 'rb'), self._encoding) else: self._stream = open(self._fileid, 'rb') if self._source is not None: self._stream = SourcedStringStream(self._stream, self._source) def close(self): """ Close the file stream associated with this corpus view. This can be useful if you are worried about running out of file handles (although the stream should automatically be closed upon garbage collection of the corpus view). If the corpus view is accessed after it is closed, it will be automatically re-opened. """ if self._stream is not None: self._stream.close() self._stream = None def __len__(self): if self._len is None: # iterate_from() sets self._len when it reaches the end # of the file: for tok in self.iterate_from(self._toknum[-1]): pass return self._len def __getitem__(self, i): if isinstance(i, slice): start, stop = slice_bounds(self, i) # Check if it's in the cache. offset = self._cache[0] if offset <= start and stop <= self._cache[1]: return self._cache[2][start-offset:stop-offset] # Construct & return the result. return LazySubsequence(self, start, stop) else: # Handle negative indices if i < 0: i += len(self) if i < 0: raise IndexError('index out of range') # Check if it's in the cache. offset = self._cache[0] if offset <= i < self._cache[1]: return self._cache[2][i-offset] # Use iterate_from to extract it. try: return self.iterate_from(i).next() except StopIteration: raise IndexError('index out of range') # If we wanted to be thread-safe, then this method would need to # do some locking. def iterate_from(self, start_tok): # Start by feeding from the cache, if possible. if self._cache[0] <= start_tok < self._cache[1]: for tok in self._cache[2][start_tok-self._cache[0]:]: yield tok start_tok += 1 # Decide where in the file we should start. If `start` is in # our mapping, then we can jump straight to the correct block; # otherwise, start at the last block we've processed. if start_tok < self._toknum[-1]: block_index = bisect.bisect_right(self._toknum, start_tok)-1 toknum = self._toknum[block_index] filepos = self._filepos[block_index] else: block_index = len(self._toknum)-1 toknum = self._toknum[-1] filepos = self._filepos[-1] # Open the stream, if it's not open already. if self._stream is None: self._open() # Each iteration through this loop, we read a single block # from the stream. while filepos < self._eofpos: # Read the next block. self._stream.seek(filepos) self._current_toknum = toknum self._current_blocknum = block_index tokens = self.read_block(self._stream) assert isinstance(tokens, (tuple, list, AbstractLazySequence)), ( 'block reader %s() should return list or tuple.' % self.read_block.__name__) num_toks = len(tokens) new_filepos = self._stream.tell() assert new_filepos > filepos, ( 'block reader %s() should consume at least 1 byte (filepos=%d)' % (self.read_block.__name__, filepos)) # Update our cache. self._cache = (toknum, toknum+num_toks, list(tokens)) # Update our mapping. assert toknum <= self._toknum[-1] if num_toks > 0: block_index += 1 if toknum == self._toknum[-1]: assert new_filepos > self._filepos[-1] # monotonic! self._filepos.append(new_filepos) self._toknum.append(toknum+num_toks) else: # Check for consistency: assert new_filepos == self._filepos[block_index], ( 'inconsistent block reader (num chars read)') assert toknum+num_toks == self._toknum[block_index], ( 'inconsistent block reader (num tokens returned)') # If we reached the end of the file, then update self._len if new_filepos == self._eofpos: self._len = toknum + num_toks # Generate the tokens in this block (but skip any tokens # before start_tok). Note that between yields, our state # may be modified. for tok in tokens[max(0, start_tok-toknum):]: yield tok # If we're at the end of the file, then we're done. assert new_filepos <= self._eofpos if new_filepos == self._eofpos: break # Update our indices toknum += num_toks filepos = new_filepos # If we reach this point, then we should know our length. assert self._len is not None # Use concat for these, so we can use a ConcatenatedCorpusView # when possible. def __add__(self, other): return concat([self, other]) def __radd__(self, other): return concat([other, self]) def __mul__(self, count): return concat([self] * count) def __rmul__(self, count): return concat([self] * count) class ConcatenatedCorpusView(AbstractLazySequence): """ A 'view' of a corpus file that joins together one or more L{StreamBackedCorpusViews}. At most one file handle is left open at any time. """ def __init__(self, corpus_views): self._pieces = corpus_views """A list of the corpus subviews that make up this concatenation.""" self._offsets = [0] """A list of offsets, indicating the index at which each subview begins. In particular:: offsets[i] = sum([len(p) for p in pieces[:i]])""" self._open_piece = None """The most recently accessed corpus subview (or C{None}). Before a new subview is accessed, this subview will be closed.""" def __len__(self): if len(self._offsets) <= len(self._pieces): # Iterate to the end of the corpus. for tok in self.iterate_from(self._offsets[-1]): pass return self._offsets[-1] def close(self): for piece in self._pieces: piece.close() def iterate_from(self, start_tok): piecenum = bisect.bisect_right(self._offsets, start_tok)-1 while piecenum < len(self._pieces): offset = self._offsets[piecenum] piece = self._pieces[piecenum] # If we've got another piece open, close it first. if self._open_piece is not piece: if self._open_piece is not None: self._open_piece.close() self._open_piece = piece # Get everything we can from this piece. for tok in piece.iterate_from(max(0, start_tok-offset)): yield tok # Update the offset table. if piecenum+1 == len(self._offsets): self._offsets.append(self._offsets[-1] + len(piece)) # Move on to the next piece. piecenum += 1 def concat(docs): """ Concatenate together the contents of multiple documents from a single corpus, using an appropriate concatenation function. This utility function is used by corpus readers when the user requests more than one document at a time. """ if len(docs) == 1: return docs[0] if len(docs) == 0: raise ValueError('concat() expects at least one object!') types = set([d.__class__ for d in docs]) # If they're all strings, use string concatenation. if types.issubset([str, unicode, basestring]): return reduce((lambda a,b:a+b), docs, '') # If they're all corpus views, then use ConcatenatedCorpusView. for typ in types: if not issubclass(typ, (StreamBackedCorpusView, ConcatenatedCorpusView)): break else: return ConcatenatedCorpusView(docs) # If they're all lazy sequences, use a lazy concatenation for typ in types: if not issubclass(typ, AbstractLazySequence): break else: return LazyConcatenation(docs) # Otherwise, see what we can do: if len(types) == 1: typ = list(types)[0] if issubclass(typ, list): return reduce((lambda a,b:a+b), docs, []) if issubclass(typ, tuple): return reduce((lambda a,b:a+b), docs, ()) if ElementTree.iselement(typ): xmltree = ElementTree.Element('documents') for doc in docs: xmltree.append(doc) return xmltree # No method found! raise ValueError("Don't know how to concatenate types: %r" % types) ###################################################################### #{ Corpus View for Pickled Sequences ###################################################################### class PickleCorpusView(StreamBackedCorpusView): """ A stream backed corpus view for corpus files that consist of sequences of serialized Python objects (serialized using C{pickle.dump}). One use case for this class is to store the result of running feature detection on a corpus to disk. This can be useful when performing feature detection is expensive (so we don't want to repeat it); but the corpus is too large to store in memory. The following example illustrates this technique: >>> feature_corpus = LazyMap(detect_features, corpus) >>> PickleCorpusView.write(feature_corpus, some_fileid) >>> pcv = PickledCorpusView(some_fileid) """ BLOCK_SIZE = 100 PROTOCOL = -1 def __init__(self, fileid, delete_on_gc=False): """ Create a new corpus view that reads the pickle corpus C{fileid}. @param delete_on_gc: If true, then C{fileid} will be deleted whenever this object gets garbage-collected. """ self._delete_on_gc = delete_on_gc StreamBackedCorpusView.__init__(self, fileid) def read_block(self, stream): result = [] for i in range(self.BLOCK_SIZE): try: result.append(pickle.load(stream)) except EOFError: break return result def __del__(self): """ If C{delete_on_gc} was set to true when this C{PickleCorpusView} was created, then delete the corpus view's fileid. (This method is called whenever a C{PickledCorpusView} is garbage-collected. """ if getattr(self, '_delete_on_gc'): if os.path.exists(self._fileid): try: os.remove(self._fileid) except (OSError, IOError): pass self.__dict__.clear() # make the garbage collector's job easier @classmethod def write(cls, sequence, output_file): if isinstance(output_file, basestring): output_file = open(output_file, 'wb') for item in sequence: pickle.dump(item, output_file, cls.PROTOCOL) @classmethod def cache_to_tempfile(cls, sequence, delete_on_gc=True): """ Write the given sequence to a temporary file as a pickle corpus; and then return a C{PickleCorpusView} view for that temporary corpus file. @param delete_on_gc: If true, then the temporary file will be deleted whenever this object gets garbage-collected. """ try: fd, output_file_name = tempfile.mkstemp('.pcv', 'nltk-') output_file = os.fdopen(fd, 'wb') cls.write(sequence, output_file) output_file.close() return PickleCorpusView(output_file_name, delete_on_gc) except (OSError, IOError), e: raise ValueError('Error while creating temp file: %s' % e) ###################################################################### #{ Block Readers ###################################################################### def read_whitespace_block(stream): toks = [] for i in range(20): # Read 20 lines at a time. toks.extend(stream.readline().split()) return toks def read_wordpunct_block(stream): toks = [] for i in range(20): # Read 20 lines at a time. toks.extend(wordpunct_tokenize(stream.readline())) return toks def read_line_block(stream): toks = [] for i in range(20): line = stream.readline() if not line: return toks toks.append(line.rstrip('\n')) return toks def read_blankline_block(stream): s = '' while True: line = stream.readline() # End of file: if not line: if s: return [s] else: return [] # Blank line: elif line and not line.strip(): if s: return [s] # Other line: else: s += line def read_regexp_block(stream, start_re, end_re=None): """ Read a sequence of tokens from a stream, where tokens begin with lines that match C{start_re}. If C{end_re} is specified, then tokens end with lines that match C{end_re}; otherwise, tokens end whenever the next line matching C{start_re} or EOF is found. """ # Scan until we find a line matching the start regexp. while True: line = stream.readline() if not line: return [] # end of file. if re.match(start_re, line): break # Scan until we find another line matching the regexp, or EOF. lines = [line] while True: oldpos = stream.tell() line = stream.readline() # End of file: if not line: return [''.join(lines)] # End of token: if end_re is not None and re.match(end_re, line): return [''.join(lines)] # Start of new token: backup to just before it starts, and # return the token we've already collected. if end_re is None and re.match(start_re, line): stream.seek(oldpos) return [''.join(lines)] # Anything else is part of the token. lines.append(line) def read_sexpr_block(stream, block_size=16384, comment_char=None): """ Read a sequence of s-expressions from the stream, and leave the stream's file position at the end the last complete s-expression read. This function will always return at least one s-expression, unless there are no more s-expressions in the file. If the file ends in in the middle of an s-expression, then that incomplete s-expression is returned when the end of the file is reached. @param block_size: The default block size for reading. If an s-expression is longer than one block, then more than one block will be read. @param comment_char: A character that marks comments. Any lines that begin with this character will be stripped out. (If spaces or tabs preceed the comment character, then the line will not be stripped.) """ start = stream.tell() block = stream.read(block_size) encoding = getattr(stream, 'encoding', None) assert encoding is not None or isinstance(block, str) if encoding not in (None, 'utf-8'): import warnings warnings.warn('Parsing may fail, depending on the properties ' 'of the %s encoding!' % encoding) # (e.g., the utf-16 encoding does not work because it insists # on adding BOMs to the beginning of encoded strings.) if comment_char: COMMENT = re.compile('(?m)^%s.*$' % re.escape(comment_char)) while True: try: # If we're stripping comments, then make sure our block ends # on a line boundary; and then replace any comments with # space characters. (We can't just strip them out -- that # would make our offset wrong.) if comment_char: block += stream.readline() block = re.sub(COMMENT, _sub_space, block) # Read the block. tokens, offset = _parse_sexpr_block(block) # Skip whitespace offset = re.compile(r'\s*').search(block, offset).end() # Move to the end position. if encoding is None: stream.seek(start+offset) else: stream.seek(start+len(block[:offset].encode(encoding))) # Return the list of tokens we processed return tokens except ValueError, e: if e.args[0] == 'Block too small': next_block = stream.read(block_size) if next_block: block += next_block continue else: # The file ended mid-sexpr -- return what we got. return [block.strip()] else: raise def _sub_space(m): """Helper function: given a regexp match, return a string of spaces that's the same length as the matched string.""" return ' '*(m.end()-m.start()) def _parse_sexpr_block(block): tokens = [] start = end = 0 while end < len(block): m = re.compile(r'\S').search(block, end) if not m: return tokens, end start = m.start() # Case 1: sexpr is not parenthesized. if m.group() != '(': m2 = re.compile(r'[\s(]').search(block, start) if m2: end = m2.start() else: if tokens: return tokens, end raise ValueError('Block too small') # Case 2: parenthesized sexpr. else: nesting = 0 for m in re.compile(r'[()]').finditer(block, start): if m.group()=='(': nesting += 1 else: nesting -= 1 if nesting == 0: end = m.end() break else: if tokens: return tokens, end raise ValueError('Block too small') tokens.append(block[start:end]) return tokens, end ###################################################################### #{ Finding Corpus Items ###################################################################### def find_corpus_fileids(root, regexp): if not isinstance(root, PathPointer): raise TypeError('find_corpus_fileids: expected a PathPointer') regexp += '$' # Find fileids in a zipfile: scan the zipfile's namelist. Filter # out entries that end in '/' -- they're directories. if isinstance(root, ZipFilePathPointer): fileids = [name[len(root.entry):] for name in root.zipfile.namelist() if not name.endswith('/')] items = [name for name in fileids if re.match(regexp, name)] return sorted(items) # Find fileids in a directory: use os.walk to search all # subdirectories, and match paths against the regexp. elif isinstance(root, FileSystemPathPointer): items = [] for dirname, subdirs, fileids in os.walk(root.path): prefix = ''.join('%s/' % p for p in _path_from(root.path, dirname)) items += [prefix+fileid for fileid in fileids if re.match(regexp, prefix+fileid)] # Don't visit svn directories: if '.svn' in subdirs: subdirs.remove('.svn') return sorted(items) else: raise AssertionError("Don't know how to handle %r" % root) def _path_from(parent, child): if os.path.split(parent)[1] == '': parent = os.path.split(parent)[0] path = [] while parent != child: child, dirname = os.path.split(child) path.insert(0, dirname) assert os.path.split(child)[0] != child return path ###################################################################### #{ Paragraph structure in Treebank files ###################################################################### def tagged_treebank_para_block_reader(stream): # Read the next paragraph. para = '' while True: line = stream.readline() # End of paragraph: if re.match('======+\s*$', line): if para.strip(): return [para] # End of file: elif line == '': if para.strip(): return [para] else: return [] # Content line: else: para += line nltk-2.0~b9/nltk/corpus/reader/toolbox.py0000644000175000017500000000460011327451601020343 0ustar bhavanibhavani# Natural Language Toolkit: Toolbox Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Greg Aumann # Stuart Robinson # Steven Bird # URL: # For license information, see LICENSE.TXT """ Module for reading, writing and manipulating Toolbox databases and settings fileids. """ import os import re import codecs from nltk.toolbox import ToolboxData from nltk.internals import deprecated from util import * from api import * class ToolboxCorpusReader(CorpusReader): def xml(self, fileids, key=None): return concat([ToolboxData(path, enc).parse(key) for (path, enc) in self.abspaths(fileids, True)]) def fields(self, fileids, strip=True, unwrap=True, encoding=None, errors='strict', unicode_fields=None): return concat([list(ToolboxData(fileid,enc).fields( strip, unwrap, encoding, errors, unicode_fields)) for (fileid, enc) in self.abspaths(fileids, include_encoding=True)]) # should probably be done lazily: def entries(self, fileids, **kwargs): if 'key' in kwargs: key = kwargs['key'] del kwargs['key'] else: key = 'lx' # the default key in MDF entries = [] for marker, contents in self.fields(fileids, **kwargs): if marker == key: entries.append((contents, [])) else: try: entries[-1][-1].append((marker, contents)) except IndexError: pass return entries def words(self, fileids, key='lx'): return [contents for marker, contents in self.fields(fileids) if marker == key] def raw(self, fileids): if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) #{ Deprecated since 0.8 @deprecated("Use .xml() instead.") def dictionary(self, fileids=None): raise ValueError("no longer supported -- use .xml() instead") @deprecated("Use .xml() instead.") def parse_corpus(self, fileids=None, key=None): return self.xml(items, key) #} def demo(): pass if __name__ == '__main__': demo() nltk-2.0~b9/nltk/corpus/reader/timit.py0000644000175000017500000004265411146727735020033 0ustar bhavanibhavani# Natural Language Toolkit: TIMIT Corpus Reader # # Copyright (C) 2001-2007 NLTK Project # Author: Haejoong Lee # Steven Bird # URL: # For license information, see LICENSE.TXT # [xx] this docstring is out-of-date: """ Read tokens, phonemes and audio data from the NLTK TIMIT Corpus. This corpus contains selected portion of the TIMIT corpus. - 16 speakers from 8 dialect regions - 1 male and 1 female from each dialect region - total 130 sentences (10 sentences per speaker. Note that some sentences are shared among other speakers, especially sa1 and sa2 are spoken by all speakers.) - total 160 recording of sentences (10 recordings per speaker) - audio format: NIST Sphere, single channel, 16kHz sampling, 16 bit sample, PCM encoding Module contents =============== The timit corpus reader provides 4 functions and 4 data items. - utterances List of utterances in the corpus. There are total 160 utterances, each of which corresponds to a unique utterance of a speaker. Here's an example of an utterance identifier in the list:: dr1-fvmh0/sx206 - _---- _--- | | | | | | | | | | | | | | `--- sentence number | | | `----- sentence type (a:all, i:shared, x:exclusive) | | `--------- speaker ID | `------------ sex (m:male, f:female) `-------------- dialect region (1..8) - speakers List of speaker IDs. An example of speaker ID:: dr1-fvmh0 Note that if you split an item ID with colon and take the first element of the result, you will get a speaker ID. >>> itemid = dr1-fvmh0/sx206 >>> spkrid,sentid = itemid.split('/') >>> spkrid 'dr1-fvmh0' The second element of the result is a sentence ID. - dictionary() Phonetic dictionary of words contained in this corpus. This is a Python dictionary from words to phoneme lists. - spkrinfo() Speaker information table. It's a Python dictionary from speaker IDs to records of 10 fields. Speaker IDs the same as the ones in timie.speakers. Each record is a dictionary from field names to values, and the fields are as follows:: id speaker ID as defined in the original TIMIT speaker info table sex speaker gender (M:male, F:female) dr speaker dialect region (1:new england, 2:northern, 3:north midland, 4:south midland, 5:southern, 6:new york city, 7:western, 8:army brat (moved around)) use corpus type (TRN:training, TST:test) in this sample corpus only TRN is available recdate recording date birthdate speaker birth date ht speaker height race speaker race (WHT:white, BLK:black, AMR:american indian, SPN:spanish-american, ORN:oriental,???:unknown) edu speaker education level (HS:high school, AS:associate degree, BS:bachelor's degree (BS or BA), MS:master's degree (MS or MA), PHD:doctorate degree (PhD,JD,MD), ??:unknown) comments comments by the recorder The 4 functions are as follows. - tokenized(sentences=items, offset=False) Given a list of items, returns an iterator of a list of word lists, each of which corresponds to an item (sentence). If offset is set to True, each element of the word list is a tuple of word(string), start offset and end offset, where offset is represented as a number of 16kHz samples. - phonetic(sentences=items, offset=False) Given a list of items, returns an iterator of a list of phoneme lists, each of which corresponds to an item (sentence). If offset is set to True, each element of the phoneme list is a tuple of word(string), start offset and end offset, where offset is represented as a number of 16kHz samples. - audiodata(item, start=0, end=None) Given an item, returns a chunk of audio samples formatted into a string. When the fuction is called, if start and end are omitted, the entire samples of the recording will be returned. If only end is omitted, samples from the start offset to the end of the recording will be returned. - play(data) Play the given audio samples. The audio samples can be obtained from the timit.audiodata function. """ import sys import os import re import tempfile import time from nltk.tree import Tree from nltk.internals import deprecated, import_from_stdlib from util import * from api import * class TimitCorpusReader(CorpusReader): """ Reader for the TIMIT corpus (or any other corpus with the same file layout and use of file formats). The corpus root directory should contain the following files: - timitdic.txt: dictionary of standard transcriptions - spkrinfo.txt: table of speaker information In addition, the root directory should contain one subdirectory for each speaker, containing three files for each utterance: - .txt: text content of utterances - .wrd: tokenized text content of utterances - .phn: phonetic transcription of utterances - .wav: utterance sound file """ _FILE_RE = (r'(\w+-\w+/\w+\.(phn|txt|wav|wrd))|' + r'timitdic\.txt|spkrinfo\.txt') """A regexp matching fileids that are used by this corpus reader.""" _UTTERANCE_RE = r'\w+-\w+/\w+\.txt' def __init__(self, root, encoding=None): """ Construct a new TIMIT corpus reader in the given directory. @param root: The root directory for this corpus. """ # Ensure that wave files don't get treated as unicode data: if isinstance(encoding, basestring): encoding = [('.*\.wav', None), ('.*', encoding)] CorpusReader.__init__(self, root, find_corpus_fileids(root, self._FILE_RE), encoding=encoding) self._utterances = [name[:-4] for name in find_corpus_fileids(root, self._UTTERANCE_RE)] """A list of the utterance identifiers for all utterances in this corpus.""" self._speakerinfo = None self._root = root self.speakers = sorted(set(u.split('/')[0] for u in self._utterances)) def fileids(self, filetype=None): """ Return a list of file identifiers for the files that make up this corpus. @param filetype: If specified, then C{filetype} indicates that only the files that have the given type should be returned. Accepted values are: C{txt}, C{wrd}, C{phn}, C{wav}, or C{metadata}, """ if filetype is None: return CorpusReader.fileids(self) elif filetype in ('txt', 'wrd', 'phn', 'wav'): return ['%s.%s' % (u, filetype) for u in self._utterances] elif filetype == 'metadata': return ['timitdic.txt', 'spkrinfo.txt'] else: raise ValueError('Bad value for filetype: %r' % filetype) def utteranceids(self, dialect=None, sex=None, spkrid=None, sent_type=None, sentid=None): """ @return: A list of the utterance identifiers for all utterances in this corpus, or for the given speaker, dialect region, gender, sentence type, or sentence number, if specified. """ if isinstance(dialect, basestring): dialect = [dialect] if isinstance(sex, basestring): sex = [sex] if isinstance(spkrid, basestring): spkrid = [spkrid] if isinstance(sent_type, basestring): sent_type = [sent_type] if isinstance(sentid, basestring): sentid = [sentid] utterances = self._utterances[:] if dialect is not None: utterances = [u for u in utterances if u[2] in dialect] if sex is not None: utterances = [u for u in utterances if u[4] in sex] if spkrid is not None: utterances = [u for u in utterances if u[:9] in spkrid] if sent_type is not None: utterances = [u for u in utterances if u[11] in sent_type] if sentid is not None: utterances = [u for u in utterances if u[10:] in spkrid] return utterances def transcription_dict(self): """ @return: A dictionary giving the 'standard' transcription for each word. """ _transcriptions = {} for line in self.open('timitdic.txt'): if not line.strip() or line[0] == ';': continue m = re.match(r'\s*(\S+)\s+/(.*)/\s*$', line) if not m: raise ValueError('Bad line: %r' % line) _transcriptions[m.group(1)] = m.group(2).split() return _transcriptions def spkrid(self, utterance): return utterance.split('/')[0] def sentid(self, utterance): return utterance.split('/')[1] def utterance(self, spkrid, sentid): return '%s/%s' % (spkrid, sentid) def spkrutteranceids(self, speaker): """ @return: A list of all utterances associated with a given speaker. """ return [utterance for utterance in self._utterances if utterance.startswith(speaker+'/')] def spkrinfo(self, speaker): """ @return: A dictionary mapping .. something. """ if speaker in self._utterances: speaker = self.spkrid(speaker) if self._speakerinfo is None: self._speakerinfo = {} for line in self.open('spkrinfo.txt'): if not line.strip() or line[0] == ';': continue rec = line.strip().split(None, 9) key = "dr%s-%s%s" % (rec[2],rec[1].lower(),rec[0].lower()) self._speakerinfo[key] = SpeakerInfo(*rec) return self._speakerinfo[speaker] def phones(self, utterances=None): return [line.split()[-1] for fileid in self._utterance_fileids(utterances, '.phn') for line in self.open(fileid) if line.strip()] def phone_times(self, utterances=None): """ offset is represented as a number of 16kHz samples! """ return [(line.split()[2], int(line.split()[0]), int(line.split()[1])) for fileid in self._utterance_fileids(utterances, '.phn') for line in self.open(fileid) if line.strip()] def words(self, utterances=None): return [line.split()[-1] for fileid in self._utterance_fileids(utterances, '.wrd') for line in self.open(fileid) if line.strip()] def word_times(self, utterances=None): return [(line.split()[2], int(line.split()[0]), int(line.split()[1])) for fileid in self._utterance_fileids(utterances, '.wrd') for line in self.open(fileid) if line.strip()] def sents(self, utterances=None): return [[line.split()[-1] for line in self.open(fileid) if line.strip()] for fileid in self._utterance_fileids(utterances, '.wrd')] def sent_times(self, utterances=None): return [(line.split(None,2)[-1].strip(), int(line.split()[0]), int(line.split()[1])) for fileid in self._utterance_fileids(utterances, '.txt') for line in self.open(fileid) if line.strip()] def phone_trees(self, utterances=None): if utterances is None: utterances = self._utterances if isinstance(utterances, basestring): utterances = [utterances] trees = [] for utterance in utterances: word_times = self.word_times(utterance) phone_times = self.phone_times(utterance) sent_times = self.sent_times(utterance) while sent_times: (sent, sent_start, sent_end) = sent_times.pop(0) trees.append(Tree('S', [])) while (word_times and phone_times and phone_times[0][2] <= word_times[0][1]): trees[-1].append(phone_times.pop(0)[0]) while word_times and word_times[0][2] <= sent_end: (word, word_start, word_end) = word_times.pop(0) trees[-1].append(Tree(word, [])) while phone_times and phone_times[0][2] <= word_end: trees[-1][-1].append(phone_times.pop(0)[0]) while phone_times and phone_times[0][2] <= sent_end: trees[-1].append(phone_times.pop(0)[0]) return trees # [xx] NOTE: This is currently broken -- we're assuming that the # fileids are WAV fileids (aka RIFF), but they're actually NIST SPHERE # fileids. def wav(self, utterance, start=0, end=None): # nltk.chunk conflicts with the stdlib module 'chunk' wave = import_from_stdlib('wave') w = wave.open(self.open(utterance+'.wav'), 'rb') # If they want the whole thing, return it as-is. if start==0 and end is None: return w.read() # Select the piece we want using the 'wave' module. else: # Skip past frames before start. w.readframes(start) # Read the frames we want. frames = w.readframes(end-start) # Open a new temporary file -- the wave module requires # an actual file, and won't work w/ stringio. :( tf = tempfile.TemporaryFile() out = wave.open(tf, 'w') # Write the parameters & data to the new file. out.setparams(w.getparams()) out.writeframes(frames) out.close() # Read the data back from the file, and return it. The # file will automatically be deleted when we return. tf.seek(0) return tf.read() def audiodata(self, utterance, start=0, end=None): assert(end is None or end > start) headersize = 44 if end is None: data = self.open(utterance+'.wav').read() else: data = self.open(utterance+'.wav').read(headersize+end*2) return data[headersize+start*2:] def _utterance_fileids(self, utterances, extension): if utterances is None: utterances = self._utterances if isinstance(utterances, basestring): utterances = [utterances] return ['%s%s' % (u, extension) for u in utterances] def play(self, utterance, start=0, end=None): """ Play the given audio sample. @param utterance: The utterance id of the sample to play """ # Method 1: os audio dev. try: import ossaudiodev try: dsp = ossaudiodev.open('w') dsp.setfmt(ossaudiodev.AFMT_S16_LE) dsp.channels(1) dsp.speed(16000) dsp.write(self.audiodata(utterance, start, end)) dsp.close() except IOError, e: print >>sys.stderr, ("can't acquire the audio device; please " "activate your audio device.") print >>sys.stderr, "system error message:", str(e) return except ImportError: pass # Method 2: pygame try: import pygame.mixer, StringIO pygame.mixer.init(16000) f = StringIO.StringIO(self.wav(utterance, start, end)) pygame.mixer.Sound(f).play() while pygame.mixer.get_busy(): time.sleep(0.01) return except ImportError: pass # Method 3: complain. :) print >>sys.stderr, ("you must install pygame or ossaudiodev " "for audio playback.") #{ Deprecated since 0.9.7 @deprecated("Use corpus.fileids() instead") def files(self, filetype=None): return self.fileids(filetype) @deprecated("Use corpus.utteranceids() instead") def utterances(self, dialect=None, sex=None, spkrid=None, sent_type=None, sentid=None): return self.utteranceids(dialect, sex, spkrid, sent_type, sentid) @deprecated("Use corpus.spkrutteranceids() instead") def spkrutterances(self, speaker): return self.utteranceids(speaker) #} #{ Deprecated since 0.9.1 @deprecated("Use utteranceids(spkrid=...) instead.") def spkritems(self, spkrid): return self.utteranceids(spkrid=spkrid) #} #{ Deprecated since 0.8 @deprecated("Use .sents() or .sent_times() instead.") def tokenized(self, utterances=None, offset=True): if offset: return self.sent_times(utterances) else: return self.sents(utterances) @deprecated("Use .phones() or .phone_times() instead.") def phonetic(self, utterances=None, offset=True): if offset: return self.phone_times(utterances) else: return self.phones(utterances) #} class SpeakerInfo: def __init__(self, id, sex, dr, use, recdate, birthdate, ht, race, edu, comments=None): self.id = id self.sex = sex self.dr = dr self.use = use self.recdate = recdate self.birthdate = birthdate self.ht = ht self.race = race self.edu = edu self.comments = comments def __repr__(self): attribs = 'id sex dr use recdate birthdate ht race edu comments' args = ['%s=%r' % (attr, getattr(self, attr)) for attr in attribs.split()] return 'SpeakerInfo(%s)' % (', '.join(args)) nltk-2.0~b9/nltk/corpus/reader/tagged.py0000644000175000017500000002766511327451600020127 0ustar bhavanibhavani# Natural Language Toolkit: Tagged Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # Steven Bird # URL: # For license information, see LICENSE.TXT """ A reader for corpora whose documents contain part-of-speech-tagged words. """ import os from nltk.tag import str2tuple from nltk.tokenize import * from nltk.internals import deprecated from api import * from util import * class TaggedCorpusReader(CorpusReader): """ Reader for simple part-of-speech tagged corpora. Paragraphs are assumed to be split using blank lines. Sentences and words can be tokenized using the default tokenizers, or by custom tokenizers specified as parameters to the constructor. Words are parsed using L{nltk.tag.str2tuple}. By default, C{'/'} is used as the separator. I.e., words should have the form:: word1/tag1 word2/tag2 word3/tag3 ... But custom separators may be specified as parameters to the constructor. Part of speech tags are case-normalized to upper case. """ def __init__(self, root, fileids, sep='/', word_tokenizer=WhitespaceTokenizer(), sent_tokenizer=RegexpTokenizer('\n', gaps=True), para_block_reader=read_blankline_block, encoding=None, tag_mapping_function=None): """ Construct a new Tagged Corpus reader for a set of documents located at the given root directory. Example usage: >>> root = '/...path to corpus.../' >>> reader = TaggedCorpusReader(root, '.*', '.txt') @param root: The root directory for this corpus. @param fileids: A list or regexp specifying the fileids in this corpus. """ CorpusReader.__init__(self, root, fileids, encoding) self._sep = sep self._word_tokenizer = word_tokenizer self._sent_tokenizer = sent_tokenizer self._para_block_reader = para_block_reader self._tag_mapping_function = tag_mapping_function def raw(self, fileids=None): """ @return: the given file(s) as a single string. @rtype: C{str} """ if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) def words(self, fileids=None): """ @return: the given file(s) as a list of words and punctuation symbols. @rtype: C{list} of C{str} """ return concat([TaggedCorpusView(fileid, enc, False, False, False, self._sep, self._word_tokenizer, self._sent_tokenizer, self._para_block_reader, None) for (fileid, enc) in self.abspaths(fileids, True)]) def sents(self, fileids=None): """ @return: the given file(s) as a list of sentences or utterances, each encoded as a list of word strings. @rtype: C{list} of (C{list} of C{str}) """ return concat([TaggedCorpusView(fileid, enc, False, True, False, self._sep, self._word_tokenizer, self._sent_tokenizer, self._para_block_reader, None) for (fileid, enc) in self.abspaths(fileids, True)]) def paras(self, fileids=None): """ @return: the given file(s) as a list of paragraphs, each encoded as a list of sentences, which are in turn encoded as lists of word strings. @rtype: C{list} of (C{list} of (C{list} of C{str})) """ return concat([TaggedCorpusView(fileid, enc, False, True, True, self._sep, self._word_tokenizer, self._sent_tokenizer, self._para_block_reader, None) for (fileid, enc) in self.abspaths(fileids, True)]) def tagged_words(self, fileids=None, simplify_tags=False): """ @return: the given file(s) as a list of tagged words and punctuation symbols, encoded as tuples C{(word,tag)}. @rtype: C{list} of C{(str,str)} """ if simplify_tags: tag_mapping_function = self._tag_mapping_function else: tag_mapping_function = None return concat([TaggedCorpusView(fileid, enc, True, False, False, self._sep, self._word_tokenizer, self._sent_tokenizer, self._para_block_reader, tag_mapping_function) for (fileid, enc) in self.abspaths(fileids, True)]) def tagged_sents(self, fileids=None, simplify_tags=False): """ @return: the given file(s) as a list of sentences, each encoded as a list of C{(word,tag)} tuples. @rtype: C{list} of (C{list} of C{(str,str)}) """ if simplify_tags: tag_mapping_function = self._tag_mapping_function else: tag_mapping_function = None return concat([TaggedCorpusView(fileid, enc, True, True, False, self._sep, self._word_tokenizer, self._sent_tokenizer, self._para_block_reader, tag_mapping_function) for (fileid, enc) in self.abspaths(fileids, True)]) def tagged_paras(self, fileids=None, simplify_tags=False): """ @return: the given file(s) as a list of paragraphs, each encoded as a list of sentences, which are in turn encoded as lists of C{(word,tag)} tuples. @rtype: C{list} of (C{list} of (C{list} of C{(str,str)})) """ if simplify_tags: tag_mapping_function = self._tag_mapping_function else: tag_mapping_function = None return concat([TaggedCorpusView(fileid, enc, True, True, True, self._sep, self._word_tokenizer, self._sent_tokenizer, self._para_block_reader, tag_mapping_function) for (fileid, enc) in self.abspaths(fileids, True)]) class CategorizedTaggedCorpusReader(CategorizedCorpusReader, TaggedCorpusReader): """ A reader for part-of-speech tagged corpora whose documents are divided into categories based on their file identifiers. """ def __init__(self, *args, **kwargs): """ Initialize the corpus reader. Categorization arguments (C{cat_pattern}, C{cat_map}, and C{cat_file}) are passed to the L{CategorizedCorpusReader constructor }. The remaining arguments are passed to the L{TaggedCorpusReader constructor }. """ CategorizedCorpusReader.__init__(self, kwargs) TaggedCorpusReader.__init__(self, *args, **kwargs) def _resolve(self, fileids, categories): if fileids is not None and categories is not None: raise ValueError('Specify fileids or categories, not both') if categories is not None: return self.fileids(categories) else: return fileids def raw(self, fileids=None, categories=None): return TaggedCorpusReader.raw( self, self._resolve(fileids, categories)) def words(self, fileids=None, categories=None): return TaggedCorpusReader.words( self, self._resolve(fileids, categories)) def sents(self, fileids=None, categories=None): return TaggedCorpusReader.sents( self, self._resolve(fileids, categories)) def paras(self, fileids=None, categories=None): return TaggedCorpusReader.paras( self, self._resolve(fileids, categories)) def tagged_words(self, fileids=None, categories=None, simplify_tags=False): return TaggedCorpusReader.tagged_words( self, self._resolve(fileids, categories), simplify_tags) def tagged_sents(self, fileids=None, categories=None, simplify_tags=False): return TaggedCorpusReader.tagged_sents( self, self._resolve(fileids, categories), simplify_tags) def tagged_paras(self, fileids=None, categories=None, simplify_tags=False): return TaggedCorpusReader.tagged_paras( self, self._resolve(fileids, categories), simplify_tags) class TaggedCorpusView(StreamBackedCorpusView): """ A specialized corpus view for tagged documents. It can be customized via flags to divide the tagged corpus documents up by sentence or paragraph, and to include or omit part of speech tags. C{TaggedCorpusView} objects are typically created by L{TaggedCorpusReader} (not directly by nltk users). """ def __init__(self, corpus_file, encoding, tagged, group_by_sent, group_by_para, sep, word_tokenizer, sent_tokenizer, para_block_reader, tag_mapping_function=None): self._tagged = tagged self._group_by_sent = group_by_sent self._group_by_para = group_by_para self._sep = sep self._word_tokenizer = word_tokenizer self._sent_tokenizer = sent_tokenizer self._para_block_reader = para_block_reader self._tag_mapping_function = tag_mapping_function StreamBackedCorpusView.__init__(self, corpus_file, encoding=encoding) def read_block(self, stream): """Reads one paragraph at a time.""" block = [] for para_str in self._para_block_reader(stream): para = [] for sent_str in self._sent_tokenizer.tokenize(para_str): sent = [str2tuple(s, self._sep) for s in self._word_tokenizer.tokenize(sent_str)] if self._tag_mapping_function: sent = [(w, self._tag_mapping_function(t)) for (w,t) in sent] if not self._tagged: sent = [w for (w,t) in sent] if self._group_by_sent: para.append(sent) else: para.extend(sent) if self._group_by_para: block.append(para) else: block.extend(para) return block # needs to implement simplified tags class MacMorphoCorpusReader(TaggedCorpusReader): """ A corpus reader for the MAC_MORPHO corpus. Each line contains a single tagged word, using '_' as a separator. Sentence boundaries are based on the end-sentence tag ('_.'). Paragraph information is not included in the corpus, so each paragraph returned by L{self.paras()} and L{self.tagged_paras()} contains a single sentence. """ def __init__(self, root, fileids, encoding=None, tag_mapping_function=None): TaggedCorpusReader.__init__( self, root, fileids, sep='_', word_tokenizer=LineTokenizer(), sent_tokenizer=RegexpTokenizer('.*\n'), para_block_reader=self._read_block, encoding=encoding, tag_mapping_function=tag_mapping_function) def _read_block(self, stream): return read_regexp_block(stream, r'.*', r'.*_\.') nltk-2.0~b9/nltk/corpus/reader/switchboard.py0000644000175000017500000000736511377057401021206 0ustar bhavanibhavani# Natural Language Toolkit: Switchboard Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT import re from nltk.tag import str2tuple from util import * from api import * class SwitchboardTurn(list): """ A specialized list object used to encode switchboard utterances. The elements of the list are the words in the utterance; and two attributes, C{speaker} and C{id}, are provided to retrieve the spearker identifier and utterance id. Note that utterance ids are only unique within a given discourse. """ def __init__(self, words, speaker, id): list.__init__(self, words) self.speaker = speaker self.id = int(id) def __repr__(self): if len(self) == 0: text = '' elif isinstance(self[0], tuple): text = ' '.join('%s/%s' % w for w in self) else: text = ' '.join(self) return '<%s.%s: %r>' % (self.speaker, self.id, text) class SwitchboardCorpusReader(CorpusReader): _FILES = ['tagged'] # Use the "tagged" file even for non-tagged data methods, since # it's tokenized. def __init__(self, root): CorpusReader.__init__(self, root, self._FILES) def words(self): return StreamBackedCorpusView(self.abspath('tagged'), self._words_block_reader) def tagged_words(self): return StreamBackedCorpusView(self.abspath('tagged'), self._tagged_words_block_reader) def turns(self): return StreamBackedCorpusView(self.abspath('tagged'), self._turns_block_reader) def tagged_turns(self): return StreamBackedCorpusView(self.abspath('tagged'), self._tagged_turns_block_reader) def discourses(self): return StreamBackedCorpusView(self.abspath('tagged'), self._discourses_block_reader) def tagged_discourses(self): return StreamBackedCorpusView(self.abspath('tagged'), self._tagged_discourses_block_reader) def _discourses_block_reader(self, stream): # returns at most 1 discourse. (The other methods depend on this.) return [[self._parse_utterance(u, include_tag=False) for b in read_blankline_block(stream) for u in b.split('\n') if u.strip()]] def _tagged_discourses_block_reader(self, stream): # returns at most 1 discourse. (The other methods depend on this.) return [[self._parse_utterance(u, include_tag=True) for b in read_blankline_block(stream) for u in b.split('\n') if u.strip()]] def _turns_block_reader(self, stream): return self._discourses_block_reader(stream)[0] def _tagged_turns_block_reader(self, stream): return self._tagged_discourses_block_reader(stream)[0] def _words_block_reader(self, stream): return sum(self._discourses_block_reader(stream)[0], []) def _tagged_words_block_reader(self, stream): return sum(self._tagged_discourses_block_reader(stream)[0], []) _UTTERANCE_RE = re.compile('(\w+)\.(\d+)\:\s*(.*)') _SEP = '/' def _parse_utterance(self, utterance, include_tag): m = self._UTTERANCE_RE.match(utterance) if m is None: raise ValueError('Bad utterance %r' % utterance) speaker, id, text = m.groups() words = [str2tuple(s, self._SEP) for s in text.split()] if not include_tag: words = [w for (w,t) in words] return SwitchboardTurn(words, speaker, id) nltk-2.0~b9/nltk/corpus/reader/string_category.py0000644000175000017500000000414211327451600022060 0ustar bhavanibhavani# Natural Language Toolkit: String Category Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT """ Read tuples from a corpus consisting of categorized strings. For example, from the question classification corpus: NUM:dist How far is it from Denver to Aspen ? LOC:city What county is Modesto , California in ? HUM:desc Who was Galileo ? DESC:def What is an atom ? NUM:date When did Hawaii become a state ? """ # based on PPAttachmentCorpusReader import os from util import * from api import * # [xx] Should the order of the tuple be reversed -- in most other places # in nltk, we use the form (data, tag) -- e.g., tagged words and # labeled texts for classifiers. class StringCategoryCorpusReader(CorpusReader): def __init__(self, root, fileids, delimiter=' ', encoding=None): """ @param root: The root directory for this corpus. @param fileids: A list or regexp specifying the fileids in this corpus. @param delimiter: Field delimiter """ CorpusReader.__init__(self, root, fileids, encoding) self._delimiter = delimiter def tuples(self, fileids=None): if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([StreamBackedCorpusView(fileid, self._read_tuple_block, encoding=enc) for (fileid, enc) in self.abspaths(fileids, True)]) def raw(self, fileids=None): """ @return: the text contents of the given fileids, as a single string. """ if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) def _read_tuple_block(self, stream): line = stream.readline().strip() if line: return [tuple(line.split(self._delimiter, 1))] else: return [] nltk-2.0~b9/nltk/corpus/reader/sinica_treebank.py0000644000175000017500000000455011327451600022001 0ustar bhavanibhavani# Natural Language Toolkit: Sinica Treebank Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT """ Sinica Treebank Corpus Sample http://rocling.iis.sinica.edu.tw/CKIP/engversion/treebank.htm 10,000 parsed sentences, drawn from the Academia Sinica Balanced Corpus of Modern Chinese. Parse tree notation is based on Information-based Case Grammar. Tagset documentation is available at http://www.sinica.edu.tw/SinicaCorpus/modern_e_wordtype.html Language and Knowledge Processing Group, Institute of Information Science, Academia Sinica It is distributed with the Natural Language Toolkit under the terms of the Creative Commons Attribution-NonCommercial-ShareAlike License [http://creativecommons.org/licenses/by-nc-sa/2.5/]. References: Feng-Yi Chen, Pi-Fang Tsai, Keh-Jiann Chen, and Chu-Ren Huang (1999) The Construction of Sinica Treebank. Computational Linguistics and Chinese Language Processing, 4, pp 87-104. Huang Chu-Ren, Keh-Jiann Chen, Feng-Yi Chen, Keh-Jiann Chen, Zhao-Ming Gao, and Kuang-Yu Chen. 2000. Sinica Treebank: Design Criteria, Annotation Guidelines, and On-line Interface. Proceedings of 2nd Chinese Language Processing Workshop, Association for Computational Linguistics. Chen Keh-Jiann and Yu-Ming Hsieh (2004) Chinese Treebanks and Grammar Extraction, Proceedings of IJCNLP-04, pp560-565. """ import os import re import nltk from nltk.internals import deprecated from util import * from api import * IDENTIFIER = re.compile(r'^#\S+\s') APPENDIX = re.compile(r'(?<=\))#.*$') TAGWORD = re.compile(r':([^:()|]+):([^:()|]+)') WORD = re.compile(r':[^:()|]+:([^:()|]+)') class SinicaTreebankCorpusReader(SyntaxCorpusReader): """ Reader for the sinica treebank. """ def _read_block(self, stream): sent = stream.readline() sent = IDENTIFIER.sub('', sent) sent = APPENDIX.sub('', sent) return [sent] def _parse(self, sent): return nltk.tree.sinica_parse(sent) def _tag(self, sent, simplify_tags=None): tagged_sent = [(w,t) for (t,w) in TAGWORD.findall(sent)] if simplify_tags: tagged_sent = [(w, self._tag_mapping_function(t)) for (w,t) in tagged_sent] return tagged_sent def _word(self, sent): return WORD.findall(sent) nltk-2.0~b9/nltk/corpus/reader/senseval.py0000644000175000017500000002011111327451600020467 0ustar bhavanibhavani# Natural Language Toolkit: Senseval 2 Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Trevor Cohn # Steven Bird (modifications) # URL: # For license information, see LICENSE.TXT """ Read from the Senseval 2 Corpus. SENSEVAL [http://www.senseval.org/] Evaluation exercises for Word Sense Disambiguation. Organized by ACL-SIGLEX [http://www.siglex.org/] Prepared by Ted Pedersen , University of Minnesota, http://www.d.umn.edu/~tpederse/data.html Distributed with permission. The NLTK version of the Senseval 2 files uses well-formed XML. Each instance of the ambiguous words "hard", "interest", "line", and "serve" is tagged with a sense identifier, and supplied with context. """ import os import re import xml.sax from xmldocs import XMLCorpusReader from nltk.tokenize import * from nltk.etree import ElementTree from nltk.internals import deprecated from util import * from api import * class SensevalInstance(object): def __init__(self, word, position, context, senses): self.word = word self.senses = tuple(senses) self.position = position self.context = context def __repr__(self): return ('SensevalInstance(word=%r, position=%r, ' 'context=%r, senses=%r)' % (self.word, self.position, self.context, self.senses)) class SensevalCorpusReader(CorpusReader): def instances(self, fileids=None): return concat([SensevalCorpusView(fileid, enc) for (fileid, enc) in self.abspaths(fileids, True)]) def raw(self, fileids=None): """ @return: the text contents of the given fileids, as a single string. """ if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) def _entry(self, tree): elts = [] for lexelt in tree.findall('lexelt'): for inst in lexelt.findall('instance'): sense = inst[0].attrib['senseid'] context = [(w.text, w.attrib['pos']) for w in inst[1]] elts.append( (sense, context) ) return elts #{ Deprecated since 0.8 @deprecated("Use .instances() or .raw() instead.") def read(self, items, format='listed'): if format == 'listed': return self.instances(items) if format == 'raw': return self.raw(items) raise ValueError('bad format %r' % format) @deprecated("Use .instances() instead.") def listed(self, items): return self.instances(items) #} class SensevalCorpusView(StreamBackedCorpusView): def __init__(self, fileid, encoding): StreamBackedCorpusView.__init__(self, fileid, encoding=encoding) self._word_tokenizer = WhitespaceTokenizer() self._lexelt_starts = [0] # list of streampos self._lexelts = [None] # list of lexelt names def read_block(self, stream): # Decide which lexical element we're in. lexelt_num = bisect.bisect_right(self._lexelt_starts, stream.tell())-1 lexelt = self._lexelts[lexelt_num] instance_lines = [] in_instance = False while True: line = stream.readline() if line == '': assert instance_lines == [] return [] # Start of a lexical element? if line.lstrip().startswith(' has no 'item=...' lexelt = m.group(1)[1:-1] if lexelt_num < len(self._lexelts): assert lexelt == self._lexelts[lexelt_num] else: self._lexelts.append(lexelt) self._lexelt_starts.append(stream.tell()) # Start of an instance? if line.lstrip().startswith('' elif cword.tag == 'wf': context.append((cword.text, cword.attrib['pos'])) elif cword.tag == 's': pass # Sentence boundary marker. else: print 'ACK', cword.tag assert False, 'expected CDATA or or ' if cword.tail: context += self._word_tokenizer.tokenize(cword.tail) else: assert False, 'unexpected tag %s' % child.tag return SensevalInstance(lexelt, position, context, senses) def _fixXML(text): """ Fix the various issues with Senseval pseudo-XML. """ # <~> or <^> => ~ or ^ text = re.sub(r'<([~\^])>', r'\1', text) # fix lone & text = re.sub(r'(\s+)\&(\s+)', r'\1&\2', text) # fix """ text = re.sub(r'"""', '\'"\'', text) # fix => text = re.sub(r'(<[^<]*snum=)([^">]+)>', r'\1"\2"/>', text) # fix foreign word tag text = re.sub(r'<\&frasl>\s*]*>', 'FRASL', text) # remove <&I .> text = re.sub(r'<\&I[^>]*>', '', text) # fix <{word}> text = re.sub(r'<{([^}]+)}>', r'\1', text) # remove <@>,

    ,

    text = re.sub(r'<(@|/?p)>', r'', text) # remove <&M .> and <&T .> and <&Ms .> text = re.sub(r'<&\w+ \.>', r'', text) # remove lines text = re.sub(r']*>', r'', text) # remove <[hi]> and <[/p]> etc text = re.sub(r'<\[\/?[^>]+\]*>', r'', text) # take the thing out of the brackets: <…> text = re.sub(r'<(\&\w+;)>', r'\1', text) # and remove the & for those patterns that aren't regular XML text = re.sub(r'&(?!amp|gt|lt|apos|quot)', r'', text) # fix 'abc ' style tags - now abc text = re.sub(r'[ \t]*([^<>\s]+?)[ \t]*', r' \1', text) text = re.sub(r'\s*"\s*', " \"", text) return text nltk-2.0~b9/nltk/corpus/reader/rte.py0000644000175000017500000001106711327451600017453 0ustar bhavanibhavani# Natural Language Toolkit: RTE Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Ewan Klein # URL: # For license information, see LICENSE.TXT """ Corpus reader for the Recognizing Textual Entailment (RTE) Challenge Corpora. The files were taken from the RTE1, RTE2 and RTE3 datasets and the files were regularized. Filenames are of the form rte*_dev.xml and rte*_test.xml. The latter are the gold standard annotated files. Each entailment corpus is a list of 'text'/'hypothesis' pairs. The following example is taken from RTE3:: The sale was made to pay Yukos' US$ 27.5 billion tax bill, Yuganskneftegaz was originally sold for US$ 9.4 billion to a little known company Baikalfinansgroup which was later bought by the Russian state-owned oil company Rosneft . Baikalfinansgroup was sold to Rosneft. In order to provide globally unique IDs for each pair, a new attribute C{challenge} has been added to the root element C{entailment-corpus} of each file, taking values 1, 2 or 3. The GID is formatted 'm-n', where 'm' is the challenge number and 'n' is the pair ID. """ from util import * from api import * from xmldocs import * def norm(value_string): """ Normalize the string value in an RTE pair's C{value} or C{entailment} attribute as an integer (1, 0). @param value_string: the label used to classify a text/hypothesis pair @type value_string: C{str} @rtype: C{int} """ valdict = {"TRUE": 1, "FALSE": 0, "YES": 1, "NO": 0} return valdict[value_string.upper()] class RTEPair: """ Container for RTE text-hypothesis pairs. The entailment relation is signalled by the C{value} attribute in RTE1, and by C{entailment} in RTE2 and RTE3. These both get mapped on to the C{entailment} attribute of this class. """ def __init__(self, pair, challenge=None, id=None, text=None, hyp=None, value=None, task=None, length=None): """ @param challenge: version of the RTE challenge (i.e., RTE1, RTE2 or RTE3) @param id: identifier for the pair @param text: the text component of the pair @param hyp: the hypothesis component of the pair @param value: classification label for the pair @param task: attribute for the particular NLP task that the data was drawn from @param length: attribute for the length of the text of the pair """ self.challenge = challenge self.id = pair.attrib["id"] self.gid = "%s-%s" % (self.challenge, self.id) self.text = pair[0].text self.hyp = pair[1].text if "value" in pair.attrib: self.value = norm(pair.attrib["value"]) elif "entailment" in pair.attrib: self.value = norm(pair.attrib["entailment"]) else: self.value = value if "task" in pair.attrib: self.task = pair.attrib["task"] else: self.task = task if "length" in pair.attrib: self.length = pair.attrib["length"] else: self.length = length def __repr__(self): if self.challenge: return '' % (self.challenge, self.id) else: return '' % self.id class RTECorpusReader(XMLCorpusReader): """ Corpus reader for corpora in RTE challenges. This is just a wrapper around the XMLCorpusReader. See module docstring above for the expected structure of input documents. """ def _read_etree(self, doc): """ Map the XML input into an RTEPair. This uses the C{getiterator()} method from the ElementTree package to find all the C{} elements. @param doc: a parsed XML document @rtype: C{list} of L{RTEPair}s """ try: challenge = doc.attrib['challenge'] except KeyError: challenge = None return [RTEPair(pair, challenge=challenge) for pair in doc.getiterator("pair")] def pairs(self, fileids): """ Build a list of RTEPairs from a RTE corpus. @param fileids: a list of RTE corpus fileids @type: C{list} @rtype: C{list} of L{RTEPair}s """ if isinstance(fileids, basestring): fileids = [fileids] return concat([self._read_etree(self.xml(fileid)) for fileid in fileids]) nltk-2.0~b9/nltk/corpus/reader/propbank.py0000644000175000017500000003654111327451600020501 0ustar bhavanibhavani# Natural Language Toolkit: PropBank Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT import re import codecs from nltk.tree import Tree from nltk.etree import ElementTree from util import * from api import * class PropbankCorpusReader(CorpusReader): """ Corpus reader for the propbank corpus, which augments the Penn Treebank with information about the predicate argument structure of every verb instance. The corpus consists of two parts: the predicate-argument annotations themselves, and a set of X{frameset files} which define the argument labels used by the annotations, on a per-verb basis. Each X{frameset file} contains one or more predicates, such as C{'turn'} or C{'turn_on'}, each of which is divided into coarse-grained word senses called X{rolesets}. For each X{roleset}, the frameset file provides descriptions of the argument roles, along with examples. """ def __init__(self, root, propfile, framefiles='', verbsfile=None, parse_fileid_xform=None, parse_corpus=None, encoding=None): """ @param root: The root directory for this corpus. @param propfile: The name of the file containing the predicate- argument annotations (relative to C{root}). @param framefiles: A list or regexp specifying the frameset fileids for this corpus. @param parse_fileid_xform: A transform that should be applied to the fileids in this corpus. This should be a function of one argument (a fileid) that returns a string (the new fileid). @param parse_corpus: The corpus containing the parse trees corresponding to this corpus. These parse trees are necessary to resolve the tree pointers used by propbank. """ # If framefiles is specified as a regexp, expand it. if isinstance(framefiles, basestring): framefiles = find_corpus_fileids(root, framefiles) framefiles = list(framefiles) # Initialze the corpus reader. CorpusReader.__init__(self, root, [propfile, verbsfile] + framefiles, encoding) # Record our frame fileids & prop file. self._propfile = propfile self._framefiles = framefiles self._verbsfile = verbsfile self._parse_fileid_xform = parse_fileid_xform self._parse_corpus = parse_corpus def raw(self, fileids=None): """ @return: the text contents of the given fileids, as a single string. """ if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) def instances(self): """ @return: a corpus view that acts as a list of L{PropbankInstance} objects, one for each verb in the corpus. """ return StreamBackedCorpusView(self.abspath(self._propfile), self._read_instance_block, encoding=self.encoding(self._propfile)) def lines(self): """ @return: a corpus view that acts as a list of strings, one for each line in the predicate-argument annotation file. """ return StreamBackedCorpusView(self.abspath(self._propfile), read_line_block, encoding=self.encoding(self._propfile)) def roleset(self, roleset_id): """ @return: the xml description for the given roleset. """ lemma = roleset_id.split('.')[0] framefile = 'frames/%s.xml' % lemma if framefile not in self._framefiles: raise ValueError('Frameset file for %s not found' % roleset_id) # n.b.: The encoding for XML fileids is specified by the file # itself; so we ignore self._encoding here. etree = ElementTree.parse(self.abspath(framefile).open()).getroot() for roleset in etree.findall('predicate/roleset'): if roleset.attrib['id'] == roleset_id: return roleset else: raise ValueError('Roleset %s not found in %s' % (roleset_id, framefile)) def verbs(self): """ @return: a corpus view that acts as a list of all verb lemmas in this corpus (from the verbs.txt file). """ return StreamBackedCorpusView(self.abspath(self._verbsfile), read_line_block, encoding=self.encoding(self._verbsfile)) def _read_instance_block(self, stream): block = [] # Read 100 at a time. for i in range(100): line = stream.readline().strip() if line: block.append(PropbankInstance.parse( line, self._parse_fileid_xform, self._parse_corpus)) return block ###################################################################### #{ Propbank Instance & related datatypes ###################################################################### class PropbankInstance(object): def __init__(self, fileid, sentnum, wordnum, tagger, roleset, inflection, predicate, arguments, parse_corpus=None): self.fileid = fileid """The name of the file containing the parse tree for this instance's sentence.""" self.sentnum = sentnum """The sentence number of this sentence within L{fileid}. Indexing starts from zero.""" self.wordnum = wordnum """The word number of this instance's predicate within its containing sentence. Word numbers are indexed starting from zero, and include traces and other empty parse elements.""" self.tagger = tagger """An identifier for the tagger who tagged this instance; or C{'gold'} if this is an adjuticated instance.""" self.roleset = roleset """The name of the roleset used by this instance's predicate. Use L{propbank.roleset() } to look up information about the roleset.""" self.inflection = inflection """A {PropbankInflection} object describing the inflection of this instance's predicate.""" self.predicate = predicate """A L{PropbankTreePointer} indicating the position of this instance's predicate within its containing sentence.""" self.arguments = tuple(arguments) """A list of tuples (argloc, argid), specifying the location and identifier for each of the predicate's argument in the containing sentence. Argument identifiers are strings such as C{'ARG0'} or C{'ARGM-TMP'}. This list does *not* contain the predicate.""" self.parse_corpus = parse_corpus """A corpus reader for the parse trees corresponding to the instances in this propbank corpus.""" def __repr__(self): return ('' % (self.fileid, self.sentnum, self.wordnum)) def __str__(self): s = '%s %s %s %s %s %s' % (self.fileid, self.sentnum, self.wordnum, self.tagger, self.roleset, self.inflection) items = self.arguments + ((self.predicate, 'rel'),) for (argloc, argid) in sorted(items): s += ' %s-%s' % (argloc, argid) return s def _get_tree(self): if self.parse_corpus is None: return None if self.fileid not in self.parse_corpus.fileids(): return None return self.parse_corpus.parsed_sents(self.fileid)[self.sentnum] tree = property(_get_tree, doc=""" The parse tree corresponding to this instance, or C{None} if the corresponding tree is not available.""") @staticmethod def parse(s, parse_fileid_xform=None, parse_corpus=None): pieces = s.split() if len(pieces) < 7: raise ValueError('Badly formatted propbank line: %r' % s) # Divide the line into its basic pieces. (fileid, sentnum, wordnum, tagger, roleset, inflection) = pieces[:6] rel = [p for p in pieces[6:] if p.endswith('-rel')] args = [p for p in pieces[6:] if not p.endswith('-rel')] if len(rel) != 1: raise ValueError('Badly formatted propbank line: %r' % s) # Apply the fileid selector, if any. if parse_fileid_xform is not None: fileid = parse_fileid_xform(fileid) # Convert sentence & word numbers to ints. sentnum = int(sentnum) wordnum = int(wordnum) # Parse the inflection inflection = PropbankInflection.parse(inflection) # Parse the predicate location. predicate = PropbankTreePointer.parse(rel[0][:-4]) # Parse the arguments. arguments = [] for arg in args: argloc, argid = arg.split('-', 1) arguments.append( (PropbankTreePointer.parse(argloc), argid) ) # Put it all together. return PropbankInstance(fileid, sentnum, wordnum, tagger, roleset, inflection, predicate, arguments, parse_corpus) class PropbankPointer(object): """ A pointer used by propbank to identify one or more constituents in a parse tree. C{PropbankPointer} is an abstract base class with three concrete subclasses: - L{PropbankTreePointer} is used to point to single constituents. - L{PropbankSplitTreePointer} is used to point to 'split' constituents, which consist of a sequence of two or more C{PropbankTreePointer}s. - L{PropbankChainTreePointer} is used to point to entire trace chains in a tree. It consists of a sequence of pieces, which can be C{PropbankTreePointer}s or C{PropbankSplitTreePointer}s. """ def __init__(self): if self.__class__ == PropbankPoitner: raise AssertionError('PropbankPointer is an abstract base class') class PropbankChainTreePointer(PropbankPointer): def __init__(self, pieces): self.pieces = pieces """A list of the pieces that make up this chain. Elements may be either L{PropbankSplitTreePointer}s or L{PropbankTreePointer}s.""" def __str__(self): return '*'.join('%s' % p for p in self.pieces) def __repr__(self): return '' % self def select(self, tree): if tree is None: raise ValueError('Parse tree not avaialable') return Tree('*CHAIN*', [p.select(tree) for p in self.pieces]) class PropbankSplitTreePointer(PropbankPointer): def __init__(self, pieces): self.pieces = pieces """A list of the pieces that make up this chain. Elements are all L{PropbankTreePointer}s.""" def __str__(self): return ','.join('%s' % p for p in self.pieces) def __repr__(self): return '' % self def select(self, tree): if tree is None: raise ValueError('Parse tree not avaialable') return Tree('*SPLIT*', [p.select(tree) for p in self.pieces]) class PropbankTreePointer(PropbankPointer): """ wordnum:height*wordnum:height*... wordnum:height, """ def __init__(self, wordnum, height): self.wordnum = wordnum self.height = height @staticmethod def parse(s): # Deal with chains (xx*yy*zz) pieces = s.split('*') if len(pieces) > 1: return PropbankChainTreePointer([PropbankTreePointer.parse(elt) for elt in pieces]) # Deal with split args (xx,yy,zz) pieces = s.split(',') if len(pieces) > 1: return PropbankSplitTreePointer([PropbankTreePointer.parse(elt) for elt in pieces]) # Deal with normal pointers. pieces = s.split(':') if len(pieces) != 2: raise ValueError('bad propbank pointer %r' % s) return PropbankTreePointer(int(pieces[0]), int(pieces[1])) def __str__(self): return '%s:%s' % (self.wordnum, self.height) def __repr__(self): return 'PropbankTreePointer(%d, %d)' % (self.wordnum, self.height) def __cmp__(self, other): while isinstance(other, (PropbankChainTreePointer, PropbankSplitTreePointer)): other = other.pieces[0] if not isinstance(other, PropbankTreePointer): return cmp(id(self), id(other)) return cmp( (self.wordnum, -self.height), (other.wordnum, -other.height) ) def select(self, tree): if tree is None: raise ValueError('Parse tree not avaialable') return tree[self.treepos(tree)] def treepos(self, tree): """ Convert this pointer to a standard 'tree position' pointer, given that it points to the given tree. """ if tree is None: raise ValueError('Parse tree not avaialable') stack = [tree] treepos = [] wordnum = 0 while True: #print treepos #print stack[-1] # tree node: if isinstance(stack[-1], Tree): # Select the next child. if len(treepos) < len(stack): treepos.append(0) else: treepos[-1] += 1 # Update the stack. if treepos[-1] < len(stack[-1]): stack.append(stack[-1][treepos[-1]]) else: # End of node's child list: pop up a level. stack.pop() treepos.pop() # word node: else: if wordnum == self.wordnum: return tuple(treepos[:len(treepos)-self.height-1]) else: wordnum += 1 stack.pop() class PropbankInflection(object): #{ Inflection Form INFINITIVE = 'i' GERUND = 'g' PARTICIPLE = 'p' FINITE = 'v' #{ Inflection Tense FUTURE = 'f' PAST = 'p' PRESENT = 'n' #{ Inflection Aspect PERFECT = 'p' PROGRESSIVE = 'o' PERFECT_AND_PROGRESSIVE = 'b' #{ Inflection Person THIRD_PERSON = '3' #{ Inflection Voice ACTIVE = 'a' PASSIVE = 'p' #{ Inflection NONE = '-' #} def __init__(self, form='-', tense='-', aspect='-', person='-', voice='-'): self.form = form self.tense = tense self.aspect = aspect self.person = person self.voice = voice def __str__(self): return self.form+self.tense+self.aspect+self.person+self.voice def __repr__(self): return '' % self _VALIDATE = re.compile(r'[igpv\-][fpn\-][pob\-][3\-][ap\-]$') @staticmethod def parse(s): if not isinstance(s, basestring): raise TypeError('expected a string') if (len(s) != 5 or not PropbankInflection._VALIDATE.match(s)): raise ValueError('Bad propbank inflection string %r' % s) return PropbankInflection(*s) nltk-2.0~b9/nltk/corpus/reader/ppattach.py0000644000175000017500000000644111327451600020465 0ustar bhavanibhavani# Natural Language Toolkit: PP Attachment Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT """ Read lines from the Prepositional Phrase Attachment Corpus. The PP Attachment Corpus contains several files having the format: sentence_id verb noun1 preposition noun2 attachment For example: 42960 gives authority to administration V 46742 gives inventors of microchip N The PP attachment is to the verb phrase (V) or noun phrase (N), i.e.: (VP gives (NP authority) (PP to administration)) (VP gives (NP inventors (PP of microchip))) The corpus contains the following files: training: training set devset: development test set, used for algorithm development. test: test set, used to report results bitstrings: word classes derived from Mutual Information Clustering for the Wall Street Journal. Ratnaparkhi, Adwait (1994). A Maximum Entropy Model for Prepositional Phrase Attachment. Proceedings of the ARPA Human Language Technology Conference. [http://www.cis.upenn.edu/~adwait/papers/hlt94.ps] The PP Attachment Corpus is distributed with NLTK with the permission of the author. """ import codecs from nltk.internals import deprecated from util import * from api import * class PPAttachment: def __init__(self, sent, verb, noun1, prep, noun2, attachment): self.sent = sent self.verb = verb self.noun1 = noun1 self.prep = prep self.noun2 = noun2 self.attachment = attachment def __repr__(self): return ('PPAttachment(sent=%r, verb=%r, noun1=%r, prep=%r, ' 'noun2=%r, attachment=%r)' % (self.sent, self.verb, self.noun1, self.prep, self.noun2, self.attachment)) class PPAttachmentCorpusReader(CorpusReader): """ sentence_id verb noun1 preposition noun2 attachment """ def attachments(self, fileids): return concat([StreamBackedCorpusView(fileid, self._read_obj_block, encoding=enc) for (fileid, enc) in self.abspaths(fileids, True)]) def tuples(self, fileids): return concat([StreamBackedCorpusView(fileid, self._read_tuple_block, encoding=enc) for (fileid, enc) in self.abspaths(fileids, True)]) def raw(self, fileids=None): if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) def _read_tuple_block(self, stream): line = stream.readline() if line: return [tuple(line.split())] else: return [] def _read_obj_block(self, stream): line = stream.readline() if line: return [PPAttachment(*line.split())] else: return [] #{ Deprecated since 0.8 @deprecated("Use .tuples() or .raw() or .attachments() instead.") def read(self, items, format='tuple'): if format == 'tuple': return self.tuples(items) if format == 'raw': return self.raw(items) raise ValueError('bad format %r' % format) #} nltk-2.0~b9/nltk/corpus/reader/plaintext.py0000644000175000017500000002531011377057401020673 0ustar bhavanibhavani# Natural Language Toolkit: Plaintext Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # Nitin Madnani # URL: # For license information, see LICENSE.TXT """ A reader for corpora that consist of plaintext documents. """ import codecs import nltk.data from nltk.tokenize import * from nltk.internals import deprecated from util import * from api import * class PlaintextCorpusReader(CorpusReader): """ Reader for corpora that consist of plaintext documents. Paragraphs are assumed to be split using blank lines. Sentences and words can be tokenized using the default tokenizers, or by custom tokenizers specificed as parameters to the constructor. This corpus reader can be customized (e.g., to skip preface sections of specific document formats) by creating a subclass and overriding the L{CorpusView} class variable. """ CorpusView = StreamBackedCorpusView """The corpus view class used by this reader. Subclasses of L{PlaintextCorpusReader} may specify alternative corpus view classes (e.g., to skip the preface sections of documents.)""" def __init__(self, root, fileids, word_tokenizer=WordPunctTokenizer(), sent_tokenizer=nltk.data.LazyLoader( 'tokenizers/punkt/english.pickle'), para_block_reader=read_blankline_block, encoding=None): """ Construct a new plaintext corpus reader for a set of documents located at the given root directory. Example usage: >>> root = '/...path to corpus.../' >>> reader = PlaintextCorpusReader(root, '.*\.txt') @param root: The root directory for this corpus. @param fileids: A list or regexp specifying the fileids in this corpus. @param word_tokenizer: Tokenizer for breaking sentences or paragraphs into words. @param sent_tokenizer: Tokenizer for breaking paragraphs into words. @param para_block_reader: The block reader used to divide the corpus into paragraph blocks. """ CorpusReader.__init__(self, root, fileids, encoding) self._word_tokenizer = word_tokenizer self._sent_tokenizer = sent_tokenizer self._para_block_reader = para_block_reader def raw(self, fileids=None, sourced=False): """ @return: the given file(s) as a single string. @rtype: C{str} """ if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f, sourced).read() for f in fileids]) def words(self, fileids=None, sourced=False): """ @return: the given file(s) as a list of words and punctuation symbols. @rtype: C{list} of C{str} """ # Once we require Python 2.5, use source=(fileid if sourced else None) if sourced: return concat([self.CorpusView(path, self._read_word_block, encoding=enc, source=fileid) for (path, enc, fileid) in self.abspaths(fileids, True, True)]) else: return concat([self.CorpusView(path, self._read_word_block, encoding=enc) for (path, enc, fileid) in self.abspaths(fileids, True, True)]) def sents(self, fileids=None, sourced=False): """ @return: the given file(s) as a list of sentences or utterances, each encoded as a list of word strings. @rtype: C{list} of (C{list} of C{str}) """ if self._sent_tokenizer is None: raise ValueError('No sentence tokenizer for this corpus') if sourced: return concat([self.CorpusView(path, self._read_sent_block, encoding=enc, source=fileid) for (path, enc, fileid) in self.abspaths(fileids, True, True)]) else: return concat([self.CorpusView(path, self._read_sent_block, encoding=enc) for (path, enc, fileid) in self.abspaths(fileids, True, True)]) def paras(self, fileids=None, sourced=False): """ @return: the given file(s) as a list of paragraphs, each encoded as a list of sentences, which are in turn encoded as lists of word strings. @rtype: C{list} of (C{list} of (C{list} of C{str})) """ if self._sent_tokenizer is None: raise ValueError('No sentence tokenizer for this corpus') if sourced: return concat([self.CorpusView(path, self._read_para_block, encoding=enc, source=fileid) for (path, enc, fileid) in self.abspaths(fileids, True, True)]) else: return concat([self.CorpusView(path, self._read_para_block, encoding=enc) for (path, enc, fileid) in self.abspaths(fileids, True, True)]) def _read_word_block(self, stream): words = [] for i in range(20): # Read 20 lines at a time. words.extend(self._word_tokenizer.tokenize(stream.readline())) return words def _read_sent_block(self, stream): sents = [] for para in self._para_block_reader(stream): sents.extend([self._word_tokenizer.tokenize(sent) for sent in self._sent_tokenizer.tokenize(para)]) return sents def _read_para_block(self, stream): paras = [] for para in self._para_block_reader(stream): paras.append([self._word_tokenizer.tokenize(sent) for sent in self._sent_tokenizer.tokenize(para)]) return paras #{ Deprecated since 0.8 @deprecated("Use .raw() or .words() instead.") def read(self, items=None, format='tokenized'): if format == 'raw': return self.raw(items) if format == 'tokenized': return self.words(items) raise ValueError('bad format %r' % format) @deprecated("Use .words() instead.") def tokenized(self, items=None): return self.words(items) #} class CategorizedPlaintextCorpusReader(CategorizedCorpusReader, PlaintextCorpusReader): """ A reader for plaintext corpora whose documents are divided into categories based on their file identifiers. """ def __init__(self, *args, **kwargs): """ Initialize the corpus reader. Categorization arguments (C{cat_pattern}, C{cat_map}, and C{cat_file}) are passed to the L{CategorizedCorpusReader constructor }. The remaining arguments are passed to the L{PlaintextCorpusReader constructor }. """ CategorizedCorpusReader.__init__(self, kwargs) PlaintextCorpusReader.__init__(self, *args, **kwargs) def _resolve(self, fileids, categories): if fileids is not None and categories is not None: raise ValueError('Specify fileids or categories, not both') if categories is not None: return self.fileids(categories) else: return fileids def raw(self, fileids=None, categories=None): return PlaintextCorpusReader.raw( self, self._resolve(fileids, categories)) def words(self, fileids=None, categories=None): return PlaintextCorpusReader.words( self, self._resolve(fileids, categories)) def sents(self, fileids=None, categories=None): return PlaintextCorpusReader.sents( self, self._resolve(fileids, categories)) def paras(self, fileids=None, categories=None): return PlaintextCorpusReader.paras( self, self._resolve(fileids, categories)) # is there a better way? class PortugueseCategorizedPlaintextCorpusReader(CategorizedPlaintextCorpusReader): def __init__(self, *args, **kwargs): CategorizedCorpusReader.__init__(self, kwargs) kwargs['sent_tokenizer'] = nltk.data.LazyLoader('tokenizers/punkt/portuguese.pickle') PlaintextCorpusReader.__init__(self, *args, **kwargs) class EuroparlCorpusReader(PlaintextCorpusReader): """ Reader for Europarl corpora that consist of plaintext documents. Documents are divided into chapters instead of paragraphs as for regular plaintext documents. Chapters are separated using blank lines. Everything is inherited from L{PlaintextCorpusReader} except that: - Since the corpus is pre-processed and pre-tokenized, the word tokenizer should just split the line at whitespaces. - For the same reason, the sentence tokenizer should just split the paragraph at line breaks. - There is a new 'chapters()' method that returns chapters instead instead of paragraphs. - The 'paras()' method inherited from PlaintextCorpusReader is made non-functional to remove any confusion between chapters and paragraphs for Europarl. """ def _read_word_block(self, stream): words = [] for i in range(20): # Read 20 lines at a time. words.extend(stream.readline().split()) return words def _read_sent_block(self, stream): sents = [] for para in self._para_block_reader(stream): sents.extend([sent.split() for sent in para.splitlines()]) return sents def _read_para_block(self, stream): paras = [] for para in self._para_block_reader(stream): paras.append([sent.split() for sent in para.splitlines()]) return paras def chapters(self, fileids=None): """ @return: the given file(s) as a list of chapters, each encoded as a list of sentences, which are in turn encoded as lists of word strings. @rtype: C{list} of (C{list} of (C{list} of C{str})) """ return concat([self.CorpusView(fileid, self._read_para_block, encoding=enc) for (fileid, enc) in self.abspaths(fileids, True)]) def paras(self, fileids=None): raise NotImplementedError('The Europarl corpus reader does not support paragraphs. Please use chapters() instead.') nltk-2.0~b9/nltk/corpus/reader/pl196x.py0000644000175000017500000002100411327451600017714 0ustar bhavanibhavani# Natural Language Toolkit: # # Copyright (C) 2001-2010 NLTK Project # Author: Piotr Kasprzyk # URL: # For license information, see LICENSE.TXT import os import re from nltk import tokenize, tree from nltk.internals import deprecated from util import * from api import * from xmldocs import XMLCorpusReader # (?:something) -- non-grouping parentheses! PARA = re.compile(r']*){0,1}>(.*?)

    ') SENT = re.compile(r']*){0,1}>(.*?)
    ') TAGGEDWORD = re.compile(r'<([wc](?: [^>]*){0,1}>)(.*?)') WORD = re.compile(r'<[wc](?: [^>]*){0,1}>(.*?)') TYPE = re.compile(r'type="(.*?)"') ANA = re.compile(r'ana="(.*?)"') TEXTID = re.compile(r'text id="(.*?)"') class TEICorpusView(StreamBackedCorpusView): def __init__(self, corpus_file, tagged, group_by_sent, group_by_para, tag_mapping_function=None, headLen=0, textids=None): self._tagged = tagged self._textids = textids self._group_by_sent = group_by_sent self._group_by_para = group_by_para # WARNING -- skip header StreamBackedCorpusView.__init__(self, corpus_file, startpos=headLen) _pagesize = 4096 def read_block(self, stream): block = stream.readlines(self._pagesize) block = concat(block) while (block.count(' block.count('')) \ or block.count('')+len('') block = block[ :beg]+block[beg+end: ] output = [] for para_str in PARA.findall(block): para = [] for sent_str in SENT.findall(para_str): if not self._tagged: sent = WORD.findall(sent_str) else: sent = map(self._parse_tag, TAGGEDWORD.findall(sent_str)) if self._group_by_sent: para.append(sent) else: para.extend(sent) if self._group_by_para: output.append(para) else: output.extend(para) return output def _parse_tag(self, (tag, word)): if tag.startswith('w'): tag = ANA.search(tag).group(1) else: # tag.startswith('c') tag = TYPE.search(tag).group(1) return (word, tag) class Pl196xCorpusReader(CategorizedCorpusReader, XMLCorpusReader): headLen = 2770 def __init__(self, *args, **kwargs): if 'textid_file' in kwargs: self._textids = kwargs['textid_file'] else: self._textids = None XMLCorpusReader.__init__(self, *args) CategorizedCorpusReader.__init__(self, kwargs) self._init_textids() def _init_textids(self): self._f2t = defaultdict(list) self._t2f = defaultdict(list) if self._textids is not None: for line in self.open(self._textids).readlines(): line = line.strip() file_id, text_ids = line.split(' ', 1) if file_id not in self.fileids(): raise ValueError('In text_id mapping file %s: %s ' 'not found' % (catfile, file_id)) for text_id in text_ids.split(self._delimiter): self._add_textids(file_id, text_id) def _add_textids(self, file_id, text_id): self._f2t[file_id].append(text_id) self._t2f[text_id].append(file_id) def _resolve(self, fileids, categories, textids=None): tmp = None if fileids is not None: if not tmp: tmp = fileids, None else: raise ValueError('Specify only fileids, categories or textids') if categories is not None: if not tmp: tmp = self.fileids(categories), None else: raise ValueError('Specify only fileids, categories or textids') if textids is not None: if not tmp: if isinstance(textids, basestring): textids = [textids] files = sum((self._t2f[t] for t in textids), []) tdict = dict() for f in files: tdict[f] = (set(self._f2t[f]) & set(textids)) tmp = files, tdict else: raise ValueError('Specify only fileids, categories or textids') return None, None def decode_tag(self, tag): # to be implemented return tag def textids(self, fileids=None, categories=None): """ In the pl196x corpus each category is stored in single file and thus both methods provide identical functionality. In order to accommodate finer granularity, a non-standard textids() method was implemented. All the main functions can be supplied with a list of required chunks---giving much more control to the user. """ fileids, _ = self._resolve(fileids, categories) if fileids is None: return sorted(self._t2f) if isinstance(fileids, basestring): fileids = [fileids] return sorted(sum((self._f2t[d] for d in fileids), [])) def words(self, fileids=None, categories=None, textids=None): fileids, textids = self._resolve(fileids, categories, textids) if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] if textids: return concat([TEICorpusView(self.abspath(fileid), False, False, False, headLen=self.headLen, textids=textids[fileid]) for fileid in fileids]) else: return concat([TEICorpusView(self.abspath(fileid), False, False, False, headLen=self.headLen) for fileid in fileids]) def sents(self, fileids=None, categories=None, textids=None): fileids, textids = self._resolve(fileids, categories, textids) if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] if textids: return concat([TEICorpusView(self.abspath(fileid), False, True, False, headLen=self.headLen, textids=textids[fileid]) for fileid in fileids]) else: return concat([TEICorpusView(self.abspath(fileid), False, True, False, headLen=self.headLen) for fileid in fileids]) def paras(self, fileids=None, categories=None, textids=None): fileids, textids = self._resolve(fileids, categories, textids) if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] if textids: return concat([TEICorpusView(self.abspath(fileid), False, True, True, headLen=self.headLen, textids=textids[fileid]) for fileid in fileids]) else: return concat([TEICorpusView(self.abspath(fileid), False, True, True, headLen=self.headLen) for fileid in fileids]) def tagged_words(self, fileids=None, categories=None, textids=None): fileids, textids = self._resolve(fileids, categories, textids) if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] if textids: return concat([TEICorpusView(self.abspath(fileid), True, False, False, headLen=self.headLen, textids=textids[fileid]) for fileid in fileids]) else: return concat([TEICorpusView(self.abspath(fileid), True, False, False, headLen=self.headLen) for fileid in fileids]) def tagged_sents(self, fileids=None, categories=None, textids=None): fileids, textids = self._resolve(fileids, categories, textids) if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] if textids: return concat([TEICorpusView(self.abspath(fileid), True, True, False, headLen=self.headLen, textids=textids[fileid]) for fileid in fileids]) else: return concat([TEICorpusView(self.abspath(fileid), True, True, False, headLen=self.headLen) for fileid in fileids]) def tagged_paras(self, fileids=None, categories=None, textids=None): fileids, textids = self._resolve(fileids, categories, textids) if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] if textids: return concat([TEICorpusView(self.abspath(fileid), True, True, True, headLen=self.headLen, textids=textids[fileid]) for fileid in fileids]) else: return concat([TEICorpusView(self.abspath(fileid), True, True, True, headLen=self.headLen) for fileid in fileids]) def xml(self, fileids=None, categories=None): fileids, _ = self._resolve(fileids, categories) if len(fileids) == 1: return XMLCorpusReader.xml(self, fileids[0]) else: raise TypeError('Expected a single file') def raw(self, fileids=None, categories=None): fileids, _ = self._resolve(fileids, categories) if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) nltk-2.0~b9/nltk/corpus/reader/nps_chat.py0000644000175000017500000000514411327451600020457 0ustar bhavanibhavani# Natural Language Toolkit: NPS Chat Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT import re import textwrap from nltk.compat import * from nltk.util import LazyConcatenation from nltk.internals import ElementWrapper from util import * from api import * from xmldocs import * class NPSChatCorpusReader(XMLCorpusReader): def __init__(self, root, fileids, wrap_etree=False, tag_mapping_function=None): XMLCorpusReader.__init__(self, root, fileids, wrap_etree) self._tag_mapping_function = tag_mapping_function def xml_posts(self, fileids=None): if self._wrap_etree: return concat([XMLCorpusView(fileid, 'Session/Posts/Post', self._wrap_elt) for fileid in self.abspaths(fileids)]) else: return concat([XMLCorpusView(fileid, 'Session/Posts/Post') for fileid in self.abspaths(fileids)]) def posts(self, fileids=None): return concat([XMLCorpusView(fileid, 'Session/Posts/Post/terminals', self._elt_to_words) for fileid in self.abspaths(fileids)]) def tagged_posts(self, fileids=None, simplify_tags=False): def reader(elt, handler): return self._elt_to_tagged_words(elt, handler, simplify_tags) return concat([XMLCorpusView(fileid, 'Session/Posts/Post/terminals', reader) for fileid in self.abspaths(fileids)]) def words(self, fileids=None): return LazyConcatenation(self.posts(fileids)) def tagged_words(self, fileids=None, simplify_tags=False): return LazyConcatenation(self.tagged_posts(fileids, simplify_tags)) def _wrap_elt(self, elt, handler): return ElementWrapper(elt) def _elt_to_words(self, elt, handler): return [self._simplify_username(t.attrib['word']) for t in elt.findall('t')] def _elt_to_tagged_words(self, elt, handler, simplify_tags=False): tagged_post = [(self._simplify_username(t.attrib['word']), t.attrib['pos']) for t in elt.findall('t')] if simplify_tags: tagged_post = [(w, self._tag_mapping_function(t)) for (w,t) in tagged_post] return tagged_post @staticmethod def _simplify_username(word): if 'User' in word: word = 'U' + word.split('User', 1)[1] return word nltk-2.0~b9/nltk/corpus/reader/nombank.py0000644000175000017500000003407011327451600020305 0ustar bhavanibhavani# Natural Language Toolkit: NomBank Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Authors: Paul Bedaride # Edward Loper # URL: # For license information, see LICENSE.TXT import re import codecs from nltk.tree import Tree from nltk.etree import ElementTree from nltk.corpus.reader.util import * from nltk.corpus.reader.api import * class NombankCorpusReader(CorpusReader): """ Corpus reader for the nombank corpus, which augments the Penn Treebank with information about the predicate argument structure of every noun instance. The corpus consists of two parts: the predicate-argument annotations themselves, and a set of X{frameset files} which define the argument labels used by the annotations, on a per-noun basis. Each X{frameset file} contains one or more predicates, such as C{'turn'} or C{'turn_on'}, each of which is divided into coarse-grained word senses called X{rolesets}. For each X{roleset}, the frameset file provides descriptions of the argument roles, along with examples. """ def __init__(self, root, nomfile, framefiles='', nounsfile=None, parse_fileid_xform=None, parse_corpus=None, encoding=None): """ @param root: The root directory for this corpus. @param nomfile: The name of the file containing the predicate- argument annotations (relative to C{root}). @param framefiles: A list or regexp specifying the frameset fileids for this corpus. @param parse_fileid_xform: A transform that should be applied to the fileids in this corpus. This should be a function of one argument (a fileid) that returns a string (the new fileid). @param parse_corpus: The corpus containing the parse trees corresponding to this corpus. These parse trees are necessary to resolve the tree pointers used by nombank. """ # If framefiles is specified as a regexp, expand it. if isinstance(framefiles, basestring): framefiles = find_corpus_fileids(root, framefiles) framefiles = list(framefiles) # Initialze the corpus reader. CorpusReader.__init__(self, root, [nomfile, nounsfile] + framefiles, encoding) # Record our frame fileids & nom file. self._nomfile = nomfile self._framefiles = framefiles self._nounsfile = nounsfile self._parse_fileid_xform = parse_fileid_xform self._parse_corpus = parse_corpus def raw(self, fileids=None): """ @return: the text contents of the given fileids, as a single string. """ if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) def instances(self): """ @return: a corpus view that acts as a list of L{NombankInstance} objects, one for each noun in the corpus. """ return StreamBackedCorpusView(self.abspath(self._nomfile), self._read_instance_block, encoding=self.encoding(self._nomfile)) def lines(self): """ @return: a corpus view that acts as a list of strings, one for each line in the predicate-argument annotation file. """ return StreamBackedCorpusView(self.abspath(self._nomfile), read_line_block, encoding=self.encoding(self._nomfile)) def roleset(self, roleset_id): """ @return: the xml description for the given roleset. """ lemma = roleset_id.split('.')[0] framefile = 'frames/%s.xml' % lemma if framefile not in self._framefiles: raise ValueError('Frameset file for %s not found' % roleset_id) # n.b.: The encoding for XML fileids is specified by the file # itself; so we ignore self._encoding here. etree = ElementTree.parse(self.abspath(framefile).open()).getroot() for roleset in etree.findall('predicate/roleset'): if roleset.attrib['id'] == roleset_id: return roleset else: raise ValueError('Roleset %s not found in %s' % (roleset_id, framefile)) def nouns(self): """ @return: a corpus view that acts as a list of all noun lemmas in this corpus (from the nombank.1.0.words file). """ return StreamBackedCorpusView(self.abspath(self._nounsfile), read_line_block, encoding=self.encoding(self._nounsfile)) def _read_instance_block(self, stream): block = [] # Read 100 at a time. for i in range(100): line = stream.readline().strip() if line: block.append(NombankInstance.parse( line, self._parse_fileid_xform, self._parse_corpus)) return block ###################################################################### #{ Nombank Instance & related datatypes ###################################################################### class NombankInstance(object): def __init__(self, fileid, sentnum, wordnum, baseform, sensenumber, predicate, predid, arguments, parse_corpus=None): self.fileid = fileid """The name of the file containing the parse tree for this instance's sentence.""" self.sentnum = sentnum """The sentence number of this sentence within L{fileid}. Indexing starts from zero.""" self.wordnum = wordnum """The word number of this instance's predicate within its containing sentence. Word numbers are indexed starting from zero, and include traces and other empty parse elements.""" self.baseform = baseform """The baseform of the predicate.""" self.sensenumber = sensenumber """The sense number os the predicate""" self.predicate = predicate """A L{NombankTreePointer} indicating the position of this instance's predicate within its containing sentence.""" self.predid = predid """Identifier of the predicate """ self.arguments = tuple(arguments) """A list of tuples (argloc, argid), specifying the location and identifier for each of the predicate's argument in the containing sentence. Argument identifiers are strings such as C{'ARG0'} or C{'ARGM-TMP'}. This list does *not* contain the predicate.""" self.parse_corpus = parse_corpus """A corpus reader for the parse trees corresponding to the instances in this nombank corpus.""" @property def roleset(self): """The name of the roleset used by this instance's predicate. Use L{nombank.roleset() } to look up information about the roleset.""" return '%s.%s'%(self.baseform, self.sensenumber) def __repr__(self): return ('' % (self.fileid, self.sentnum, self.wordnum)) def __str__(self): s = '%s %s %s %s %s' % (self.fileid, self.sentnum, self.wordnum, self.basename, self.sensenumber) items = self.arguments + ((self.predicate, 'rel'),) for (argloc, argid) in sorted(items): s += ' %s-%s' % (argloc, argid) return s def _get_tree(self): if self.parse_corpus is None: return None if self.fileid not in self.parse_corpus.fileids(): return None return self.parse_corpus.parsed_sents(self.fileid)[self.sentnum] tree = property(_get_tree, doc=""" The parse tree corresponding to this instance, or C{None} if the corresponding tree is not available.""") @staticmethod def parse(s, parse_fileid_xform=None, parse_corpus=None): pieces = s.split() if len(pieces) < 6: raise ValueError('Badly formatted nombank line: %r' % s) # Divide the line into its basic pieces. (fileid, sentnum, wordnum, baseform, sensenumber) = pieces[:5] args = pieces[5:] rel = [args.pop(i) for i,p in enumerate(args) if '-rel' in p] if len(rel) != 1: raise ValueError('Badly formatted nombank line: %r' % s) # Apply the fileid selector, if any. if parse_fileid_xform is not None: fileid = parse_fileid_xform(fileid) # Convert sentence & word numbers to ints. sentnum = int(sentnum) wordnum = int(wordnum) # Parse the predicate location. predloc, predid = rel[0].split('-', 1) predicate = NombankTreePointer.parse(predloc) # Parse the arguments. arguments = [] for arg in args: argloc, argid = arg.split('-', 1) arguments.append( (NombankTreePointer.parse(argloc), argid) ) # Put it all together. return NombankInstance(fileid, sentnum, wordnum, baseform, sensenumber, predicate, predid, arguments, parse_corpus) class NombankPointer(object): """ A pointer used by nombank to identify one or more constituents in a parse tree. C{NombankPointer} is an abstract base class with three concrete subclasses: - L{NombankTreePointer} is used to point to single constituents. - L{NombankSplitTreePointer} is used to point to 'split' constituents, which consist of a sequence of two or more C{NombankTreePointer}s. - L{NombankChainTreePointer} is used to point to entire trace chains in a tree. It consists of a sequence of pieces, which can be C{NombankTreePointer}s or C{NombankSplitTreePointer}s. """ def __init__(self): if self.__class__ == NombankPoitner: raise AssertionError('NombankPointer is an abstract base class') class NombankChainTreePointer(NombankPointer): def __init__(self, pieces): self.pieces = pieces """A list of the pieces that make up this chain. Elements may be either L{NombankSplitTreePointer}s or L{NombankTreePointer}s.""" def __str__(self): return '*'.join('%s' % p for p in self.pieces) def __repr__(self): return '' % self def select(self, tree): if tree is None: raise ValueError('Parse tree not avaialable') return Tree('*CHAIN*', [p.select(tree) for p in self.pieces]) class NombankSplitTreePointer(NombankPointer): def __init__(self, pieces): self.pieces = pieces """A list of the pieces that make up this chain. Elements are all L{NombankTreePointer}s.""" def __str__(self): return ','.join('%s' % p for p in self.pieces) def __repr__(self): return '' % self def select(self, tree): if tree is None: raise ValueError('Parse tree not avaialable') return Tree('*SPLIT*', [p.select(tree) for p in self.pieces]) class NombankTreePointer(NombankPointer): """ wordnum:height*wordnum:height*... wordnum:height, """ def __init__(self, wordnum, height): self.wordnum = wordnum self.height = height @staticmethod def parse(s): # Deal with chains (xx*yy*zz) pieces = s.split('*') if len(pieces) > 1: return NombankChainTreePointer([NombankTreePointer.parse(elt) for elt in pieces]) # Deal with split args (xx,yy,zz) pieces = s.split(',') if len(pieces) > 1: return NombankSplitTreePointer([NombankTreePointer.parse(elt) for elt in pieces]) # Deal with normal pointers. pieces = s.split(':') if len(pieces) != 2: raise ValueError('bad nombank pointer %r' % s) return NombankTreePointer(int(pieces[0]), int(pieces[1])) def __str__(self): return '%s:%s' % (self.wordnum, self.height) def __repr__(self): return 'NombankTreePointer(%d, %d)' % (self.wordnum, self.height) def __cmp__(self, other): while isinstance(other, (NombankChainTreePointer, NombankSplitTreePointer)): other = other.pieces[0] if not isinstance(other, NombankTreePointer): return cmp(id(self), id(other)) return cmp( (self.wordnum, -self.height), (other.wordnum, -other.height) ) def select(self, tree): if tree is None: raise ValueError('Parse tree not avaialable') return tree[self.treepos(tree)] def treepos(self, tree): """ Convert this pointer to a standard 'tree position' pointer, given that it points to the given tree. """ if tree is None: raise ValueError('Parse tree not avaialable') stack = [tree] treepos = [] wordnum = 0 while True: #print treepos #print stack[-1] # tree node: if isinstance(stack[-1], Tree): # Select the next child. if len(treepos) < len(stack): treepos.append(0) else: treepos[-1] += 1 # Update the stack. if treepos[-1] < len(stack[-1]): stack.append(stack[-1][treepos[-1]]) else: # End of node's child list: pop up a level. stack.pop() treepos.pop() # word node: else: if wordnum == self.wordnum: return tuple(treepos[:len(treepos)-self.height-1]) else: wordnum += 1 stack.pop() nltk-2.0~b9/nltk/corpus/reader/ipipan.py0000644000175000017500000003133711327451600020143 0ustar bhavanibhavani# Natural Language Toolkit: IPI PAN Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Konrad Goluchowski # URL: # For license information, see LICENSE.TXT # import functools from nltk.compat import * from nltk.corpus.reader.util import StreamBackedCorpusView, concat from nltk.corpus.reader.api import CorpusReader def _parse_args(fun): def decorator(self, fileids=None, **kwargs): kwargs.pop('tags', None) if not fileids: fileids = self.fileids() return fun(self, fileids, **kwargs) decorator.__name__ = fun.__name__ decorator.__doc__ = fun.__doc__ decorator.__module__ = fun.__module__ return decorator # Assumes Python >=2.5 # def _parse_args(fun): # @functools.wraps(fun) # def decorator(self, fileids=None, **kwargs): # kwargs.pop('tags', None) # if not fileids: # fileids = self.fileids() # return fun(self, fileids, **kwargs) # return decorator class IPIPANCorpusReader(CorpusReader): """Corpus reader designed to work with corpus created by IPI PAN. See http://korpus.pl/en/ for more details about IPI PAN corpus. The corpus includes information about text domain, channel and categories. You can access possible values using ipipan.domains(), ipipan.channels() and ipipan.categories(). You can use also this metadata to filter files, e.g.: ipipan.fileids(channel='prasa') ipipan.fileids(categories='publicystyczny') The reader supports methods: words, sents, paras and their tagged versions. You can get part of speech instead of full tag by giving "simplify_tags=True" parameter, e.g.: ipipan.tagged_sents(simplify_tags=True) Also you can get all tags disambiguated tags specifying parameter "one_tag=False", e.g.: ipipan.tagged_paras(one_tag=False) You can get all tags that were assigned by a morphological analyzer specifying parameter "disamb_only=False", e.g. ipipan.tagged_words(disamb_only=False) The IPIPAN Corpus contains tags indicating if there is a space between two tokens. To add special "no space" markers, you should specify parameter "append_no_space=True", e.g. ipipan.tagged_words(append_no_space=True) As a result in place where there should be no space between two tokens new pair ('', 'no-space') will be inserted (for tagged data) and just '' for methods without tags. The corpus reader can also try to append spaces between words. To enable this option, specify parameter "append_space=True", e.g. ipipan.words(append_space=True) As a result either ' ' or (' ', 'space') will be inserted between tokens. By default, xml entities like " and & are replaced by corresponding characters. You can turn off this feature, specifying parameter "replace_xmlentities=False", e.g. ipipan.words(replace_xmlentities=False) """ def __init__(self, root, fileids): CorpusReader.__init__(self, root, fileids, None, None) def raw(self, fileids=None): if not fileids: fileids = self.fileids() return ''.join([open(fileid, 'r').read() for fileid in self._list_morph_files(fileids)]) def channels(self, fileids=None): if not fileids: fileids = self.fileids() return self._parse_header(fileids, 'channel') def domains(self, fileids=None): if not fileids: fileids = self.fileids() return self._parse_header(fileids, 'domain') def categories(self, fileids=None): if not fileids: fileids = self.fileids() return [self._map_category(cat) for cat in self._parse_header(fileids, 'keyTerm')] def fileids(self, channels=None, domains=None, categories=None): if channels is not None and domains is not None and \ categories is not None: raise ValueError('You can specify only one of channels, domains ' 'and categories parameter at once') if channels is None and domains is None and \ categories is None: return CorpusReader.fileids(self) if isinstance(channels, basestring): channels = [channels] if isinstance(domains, basestring): domains = [domains] if isinstance(categories, basestring): categories = [categories] if channels: return self._list_morph_files_by('channel', channels) elif domains: return self._list_morph_files_by('domain', domains) else: return self._list_morph_files_by('keyTerm', categories, map=self._map_category) @_parse_args def sents(self, fileids=None, **kwargs): return concat([self._view(fileid, mode=IPIPANCorpusView.SENTS_MODE, tags=False, **kwargs) for fileid in self._list_morph_files(fileids)]) @_parse_args def paras(self, fileids=None, **kwargs): return concat([self._view(fileid, mode=IPIPANCorpusView.PARAS_MODE, tags=False, **kwargs) for fileid in self._list_morph_files(fileids)]) @_parse_args def words(self, fileids=None, **kwargs): return concat([self._view(fileid, tags=False, **kwargs) for fileid in self._list_morph_files(fileids)]) @_parse_args def tagged_sents(self, fileids=None, **kwargs): return concat([self._view(fileid, mode=IPIPANCorpusView.SENTS_MODE, **kwargs) for fileid in self._list_morph_files(fileids)]) @_parse_args def tagged_paras(self, fileids=None, **kwargs): return concat([self._view(fileid, mode=IPIPANCorpusView.PARAS_MODE, **kwargs) for fileid in self._list_morph_files(fileids)]) @_parse_args def tagged_words(self, fileids=None, **kwargs): return concat([self._view(fileid, **kwargs) for fileid in self._list_morph_files(fileids)]) def _list_morph_files(self, fileids): return [f for f in self.abspaths(fileids)] def _list_header_files(self, fileids): return [f.replace('morph.xml', 'header.xml') for f in self._list_morph_files(fileids)] def _parse_header(self, fileids, tag): values = set() for f in self._list_header_files(fileids): values_list = self._get_tag(f, tag) for v in values_list: values.add(v) return list(values) def _list_morph_files_by(self, tag, values, map=None): fileids = self.fileids() ret_fileids = set() for f in fileids: fp = self.abspath(f).replace('morph.xml', 'header.xml') values_list = self._get_tag(fp, tag) for value in values_list: if map is not None: value = map(value) if value in values: ret_fileids.add(f) return list(ret_fileids) def _get_tag(self, f, tag): tags = [] header = open(f, 'r').read() tag_end = 0 while True: tag_pos = header.find('<'+tag, tag_end) if tag_pos < 0: return tags tag_end = header.find('', tag_pos) tags.append(header[tag_pos+len(tag)+2:tag_end]) def _map_category(self, cat): pos = cat.find('>') if pos == -1: return cat else: return cat[pos+1:] def _view(self, filename, **kwargs): tags = kwargs.pop('tags', True) mode = kwargs.pop('mode', 0) simplify_tags = kwargs.pop('simplify_tags', False) one_tag = kwargs.pop('one_tag', True) disamb_only = kwargs.pop('disamb_only', True) append_no_space = kwargs.pop('append_no_space', False) append_space = kwargs.pop('append_space', False) replace_xmlentities = kwargs.pop('replace_xmlentities', True) if len(kwargs) > 0: raise ValueError('Unexpected arguments: %s' % kwargs.keys()) if not one_tag and not disamb_only: raise ValueError('You cannot specify both one_tag=False and ' 'disamb_only=False') if not tags and (simplify_tags or not one_tag or not disamb_only): raise ValueError('You cannot specify simplify_tags, one_tag or ' 'disamb_only with functions other than tagged_*') return IPIPANCorpusView(filename, tags=tags, mode=mode, simplify_tags=simplify_tags, one_tag=one_tag, disamb_only=disamb_only, append_no_space=append_no_space, append_space=append_space, replace_xmlentities=replace_xmlentities ) class IPIPANCorpusView(StreamBackedCorpusView): WORDS_MODE = 0 SENTS_MODE = 1 PARAS_MODE = 2 def __init__(self, filename, startpos=0, **kwargs): StreamBackedCorpusView.__init__(self, filename, None, startpos, None) self.in_sentence = False self.position = 0 self.show_tags = kwargs.pop('tags', True) self.disamb_only = kwargs.pop('disamb_only', True) self.mode = kwargs.pop('mode', IPIPANCorpusView.WORDS_MODE) self.simplify_tags = kwargs.pop('simplify_tags', False) self.one_tag = kwargs.pop('one_tag', True) self.append_no_space = kwargs.pop('append_no_space', False) self.append_space = kwargs.pop('append_space', False) self.replace_xmlentities = kwargs.pop('replace_xmlentities', True) def read_block(self, stream): sentence = [] sentences = [] space = False no_space = False tags = set() lines = self._read_data(stream) while True: # we may have only part of last line if len(lines) <= 1: self._seek(stream) lines = self._read_data(stream) if lines == ['']: assert not sentences return [] line = lines.pop() self.position += len(line) + 1 if line.startswith(''): if self.append_space: no_space = True if self.append_no_space: if self.show_tags: sentence.append(('', 'no-space')) else: sentence.append('') elif line.startswith(' # Edward Loper # URL: # For license information, see LICENSE.TXT """ Indian Language POS-Tagged Corpus Collected by A Kumaran, Microsoft Research, India Distributed with permission Contents: - Bangla: IIT Kharagpur - Hindi: Microsoft Research India - Marathi: IIT Bombay - Telugu: IIIT Hyderabad """ import codecs from nltk.internals import deprecated from nltk.tag.util import str2tuple from util import * from api import * class IndianCorpusReader(CorpusReader): """ List of words, one per line. Blank lines are ignored. """ def words(self, fileids=None): return concat([IndianCorpusView(fileid, enc, False, False) for (fileid, enc) in self.abspaths(fileids, True)]) def tagged_words(self, fileids=None, simplify_tags=False): if simplify_tags: tag_mapping_function = self._tag_mapping_function else: tag_mapping_function = None return concat([IndianCorpusView(fileid, enc, True, False, tag_mapping_function) for (fileid, enc) in self.abspaths(fileids, True)]) def sents(self, fileids=None): return concat([IndianCorpusView(fileid, enc, False, True) for (fileid, enc) in self.abspaths(fileids, True)]) def tagged_sents(self, fileids=None, simplify_tags=False): if simplify_tags: tag_mapping_function = self._tag_mapping_function else: tag_mapping_function = None return concat([IndianCorpusView(fileid, enc, True, True, tag_mapping_function) for (fileid, enc) in self.abspaths(fileids, True)]) def raw(self, fileids=None): if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) #{ Deprecated since 0.8 @deprecated("Use .raw() or .words() or .tagged_words() instead.") def read(self, items, format='tagged'): if format == 'raw': return self.raw(items) if format == 'tokenized': return self.words(items) if format == 'tagged': return self.tagged_words(items) raise ValueError('bad format %r' % format) @deprecated("Use .words() instead.") def tokenized(self, items): return self.words(items) @deprecated("Use .tagged_words() instead.") def tagged(self, items): return self.tagged_words(items) #} class IndianCorpusView(StreamBackedCorpusView): def __init__(self, corpus_file, encoding, tagged, group_by_sent, tag_mapping_function=None): self._tagged = tagged self._group_by_sent = group_by_sent self._tag_mapping_function = tag_mapping_function StreamBackedCorpusView.__init__(self, corpus_file, encoding=encoding) def read_block(self, stream): line = stream.readline() if line.startswith('<'): return [] sent = [str2tuple(word, sep='_') for word in line.split()] if self._tag_mapping_function: sent = [(w, self._tag_mapping_function(t)) for (w,t) in sent] if not self._tagged: sent = [w for (w,t) in sent] if self._group_by_sent: return [sent] else: return sent nltk-2.0~b9/nltk/corpus/reader/ieer.py0000644000175000017500000001053111327451600017600 0ustar bhavanibhavani# Natural Language Toolkit: IEER Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT """ Corpus reader for the Information Extraction and Entity Recognition Corpus. NIST 1999 Information Extraction: Entity Recognition Evaluation http://www.itl.nist.gov/iad/894.01/tests/ie-er/er_99/er_99.htm This corpus contains the NEWSWIRE development test data for the NIST 1999 IE-ER Evaluation. The files were taken from the subdirectory: /ie_er_99/english/devtest/newswire/*.ref.nwt and filenames were shortened. The corpus contains the following files: APW_19980314, APW_19980424, APW_19980429, NYT_19980315, NYT_19980403, and NYT_19980407. """ import codecs import nltk from nltk.internals import deprecated from api import * from util import * #: A dictionary whose keys are the names of documents in this corpus; #: and whose values are descriptions of those documents' contents. titles = { 'APW_19980314': 'Associated Press Weekly, 14 March 1998', 'APW_19980424': 'Associated Press Weekly, 24 April 1998', 'APW_19980429': 'Associated Press Weekly, 29 April 1998', 'NYT_19980315': 'New York Times, 15 March 1998', 'NYT_19980403': 'New York Times, 3 April 1998', 'NYT_19980407': 'New York Times, 7 April 1998', } #: A list of all documents in this corpus. documents = sorted(titles) class IEERDocument: def __init__(self, text, docno=None, doctype=None, date_time=None, headline=''): self.text = text self.docno = docno self.doctype = doctype self.date_time = date_time self.headline = headline def __repr__(self): if self.headline: headline = ' '.join(self.headline.leaves()) else: headline = ' '.join([w for w in self.text.leaves() if w[:1] != '<'][:12])+'...' if self.docno is not None: return '' % (self.docno, headline) else: return '' % headline class IEERCorpusReader(CorpusReader): """ """ def raw(self, fileids=None): if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) def docs(self, fileids=None): return concat([StreamBackedCorpusView(fileid, self._read_block, encoding=enc) for (fileid, enc) in self.abspaths(fileids, True)]) def parsed_docs(self, fileids=None): return concat([StreamBackedCorpusView(fileid, self._read_parsed_block, encoding=enc) for (fileid, enc) in self.abspaths(fileids, True)]) def _read_parsed_block(self,stream): # TODO: figure out while empty documents are being returned return [self._parse(doc) for doc in self._read_block(stream) if self._parse(doc).docno is not None] def _parse(self, doc): val = nltk.chunk.ieerstr2tree(doc, top_node="DOCUMENT") if isinstance(val, dict): return IEERDocument(**val) else: return IEERDocument(val) def _read_block(self, stream): out = [] # Skip any preamble. while True: line = stream.readline() if not line: break if line.strip() == '': break out.append(line) # Read the document while True: line = stream.readline() if not line: break out.append(line) if line.strip() == '': break # Return the document return ['\n'.join(out)] #{ Deprecated since 0.8 @deprecated("Use .parsed_docs() or .raw() or .docs() instead.") def read(self, items, format='parsed'): if format == 'parsed': return self.parsed_docs(items) if format == 'raw': return self.raw(items) if format == 'docs': return self.docs(items) raise ValueError('bad format %r' % format) @deprecated("Use .parsed_docs() instead.") def parsed(self, items): return self.parsed_docs(items) #} nltk-2.0~b9/nltk/corpus/reader/dependency.py0000644000175000017500000000671411327451600021002 0ustar bhavanibhavani# Natural Language Toolkit: Dependency Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Kepa Sarasola # Iker Manterola # # URL: # For license information, see LICENSE.TXT from nltk.parse import DependencyGraph from nltk.tokenize import * from util import * from api import * class DependencyCorpusReader(SyntaxCorpusReader): def __init__(self, root, fileids, encoding=None, word_tokenizer=TabTokenizer(), sent_tokenizer=RegexpTokenizer('\n', gaps=True), para_block_reader=read_blankline_block): CorpusReader.__init__(self, root, fileids, encoding) ######################################################### def raw(self, fileids=None): """ @return: the given file(s) as a single string. @rtype: C{str} """ return concat([open(fileid).read() for fileid in self.abspaths(fileids)]) def words(self, fileids=None): return concat([DependencyCorpusView(fileid, False, False, False) for fileid in self.abspaths(fileids)]) def tagged_words(self, fileids=None): return concat([DependencyCorpusView(fileid, True, False, False) for fileid in self.abspaths(fileids)]) def sents(self, fileids=None): return concat([DependencyCorpusView(fileid, False, True, False) for fileid in self.abspaths(fileids)]) def tagged_sents(self, fileids=None): return concat([DependencyCorpusView(fileid, True, True, False) for fileid in self.abspaths(fileids)]) def parsed_sents(self, fileids=None): sents=concat([DependencyCorpusView(fileid, False, True, True) for fileid in self.abspaths(fileids)]) return [DependencyGraph(sent) for sent in sents] class DependencyCorpusView(StreamBackedCorpusView): _DOCSTART = '-DOCSTART- -DOCSTART- O\n' #dokumentu hasiera definitzen da def __init__(self, corpus_file, tagged, group_by_sent, dependencies, chunk_types=None): self._tagged = tagged self._dependencies = dependencies self._group_by_sent = group_by_sent self._chunk_types = chunk_types StreamBackedCorpusView.__init__(self, corpus_file) def read_block(self, stream): # Read the next sentence. sent = read_blankline_block(stream)[0].strip() # Strip off the docstart marker, if present. if sent.startswith(self._DOCSTART): sent = sent[len(self._DOCSTART):].lstrip() # extract word and tag from any of the formats if not self._dependencies: lines = [line.split('\t') for line in sent.split('\n')] if len(lines[0]) == 3 or len(lines[0]) == 4: sent = [(line[0], line[1]) for line in lines] elif len(lines[0]) == 10: sent = [(line[1], line[4]) for line in lines] else: raise ValueError('Unexpected number of fields in dependency tree file') # discard tags if they weren't requested if not self._tagged: sent = [word for (word, tag) in sent] # Return the result. if self._group_by_sent: return [sent] else: return list(sent) nltk-2.0~b9/nltk/corpus/reader/conll.py0000644000175000017500000005171111327451600017770 0ustar bhavanibhavani# Natural Language Toolkit: CONLL Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT """ Read CoNLL-style chunk fileids. """ import os import codecs import textwrap from nltk.internals import deprecated from nltk.tree import Tree from nltk.util import LazyMap, LazyConcatenation from util import * from api import * class ConllCorpusReader(CorpusReader): """ A corpus reader for CoNLL-style files. These files consist of a series of sentences, separated by blank lines. Each sentence is encoded using a table (or I{grid}) of values, where each line corresponds to a single word, and each column corresponds to an annotation type. The set of columns used by CoNLL-style files can vary from corpus to corpus; the C{ConllCorpusReader} constructor therefore takes an argument, C{columntypes}, which is used to specify the columns that are used by a given corpus. @todo: Add support for reading from corpora where different parallel files contain different columns. @todo: Possibly add caching of the grid corpus view? This would allow the same grid view to be used by different data access methods (eg words() and parsed_sents() could both share the same grid corpus view object). @todo: Better support for -DOCSTART-. Currently, we just ignore it, but it could be used to define methods that retrieve a document at a time (eg parsed_documents()). """ #///////////////////////////////////////////////////////////////// # Column Types #///////////////////////////////////////////////////////////////// WORDS = 'words' #: column type for words POS = 'pos' #: column type for part-of-speech tags TREE = 'tree' #: column type for parse trees CHUNK = 'chunk' #: column type for chunk structures NE = 'ne' #: column type for named entities SRL = 'srl' #: column type for semantic role labels IGNORE = 'ignore' #: column type for column that should be ignored #: A list of all column types supported by the conll corpus reader. COLUMN_TYPES = (WORDS, POS, TREE, CHUNK, NE, SRL, IGNORE) #///////////////////////////////////////////////////////////////// # Constructor #///////////////////////////////////////////////////////////////// def __init__(self, root, fileids, columntypes, chunk_types=None, top_node='S', pos_in_tree=False, srl_includes_roleset=True, encoding=None, tree_class=Tree): for columntype in columntypes: if columntype not in self.COLUMN_TYPES: raise ValueError('Bad column type %r' % columntype) if isinstance(chunk_types, basestring): chunk_types = [chunk_types] self._chunk_types = chunk_types self._colmap = dict((c,i) for (i,c) in enumerate(columntypes)) self._pos_in_tree = pos_in_tree self._top_node = top_node # for chunks self._srl_includes_roleset = srl_includes_roleset self._tree_class = tree_class CorpusReader.__init__(self, root, fileids, encoding) #///////////////////////////////////////////////////////////////// # Data Access Methods #///////////////////////////////////////////////////////////////// def raw(self, fileids=None): if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) def words(self, fileids=None): self._require(self.WORDS) return LazyConcatenation(LazyMap(self._get_words, self._grids(fileids))) def sents(self, fileids=None): self._require(self.WORDS) return LazyMap(self._get_words, self._grids(fileids)) def tagged_words(self, fileids=None): self._require(self.WORDS, self.POS) return LazyConcatenation(LazyMap(self._get_tagged_words, self._grids(fileids))) def tagged_sents(self, fileids=None): self._require(self.WORDS, self.POS) return LazyMap(self._get_tagged_words, self._grids(fileids)) def chunked_words(self, fileids=None, chunk_types=None): self._require(self.WORDS, self.POS, self.CHUNK) if chunk_types is None: chunk_types = self._chunk_types def get_chunked_words(grid): # capture chunk_types as local var return self._get_chunked_words(grid, chunk_types) return LazyConcatenation(LazyMap(get_chunked_words, self._grids(fileids))) def chunked_sents(self, fileids=None, chunk_types=None): self._require(self.WORDS, self.POS, self.CHUNK) if chunk_types is None: chunk_types = self._chunk_types def get_chunked_words(grid): # capture chunk_types as local var return self._get_chunked_words(grid, chunk_types) return LazyMap(get_chunked_words, self._grids(fileids)) def parsed_sents(self, fileids=None, pos_in_tree=None): self._require(self.WORDS, self.POS, self.TREE) if pos_in_tree is None: pos_in_tree = self._pos_in_tree def get_parsed_sent(grid): # capture pos_in_tree as local var return self._get_parsed_sent(grid, pos_in_tree) return LazyMap(get_parsed_sent, self._grids(fileids)) def srl_spans(self, fileids=None): self._require(self.SRL) return LazyMap(self._get_srl_spans, self._grids(fileids)) def srl_instances(self, fileids=None, pos_in_tree=None, flatten=True): self._require(self.WORDS, self.POS, self.TREE, self.SRL) if pos_in_tree is None: pos_in_tree = self._pos_in_tree def get_srl_instances(grid): # capture pos_in_tree as local var return self._get_srl_instances(grid, pos_in_tree) result = LazyMap(get_srl_instances, self._grids(fileids)) if flatten: result = LazyConcatenation(result) return result def iob_words(self, fileids=None): """ @return: a list of word/tag/IOB tuples @rtype: C{list} of C{tuple} @param fileids: the list of fileids that make up this corpus @type fileids: C{None} or C{str} or C{list} """ self._require(self.WORDS, self.POS, self.CHUNK) return LazyConcatenation(LazyMap(self._get_iob_words, self._grids(fileids))) def iob_sents(self, fileids=None): """ @return: a list of lists of word/tag/IOB tuples @rtype: C{list} of C{list} @param fileids: the list of fileids that make up this corpus @type fileids: C{None} or C{str} or C{list} """ self._require(self.WORDS, self.POS, self.CHUNK) return LazyMap(self._get_iob_words, self._grids(fileids)) #///////////////////////////////////////////////////////////////// # Grid Reading #///////////////////////////////////////////////////////////////// def _grids(self, fileids=None): # n.b.: we could cache the object returned here (keyed on # fileids), which would let us reuse the same corpus view for # different things (eg srl and parse trees). return concat([StreamBackedCorpusView(fileid, self._read_grid_block, encoding=enc) for (fileid, enc) in self.abspaths(fileids, True)]) def _read_grid_block(self, stream): grids = [] for block in read_blankline_block(stream): block = block.strip() if not block: continue grid = [line.split() for line in block.split('\n')] # If there's a docstart row, then discard. ([xx] eventually it # would be good to actually use it) if grid[0][self._colmap.get('words', 0)] == '-DOCSTART-': del grid[0] # Check that the grid is consistent. for row in grid: if len(row) != len(grid[0]): raise ValueError('Inconsistent number of columns:\n%s' % block) grids.append(grid) return grids #///////////////////////////////////////////////////////////////// # Transforms #///////////////////////////////////////////////////////////////// # given a grid, transform it into some representation (e.g., # a list of words or a parse tree). def _get_words(self, grid): return self._get_column(grid, self._colmap['words']) def _get_tagged_words(self, grid): return zip(self._get_column(grid, self._colmap['words']), self._get_column(grid, self._colmap['pos'])) def _get_iob_words(self, grid): return zip(self._get_column(grid, self._colmap['words']), self._get_column(grid, self._colmap['pos']), self._get_column(grid, self._colmap['chunk'])) def _get_chunked_words(self, grid, chunk_types): # n.b.: this method is very similar to conllstr2tree. words = self._get_column(grid, self._colmap['words']) pos_tags = self._get_column(grid, self._colmap['pos']) chunk_tags = self._get_column(grid, self._colmap['chunk']) stack = [Tree(self._top_node, [])] for (word, pos_tag, chunk_tag) in zip(words, pos_tags, chunk_tags): if chunk_tag == 'O': state, chunk_type = 'O', '' else: (state, chunk_type) = chunk_tag.split('-') # If it's a chunk we don't care about, treat it as O. if chunk_types is not None and chunk_type not in chunk_types: state = 'O' # Treat a mismatching I like a B. if state == 'I' and chunk_type != stack[-1].node: state = 'B' # For B or I: close any open chunks if state in 'BO' and len(stack) == 2: stack.pop() # For B: start a new chunk. if state == 'B': new_chunk = Tree(chunk_type, []) stack[-1].append(new_chunk) stack.append(new_chunk) # Add the word token. stack[-1].append((word, pos_tag)) return stack[0] def _get_parsed_sent(self, grid, pos_in_tree): words = self._get_column(grid, self._colmap['words']) pos_tags = self._get_column(grid, self._colmap['pos']) parse_tags = self._get_column(grid, self._colmap['tree']) treestr = '' for (word, pos_tag, parse_tag) in zip(words, pos_tags, parse_tags): if word == '(': word = '-LRB-' if word == ')': word = '-RRB-' if pos_tag == '(': pos_tag = '-LRB-' if pos_tag == ')': pos_tag = '-RRB-' (left, right) = parse_tag.split('*') right = right.count(')')*')' # only keep ')'. treestr += '%s (%s %s) %s' % (left, pos_tag, word, right) try: tree = self._tree_class.parse(treestr) except (ValueError, IndexError): tree = self._tree_class.parse('(%s %s)' % (self._top_node, treestr)) if not pos_in_tree: for subtree in tree.subtrees(): for i, child in enumerate(subtree): if (isinstance(child, nltk.Tree) and len(child)==1 and isinstance(child[0], basestring)): subtree[i] = (child[0], child.node) return tree def _get_srl_spans(self, grid): """ list of list of (start, end), tag) tuples """ if self._srl_includes_roleset: predicates = self._get_column(grid, self._colmap['srl']+1) start_col = self._colmap['srl']+2 else: predicates = self._get_column(grid, self._colmap['srl']) start_col = self._colmap['srl']+1 # Count how many predicates there are. This tells us how many # columns to expect for SRL data. num_preds = len([p for p in predicates if p != '-']) spanlists = [] for i in range(num_preds): col = self._get_column(grid, start_col+i) spanlist = [] stack = [] for wordnum, srl_tag in enumerate(col): (left, right) = srl_tag.split('*') for tag in left.split('('): if tag: stack.append((tag, wordnum)) for i in range(right.count(')')): (tag, start) = stack.pop() spanlist.append( ((start, wordnum+1), tag) ) spanlists.append(spanlist) return spanlists def _get_srl_instances(self, grid, pos_in_tree): tree = self._get_parsed_sent(grid, pos_in_tree) spanlists = self._get_srl_spans(grid) if self._srl_includes_roleset: predicates = self._get_column(grid, self._colmap['srl']+1) rolesets = self._get_column(grid, self._colmap['srl']) else: predicates = self._get_column(grid, self._colmap['srl']) rolesets = [None] * len(predicates) instances = ConllSRLInstanceList(tree) for wordnum, predicate in enumerate(predicates): if predicate == '-': continue # Decide which spanlist to use. Don't assume that they're # sorted in the same order as the predicates (even though # they usually are). for spanlist in spanlists: for (start, end), tag in spanlist: if wordnum in range(start,end) and tag in ('V', 'C-V'): break else: continue break else: raise ValueError('No srl column found for %r' % predicate) instances.append(ConllSRLInstance(tree, wordnum, predicate, rolesets[wordnum], spanlist)) return instances #///////////////////////////////////////////////////////////////// # Helper Methods #///////////////////////////////////////////////////////////////// def _require(self, *columntypes): for columntype in columntypes: if columntype not in self._colmap: raise ValueError('This corpus does not contain a %s ' 'column.' % columntype) @staticmethod def _get_column(grid, column_index): return [grid[i][column_index] for i in range(len(grid))] #///////////////////////////////////////////////////////////////// #{ Deprecated since 0.8 #///////////////////////////////////////////////////////////////// @deprecated("Use .raw() or .words() or .tagged_words() or " ".chunked_sents() instead.") def read(self, items, format='chunked', chunk_types=None): if format == 'chunked': return self.chunked_sents(items, chunk_types) if format == 'raw': return self.raw(items) if format == 'tokenized': return self.words(items) if format == 'tagged': return self.tagged_words(items) raise ValueError('bad format %r' % format) @deprecated("Use .chunked_sents() instead.") def chunked(self, items, chunk_types=None): return self.chunked_sents(items, chunk_types) @deprecated("Use .words() instead.") def tokenized(self, items): return self.words(items) @deprecated("Use .tagged_words() instead.") def tagged(self, items): return self.tagged_words(items) #} class ConllSRLInstance(object): """ An SRL instance from a CoNLL corpus, which identifies and providing labels for the arguments of a single verb. """ # [xx] add inst.core_arguments, inst.argm_arguments? def __init__(self, tree, verb_head, verb_stem, roleset, tagged_spans): self.verb = [] """A list of the word indices of the words that compose the verb whose arguments are identified by this instance. This will contain multiple word indices when multi-word verbs are used (e.g. 'turn on').""" self.verb_head = verb_head """The word index of the head word of the verb whose arguments are identified by this instance. E.g., for a sentence that uses the verb 'turn on,' C{verb_head} will be the word index of the word 'turn'.""" self.verb_stem = verb_stem self.roleset = roleset self.arguments = [] """A list of C{(argspan, argid)} tuples, specifying the location and type for each of the arguments identified by this instance. C{argspan} is a tuple C{start, end}, indicating that the argument consists of the C{words[start:end]}.""" self.tagged_spans = tagged_spans """A list of C{(span, id)} tuples, specifying the location and type for each of the arguments, as well as the verb pieces, that make up this instance.""" self.tree = tree """The parse tree for the sentence containing this instance.""" self.words = tree.leaves() """A list of the words in the sentence containing this instance.""" # Fill in the self.verb and self.arguments values. for (start, end), tag in tagged_spans: if tag in ('V', 'C-V'): self.verb += range(start, end) else: self.arguments.append( ((start, end), tag) ) def __repr__(self): plural = len(self.arguments)!=1 and 's' or '' return '' % ( (self.verb_stem, len(self.arguments), plural)) def pprint(self): verbstr = ' '.join(self.words[i][0] for i in self.verb) hdr = 'SRL for %r (stem=%r):\n' % (verbstr, self.verb_stem) s = '' for i, word in enumerate(self.words): if isinstance(word, tuple): word = word[0] for (start, end), argid in self.arguments: if i == start: s += '[%s ' % argid if i == end: s += '] ' if i in self.verb: word = '<<%s>>' % word s += word + ' ' return hdr + textwrap.fill(s.replace(' ]', ']'), initial_indent=' ', subsequent_indent=' ') class ConllSRLInstanceList(list): """ Set of instances for a single sentence """ def __init__(self, tree, instances=()): self.tree = tree list.__init__(self, instances) def __str__(self): return self.pprint() def pprint(self, include_tree=False): # Sanity check: trees should be the same for inst in self: if inst.tree != self.tree: raise ValueError('Tree mismatch!') # If desired, add trees: if include_tree: words = self.tree.leaves() pos = [None] * len(words) synt = ['*'] * len(words) self._tree2conll(self.tree, 0, words, pos, synt) s = '' for i in range(len(words)): # optional tree columns if include_tree: s += '%-20s ' % words[i] s += '%-8s ' % pos[i] s += '%15s*%-8s ' % tuple(synt[i].split('*')) # verb head column for inst in self: if i == inst.verb_head: s += '%-20s ' % inst.verb_stem break else: s += '%-20s ' % '-' # Remaining columns: self for inst in self: argstr = '*' for (start, end), argid in inst.tagged_spans: if i==start: argstr = '(%s%s' % (argid, argstr) if i==(end-1): argstr += ')' s += '%-12s ' % argstr s += '\n' return s def _tree2conll(self, tree, wordnum, words, pos, synt): assert isinstance(tree, Tree) if len(tree) == 1 and isinstance(tree[0], basestring): pos[wordnum] = tree.node assert words[wordnum] == tree[0] return wordnum+1 elif len(tree) == 1 and isinstance(tree[0], tuple): assert len(tree[0]) == 2 pos[wordnum], pos[wordnum] = tree[0] return wordnum+1 else: synt[wordnum] = '(%s%s' % (tree.node, synt[wordnum]) for child in tree: wordnum = self._tree2conll(child, wordnum, words, pos, synt) synt[wordnum-1] += ')' return wordnum class ConllChunkCorpusReader(ConllCorpusReader): """ A ConllCorpusReader whose data file contains three columns: words, pos, and chunk. """ def __init__(self, root, fileids, chunk_types, encoding=None): ConllCorpusReader.__init__( self, root, fileids, ('words', 'pos', 'chunk'), chunk_types=chunk_types, encoding=encoding) nltk-2.0~b9/nltk/corpus/reader/cmudict.py0000644000175000017500000000774011327451600020314 0ustar bhavanibhavani# Natural Language Toolkit: Genesis Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # URL: # For license information, see LICENSE.TXT """ The Carnegie Mellon Pronouncing Dictionary [cmudict.0.6] ftp://ftp.cs.cmu.edu/project/speech/dict/ Copyright 1998 Carnegie Mellon University File Format: Each line consists of an uppercased word, a counter (for alternative pronunciations), and a transcription. Vowels are marked for stress (1=primary, 2=secondary, 0=no stress). E.g.: NATURAL 1 N AE1 CH ER0 AH0 L The dictionary contains 127069 entries. Of these, 119400 words are assigned a unique pronunciation, 6830 words have two pronunciations, and 839 words have three or more pronunciations. Many of these are fast-speech variants. Phonemes: There are 39 phonemes, as shown below: Phoneme Example Translation Phoneme Example Translation ------- ------- ----------- ------- ------- ----------- AA odd AA D AE at AE T AH hut HH AH T AO ought AO T AW cow K AW AY hide HH AY D B be B IY CH cheese CH IY Z D dee D IY DH thee DH IY EH Ed EH D ER hurt HH ER T EY ate EY T F fee F IY G green G R IY N HH he HH IY IH it IH T IY eat IY T JH gee JH IY K key K IY L lee L IY M me M IY N knee N IY NG ping P IH NG OW oat OW T OY toy T OY P pee P IY R read R IY D S sea S IY SH she SH IY T tea T IY TH theta TH EY T AH UH hood HH UH D UW two T UW V vee V IY W we W IY Y yield Y IY L D Z zee Z IY ZH seizure S IY ZH ER """ import codecs from nltk.internals import deprecated from nltk.util import Index from util import * from api import * class CMUDictCorpusReader(CorpusReader): def entries(self): """ @return: the cmudict lexicon as a list of entries containing (word, transcriptions) tuples. """ return concat([StreamBackedCorpusView(fileid, read_cmudict_block, encoding=enc) for fileid, enc in self.abspaths(None, True)]) def raw(self): """ @return: the cmudict lexicon as a raw string. """ if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) def words(self): """ @return: a list of all words defined in the cmudict lexicon. """ return [word.lower() for (word, _) in self.entries()] def dict(self): """ @return: the cmudict lexicon as a dictionary, whose keys are lowercase words and whose values are lists of pronunciations. """ return dict(Index(self.entries())) #{ Deprecated since 0.8 @deprecated("Use .entries() or .transcriptions() instead.") def read(self, items='cmudict', format='listed'): if format == 'listed': return self.entries(items) if format == 'dictionary': return self.dict(items) raise ValueError('bad format %r' % format) @deprecated("Use .dict() instead.") def dictionary(self, items='cmudict'): return self.dict(items) @deprecated("Use .entries() instead.") def listed(self, items='cmudict'): return self.entries(items) #} def read_cmudict_block(stream): entries = [] while len(entries) < 100: # Read 100 at a time. line = stream.readline() if line == '': return entries # end of file. pieces = line.split() entries.append( (pieces[0].lower(), pieces[2:]) ) return entries nltk-2.0~b9/nltk/corpus/reader/chunked.py0000644000175000017500000002020411327451600020273 0ustar bhavanibhavani# Natural Language Toolkit: Chunked Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT """ A reader for corpora that contain chunked (and optionally tagged) documents. """ import os.path, codecs import nltk from nltk.corpus.reader.bracket_parse import BracketParseCorpusReader from nltk.tree import Tree from nltk.tokenize import * from util import * from api import * class ChunkedCorpusReader(CorpusReader): """ Reader for chunked (and optionally tagged) corpora. Paragraphs are split using a block reader. They are then tokenized into sentences using a sentence tokenizer. Finally, these sentences are parsed into chunk trees using a string-to-chunktree conversion function. Each of these steps can be performed using a default function or a custom function. By default, paragraphs are split on blank lines; sentences are listed one per line; and sentences are parsed into chunk trees using L{nltk.chunk.tagstr2tree}. """ def __init__(self, root, fileids, extension='', str2chunktree=nltk.chunk.tagstr2tree, sent_tokenizer=RegexpTokenizer('\n', gaps=True), para_block_reader=read_blankline_block, encoding=None): """ @param root: The root directory for this corpus. @param fileids: A list or regexp specifying the fileids in this corpus. """ CorpusReader.__init__(self, root, fileids, encoding) self._cv_args = (str2chunktree, sent_tokenizer, para_block_reader) """Arguments for corpus views generated by this corpus: a tuple (str2chunktree, sent_tokenizer, para_block_tokenizer)""" def raw(self, fileids=None): """ @return: the given file(s) as a single string. @rtype: C{str} """ if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) def words(self, fileids=None): """ @return: the given file(s) as a list of words and punctuation symbols. @rtype: C{list} of C{str} """ return concat([ChunkedCorpusView(f, enc, 0, 0, 0, 0, *self._cv_args) for (f, enc) in self.abspaths(fileids, True)]) def sents(self, fileids=None): """ @return: the given file(s) as a list of sentences or utterances, each encoded as a list of word strings. @rtype: C{list} of (C{list} of C{str}) """ return concat([ChunkedCorpusView(f, enc, 0, 1, 0, 0, *self._cv_args) for (f, enc) in self.abspaths(fileids, True)]) def paras(self, fileids=None): """ @return: the given file(s) as a list of paragraphs, each encoded as a list of sentences, which are in turn encoded as lists of word strings. @rtype: C{list} of (C{list} of (C{list} of C{str})) """ return concat([ChunkedCorpusView(f, enc, 0, 1, 1, 0, *self._cv_args) for (f, enc) in self.abspaths(fileids, True)]) def tagged_words(self, fileids=None): """ @return: the given file(s) as a list of tagged words and punctuation symbols, encoded as tuples C{(word,tag)}. @rtype: C{list} of C{(str,str)} """ return concat([ChunkedCorpusView(f, enc, 1, 0, 0, 0, *self._cv_args) for (f, enc) in self.abspaths(fileids, True)]) def tagged_sents(self, fileids=None): """ @return: the given file(s) as a list of sentences, each encoded as a list of C{(word,tag)} tuples. @rtype: C{list} of (C{list} of C{(str,str)}) """ return concat([ChunkedCorpusView(f, enc, 1, 1, 0, 0, *self._cv_args) for (f, enc) in self.abspaths(fileids, True)]) def tagged_paras(self, fileids=None): """ @return: the given file(s) as a list of paragraphs, each encoded as a list of sentences, which are in turn encoded as lists of C{(word,tag)} tuples. @rtype: C{list} of (C{list} of (C{list} of C{(str,str)})) """ return concat([ChunkedCorpusView(f, enc, 1, 1, 1, 0, *self._cv_args) for (f, enc) in self.abspaths(fileids, True)]) def chunked_words(self, fileids=None): """ @return: the given file(s) as a list of tagged words and chunks. Words are encoded as C{(word, tag)} tuples (if the corpus has tags) or word strings (if the corpus has no tags). Chunks are encoded as depth-one trees over C{(word,tag)} tuples or word strings. @rtype: C{list} of (C{(str,str)} and L{Tree}) """ return concat([ChunkedCorpusView(f, enc, 1, 0, 0, 1, *self._cv_args) for (f, enc) in self.abspaths(fileids, True)]) def chunked_sents(self, fileids=None): """ @return: the given file(s) as a list of sentences, each encoded as a shallow C{Tree}. The leaves of these trees are encoded as C{(word, tag)} tuples (if the corpus has tags) or word strings (if the corpus has no tags). @rtype: C{list} of L{Tree} """ return concat([ChunkedCorpusView(f, enc, 1, 1, 0, 1, *self._cv_args) for (f, enc) in self.abspaths(fileids, True)]) def chunked_paras(self, fileids=None): """ @return: the given file(s) as a list of paragraphs, each encoded as a list of sentences, which are in turn encoded as a shallow C{Tree}. The leaves of these trees are encoded as C{(word, tag)} tuples (if the corpus has tags) or word strings (if the corpus has no tags). @rtype: C{list} of (C{list} of L{Tree}) """ return concat([ChunkedCorpusView(f, enc, 1, 1, 1, 1, *self._cv_args) for (f, enc) in self.abspaths(fileids, True)]) def _read_block(self, stream): return [nltk.chunk.tagstr2tree(t) for t in read_blankline_block(stream)] class ChunkedCorpusView(StreamBackedCorpusView): def __init__(self, fileid, encoding, tagged, group_by_sent, group_by_para, chunked, str2chunktree, sent_tokenizer, para_block_reader): StreamBackedCorpusView.__init__(self, fileid, encoding=encoding) self._tagged = tagged self._group_by_sent = group_by_sent self._group_by_para = group_by_para self._chunked = chunked self._str2chunktree = str2chunktree self._sent_tokenizer = sent_tokenizer self._para_block_reader = para_block_reader def read_block(self, stream): block = [] for para_str in self._para_block_reader(stream): para = [] for sent_str in self._sent_tokenizer.tokenize(para_str): sent = self._str2chunktree(sent_str) # If requested, throw away the tags. if not self._tagged: sent = self._untag(sent) # If requested, throw away the chunks. if not self._chunked: sent = sent.leaves() # Add the sentence to `para`. if self._group_by_sent: para.append(sent) else: para.extend(sent) # Add the paragraph to `block`. if self._group_by_para: block.append(para) else: block.extend(para) # Return the block return block def _untag(self, tree): for i, child in enumerate(tree): if isinstance(child, Tree): self._untag(child) elif isinstance(child, tuple): tree[i] = child[0] else: raise ValueError('expected child to be Tree or tuple') return tree nltk-2.0~b9/nltk/corpus/reader/bracket_parse.py0000644000175000017500000001157311327451600021470 0ustar bhavanibhavani# Natural Language Toolkit: Penn Treebank Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT import sys from nltk.tree import bracket_parse, Tree from util import * from api import * """ Corpus reader for corpora that consist of parenthesis-delineated parse trees. """ # we use [^\s()]+ instead of \S+? to avoid matching () TAGWORD = re.compile(r'\(([^\s()]+) ([^\s()]+)\)') WORD = re.compile(r'\([^\s()]+ ([^\s()]+)\)') EMPTY_BRACKETS = re.compile(r'\s*\(\s*\(') class BracketParseCorpusReader(SyntaxCorpusReader): """ Reader for corpora that consist of parenthesis-delineated parse trees. """ def __init__(self, root, fileids, comment_char=None, detect_blocks='unindented_paren', encoding=None, tag_mapping_function=None): """ @param root: The root directory for this corpus. @param fileids: A list or regexp specifying the fileids in this corpus. @param comment_char: The character which can appear at the start of a line to indicate that the rest of the line is a comment. @param detect_blocks: The method that is used to find blocks in the corpus; can be 'unindented_paren' (every unindented parenthesis starts a new parse) or 'sexpr' (brackets are matched). """ CorpusReader.__init__(self, root, fileids, encoding) self._comment_char = comment_char self._detect_blocks = detect_blocks self._tag_mapping_function = tag_mapping_function def _read_block(self, stream): if self._detect_blocks == 'sexpr': return read_sexpr_block(stream, comment_char=self._comment_char) elif self._detect_blocks == 'blankline': return read_blankline_block(stream) elif self._detect_blocks == 'unindented_paren': # Tokens start with unindented left parens. toks = read_regexp_block(stream, start_re=r'^\(') # Strip any comments out of the tokens. if self._comment_char: toks = [re.sub('(?m)^%s.*'%re.escape(self._comment_char), '', tok) for tok in toks] return toks else: assert 0, 'bad block type' def _normalize(self, t): # If there's an empty set of brackets surrounding the actual # parse, then strip them off. if EMPTY_BRACKETS.match(t): t = t.strip()[1:-1] # Replace leaves of the form (!), (,), with (! !), (, ,) t = re.sub(r"\((.)\)", r"(\1 \1)", t) # Replace leaves of the form (tag word root) with (tag word) t = re.sub(r"\(([^\s()]+) ([^\s()]+) [^\s()]+\)", r"(\1 \2)", t) return t def _parse(self, t): try: return bracket_parse(self._normalize(t)) except ValueError, e: sys.stderr.write("Bad tree detected; trying to recover...\n") # Try to recover, if we can: if e.args == ('mismatched parens',): for n in range(1, 5): try: v = bracket_parse(self._normalize(t+')'*n)) sys.stderr.write(" Recovered by adding %d close " "paren(s)\n" % n) return v except ValueError: pass # Try something else: sys.stderr.write(" Recovered by returning a flat parse.\n") #sys.stderr.write(' '.join(t.split())+'\n') return Tree('S', self._tag(t)) def _tag(self, t, simplify_tags=False): tagged_sent = [(w,t) for (t,w) in TAGWORD.findall(self._normalize(t))] if simplify_tags: tagged_sent = [(w, self._tag_mapping_function(t)) for (w,t) in tagged_sent] return tagged_sent def _word(self, t): return WORD.findall(self._normalize(t)) class AlpinoCorpusReader(BracketParseCorpusReader): """ Reader for the Alpino Dutch Treebank. """ def __init__(self, root, encoding=None, tag_mapping_function=None): BracketParseCorpusReader.__init__(self, root, 'alpino\.xml', detect_blocks='blankline', encoding=encoding, tag_mapping_function=tag_mapping_function) def _normalize(self, t): if t[:10] != "', r"(\1", t) t = re.sub(r' ', r"(\1 \2)", t) t = re.sub(r" ", r")", t) t = re.sub(r".*", r"", t) t = re.sub(r"", r"", t) return t nltk-2.0~b9/nltk/corpus/reader/bnc.py0000644000175000017500000002232711327451600017424 0ustar bhavanibhavani# Natural Language Toolkit: Plaintext Corpus Reader # # Copyright (C) 2001-2010 NLTK Project # Author: Edward Loper # URL: # For license information, see LICENSE.TXT """ Corpus reader for the XML version of the British National Corpus. """ __docformat__ = 'epytext en' import re import nltk.etree.ElementTree as ET from api import * from util import * from xmldocs import * class BNCCorpusReader(XMLCorpusReader): """ Corpus reader for the XML version of the British National Corpus. For access to the complete XML data structure, use the L{xml()} method. For access to simple word lists and tagged word lists, use L{words()}, L{sents()}, L{tagged_words()}, and L{tagged_sents()}. """ def __init__(self, root, fileids, lazy=True): XMLCorpusReader.__init__(self, root, fileids) self._lazy = lazy def words(self, fileids=None, strip_space=True, stem=False): """ @return: the given file(s) as a list of words and punctuation symbols. @rtype: C{list} of C{str} @param strip_space: If true, then strip trailing spaces from word tokens. Otherwise, leave the spaces on the tokens. @param stem: If true, then use word stems instead of word strings. """ if self._lazy: return concat([BNCWordView(fileid, False, None, strip_space, stem) for fileid in self.abspaths(fileids)]) else: return concat([self._words(fileid, False, None, strip_space, stem) for fileid in self.abspaths(fileids)]) def tagged_words(self, fileids=None, c5=False, strip_space=True, stem=False): """ @return: the given file(s) as a list of tagged words and punctuation symbols, encoded as tuples C{(word,tag)}. @rtype: C{list} of C{(str,str)} @param c5: If true, then the tags used will be the more detailed c5 tags. Otherwise, the simplified tags will be used. @param strip_space: If true, then strip trailing spaces from word tokens. Otherwise, leave the spaces on the tokens. @param stem: If true, then use word stems instead of word strings. """ if c5: tag = 'c5' else: tag = 'pos' if self._lazy: return concat([BNCWordView(fileid, False, tag, strip_space, stem) for fileid in self.abspaths(fileids)]) else: return concat([self._words(fileid, False, tag, strip_space, stem) for fileid in self.abspaths(fileids)]) def sents(self, fileids=None, strip_space=True, stem=False): """ @return: the given file(s) as a list of sentences or utterances, each encoded as a list of word strings. @rtype: C{list} of (C{list} of C{str}) @param strip_space: If true, then strip trailing spaces from word tokens. Otherwise, leave the spaces on the tokens. @param stem: If true, then use word stems instead of word strings. """ if self._lazy: return concat([BNCWordView(fileid, True, None, strip_space, stem) for fileid in self.abspaths(fileids)]) else: return concat([self._words(fileid, True, None, strip_space, stem) for fileid in self.abspaths(fileids)]) def tagged_sents(self, fileids=None, c5=False, strip_space=True, stem=False): """ @return: the given file(s) as a list of sentences, each encoded as a list of C{(word,tag)} tuples. @rtype: C{list} of (C{list} of C{(str,str)}) @param c5: If true, then the tags used will be the more detailed c5 tags. Otherwise, the simplified tags will be used. @param strip_space: If true, then strip trailing spaces from word tokens. Otherwise, leave the spaces on the tokens. @param stem: If true, then use word stems instead of word strings. """ if c5: tag = 'c5' else: tag = 'pos' if self._lazy: return concat([BNCWordView(fileid, True, tag, strip_space, stem) for fileid in self.abspaths(fileids)]) else: return concat([self._words(fileid, True, tag, strip_space, stem) for fileid in self.abspaths(fileids)]) def _words(self, fileid, bracket_sent, tag, strip_space, stem): """ Helper used to implement the view methods -- returns a list of words or a list of sentences, optionally tagged. @param fileid: The name of the underlying file. @param bracket_sent: If true, include sentence bracketing. @param tag: The name of the tagset to use, or None for no tags. @param strip_space: If true, strip spaces from word tokens. @param stem: If true, then substitute stems for words. """ result = [] xmldoc = ElementTree.parse(fileid).getroot() for xmlsent in xmldoc.findall('.//s'): sent = [] for xmlword in _all_xmlwords_in(xmlsent): word = xmlword.text if not word: word = "" # fixes issue 337? if strip_space or stem: word = word.strip() if stem: word = xmlword.get('hw', word) if tag == 'c5': word = (word, xmlword.get('c5')) elif tag == 'pos': word = (word, xmlword.get('pos', xmlword.get('c5'))) sent.append(word) if bracket_sent: result.append(BNCSentence(xmlsent.attrib['n'], sent)) else: result.extend(sent) assert None not in result return result def _all_xmlwords_in(elt, result=None): if result is None: result = [] for child in elt: if child.tag in ('c', 'w'): result.append(child) else: _all_xmlwords_in(child, result) return result class BNCSentence(list): """ A list of words, augmented by an attribute C{num} used to record the sentence identifier (the C{n} attribute from the XML). """ def __init__(self, num, items): self.num = num list.__init__(self, items) class BNCWordView(XMLCorpusView): """ A stream backed corpus view specialized for use with the BNC corpus. """ def __init__(self, fileid, sent, tag, strip_space, stem): """ @param fileid: The name of the underlying file. @param sent: If true, include sentence bracketing. @param tag: The name of the tagset to use, or None for no tags. @param strip_space: If true, strip spaces from word tokens. @param stem: If true, then substitute stems for words. """ if sent: tagspec = '.*/s' else: tagspec = '.*/s/(.*/)?(c|w)' self._sent = sent self._tag = tag self._strip_space = strip_space self._stem = stem XMLCorpusView.__init__(self, fileid, tagspec) # Read in a tasty header. self._open() self.read_block(self._stream, '.*/teiHeader$', self.handle_header) self.close() # Reset tag context. self._tag_context = {0: ()} title = None #: Title of the document. author = None #: Author of the document. editor = None #: Editor resps = None #: Statement of responsibility def handle_header(self, elt, context): # Set up some metadata! titles = elt.findall('titleStmt/title') if titles: self.title = '\n'.join( [title.text.strip() for title in titles]) authors = elt.findall('titleStmt/author') if authors: self.author = '\n'.join( [author.text.strip() for author in authors]) editors = elt.findall('titleStmt/editor') if editors: self.editor = '\n'.join( [editor.text.strip() for editor in editors]) resps = elt.findall('titleStmt/respStmt') if resps: self.resps = '\n\n'.join([ '\n'.join([resp_elt.text.strip() for resp_elt in resp]) for resp in resps]) def handle_elt(self, elt, context): if self._sent: return self.handle_sent(elt) else: return self.handle_word(elt) def handle_word(self, elt): word = elt.text if not word: word = "" # fixes issue 337? if self._strip_space or self._stem: word = word.strip() if self._stem: word = elt.get('hw', word) if self._tag == 'c5': word = (word, elt.get('c5')) elif self._tag == 'pos': word = (word, elt.get('pos', elt.get('c5'))) return word def handle_sent(self, elt): sent = [] for child in elt: if child.tag == 'mw': sent += [self.handle_word(w) for w in child] elif child.tag in ('w','c'): sent.append(self.handle_word(child)) else: raise ValueError('Unexpected element %s' % child.tag) return BNCSentence(elt.attrib['n'], sent) nltk-2.0~b9/nltk/corpus/reader/api.py0000644000175000017500000004364111377057401017443 0ustar bhavanibhavani# Natural Language Toolkit: API for Corpus Readers # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT """ API for corpus readers. """ import os import re from nltk.compat import defaultdict from nltk.internals import deprecated from nltk.data import PathPointer, FileSystemPathPointer, ZipFilePathPointer from nltk.sourcedstring import SourcedStringStream from util import * class CorpusReader(object): """ A base class for X{corpus reader} classes, each of which can be used to read a specific corpus format. Each individual corpus reader instance is used to read a specific corpus, consisting of one or more files under a common root directory. Each file is identified by its C{file identifier}, which is the relative path to the file from the root directory. A separate subclass is be defined for each corpus format. These subclasses define one or more methods that provide 'views' on the corpus contents, such as C{words()} (for a list of words) and C{parsed_sents()} (for a list of parsed sentences). Called with no arguments, these methods will return the contents of the entire corpus. For most corpora, these methods define one or more selection arguments, such as C{fileids} or C{categories}, which can be used to select which portion of the corpus should be returned. """ def __init__(self, root, fileids, encoding=None, tag_mapping_function=None): """ @type root: L{PathPointer} or C{str} @param root: A path pointer identifying the root directory for this corpus. If a string is specified, then it will be converted to a L{PathPointer} automatically. @param fileids: A list of the files that make up this corpus. This list can either be specified explicitly, as a list of strings; or implicitly, as a regular expression over file paths. The absolute path for each file will be constructed by joining the reader's root to each file name. @param encoding: The default unicode encoding for the files that make up the corpus. C{encoding}'s value can be any of the following: - B{A string}: C{encoding} is the encoding name for all files. - B{A dictionary}: C{encoding[file_id]} is the encoding name for the file whose identifier is C{file_id}. If C{file_id} is not in C{encoding}, then the file contents will be processed using non-unicode byte strings. - B{A list}: C{encoding} should be a list of C{(regexp, encoding)} tuples. The encoding for a file whose identifier is C{file_id} will be the C{encoding} value for the first tuple whose C{regexp} matches the C{file_id}. If no tuple's C{regexp} matches the C{file_id}, the file contents will be processed using non-unicode byte strings. - C{None}: the file contents of all files will be processed using non-unicode byte strings. @param tag_mapping_function: A function for normalizing or simplifying the POS tags returned by the tagged_words() or tagged_sents() methods. """ # Convert the root to a path pointer, if necessary. if isinstance(root, basestring): m = re.match('(.*\.zip)/?(.*)$|', root) zipfile, zipentry = m.groups() if zipfile: root = ZipFilePathPointer(zipfile, zipentry) else: root = FileSystemPathPointer(root) elif not isinstance(root, PathPointer): raise TypeError('CorpusReader: expected a string or a PathPointer') # If `fileids` is a regexp, then expand it. if isinstance(fileids, basestring): fileids = find_corpus_fileids(root, fileids) self._fileids = fileids """A list of the relative paths for the fileids that make up this corpus.""" self._root = root """The root directory for this corpus.""" # If encoding was specified as a list of regexps, then convert # it to a dictionary. if isinstance(encoding, list): encoding_dict = {} for fileid in self._fileids: for x in encoding: (regexp, enc) = x if re.match(regexp, fileid): encoding_dict[fileid] = enc break encoding = encoding_dict self._encoding = encoding """The default unicode encoding for the fileids that make up this corpus. If C{encoding} is C{None}, then the file contents are processed using byte strings (C{str}).""" self._tag_mapping_function = tag_mapping_function def __repr__(self): if isinstance(self._root, ZipFilePathPointer): path = '%s/%s' % (self._root.zipfile.filename, self._root.entry) else: path = '%s' % self._root.path return '<%s in %r>' % (self.__class__.__name__, path) def readme(self): """ Return the contents of the corpus README file, if it exists. """ return self.open("README").read() def fileids(self): """ Return a list of file identifiers for the fileids that make up this corpus. """ return self._fileids def abspath(self, fileid): """ Return the absolute path for the given file. @type file: C{str} @param file: The file identifier for the file whose path should be returned. @rtype: L{PathPointer} """ return self._root.join(fileid) def abspaths(self, fileids=None, include_encoding=False, include_fileid=False): """ Return a list of the absolute paths for all fileids in this corpus; or for the given list of fileids, if specified. @type fileids: C{None} or C{str} or C{list} @param fileids: Specifies the set of fileids for which paths should be returned. Can be C{None}, for all fileids; a list of file identifiers, for a specified set of fileids; or a single file identifier, for a single file. Note that the return value is always a list of paths, even if C{fileids} is a single file identifier. @param include_encoding: If true, then return a list of C{(path_pointer, encoding)} tuples. @rtype: C{list} of L{PathPointer} """ if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] paths = [self._root.join(f) for f in fileids] if include_encoding and include_fileid: return zip(paths, [self.encoding(f) for f in fileids], fileids) elif include_fileid: return zip(paths, fileid) elif include_encoding: return zip(paths, [self.encoding(f) for f in fileids]) else: return paths def open(self, file, sourced=False): """ Return an open stream that can be used to read the given file. If the file's encoding is not C{None}, then the stream will automatically decode the file's contents into unicode. @param file: The file identifier of the file to read. """ encoding = self.encoding(file) stream = self._root.join(file).open(encoding) if sourced: stream = SourcedStringStream(stream, file) return stream def encoding(self, file): """ Return the unicode encoding for the given corpus file, if known. If the encoding is unknown, or if the given file should be processed using byte strings (C{str}), then return C{None}. """ if isinstance(self._encoding, dict): return self._encoding.get(file) else: return self._encoding def _get_root(self): return self._root root = property(_get_root, doc=""" The directory where this corpus is stored. @type: L{PathPointer}""") #{ Deprecated since 0.9.7 @deprecated("Use corpus.fileids() instead") def files(self): return self.fileids() #} #{ Deprecated since 0.9.1 @deprecated("Use corpus.fileids() instead") def _get_items(self): return self.fileids() items = property(_get_items) #} ###################################################################### #{ Corpora containing categorized items ###################################################################### class CategorizedCorpusReader(object): """ A mixin class used to aid in the implementation of corpus readers for categorized corpora. This class defines the method L{categories()}, which returns a list of the categories for the corpus or for a specified set of fileids; and overrides L{fileids()} to take a C{categories} argument, restricting the set of fileids to be returned. Subclasses are expected to: - Call L{__init__()} to set up the mapping. - Override all view methods to accept a C{categories} parameter, which can be used *instead* of the C{fileids} parameter, to select which fileids should be included in the returned view. """ def __init__(self, kwargs): """ Initialize this mapping based on keyword arguments, as follows: - cat_pattern: A regular expression pattern used to find the category for each file identifier. The pattern will be applied to each file identifier, and the first matching group will be used as the category label for that file. - cat_map: A dictionary, mapping from file identifiers to category labels. - cat_file: The name of a file that contains the mapping from file identifiers to categories. The argument C{cat_delimiter} can be used to specify a delimiter. The corresponding argument will be deleted from C{kwargs}. If more than one argument is specified, an exception will be raised. """ self._f2c = None #: file-to-category mapping self._c2f = None #: category-to-file mapping self._pattern = None #: regexp specifying the mapping self._map = None #: dict specifying the mapping self._file = None #: fileid of file containing the mapping self._delimiter = None #: delimiter for L{self._file} if 'cat_pattern' in kwargs: self._pattern = kwargs['cat_pattern'] del kwargs['cat_pattern'] elif 'cat_map' in kwargs: self._map = kwargs['cat_map'] del kwargs['cat_map'] elif 'cat_file' in kwargs: self._file = kwargs['cat_file'] del kwargs['cat_file'] if 'cat_delimiter' in kwargs: self._delimiter = kwargs['cat_delimiter'] del kwargs['cat_delimiter'] else: raise ValueError('Expected keyword argument cat_pattern or ' 'cat_map or cat_file.') if ('cat_pattern' in kwargs or 'cat_map' in kwargs or 'cat_file' in kwargs): raise ValueError('Specify exactly one of: cat_pattern, ' 'cat_map, cat_file.') def _init(self): self._f2c = defaultdict(set) self._c2f = defaultdict(set) if self._pattern is not None: for file_id in self._fileids: category = re.match(self._pattern, file_id).group(1) self._add(file_id, category) elif self._map is not None: for (file_id, categories) in self._map.items(): for category in categories: self._add(file_id, category) elif self._file is not None: for line in self.open(self._file).readlines(): line = line.strip() file_id, categories = line.split(self._delimiter, 1) if file_id not in self.fileids(): raise ValueError('In category mapping file %s: %s ' 'not found' % (catfile, file_id)) for category in categories.split(self._delimiter): self._add(file_id, category) def _add(self, file_id, category): self._f2c[file_id].add(category) self._c2f[category].add(file_id) def categories(self, fileids=None): """ Return a list of the categories that are defined for this corpus, or for the file(s) if it is given. """ if self._f2c is None: self._init() if fileids is None: return sorted(self._c2f) if isinstance(fileids, basestring): fileids = [fileids] return sorted(set.union(*[self._f2c[d] for d in fileids])) def fileids(self, categories=None): """ Return a list of file identifiers for the files that make up this corpus, or that make up the given category(s) if specified. """ if categories is None: return super(CategorizedCorpusReader, self).fileids() elif isinstance(categories, basestring): if self._f2c is None: self._init() return sorted(self._c2f[categories]) else: if self._f2c is None: self._init() return sorted(set.union(*[self._c2f[c] for c in categories])) ###################################################################### #{ Treebank readers ###################################################################### #[xx] is it worth it to factor this out? class SyntaxCorpusReader(CorpusReader): """ An abstract base class for reading corpora consisting of syntactically parsed text. Subclasses should define: - L{__init__}, which specifies the location of the corpus and a method for detecting the sentence blocks in corpus files. - L{_read_block}, which reads a block from the input stream. - L{_word}, which takes a block and returns a list of list of words. - L{_tag}, which takes a block and returns a list of list of tagged words. - L{_parse}, which takes a block and returns a list of parsed sentences. """ def _parse(self, s): raise AssertionError('Abstract method') def _word(self, s): raise AssertionError('Abstract method') def _tag(self, s): raise AssertionError('Abstract method') def _read_block(self, stream): raise AssertionError('Abstract method') def raw(self, fileids=None): if fileids is None: fileids = self._fileids elif isinstance(fileids, basestring): fileids = [fileids] return concat([self.open(f).read() for f in fileids]) def parsed_sents(self, fileids=None): reader = self._read_parsed_sent_block return concat([StreamBackedCorpusView(fileid, reader, encoding=enc) for fileid, enc in self.abspaths(fileids, True)]) def tagged_sents(self, fileids=None, simplify_tags=False): def reader(stream): return self._read_tagged_sent_block(stream, simplify_tags) return concat([StreamBackedCorpusView(fileid, reader, encoding=enc) for fileid, enc in self.abspaths(fileids, True)]) def sents(self, fileids=None): reader = self._read_sent_block return concat([StreamBackedCorpusView(fileid, reader, encoding=enc) for fileid, enc in self.abspaths(fileids, True)]) def tagged_words(self, fileids=None, simplify_tags=False): def reader(stream): return self._read_tagged_word_block(stream, simplify_tags) return concat([StreamBackedCorpusView(fileid, reader, encoding=enc) for fileid, enc in self.abspaths(fileids, True)]) def words(self, fileids=None): return concat([StreamBackedCorpusView(fileid, self._read_word_block, encoding=enc) for fileid, enc in self.abspaths(fileids, True)]) #------------------------------------------------------------ #{ Block Readers def _read_word_block(self, stream): return sum(self._read_sent_block(stream), []) def _read_tagged_word_block(self, stream, simplify_tags=False): return sum(self._read_tagged_sent_block(stream, simplify_tags), []) def _read_sent_block(self, stream): return filter(None, [self._word(t) for t in self._read_block(stream)]) def _read_tagged_sent_block(self, stream, simplify_tags=False): return filter(None, [self._tag(t, simplify_tags) for t in self._read_block(stream)]) def _read_parsed_sent_block(self, stream): return filter(None, [self._parse(t) for t in self._read_block(stream)]) #} End of Block Readers #------------------------------------------------------------ #{ Deprecated since 0.8 @deprecated("Use .raw() or .sents() or .tagged_sents() or " ".parsed_sents() instead.") def read(self, items=None, format='parsed'): if format == 'parsed': return self.parsed_sents(items) if format == 'raw': return self.raw(items) if format == 'tokenized': return self.sents(items) if format == 'tagged': return self.tagged_sents(items) raise ValueError('bad format %r' % format) @deprecated("Use .parsed_sents() instead.") def parsed(self, items=None): return self.parsed_sents(items) @deprecated("Use .sents() instead.") def tokenized(self, items=None): return self.sents(items) @deprecated("Use .tagged_sents() instead.") def tagged(self, items=None): return self.tagged_sents(items) #} nltk-2.0~b9/nltk/corpus/reader/__init__.py0000644000175000017500000001170411327451600020416 0ustar bhavanibhavani# Natural Language Toolkit: Corpus Readers # # Copyright (C) 2001-2010 NLTK Project # Author: Steven Bird # Edward Loper # URL: # For license information, see LICENSE.TXT """ NLTK corpus readers. The modules in this package provide functions that can be used to read corpus fileids in a variety of formats. These functions can be used to read both the corpus fileids that are distributed in the NLTK corpus package, and corpus fileids that are part of external corpora. Corpus Reader Functions ======================= Each corpus module defines one or more X{corpus reader functions}, which can be used to read documents from that corpus. These functions take an argument, C{item}, which is used to indicate which document should be read from the corpus: - If C{item} is one of the unique identifiers listed in the corpus module's C{items} variable, then the corresponding document will be loaded from the NLTK corpus package. - If C{item} is a fileid, then that file will be read. Additionally, corpus reader functions can be given lists of item names; in which case, they will return a concatenation of the corresponding documents. Corpus reader functions are named based on the type of information they return. Some common examples, and their return types, are: - I{corpus}.words(): list of str - I{corpus}.sents(): list of (list of str) - I{corpus}.paras(): list of (list of (list of str)) - I{corpus}.tagged_words(): list of (str,str) tuple - I{corpus}.tagged_sents(): list of (list of (str,str)) - I{corpus}.tagged_paras(): list of (list of (list of (str,str))) - I{corpus}.chunked_sents(): list of (Tree w/ (str,str) leaves) - I{corpus}.parsed_sents(): list of (Tree with str leaves) - I{corpus}.parsed_paras(): list of (list of (Tree with str leaves)) - I{corpus}.xml(): A single xml ElementTree - I{corpus}.raw(): unprocessed corpus contents For example, to read a list of the words in the Brown Corpus, use C{nltk.corpus.brown.words()}: >>> from nltk.corpus import brown >>> print brown.words() ['The', 'Fulton', 'County', 'Grand', 'Jury', 'said', ...] [Work in Progress: Corpus Metadata =============== Metadata about the NLTK corpora, and their individual documents, is stored using U{Open Language Archives Community (OLAC) } metadata records. These records can be accessed using C{nltk.corpus.I{corpus}.olac()}.] """ from nltk.corpus.reader.plaintext import * from nltk.corpus.reader.util import * from nltk.corpus.reader.api import * from nltk.corpus.reader.tagged import * from nltk.corpus.reader.cmudict import * from nltk.corpus.reader.conll import * from nltk.corpus.reader.chunked import * from nltk.corpus.reader.wordlist import * from nltk.corpus.reader.xmldocs import * from nltk.corpus.reader.ppattach import * from nltk.corpus.reader.senseval import * from nltk.corpus.reader.ieer import * from nltk.corpus.reader.sinica_treebank import * from nltk.corpus.reader.bracket_parse import * from nltk.corpus.reader.indian import * from nltk.corpus.reader.toolbox import * from nltk.corpus.reader.timit import * from nltk.corpus.reader.ycoe import * from nltk.corpus.reader.rte import * from nltk.corpus.reader.string_category import * from nltk.corpus.reader.propbank import * from nltk.corpus.reader.verbnet import * from nltk.corpus.reader.bnc import * from nltk.corpus.reader.nps_chat import * from nltk.corpus.reader.wordnet import * from nltk.corpus.reader.switchboard import * from nltk.corpus.reader.dependency import * from nltk.corpus.reader.nombank import * from nltk.corpus.reader.ipipan import * from nltk.corpus.reader.pl196x import * # Make sure that nltk.corpus.reader.bracket_parse gives the module, not # the function bracket_parse() defined in nltk.tree: import bracket_parse __all__ = [ 'CorpusReader', 'CategorizedCorpusReader', 'PlaintextCorpusReader', 'find_corpus_fileids', 'TaggedCorpusReader', 'CMUDictCorpusReader', 'ConllChunkCorpusReader', 'WordListCorpusReader', 'PPAttachmentCorpusReader', 'SensevalCorpusReader', 'IEERCorpusReader', 'ChunkedCorpusReader', 'SinicaTreebankCorpusReader', 'BracketParseCorpusReader', 'IndianCorpusReader', 'ToolboxCorpusReader', 'TimitCorpusReader', 'YCOECorpusReader', 'MacMorphoCorpusReader', 'SyntaxCorpusReader', 'AlpinoCorpusReader', 'RTECorpusReader', 'StringCategoryCorpusReader','EuroparlCorpusReader', 'CategorizedTaggedCorpusReader', 'CategorizedPlaintextCorpusReader', 'PortugueseCategorizedPlaintextCorpusReader', 'tagged_treebank_para_block_reader', 'PropbankCorpusReader', 'VerbnetCorpusReader', 'BNCCorpusReader', 'ConllCorpusReader', 'XMLCorpusReader', 'NPSChatCorpusReader', 'SwadeshCorpusReader', 'WordNetCorpusReader', 'WordNetICCorpusReader', 'SwitchboardCorpusReader', 'DependencyCorpusReader', 'NombankCorpusReader', 'IPIPANCorpusReader', 'Pl196xCorpusReader', 'TEICorpusView' ] nltk-2.0~b9/nltk-2.0b9.pkg/Contents/Resources/English.lproj/Description.plist0000644000175000017500000000046011423120503026652 0ustar bhavanibhavani IFPkgDescriptionDescription IFPkgDescriptionTitle NLTK nltk-2.0~b9/nltk-2.0b9.pkg/Contents/Resources/en.lproj/Welcome0000644000175000017500000000021611140171431023642 0ustar bhavanibhavaniWelcome to the Mac OS X Installation Program. You will be guided through the steps necessary to install the Natural Language Toolkit (NLTK). nltk-2.0~b9/nltk-2.0b9.pkg/Contents/Resources/en.lproj/ReadMe0000644000175000017500000000466711327452002023425 0ustar bhavanibhavaniNatural Language Toolkit (NLTK) www.nltk.org Authors: Steven Bird Edward Loper Ewan Klein Copyright (C) 2001-2010 NLTK Project For license information, see LICENSE.txt NLTK -- the Natural Language Toolkit -- is a suite of open source Python modules, data sets and tutorials supporting research and development in Natural Language Processing. Documentation: A substantial amount of documentation about how to use NLTK, including a textbook and API documention, is available from the NLTK website: http://www.nltk.org/ - The book covers a wide range of introductory topics in NLP, and shows how to do all the processing tasks using the toolkit. - The toolkit's reference documentation describes every module, interface, class, method, function, and variable in the toolkit. This documentation should be useful to both users and developers. Mailing Lists: There are several mailing lists associated with NLTK: - nltk: Public information and announcements about NLTK (very low volume) http://groups.google.com/group/nltk - nltk-users: Discussions amongst NLTK users http://groups.google.com/group/nltk-users - nltk-dev: Discussions amongst NLTK developers http://groups.google.com/group/nltk-dev - nltk-translation: Discussions about translating the NLTK book http://groups.google.com/group/nltk-translation - nltk-commits: Subversion commit logs for NLTK http://groups.google.com/group/nltk-commits Contributing: If you would like to contribute to NLTK, please see http://www.nltk.org/contribute Donating: Have you found the toolkit helpful? Please support NLTK development by donating to the project via PayPal, using the link on the NLTK homepage. Redistributing: NLTK source code is distributed under the Apache 2.0 License. NLTK documentation is distributed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 United States license. NLTK corpora are provided under the terms given in the README file for each corpus; all are redistributable, and available for non-commercial use. NLTK may be freely redistributed, subject to the provisions of these licenses. Citing: If you publish work that uses NLTK, please cite the NLTK book, as follows: Bird, Steven, Edward Loper and Ewan Klein (2009). Natural Language Processing with Python. O'Reilly Media Inc. nltk-2.0~b9/nltk-2.0b9.pkg/Contents/Resources/en.lproj/License0000644000175000017500000000106111327452002023633 0ustar bhavanibhavaniCopyright (C) 2001-2010 NLTK Project Licensed under the Apache License, Version 2.0 (the 'License'); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. nltk-2.0~b9/javasrc/org/nltk/mallet/TrainCRF.java0000644000175000017500000002125711363770700021512 0ustar bhavanibhavani/* * Command-line interface to mallet's CRF, used by NLTK. * * Options: * --model-file FILE -- zip file containing crf-info.xml. * --train-file FILE -- training data filename: one token per line, * sequences seperated by newlines. * * Copyright (C) 2001-2010 NLTK Project * Author: Edward Loper * URL: * For license information, see LICENSE.TXT */ package org.nltk.mallet; import java.io.*; import java.util.logging.*; import java.util.regex.*; import java.util.*; import java.util.zip.*; import edu.umass.cs.mallet.base.types.*; import edu.umass.cs.mallet.base.fst.*; import edu.umass.cs.mallet.base.pipe.*; import edu.umass.cs.mallet.base.pipe.iterator.*; import edu.umass.cs.mallet.base.util.*; import org.nltk.mallet.*; public class TrainCRF { ////////////////////////////////////////////////////////////////// // Option definitions. ////////////////////////////////////////////////////////////////// private static final CommandOption.File trainFileOption = new CommandOption.File (TrainCRF.class, "train-file", "FILENAME", true, null, "The filename for the training data.", null); private static final CommandOption.File modelFileOption = new CommandOption.File (TrainCRF.class, "model-file", "FILENAME", true, null, "The CRF model file, a zip file containing crf-info.xml."+ "TrainCRF will add crf-model.ser to this file.", null); private static final CommandOption.List commandOptions = new CommandOption.List ("Train a CRF tagger.", new CommandOption[] { trainFileOption, modelFileOption}); ////////////////////////////////////////////////////////////////// // CRF Creation ////////////////////////////////////////////////////////////////// public static CRF4 createCRF(File trainingFile, CRFInfo crfInfo) throws FileNotFoundException { Reader trainingFileReader = new FileReader(trainingFile); // Create a pipe that we can use to convert the training // file to a feature vector sequence. Pipe p = new SimpleTagger.SimpleTaggerSentence2FeatureVectorSequence(); // The training file does contain tags (aka targets) p.setTargetProcessing(true); // Register the default tag with the pipe, by looking it up // in the targetAlphabet before we look up any other tag. p.getTargetAlphabet().lookupIndex(crfInfo.defaultLabel); // Create a new instancelist to hold the training data. InstanceList trainingData = new InstanceList(p); // Read in the training data. trainingData.add(new LineGroupIterator (trainingFileReader, Pattern.compile("^\\s*$"), true)); // Create the CRF model. CRF4 crf = new CRF4(p, null); // Set various config options crf.setGaussianPriorVariance(crfInfo.gaussianVariance); crf.setTransductionType(crfInfo.transductionType); // Set up the model's states. if (crfInfo.stateInfoList != null) { Iterator stateIter = crfInfo.stateInfoList.iterator(); while (stateIter.hasNext()) { CRFInfo.StateInfo state = (CRFInfo.StateInfo) stateIter.next(); crf.addState(state.name, state.initialCost, state.finalCost, state.destinationNames, state.labelNames, state.weightNames); } } else if (crfInfo.stateStructure == CRFInfo.FULLY_CONNECTED_STRUCTURE) crf.addStatesForLabelsConnectedAsIn(trainingData); else if (crfInfo.stateStructure == CRFInfo.HALF_CONNECTED_STRUCTURE) crf.addStatesForHalfLabelsConnectedAsIn(trainingData); else if (crfInfo.stateStructure == CRFInfo.THREE_QUARTERS_CONNECTED_STRUCTURE) crf.addStatesForThreeQuarterLabelsConnectedAsIn(trainingData); else if (crfInfo.stateStructure == CRFInfo.BILABELS_STRUCTURE) crf.addStatesForBiLabelsConnectedAsIn(trainingData); else throw new RuntimeException("Unexpected state structure "+ crfInfo.stateStructure); // Set up the weight groups. if (crfInfo.weightGroupInfoList != null) { Iterator wgIter = crfInfo.weightGroupInfoList.iterator(); while (wgIter.hasNext()) { CRFInfo.WeightGroupInfo wg = (CRFInfo.WeightGroupInfo) wgIter.next(); FeatureSelection fs = FeatureSelection.createFromRegex (crf.getInputAlphabet(), Pattern.compile(wg.featureSelectionRegex)); crf.setFeatureSelection(crf.getWeightsIndex(wg.name), fs); } } // Train the CRF. crf.train (trainingData, null, null, null, crfInfo.maxIterations); return crf; } /** This is (mostly) copied from CRF4.java */ public boolean[][] labelConnectionsIn(Alphabet outputAlphabet, InstanceList trainingSet, String start) { int numLabels = outputAlphabet.size(); boolean[][] connections = new boolean[numLabels][numLabels]; for (int i = 0; i < trainingSet.size(); i++) { Instance instance = trainingSet.getInstance(i); FeatureSequence output = (FeatureSequence) instance.getTarget(); for (int j = 1; j < output.size(); j++) { int sourceIndex = outputAlphabet.lookupIndex (output.get(j-1)); int destIndex = outputAlphabet.lookupIndex (output.get(j)); assert (sourceIndex >= 0 && destIndex >= 0); connections[sourceIndex][destIndex] = true; } } // Handle start state if (start != null) { int startIndex = outputAlphabet.lookupIndex (start); for (int j = 0; j < outputAlphabet.size(); j++) { connections[startIndex][j] = true; } } return connections; } ////////////////////////////////////////////////////////////////// // Command-line interface. ////////////////////////////////////////////////////////////////// public static void main (String[] args) throws Exception { Reader trainingFile = null; // Process arguments int restArgs = commandOptions.processOptions(args); // Check arguments if (restArgs != args.length) { commandOptions.printUsage(true); throw new IllegalArgumentException("Unexpected arg "+ args[restArgs]); } if (trainFileOption.value == null) { commandOptions.printUsage(true); throw new IllegalArgumentException("Expected --train-file FILE"); } if (modelFileOption.value == null) { commandOptions.printUsage(true); throw new IllegalArgumentException("Expected --model-file FILE"); } // Get the CRF structure specification. ZipFile zipFile = new ZipFile(modelFileOption.value); ZipEntry zipEntry = zipFile.getEntry("crf-info.xml"); CRFInfo crfInfo = new CRFInfo(zipFile.getInputStream(zipEntry)); StringBuffer crfInfoBuffer = new StringBuffer(); BufferedReader reader = new BufferedReader( new InputStreamReader(zipFile.getInputStream(zipEntry))); String line; while ((line = reader.readLine()) != null) { crfInfoBuffer.append(line).append('\n'); } reader.close(); // Create the CRF, and train it. CRF4 crf = createCRF(trainFileOption.value, crfInfo); // Create a new zip file for our output. This will overwrite // the file we used for input. ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(modelFileOption.value)); // Copy the CRF info xml to the output zip file. zos.putNextEntry(new ZipEntry("crf-info.xml")); BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(zos)); writer.write(crfInfoBuffer.toString()); writer.flush(); zos.closeEntry(); // Save the CRF classifier model to the output zip file. zos.putNextEntry(new ZipEntry("crf-model.ser")); ObjectOutputStream oos = new ObjectOutputStream(zos); oos.writeObject(crf); oos.flush(); zos.closeEntry(); zos.close(); } } nltk-2.0~b9/javasrc/org/nltk/mallet/RunCRF.java0000644000175000017500000001160411327452001021163 0ustar bhavanibhavani/* * Command-line interface to mallet's CRF, used by NLTK. * * Options: * --model-file FILE -- zip file containing crf-info.xml and * crf-model.ser (serialized mallet CRF model). * --test-file FILE -- test data filename: one token per line, * sequences seperated by newlines. * * Results are written to stdout. * * Copyright (C) 2001-2010 NLTK Project * Author: Edward Loper * URL: * For license information, see LICENSE.TXT */ package org.nltk.mallet; import edu.umass.cs.mallet.base.types.*; import edu.umass.cs.mallet.base.fst.*; import edu.umass.cs.mallet.base.minimize.*; import edu.umass.cs.mallet.base.minimize.tests.*; import edu.umass.cs.mallet.base.pipe.*; import edu.umass.cs.mallet.base.pipe.iterator.*; import edu.umass.cs.mallet.base.pipe.tsf.*; import edu.umass.cs.mallet.base.util.*; import junit.framework.*; import java.util.Iterator; import java.util.Random; import java.util.regex.*; import java.util.logging.*; import java.io.*; import java.util.zip.*; public class RunCRF { private static Logger logger = MalletLogger.getLogger(RunCRF.class.getName()); /** * RunCRF may not be instantiated. */ private RunCRF() {} // Regexp constant. private static Pattern blankline = Pattern.compile("^\\s*$"); ////////////////////////////////////////////////////////////////// // Option definitions. ////////////////////////////////////////////////////////////////// private static final CommandOption.File modelFileOption = new CommandOption.File (SimpleTagger.class, "model-file", "FILENAME", true, null, "The filename for the model.", null); private static final CommandOption.File testFileOption = new CommandOption.File (RunCRF.class, "test-file", "FILENAME", true, null, "The filename for the testing data.", null); private static final CommandOption.List commandOptions = new CommandOption.List ( "Run the CRF4 tagger", new CommandOption[] { modelFileOption, testFileOption }); ////////////////////////////////////////////////////////////////// // Command-line interface. ////////////////////////////////////////////////////////////////// public static void main (String[] args) throws Exception { Reader trainingFile = null, testFile = null; InstanceList trainingData = null, testData = null; Pipe p = null; CRF4 crf = null; int numEvaluations = 0; int iterationsBetweenEvals = 16; int restArgs = commandOptions.processOptions(args); // Check arguments if (restArgs != args.length) { commandOptions.printUsage(true); throw new IllegalArgumentException("Unexpected arg "+ args[restArgs]); } if (testFileOption.value == null) { commandOptions.printUsage(true); throw new IllegalArgumentException("Expected --test-file FILE"); } if (modelFileOption.value == null) { commandOptions.printUsage(true); throw new IllegalArgumentException("Expected --model-file MODEL"); } // Load the classifier model. ZipFile zipFile = new ZipFile(modelFileOption.value); ZipEntry zipEntry = zipFile.getEntry("crf-model.ser"); ObjectInputStream s = new ObjectInputStream(zipFile.getInputStream(zipEntry)); crf = (CRF4) s.readObject(); s.close(); // Look up the pipe used to generate feature vectors. p = crf.getInputPipe(); // The input file does not contain tags (aka targets) p.setTargetProcessing(false); // Open the test file. testFile = new FileReader(testFileOption.value); // Create a new instancelist to hodl the test data. testData = new InstanceList(p); // Read in the test data. testData.add(new LineGroupIterator(testFile, blankline, true)); // Print the results. for (int i=0; i * URL: * For license information, see LICENSE.TXT */ package org.nltk.mallet; import javax.xml.parsers.*; import java.util.*; import java.util.regex.Pattern; import org.w3c.dom.*; import org.xml.sax.*; import java.io.*; import java.util.logging.Logger; import edu.umass.cs.mallet.base.util.MalletLogger; import edu.umass.cs.mallet.base.fst.CRF4; public class CRFInfo { private static Logger logger = MalletLogger.getLogger(CRFInfo.class.getName()); public String uri="?"; /** The variance for the gaussian prior that is used to regularize * weights. */ public double gaussianVariance = 1.0; /** The 'default' label, which is used as history when no label is * actually available (e.g., at the beginning of an instance). */ public String defaultLabel = "O"; /** The maximum number of iterations for training. */ public int maxIterations = 500; /** If stateInfoList is null, then this can be used to pick a * structure for the CRF states. */ public int stateStructure = FULLY_CONNECTED_STRUCTURE; static public int FULLY_CONNECTED_STRUCTURE = 0; static public int HALF_CONNECTED_STRUCTURE = 1; static public int THREE_QUARTERS_CONNECTED_STRUCTURE = 2; static public int BILABELS_STRUCTURE = 3; static public int ORDER_N_STRUCTURE = 4; /** If stateStructure is ORDER_N_STRUCTURE, then these can be used * to customize the structure: (not impl yet) */ public int[] orders = null; public boolean[] defaults = null; public String start = null; public Pattern forbidden = null; public Pattern allowed = null; public boolean fullyConnected = false; /** List of StateInfo objects. */ public List stateInfoList; /** List of all weight groups. */ public List weightGroupInfoList; /** CRF4.transductionType */ public int transductionType = CRF4.VITERBI; /** The Mallet model filename */ public String modelFile = null; /** The feature detector function's name */ public String featureDetectorName = null; /** Does NLTK add a start & end state? */ public boolean addStartState = false; public boolean addEndState = false; public class StateInfo { public String name; public double initialCost=0; public double finalCost=0; public String[] destinationNames; public String[] labelNames; public String[][] weightNames; } public class WeightGroupInfo { public String name; public String featureSelectionRegex; } ////////////////////////////////////////////////////////////////////// // Constructor (reads file) ////////////////////////////////////////////////////////////////////// CRFInfo(String uri) throws ParserConfigurationException, SAXException, java.io.IOException { this.uri = uri; // Load the DOM document builder DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); // Parse the xml parse(db.parse(uri).getDocumentElement()); } CRFInfo(File file) throws ParserConfigurationException, SAXException, java.io.IOException { this.uri = file.getName(); // Load the DOM document builder DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); // Parse the xml parse(db.parse(file).getDocumentElement()); } CRFInfo(InputStream stream) throws ParserConfigurationException, SAXException, java.io.IOException { // Load the DOM document builder DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); // Parse the xml parse(db.parse(stream).getDocumentElement()); } ////////////////////////////////////////////////////////////////////// // Parsing ////////////////////////////////////////////////////////////////////// void parse(Element root) { NodeList nodes = root.getChildNodes(); for (int n=0; n(); NodeList stateNodes = statesNode.getChildNodes(); for (int i=0; is, not both"); } StateInfo readState(Element stateNode) { StateInfo stateInfo = new StateInfo(); // Get the state's name. stateInfo.name = getName(stateNode); //... if (stateNode.hasAttribute("initialCost")) stateInfo.initialCost = getAttribDouble(stateNode, "initialCost"); if (stateNode.hasAttribute("finalCost")) stateInfo.finalCost = getAttribDouble(stateNode, "finalCost"); // Get all the other info. NodeList nodes = stateNode.getChildNodes(); for (int n=0; n(); NodeList weightGroupNodes = weightGroupsNode.getChildNodes(); for (int i=0; i "); out.print(ljust("["+stateInfo.destinationNames[i]+"]", 20)); out.print(" (label='"+stateInfo.labelNames[i]+"'"); out.print(", weightGroups=["); for (int j=0; j