From ef378981504020b8caee073699f2b747a59dad8a Mon Sep 17 00:00:00 2001 From: Logan Ward Date: Tue, 6 Jul 2021 14:51:15 -0400 Subject: [PATCH 1/2] Rename method server->task server --- colmena/method_server/__init__.py | 5 - colmena/models.py | 6 +- colmena/redis/queue.py | 20 +- colmena/redis/tests/test_queue.py | 12 +- colmena/task_server/__init__.py | 5 + .../{method_server => task_server}/base.py | 27 +- .../{method_server => task_server}/parsl.py | 36 +- .../tests/test_parsl.py | 10 +- colmena/thinker/__init__.py | 4 +- demo_apps/multi-agent-thinker.py | 8 +- demo_apps/optimizer-examples/batch.py | 12 +- demo_apps/optimizer-examples/interleaved.py | 66 +-- demo_apps/optimizer-examples/streaming.py | 12 +- .../0_summarize-runs.ipynb | 75 ++- .../1_plot-run-data.ipynb | 348 +++++-------- .../figures/allocation.png | Bin 26551 -> 30177 bytes .../reallocation-example/reallocation.py | 28 +- demo_apps/synthetic-data/synthetic.py | 8 +- docs/_static/implementation.svg | 487 ++++++++++-------- docs/design.rst | 32 +- docs/how-to.rst | 99 ++-- docs/index.rst | 7 +- docs/quickstart.rst | 18 +- docs/source/colmena.thinker.rst | 13 - docs/thinker.rst | 41 +- setup.py | 2 +- 26 files changed, 647 insertions(+), 734 deletions(-) delete mode 100644 colmena/method_server/__init__.py create mode 100644 colmena/task_server/__init__.py rename colmena/{method_server => task_server}/base.py (62%) rename colmena/{method_server => task_server}/parsl.py (90%) rename colmena/{method_server => task_server}/tests/test_parsl.py (89%) diff --git a/colmena/method_server/__init__.py b/colmena/method_server/__init__.py deleted file mode 100644 index 2ea8b18..0000000 --- a/colmena/method_server/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Implementations of the method server""" - -from colmena.method_server.parsl import ParslMethodServer - -__all__ = ['ParslMethodServer'] diff --git a/colmena/models.py b/colmena/models.py index 5f29648..5b6ecf7 100644 --- a/colmena/models.py +++ b/colmena/models.py @@ -83,7 +83,7 @@ class Result(BaseModel): # Performance tracking time_created: float = Field(None, description="Time this value object was created") - time_input_received: float = Field(None, description="Time the inputs was received by the method server") + time_input_received: float = Field(None, description="Time the inputs was received by the task server") time_compute_started: float = Field(None, description="Time workflow process began executing a task") time_result_sent: float = Field(None, description="Time message was sent from the server") time_result_received: float = Field(None, description="Time value was received by client") @@ -130,7 +130,7 @@ def mark_result_received(self): self.time_result_received = datetime.now().timestamp() def mark_input_received(self): - """Mark that a method server has received a value""" + """Mark that a task server has received a value""" self.time_input_received = datetime.now().timestamp() def mark_compute_started(self): @@ -138,7 +138,7 @@ def mark_compute_started(self): self.time_compute_started = datetime.now().timestamp() def mark_result_sent(self): - """Mark when a result is sent from the method server""" + """Mark when a result is sent from the task server""" self.time_result_sent = datetime.now().timestamp() def set_result(self, result: Any, runtime: float = None): diff --git a/colmena/redis/queue.py b/colmena/redis/queue.py index 09c9379..4715ff7 100644 --- a/colmena/redis/queue.py +++ b/colmena/redis/queue.py @@ -29,7 +29,7 @@ def make_queue_pairs(hostname: str, port: int = 6379, name='method', value_server_threshold: Optional[int] = None, value_server_hostname: Optional[str] = None, value_server_port: Optional[int] = None)\ - -> Tuple['ClientQueues', 'MethodServerQueues']: + -> Tuple['ClientQueues', 'TaskServerQueues']: """Make a pair of queues for a server and client Args: @@ -48,17 +48,17 @@ def make_queue_pairs(hostname: str, port: int = 6379, name='method', the redis server for the task queues will be used. value_server_port (int): See `value_server_hostname` Returns: - (ClientQueues, MethodServerQueues): Pair of communicators set to use the correct channels + (ClientQueues, TaskServerQueues): Pair of communicators set to use the correct channels """ return (ClientQueues(hostname, port, name, serialization_method, keep_inputs, topics, value_server_threshold, value_server_hostname, value_server_port), - MethodServerQueues(hostname, port, name, topics=topics, clean_slate=clean_slate)) + TaskServerQueues(hostname, port, name, topics=topics, clean_slate=clean_slate)) class RedisQueue: - """A basic redis queue for communications used by the method server + """A basic redis queue for communications used by the task server A queue is defined by its prefix and a "topic" designation. The full list of available topics is defined when creating the queue, @@ -179,7 +179,7 @@ def put(self, input_data: str, topic: str = 'default'): queue = f'{self.prefix}_{topic}' assert queue in self._all_queues, f'Unrecognized topic: {topic}' - # Send it to the method server + # Send it to the task server try: self.redis_client.rpush(queue, input_data) except redis.exceptions.ConnectionError: @@ -204,7 +204,7 @@ def is_connected(self): class ClientQueues: - """Provides communication of method requests and results with the method server + """Provides communication of method requests and results with the task server This queue wraps communication with the underlying Redis queue and also handles communicating requests using the :class:`Result` messaging format. @@ -310,7 +310,7 @@ def send_inputs(self, *input_args: Any, method: str = None, value_server_threshold=self.value_server_threshold ) - # Push the serialized value to the method server + # Push the serialized value to the task server result.time_serialize_inputs = result.serialize() self.outbound.put(result.json(exclude_unset=True), topic=topic) logger.info(f'Client sent a {method} task with topic {topic}') @@ -345,12 +345,12 @@ def get_result(self, timeout: Optional[int] = None, topic: Optional[str] = None) return result_obj def send_kill_signal(self): - """Send the kill signal to the method server""" + """Send the kill signal to the task server""" self.outbound.put("null") -class MethodServerQueues: - """Communication wrapper for the method server +class TaskServerQueues: + """Communication wrapper for the task server Handles receiving tasks """ diff --git a/colmena/redis/tests/test_queue.py b/colmena/redis/tests/test_queue.py index 1ecd2a7..0841f33 100644 --- a/colmena/redis/tests/test_queue.py +++ b/colmena/redis/tests/test_queue.py @@ -1,5 +1,5 @@ from colmena.models import SerializationMethod -from colmena.redis.queue import RedisQueue, ClientQueues, MethodServerQueues, make_queue_pairs +from colmena.redis.queue import RedisQueue, ClientQueues, TaskServerQueues, make_queue_pairs import pickle as pkl import pytest @@ -61,16 +61,16 @@ def test_flush(queue): def test_client_method_pair(): """Make sure method client/server can talk and back and forth""" client = ClientQueues('localhost') - server = MethodServerQueues('localhost') + server = TaskServerQueues('localhost') # Ensure client and server are talking to the same queue assert client.outbound.prefix == server.inbound.prefix assert client.inbound.prefix == server.outbound.prefix - # Push inputs to method server and make sure it is received + # Push inputs to task server and make sure it is received client.send_inputs(1) topic, task = server.get_task() - task.deserialize() # Method server does not deserialize automatically + task.deserialize() # task server does not deserialize automatically assert topic == 'default' assert task.args == (1,) assert task.time_input_received is not None @@ -94,7 +94,7 @@ def test_methods(): """Test sending a method name""" client, server = make_queue_pairs('localhost') - # Push inputs to method server and make sure it is received + # Push inputs to task server and make sure it is received client.send_inputs(1, method='test') _, task = server.get_task() task.deserialize() @@ -149,7 +149,7 @@ def test_filtering(): """Test filtering tasks by topic""" client, server = make_queue_pairs('localhost', clean_slate=True, topics=['priority']) - # Simulate a result being sent through the method server + # Simulate a result being sent through the task server client.send_inputs("hello", topic="priority") topic, task = server.get_task() task.deserialize() diff --git a/colmena/task_server/__init__.py b/colmena/task_server/__init__.py new file mode 100644 index 0000000..9817076 --- /dev/null +++ b/colmena/task_server/__init__.py @@ -0,0 +1,5 @@ +"""Implementations of the task server""" + +from colmena.task_server.parsl import ParslTaskServer + +__all__ = ['ParslTaskServer'] diff --git a/colmena/method_server/base.py b/colmena/task_server/base.py similarity index 62% rename from colmena/method_server/base.py rename to colmena/task_server/base.py index f56b53a..0215f79 100644 --- a/colmena/method_server/base.py +++ b/colmena/task_server/base.py @@ -1,4 +1,4 @@ -"""Base class for the Method Server""" +"""Base class for the Task Server""" from abc import ABCMeta, abstractmethod from multiprocessing import Process @@ -6,31 +6,32 @@ import logging from colmena.exceptions import KillSignalException, TimeoutException -from colmena.redis.queue import MethodServerQueues +from colmena.redis.queue import TaskServerQueues logger = logging.getLogger(__name__) -class BaseMethodServer(Process, metaclass=ABCMeta): - """Abstract class that executes requests across distributed resources. +class BaseTaskServer(Process, metaclass=ABCMeta): + """Abstract class for the Colmena Task Server, which manages the execution + of different asks - Clients submit requests to the server by pushing them to a Redis queue, - and then receives results from a second queue. + Clients submit task requests to the server by pushing them to a Redis queue, + and then receive results from a second queue. Different implementations vary in how the queue is processed. - Start the method server by first instantiating it and then calling :meth:`start` + Start the task server by first instantiating it and then calling :meth:`start` to launch the server in a separate process. - The method server is shutdown by pushing a ``None`` to the inputs queue, - signaling that no new tests will be incoming. The remaining tasks will + The task server can be stopped by pushing a ``None`` to the task queue, + signaling that no new tasks will be incoming. The remaining tasks will continue to be pushed to the output queue. """ - def __init__(self, queues: MethodServerQueues, timeout: Optional[int] = None): + def __init__(self, queues: TaskServerQueues, timeout: Optional[int] = None): """ Args: - queues (MethodServerQueues): Queues for the method server + queues (TaskServerQueues): Queues for the task server timeout (int): Timeout, if desired """ super().__init__() @@ -56,14 +57,14 @@ def listen_and_launch(self): @abstractmethod def _cleanup(self): - """Close out any resources needed by the method server""" + """Close out any resources needed by the task server""" pass def run(self) -> None: """Launch the thread and start running tasks Blocks until the inputs queue is closed and all tasks have completed""" - logger.info(f"Started method server {self.__class__.__name__} on {self.ident}") + logger.info(f"Started task server {self.__class__.__name__} on {self.ident}") # Loop until queue has closed self.listen_and_launch() diff --git a/colmena/method_server/parsl.py b/colmena/task_server/parsl.py similarity index 90% rename from colmena/method_server/parsl.py rename to colmena/task_server/parsl.py index 68edae1..2db213a 100644 --- a/colmena/method_server/parsl.py +++ b/colmena/task_server/parsl.py @@ -1,4 +1,4 @@ -"""Parsl method server and related utilities""" +"""Parsl task server and related utilities""" import os import logging import platform @@ -17,8 +17,8 @@ from parsl.dataflow.futures import AppFuture from colmena.models import Result -from colmena.method_server.base import BaseMethodServer -from colmena.redis.queue import MethodServerQueues +from colmena.task_server.base import BaseTaskServer +from colmena.redis.queue import TaskServerQueues from colmena.proxy import resolve_proxies_async logger = logging.getLogger(__name__) @@ -81,7 +81,7 @@ def run_and_record_timing(func: Callable, result: Result) -> Result: @python_app(executors=['_output_workers']) -def output_result(queues: MethodServerQueues, topic: str, result_obj): +def output_result(queues: TaskServerQueues, topic: str, result_obj: Result): """Submit the function result to the Redis queue Args: @@ -135,7 +135,7 @@ def run(self) -> None: logger.warning(f'Task {task} with an exception: {exc}') # Pull out the result objects - queues: MethodServerQueues = task.task_def['args'][0] + queues: TaskServerQueues = task.task_def['args'][0] topic: str = task.task_def['args'][1] method_task = task.task_def['depends'][0] result_obj: Result = method_task.task_def['args'][0] @@ -150,13 +150,13 @@ def run(self) -> None: futures = not_done -class ParslMethodServer(BaseMethodServer): - """Method server based on Parsl +class ParslTaskServer(BaseTaskServer): + """Task server based on Parsl - Create a Parsl method server by first creating a resource configuration following + Create a Parsl task server by first creating a resource configuration following the recommendations in `the Parsl documentation `_. - Then instantiate a method server with a list of functions, + Then instantiate a task server with a list of Python functions, configurations defining on which Parsl executors each function can run, and the Parsl resource configuration. The executor(s) for each function can be defined with a combination @@ -164,30 +164,32 @@ class ParslMethodServer(BaseMethodServer): .. code-block:: python - ParslMethodServer([(f, {'executors': ['a']})], queues, config) + ParslTaskServer([(f, {'executors': ['a']})], queues, config) and also using a default executor .. code-block:: python - ParslMethodServer([f], queues, config, default_executors=['a']) + ParslTaskServer([f], queues, config, default_executors=['a']) Further configuration options for each method can be defined in the list of methods. **Technical Details** - The method server stores each of the supplied methods as Parsl "PythonApp" classes. + The task server stores each of the supplied methods as Parsl "PythonApp" classes. Tasks are launched using these PythonApps after being received on the queue. The Future provided when requesting the method invocation is then passed to second PythonApp that pushes the result of the function to the output queue after it completes. That second, "output_result," function runs on threads of the same - process as this method server. + process as this task server. + There is also a separate thread that monitors for Futures that yield an error + before the "output_result" function and sends back the error messages. """ def __init__(self, methods: List[Union[Callable, Tuple[Callable, Dict]]], - queues: MethodServerQueues, + queues: TaskServerQueues, config: Config, timeout: Optional[int] = None, default_executors: Union[str, List[str]] = 'all', @@ -200,7 +202,7 @@ def __init__(self, methods: List[Union[Callable, Tuple[Callable, Dict]]], is a function and the second is a dictionary of the arguments being used to create the Parsl ParslApp see `Parsl documentation `_. - queues (MethodServerQueues): Queues for the method server + queues (TaskServerQueues): Queues for the task server config: Parsl configuration timeout (int): Timeout, if desired default_executors: Executor or list of executors to use by default. @@ -225,7 +227,7 @@ def __init__(self, methods: List[Union[Callable, Tuple[Callable, Dict]]], self.methods_ = {} for method in methods: # Get the options or use the defaults - if isinstance(method, tuple): + if isinstance(method, (tuple, list)): if len(method) != 2: raise ValueError('Method description should a tuple of length 2') function, options = method @@ -291,7 +293,7 @@ def submit_application(self, method_name: str, result: Result) -> AppFuture: return self.methods_[method_name](result) def _cleanup(self): - """Close out any resources needed by the method server""" + """Close out any resources needed by the task server""" # Wait until all tasks have finished dfk = parsl.dfk() dfk.wait_for_current_tasks() diff --git a/colmena/method_server/tests/test_parsl.py b/colmena/task_server/tests/test_parsl.py similarity index 89% rename from colmena/method_server/tests/test_parsl.py rename to colmena/task_server/tests/test_parsl.py index 5d626b4..4935dc7 100644 --- a/colmena/method_server/tests/test_parsl.py +++ b/colmena/task_server/tests/test_parsl.py @@ -1,4 +1,4 @@ -"""Tests for the Parsl implementation of the method server""" +"""Tests for the Parsl implementation of the task server""" from typing import Tuple from parsl.config import Config from parsl import ThreadPoolExecutor @@ -6,7 +6,7 @@ from colmena.exceptions import KillSignalException, TimeoutException from colmena.redis.queue import ClientQueues, make_queue_pairs -from colmena.method_server.parsl import ParslMethodServer +from colmena.task_server.parsl import ParslTaskServer from pytest import fixture, raises, mark @@ -25,11 +25,11 @@ def config(): ) -# Make a simple method server +# Make a simple task server @fixture(autouse=True) -def server_and_queue(config) -> Tuple[ParslMethodServer, ClientQueues]: +def server_and_queue(config) -> Tuple[ParslTaskServer, ClientQueues]: client_q, server_q = make_queue_pairs('localhost', clean_slate=True) - server = ParslMethodServer([f], server_q, config) + server = ParslTaskServer([f], server_q, config) yield server, client_q if server.is_alive(): server.terminate() diff --git a/colmena/thinker/__init__.py b/colmena/thinker/__init__.py index ca61b5b..909d561 100644 --- a/colmena/thinker/__init__.py +++ b/colmena/thinker/__init__.py @@ -177,7 +177,7 @@ class BaseThinker(Thread): Each agent communicates with others via `queues `_ or other `threading objects `_ and - the Colmena method server via the :class:`ClientQueues`. + the Colmena task server via the :class:`ClientQueues`. The only communication method available by default is a class attribute named ``done`` that is used to signal that the program should terminate. @@ -203,7 +203,7 @@ def __init__(self, queue: ClientQueues, resource_counter: Optional[ResourceCount daemon: bool = True, **kwargs): """ Args: - queue: Queue wrapper used to communicate with method server + queue: Queue wrapper used to communicate with task server resource_counter: Utility to used track resource utilization daemon: Whether to launch this as a daemon thread **kwargs: Options passed to :class:`Thread` diff --git a/demo_apps/multi-agent-thinker.py b/demo_apps/multi-agent-thinker.py index 7fd2f5e..701c8b8 100644 --- a/demo_apps/multi-agent-thinker.py +++ b/demo_apps/multi-agent-thinker.py @@ -6,7 +6,7 @@ from parsl import HighThroughputExecutor from parsl.config import Config -from colmena.method_server import ParslMethodServer +from colmena.task_server import ParslTaskServer from colmena.redis.queue import make_queue_pairs from colmena.thinker import BaseThinker, agent @@ -33,7 +33,7 @@ def task_generator(best_to_date: float) -> float: # Define the worker configuration config = Config(executors=[HighThroughputExecutor()]) - doer = ParslMethodServer([target_function, task_generator], server_queues, config) + doer = ParslTaskServer([target_function, task_generator], server_queues, config) # Define the thinker class Thinker(BaseThinker): @@ -66,7 +66,7 @@ def producer(self): self.queues.send_inputs(result.value, method='target_function', topic='simulate') thinker = Thinker(client_queues) - logging.info('Created the method server and task generator') + logging.info('Created the task server and task generator') try: # Launch the servers @@ -80,7 +80,7 @@ def producer(self): finally: client_queues.send_kill_signal() - # Wait for the method server to complete + # Wait for the task server to complete doer.join() # Print the output result diff --git a/demo_apps/optimizer-examples/batch.py b/demo_apps/optimizer-examples/batch.py index fef92b0..455334a 100644 --- a/demo_apps/optimizer-examples/batch.py +++ b/demo_apps/optimizer-examples/batch.py @@ -1,6 +1,6 @@ """Perform GPR Active Learning where simulations are sent in batches""" from colmena.thinker import BaseThinker, agent -from colmena.method_server import ParslMethodServer +from colmena.task_server import ParslTaskServer from colmena.redis.queue import ClientQueues, make_queue_pairs from sklearn.gaussian_process import GaussianProcessRegressor, kernels from sklearn.preprocessing import MinMaxScaler @@ -63,7 +63,7 @@ def __init__(self, queues: ClientQueues, output_dir: str, dim: int = 2, dim (int): Dimensionality of optimization space batch_size (int): Number of simulations to run in parallel n_guesses (int): Number of guesses the Thinker can make - queues (ClientQueues): Queues for communicating with method server + queues (ClientQueues): Queues for communicating with task server """ super().__init__(queues) self.n_guesses = n_guesses @@ -181,13 +181,13 @@ def optimize(self): ) config.run_dir = os.path.join(out_dir, 'run-info') - # Create the method server and task generator + # Create the task server and task generator my_ackley = partial(ackley, mean_rt=np.log(args.runtime), std_rt=np.log(args.runtime_var)) update_wrapper(my_ackley, ackley) - doer = ParslMethodServer([my_ackley], server_queues, config, default_executors=['htex']) + doer = ParslTaskServer([my_ackley], server_queues, config, default_executors=['htex']) thinker = Thinker(client_queues, out_dir, dim=args.dim, n_guesses=args.num_guesses, batch_size=args.num_parallel) - logging.info('Created the method server and task generator') + logging.info('Created the task server and task generator') try: # Launch the servers @@ -201,5 +201,5 @@ def optimize(self): finally: client_queues.send_kill_signal() - # Wait for the method server to complete + # Wait for the task server to complete doer.join() diff --git a/demo_apps/optimizer-examples/interleaved.py b/demo_apps/optimizer-examples/interleaved.py index 414527b..ff31479 100644 --- a/demo_apps/optimizer-examples/interleaved.py +++ b/demo_apps/optimizer-examples/interleaved.py @@ -1,8 +1,9 @@ """Perform GPR Active Learning where one threads continually re-prioritizes a list of simulations to run and a second thread sebmits """ -from colmena.models import Result -from colmena.thinker import BaseThinker, agent, result_processor -from colmena.method_server import ParslMethodServer + + +from colmena.thinker import BaseThinker, agent +from colmena.task_server import ParslTaskServer from colmena.redis.queue import ClientQueues, make_queue_pairs from sklearn.gaussian_process import GaussianProcessRegressor, kernels from sklearn.preprocessing import MinMaxScaler @@ -100,7 +101,7 @@ def __init__(self, queues: ClientQueues, output_dir: str, dim: int = 2, dim (int): Dimensionality of optimization space batch_size (int): Number of simulations to run in parallel n_guesses (int): Number of guesses the Thinker can make - queues (ClientQueues): Queues for communicating with method server + queues (ClientQueues): Queues for communicating with task server """ super().__init__(queues) self.n_guesses = n_guesses @@ -120,37 +121,41 @@ def __init__(self, queues: ClientQueues, output_dir: str, dim: int = 2, self.queue_lock = Lock() self.done = Event() - @result_processor(topic='doer') - def simulation_worker(self, result: Result): + @agent + def simulation_worker(self): """Dispatch tasks and update""" - - # Immediately send out a new one - with self.queue_lock: + # Send out the initial tasks + for _ in range(self.batch_size): self.queues.send_inputs(self.task_queue.pop(), method='ackley', topic='doer') - # Add the old task to the database - self.database.append((result.args[0], result.value)) + # Pull and re-submit + while not self.done.is_set(): + # Get a result + result = self.queues.get_result(topic='doer') + + # Immediately send out a new one + with self.queue_lock: + self.queues.send_inputs(self.task_queue.pop(), method='ackley', topic='doer') + + # Add the old task to the database + self.database.append((result.args[0], result.value)) - # Append it to the output deck - with open(self.output_path, 'a') as fp: - print(result.json(exclude={'inputs'}), file=fp) + # Append it to the output deck + with open(self.output_path, 'a') as fp: + print(result.json(exclude={'inputs'}), file=fp) - # If have required amount, terminate program - if len(self.database) == self.n_guesses: - logging.info('Done running new calculations') - self.done.set() + # If have required amount, terminate program + if len(self.database) == self.n_guesses: + logging.info('Done running new calculations') + self.done.set() - # Mark that we have some data now - self.has_data.set() + # Mark that we have some data now + self.has_data.set() @agent def thinker_worker(self): """Reprioritize task list""" - # Send out the initial tasks - for _ in range(self.batch_size): - self.queues.send_inputs(self.task_queue.pop(), method='ackley', topic='doer') - # Make the GPR model gpr = Pipeline([ ('scale', MinMaxScaler(feature_range=(-1, 1))), @@ -159,6 +164,7 @@ def thinker_worker(self): # Wait until we have data self.has_data.wait() + logging.info('Task reprioritization worker has started') while not self.done.is_set(): # Send out an update task @@ -259,18 +265,18 @@ def thinker_worker(self): ) config.run_dir = os.path.join(out_dir, 'run-info') - # Create the method server and task generator + # Create the task server and task generator my_ackley = partial(ackley, mean_rt=args.runtime, std_rt=args.runtime_var) update_wrapper(my_ackley, ackley) my_rep = partial(reprioritize_queue, opt_delay=args.opt_delay) update_wrapper(my_rep, reprioritize_queue) - doer = ParslMethodServer([(my_ackley, {'executors': ['simulation']}), - (my_rep, {'executors': ['task_generator']})], - server_queues, config) + doer = ParslTaskServer([(my_ackley, {'executors': ['simulation']}), + (my_rep, {'executors': ['task_generator']})], + server_queues, config) thinker = Thinker(client_queues, out_dir, dim=args.dim, n_guesses=args.num_guesses, batch_size=args.num_parallel) - logging.info('Created the method server and task generator') + logging.info('Created the task server and task generator') try: # Launch the servers @@ -284,5 +290,5 @@ def thinker_worker(self): finally: client_queues.send_kill_signal() - # Wait for the method server to complete + # Wait for the task server to complete doer.join() diff --git a/demo_apps/optimizer-examples/streaming.py b/demo_apps/optimizer-examples/streaming.py index f5525c0..0660335 100644 --- a/demo_apps/optimizer-examples/streaming.py +++ b/demo_apps/optimizer-examples/streaming.py @@ -2,7 +2,7 @@ calculation as soon as one calculation completes""" from colmena.thinker import BaseThinker, agent -from colmena.method_server import ParslMethodServer +from colmena.task_server import ParslTaskServer from colmena.redis.queue import ClientQueues, make_queue_pairs from sklearn.gaussian_process import GaussianProcessRegressor, kernels from sklearn.preprocessing import MinMaxScaler @@ -66,7 +66,7 @@ def __init__(self, queues: ClientQueues, output_dir: str, dim: int = 2, dim (int): Dimensionality of optimization space batch_size (int): Number of simulations to run in parallel n_guesses (int): Number of guesses the Thinker can make - queues (ClientQueues): Queues for communicating with method server + queues (ClientQueues): Queues for communicating with task server opt_delay (float): Minimum runtime for the optimizer algorithm """ super().__init__(queues) @@ -181,13 +181,13 @@ def operate(self): ) config.run_dir = os.path.join(out_dir, 'run-info') - # Create the method server and task generator + # Create the task server and task generator my_ackley = partial(ackley, mean_rt=args.runtime, std_rt=args.runtime_var) update_wrapper(my_ackley, ackley) - doer = ParslMethodServer([my_ackley], server_queues, config, default_executors=['htex']) + doer = ParslTaskServer([my_ackley], server_queues, config, default_executors=['htex']) thinker = Thinker(client_queues, out_dir, dim=args.dim, n_guesses=args.num_guesses, batch_size=args.num_parallel, opt_delay=args.opt_delay) - logging.info('Created the method server and task generator') + logging.info('Created the task server and task generator') try: # Launch the servers @@ -201,5 +201,5 @@ def operate(self): finally: client_queues.send_kill_signal() - # Wait for the method server to complete + # Wait for the task server to complete doer.join() diff --git a/demo_apps/reallocation-example/0_summarize-runs.ipynb b/demo_apps/reallocation-example/0_summarize-runs.ipynb index b132964..078743c 100644 --- a/demo_apps/reallocation-example/0_summarize-runs.ipynb +++ b/demo_apps/reallocation-example/0_summarize-runs.ipynb @@ -11,14 +11,7 @@ { "cell_type": "code", "execution_count": 1, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:06.302643Z", - "iopub.status.busy": "2021-03-11T21:52:06.302067Z", - "iopub.status.idle": "2021-03-11T21:52:06.736123Z", - "shell.execute_reply": "2021-03-11T21:52:06.736683Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "%matplotlib inline\n", @@ -42,14 +35,7 @@ { "cell_type": "code", "execution_count": 2, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:06.740326Z", - "iopub.status.busy": "2021-03-11T21:52:06.739870Z", - "iopub.status.idle": "2021-03-11T21:52:06.742844Z", - "shell.execute_reply": "2021-03-11T21:52:06.743241Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "models = glob(os.path.join('runs', '**', 'results.json'))" @@ -58,14 +44,7 @@ { "cell_type": "code", "execution_count": 3, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:06.746623Z", - "iopub.status.busy": "2021-03-11T21:52:06.745196Z", - "iopub.status.idle": "2021-03-11T21:52:06.754656Z", - "shell.execute_reply": "2021-03-11T21:52:06.755027Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "def load_models(log_path):\n", @@ -107,14 +86,7 @@ { "cell_type": "code", "execution_count": 4, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:06.762743Z", - "iopub.status.busy": "2021-03-11T21:52:06.762190Z", - "iopub.status.idle": "2021-03-11T21:52:06.776288Z", - "shell.execute_reply": "2021-03-11T21:52:06.775690Z" - } - }, + "metadata": {}, "outputs": [ { "data": { @@ -154,8 +126,23 @@ " \n", " \n", " 0\n", - " runs/reallocate-N100-P4-110321-153204\n", - " 2021-03-11 15:32:04\n", + " runs/reallocate-N100-P8-120421-092359\n", + " 2021-04-12 09:23:59\n", + " 100\n", + " 8\n", + " 20\n", + " 4\n", + " 2\n", + " 1\n", + " 20.0\n", + " reallocation.py\n", + " 8\n", + " 100\n", + " \n", + " \n", + " 1\n", + " runs/reallocate-N100-P4-120421-090931\n", + " 2021-04-12 09:09:31\n", " 100\n", " 4\n", " 20\n", @@ -173,16 +160,19 @@ ], "text/plain": [ " path start_time num_guesses \\\n", - "0 runs/reallocate-N100-P4-110321-153204 2021-03-11 15:32:04 100 \n", + "0 runs/reallocate-N100-P8-120421-092359 2021-04-12 09:23:59 100 \n", + "1 runs/reallocate-N100-P4-120421-090931 2021-04-12 09:09:31 100 \n", "\n", " num_parallel retrain_wait dim runtime runtime_var opt_delay \\\n", - "0 4 20 4 2 1 20.0 \n", + "0 8 20 4 2 1 20.0 \n", + "1 4 20 4 2 1 20.0 \n", "\n", " file worker_count n_evals \n", - "0 reallocation.py 4 100 " + "0 reallocation.py 8 100 \n", + "1 reallocation.py 4 100 " ] }, - "execution_count": 1, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -195,14 +185,7 @@ { "cell_type": "code", "execution_count": 5, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:06.780338Z", - "iopub.status.busy": "2021-03-11T21:52:06.779872Z", - "iopub.status.idle": "2021-03-11T21:52:06.784142Z", - "shell.execute_reply": "2021-03-11T21:52:06.784679Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "results.to_csv('run_data.csv', index=False)" diff --git a/demo_apps/reallocation-example/1_plot-run-data.ipynb b/demo_apps/reallocation-example/1_plot-run-data.ipynb index 943f03c..84be78f 100644 --- a/demo_apps/reallocation-example/1_plot-run-data.ipynb +++ b/demo_apps/reallocation-example/1_plot-run-data.ipynb @@ -11,14 +11,7 @@ { "cell_type": "code", "execution_count": 1, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:10.761780Z", - "iopub.status.busy": "2021-03-11T21:52:10.761166Z", - "iopub.status.idle": "2021-03-11T21:52:11.214300Z", - "shell.execute_reply": "2021-03-11T21:52:11.214799Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "%matplotlib inline\n", @@ -39,14 +32,7 @@ { "cell_type": "code", "execution_count": 2, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:11.217494Z", - "iopub.status.busy": "2021-03-11T21:52:11.217052Z", - "iopub.status.idle": "2021-03-11T21:52:11.223987Z", - "shell.execute_reply": "2021-03-11T21:52:11.223465Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "run_info = pd.read_csv('run_data.csv')" @@ -62,30 +48,23 @@ { "cell_type": "code", "execution_count": 3, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:11.228177Z", - "iopub.status.busy": "2021-03-11T21:52:11.227607Z", - "iopub.status.idle": "2021-03-11T21:52:11.232895Z", - "shell.execute_reply": "2021-03-11T21:52:11.233374Z" - } - }, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "path runs/reallocate-N100-P4-110321-153204\n", - "start_time 2021-03-11 15:32:04\n", + "path runs/reallocate-N100-P8-120421-092359\n", + "start_time 2021-04-12 09:23:59\n", "num_guesses 100\n", - "num_parallel 4\n", + "num_parallel 8\n", "retrain_wait 20\n", "dim 4\n", "runtime 2\n", "runtime_var 1\n", "opt_delay 20.0\n", "file reallocation.py\n", - "worker_count 4\n", + "worker_count 8\n", "n_evals 100\n", "Name: 0, dtype: object\n" ] @@ -107,14 +86,7 @@ { "cell_type": "code", "execution_count": 4, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:11.237280Z", - "iopub.status.busy": "2021-03-11T21:52:11.236811Z", - "iopub.status.idle": "2021-03-11T21:52:11.253159Z", - "shell.execute_reply": "2021-03-11T21:52:11.253701Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "result_data = pd.read_json(os.path.join(run_info['path'], 'results.json'), lines=True)" @@ -130,14 +102,7 @@ { "cell_type": "code", "execution_count": 5, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:11.258117Z", - "iopub.status.busy": "2021-03-11T21:52:11.257612Z", - "iopub.status.idle": "2021-03-11T21:52:11.259244Z", - "shell.execute_reply": "2021-03-11T21:52:11.259847Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "result_data['walltime'] = result_data['time_result_received'] - result_data['time_created'].min()" @@ -161,30 +126,23 @@ { "cell_type": "code", "execution_count": 6, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:11.263619Z", - "iopub.status.busy": "2021-03-11T21:52:11.262989Z", - "iopub.status.idle": "2021-03-11T21:52:11.272945Z", - "shell.execute_reply": "2021-03-11T21:52:11.272509Z" - } - }, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ "count 100.000000\n", - "mean 11.649713\n", - "std 14.756839\n", - "min 0.534747\n", - "25% 3.007137\n", - "50% 6.007805\n", - "75% 14.934254\n", - "max 91.274221\n", + "mean 8.813667\n", + "std 6.987725\n", + "min 1.056527\n", + "25% 3.545347\n", + "50% 6.572098\n", + "75% 12.115558\n", + "max 36.930310\n", "Name: time_running, dtype: float64" ] }, - "execution_count": 1, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -196,18 +154,11 @@ { "cell_type": "code", "execution_count": 7, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:11.278550Z", - "iopub.status.busy": "2021-03-11T21:52:11.277974Z", - "iopub.status.idle": "2021-03-11T21:52:12.003527Z", - "shell.execute_reply": "2021-03-11T21:52:12.004020Z" - } - }, + "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -235,21 +186,78 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Plot the Number of Simulations Being Run at a Time\n", - "We can look at the runtime log to get when simualtions tasks start computing and when they finish" + "## Evaluate Performance\n", + "See if we are getting better over time" ] }, { "cell_type": "code", "execution_count": 8, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:12.008726Z", - "iopub.status.busy": "2021-03-11T21:52:12.008205Z", - "iopub.status.idle": "2021-03-11T21:52:12.052441Z", - "shell.execute_reply": "2021-03-11T21:52:12.053063Z" + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "count 100.000000\n", + "mean 8.813667\n", + "std 6.987725\n", + "min 1.056527\n", + "25% 3.545347\n", + "50% 6.572098\n", + "75% 12.115558\n", + "max 36.930310\n", + "Name: time_running, dtype: float64" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" } - }, + ], + "source": [ + "result_data['time_running'].describe()" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(figsize=(3.5, 2.5))\n", + "\n", + "ax.scatter(result_data['walltime'], result_data['value'])\n", + "\n", + "ax.set_xlabel('Runtime (s)')\n", + "ax.set_ylabel('Value')\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot the Number of Simulations Being Run at a Time\n", + "We can look at the runtime log to get when simualtions tasks start computing and when they finish" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, "outputs": [], "source": [ "with open(os.path.join(run_info['path'], 'runtime.log')) as fp:\n", @@ -261,15 +269,8 @@ }, { "cell_type": "code", - "execution_count": 9, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:12.063467Z", - "iopub.status.busy": "2021-03-11T21:52:12.062846Z", - "iopub.status.idle": "2021-03-11T21:52:12.089317Z", - "shell.execute_reply": "2021-03-11T21:52:12.091943Z" - } - }, + "execution_count": 11, + "metadata": {}, "outputs": [], "source": [ "log_data['time'], log_data['module'], log_data['level'], log_data['content'] = zip(*log_data['msg'].str.split(\" - \", 3))" @@ -284,15 +285,8 @@ }, { "cell_type": "code", - "execution_count": 10, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:12.097526Z", - "iopub.status.busy": "2021-03-11T21:52:12.096932Z", - "iopub.status.idle": "2021-03-11T21:52:12.193160Z", - "shell.execute_reply": "2021-03-11T21:52:12.193697Z" - } - }, + "execution_count": 12, + "metadata": {}, "outputs": [], "source": [ "log_data['time'] = pd.to_datetime(log_data['time'], utc=False)" @@ -300,15 +294,8 @@ }, { "cell_type": "code", - "execution_count": 11, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:12.198821Z", - "iopub.status.busy": "2021-03-11T21:52:12.198120Z", - "iopub.status.idle": "2021-03-11T21:52:12.230682Z", - "shell.execute_reply": "2021-03-11T21:52:12.231286Z" - } - }, + "execution_count": 13, + "metadata": {}, "outputs": [], "source": [ "log_data['walltime'] = (log_data['time'] - log_data['time'].iloc[0]).apply(lambda x: x.total_seconds())" @@ -316,15 +303,8 @@ }, { "cell_type": "code", - "execution_count": 12, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:12.234881Z", - "iopub.status.busy": "2021-03-11T21:52:12.234439Z", - "iopub.status.idle": "2021-03-11T21:52:12.236244Z", - "shell.execute_reply": "2021-03-11T21:52:12.236777Z" - } - }, + "execution_count": 14, + "metadata": {}, "outputs": [], "source": [ "start_time = log_data['time'].iloc[0].timestamp()" @@ -339,15 +319,8 @@ }, { "cell_type": "code", - "execution_count": 13, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:12.240417Z", - "iopub.status.busy": "2021-03-11T21:52:12.239979Z", - "iopub.status.idle": "2021-03-11T21:52:12.242120Z", - "shell.execute_reply": "2021-03-11T21:52:12.241663Z" - } - }, + "execution_count": 15, + "metadata": {}, "outputs": [], "source": [ "start_time = result_data['time_created'].min()" @@ -355,15 +328,8 @@ }, { "cell_type": "code", - "execution_count": 14, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:12.247800Z", - "iopub.status.busy": "2021-03-11T21:52:12.247276Z", - "iopub.status.idle": "2021-03-11T21:52:12.250361Z", - "shell.execute_reply": "2021-03-11T21:52:12.250937Z" - } - }, + "execution_count": 16, + "metadata": {}, "outputs": [], "source": [ "events = [{'time': 0, 'active_delta': 0}]\n", @@ -382,15 +348,8 @@ }, { "cell_type": "code", - "execution_count": 15, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:12.254641Z", - "iopub.status.busy": "2021-03-11T21:52:12.254184Z", - "iopub.status.idle": "2021-03-11T21:52:12.256260Z", - "shell.execute_reply": "2021-03-11T21:52:12.256733Z" - } - }, + "execution_count": 17, + "metadata": {}, "outputs": [], "source": [ "events.sort_values('time', ascending=True, inplace=True)" @@ -398,15 +357,8 @@ }, { "cell_type": "code", - "execution_count": 16, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:12.262650Z", - "iopub.status.busy": "2021-03-11T21:52:12.261824Z", - "iopub.status.idle": "2021-03-11T21:52:12.263191Z", - "shell.execute_reply": "2021-03-11T21:52:12.263731Z" - } - }, + "execution_count": 18, + "metadata": {}, "outputs": [], "source": [ "events['num_active'] = events['active_delta'].cumsum()" @@ -421,15 +373,8 @@ }, { "cell_type": "code", - "execution_count": 17, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:12.268081Z", - "iopub.status.busy": "2021-03-11T21:52:12.267634Z", - "iopub.status.idle": "2021-03-11T21:52:12.270839Z", - "shell.execute_reply": "2021-03-11T21:52:12.271197Z" - } - }, + "execution_count": 19, + "metadata": {}, "outputs": [], "source": [ "events['queue_length'] = events['num_active']" @@ -437,15 +382,8 @@ }, { "cell_type": "code", - "execution_count": 18, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:12.276026Z", - "iopub.status.busy": "2021-03-11T21:52:12.275565Z", - "iopub.status.idle": "2021-03-11T21:52:12.277367Z", - "shell.execute_reply": "2021-03-11T21:52:12.277902Z" - } - }, + "execution_count": 20, + "metadata": {}, "outputs": [], "source": [ "events['utilization'] = np.clip(events['queue_length'] / run_info['worker_count'], 0, 1)" @@ -453,15 +391,8 @@ }, { "cell_type": "code", - "execution_count": 19, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:12.280054Z", - "iopub.status.busy": "2021-03-11T21:52:12.279606Z", - "iopub.status.idle": "2021-03-11T21:52:12.283982Z", - "shell.execute_reply": "2021-03-11T21:52:12.284451Z" - } - }, + "execution_count": 21, + "metadata": {}, "outputs": [], "source": [ "utilization_int = (np.diff(events['time'].values) * events['utilization'].values[:-1]).cumsum()" @@ -476,15 +407,8 @@ }, { "cell_type": "code", - "execution_count": 20, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:12.287636Z", - "iopub.status.busy": "2021-03-11T21:52:12.287176Z", - "iopub.status.idle": "2021-03-11T21:52:12.292286Z", - "shell.execute_reply": "2021-03-11T21:52:12.291779Z" - } - }, + "execution_count": 22, + "metadata": {}, "outputs": [], "source": [ "def cumulative_utilization(time: float) -> float:\n", @@ -504,22 +428,15 @@ }, { "cell_type": "code", - "execution_count": 21, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:12.296331Z", - "iopub.status.busy": "2021-03-11T21:52:12.295727Z", - "iopub.status.idle": "2021-03-11T21:52:13.082148Z", - "shell.execute_reply": "2021-03-11T21:52:13.082646Z" - } - }, + "execution_count": 23, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 779 ms, sys: 0 ns, total: 779 ms\n", - "Wall time: 781 ms\n" + "CPU times: user 663 ms, sys: 3.63 ms, total: 666 ms\n", + "Wall time: 663 ms\n" ] } ], @@ -538,19 +455,12 @@ }, { "cell_type": "code", - "execution_count": 22, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:13.101874Z", - "iopub.status.busy": "2021-03-11T21:52:13.099865Z", - "iopub.status.idle": "2021-03-11T21:52:13.466759Z", - "shell.execute_reply": "2021-03-11T21:52:13.467361Z" - } - }, + "execution_count": 24, + "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -591,15 +501,8 @@ }, { "cell_type": "code", - "execution_count": 23, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:13.470308Z", - "iopub.status.busy": "2021-03-11T21:52:13.469701Z", - "iopub.status.idle": "2021-03-11T21:52:13.483541Z", - "shell.execute_reply": "2021-03-11T21:52:13.482898Z" - } - }, + "execution_count": 25, + "metadata": {}, "outputs": [], "source": [ "retrain_data = pd.read_json(os.path.join(run_info[\"path\"], \"retrain.json\"), lines=True)" @@ -607,19 +510,12 @@ }, { "cell_type": "code", - "execution_count": 24, - "metadata": { - "execution": { - "iopub.execute_input": "2021-03-11T21:52:13.486185Z", - "iopub.status.busy": "2021-03-11T21:52:13.485687Z", - "iopub.status.idle": "2021-03-11T21:52:14.075480Z", - "shell.execute_reply": "2021-03-11T21:52:14.074964Z" - } - }, + "execution_count": 26, + "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] diff --git a/demo_apps/reallocation-example/figures/allocation.png b/demo_apps/reallocation-example/figures/allocation.png index 9cd65d45ff8dba8ed908ce2a1b10a42e71440659..889e1b3f2c062201bba997158cf1196a05e7dda0 100644 GIT binary patch literal 30177 zcmeFZcT|&E*EgJifFo@(NRjG*WfZ9a0coRvhzLlRjv^o+1c~&nBcK8*h!CWRARU53 zZy^W*LI9OO=tR(<^cF%3d?$%B&pgjP->mz7pZA|{t#7T#8obK6&e><5U4MJuzig;` zK=1$r0^!uVplt$y>~n-bm=Et~1y^n^AJhl`Q@O2k<+drp<+lG-A7_Zc)!W`T5w~x; zT|4aO?1OYec)_LN(h8>z-?)9-8+k@X#`Dhyq!B)@GC!K(>cAjBcwewWLLhRdX+LP{ z^trPDJ<)A+{eu%azL14`baijq8!UcwRZUy`eJKg$LBpX4*l0`!YH35T$ZOG!A$dHTJA z>#VZ$KlF~mBc9M+)H{iS#?t>VdB@>M|0Cg|3q*oGf|>mPKNKL*|4mvwH}-{o3=p5J z#$w2%Gmg&pzRY#J|mbTr_<{0c(&h{{=k_xsW{>Mx|ny~ z2N)Y@@-FR8j$w8D7OyNj-zmp$A7Ak%(|651uLzAvRrDC(P$ZaJb(slZ^ZQEn@j*}7 z)jj>vm27DnPut~!bXGPttJl}wUM}|_u=xHt=>PY4pgX?~;`Y)4ZQs4}rAuGEZMf3I zs@CVr`ugPChbYbPu!458Mac|H+4b6fTvg-C_-yA50XmnrY7I}5{gZkFIhvLBE$r))@*_o5kQmJjkFz56to0H19 zTg~Q0RqNAJ`Q+bdg3WMdH*RI-_5;515p)Wru5*ph!)lE7|`%J*>U5HsIPEzTZg|8uQ`quhV$3-TiuWnwVlTMpkjFRtJhEIeUyC-EoD8Mx876!f zOpYfYENfc`73gul-B#nBU2;{$kMs|obcFO}8Zxc_vX*YOBKJrtFob^j%rn!+TYUxh z)=v_j^WuM|f74;t8*h`SPV%I@kpN0+Qc z-0$3}sJn?nXpCQMUo!o!UH6F#)aj?^wp~%1MHf%fNnT(ifL(hM6Y!(AcGh<6Nq5G@ zOmH#TlzrX+%m2qZFxMb`$HlK5TiQ8CxrYkW;fady1XNRL z&QJ6qw;n?;J$e|!2Gu=SXBT2gr|W6?h;N~Q)IyOmmY1XH>kI9IMGw#WpuL7k1rv*xY>9qWFp&-u4N~ZnHK04t>fAf2*N&0{RuX)YmQ> zYEsus9=-OPx3(y?t!-=!SiVN;&2*TN>)(Db%^Mr)_3sFu9qZv^6^nQV}dHhh~>$*xJ=EaH7BF<)%~ie zh9O+e{bs_|ozp}2(e>_i^B$(=jfpB=d6=qn>*Sm#J7N7jG_4jH8ClqMnTeNLovzNSo_@X0pe$A3>tGeuKX;2;NSy<>V-#(8gRGH2uC?rgv-4zCU{XgE| zaKxK)*(k&{Q#;i?(bkof2~&m^-C(1gWC{vQ`_&>xv$XO&eyDW1!WU>aw_p|H|@5iPS~$&8ZYCHi5a8ibD&v)g@@JZ0!3e?jnABJ{oW7Y~ zq@8-aE>I8`w1t0Yz`UoTQJc^1&t;-U^6k_aU$h2W+W4i9gS$UW`NNV9?TsTz?T}nu z?bepaXocHHmnghRj7J55go{t+A5L~9$w(Gx(;ww@g7h(qUz1)cK1bm%%xAnG2jr1Z zJQIm`OS<$WLt-l=_@ABOWqSaYO$eeKfKhbIQ2) z?JVkQmXY}639Ls%)3v9P8RC;?h+Wud`PClzhjg7=;Gn`%pfqQBV^2<;?|kbJmIdo1 zuHal|)PB;x0My_;9!!Q`n-4IZ?NcY>ebz*hW?#--FGwpyc_v!bf zfW8zu&u$%t|14m6a2USC#n`>d-=LQeHGWSqLaD+|74!aLujuPKqWBJytF#mJ6!RhX zXxwy$@z#Ut>Sy$k5@{oq9(siyBHNrF39Gow@QLUz6gz(AmDyHQX}d8pai-RX{=A6< zbBgF=lu#ssw@z%}vur!>u#G^ddm{a&djj;*;>N`+>e?}cM5udKsI$xI`gn#XC3uf^ z??B69RK<@0)7pNqF|N$*e07?Hmv!`aXM({w6NgtbA*65bIcYceG*HDSKViS!TX-G; zx3Ns8KSTv8EBZRgGV-Bd9n%InX2V@u?WDL1M7#SFY}uG-;%?c+F~roM3|Fcw@}g5b zXe_pn{c>01{tZ!YGp&if8L<)ZNohw&ib^NOCbH=ruB%hpgueG-lf1I($KZ=hy9YM5 ze&IG^_H$ZW`ocxb!JRVHrE{~MDL4mr~%9$ykLJ z*tDytKn?FO?Kx&)X5sxfwgspcOV|@sQ$g{s^eJtDO%m(CX+j-}>bMwJl1v}dU;lT# z%yAYyyDvcWMo7I9ovKqU|BEDb#+!|V7w@iX5)4De%=|6X%d z%>0DU0=hzxm6g>N$2j*%Y5%0!|H+T9&axm}X$(^AN|r&_aXsE{e8ughN z#D4-2gj#$>EpDKE{1BdJTpL?QcWnM$e*HU#>Js<;m?;VIye(t$DxLca2m=2ant>Ne z|N3f6ed$x-L&ifBG(%R?nG|kr_Z$7pP<#G~-v8vf2MW!I^26Yf5)sshV5mpLNMsT! zFf^%>ekSoUK+TS5Mbt;uW~&Rpl$iOM#kz4t!&WJbueJi?xqIZ@)Hah0i@hLh8j9|~ zsdoKxMEseN58;)R6kQ;?3B-}0p zGnGd`9p#pUi4U>Q-h^#4PdtbT9kDE=f2pxo(lMwjGlHLK+L_d5EIM(?6E;2@xpzON zFP}w`+ilRrfq0X+!AGdNmar|w!dLM-LqiWUWaVPSgyL(eFKl@~F&Nt-C>ms@t#$(5 zH1uYe^?12M=)6E4!(pvU1-_Oo^&-j@>cP%U^<-hhVDO0?j@mA}hc<3!ZEwV+*!|&M z3Nj0D)|S1`h^~&HE0Thv9F7W~3E68>?!mlt_9q{vJ>v8f6I|JNfKJk&$I#fQxic3h z#^$BZ(Voe%zYfX%nIiNGVLnpC*j~0nfMAIS=DV?oqF)#^w0D9?h+nfLA`XmMUMI~Q z=wx^_)4+O(iHXr$P!ictli(j!M;0p5Ka(Q}H2?DJn1FEYImHhm;@7x|vsk@*(WNi> zI_W1fhfg-*n+SWP64M?(Y~o=|cuY(*A&=jaABL2egu#~gB4!0@*Sp5B>~N%wO(A1_ z+&BW+8ZJgHa_q-JmljakTUbnwE=@|_;vd48bKxZV>p z`wSM^=xy&`pApc6k`r>_#ujh3R(vzJG_BLr_E~BciY9kwRwva8Aqw-wEBi`Ut9~D8 zX5*gS*1{bdc>Ab_8kJKy^9;7)BExdyAL@h-pl|7>ymVayloYjXlv zP|nOL+nUGGYM*~rTg&2hewT1m`_g=}k)DK<)$hNtu=6QY_BI5rUa9f^nVFdxmts6l z7P))(Zt<1Z*Yaz{;cWVI?y<$7(_DcFz62d~e~vxgdHYNXd9r{(<@Y;z5s5Aa)JuOEAU+EZ{q z$zz}x6R_aENC@vqQ{k~TQ-TlG`AUMTSG!XcZ;sXZ4o(T3zDYC+-{RSxuXgD2nDYYx zG(kW|4lchh)->))eIaF2wX{947pA@UQs?2IXZ09|Xb#tFKm++i)rFe7=0z5q!8d8+ z4gLNKe=o592_=@!m2x29sQwc1;ib+TW_uw@#w7awe!q!Oe?BQ|g~`_+Fv;8L^tgIX z{Hn1Np(Z(_>#==^VafX$!cp<6J>JlT(b|aF3;Cg>p7dE>%acja z*t#}Ff^EyKb`eY8Lx5f<-!-#|;oS1lS+PGy3o~Ikl}0aL)tNc2|LoMQ?w^x7|4FU= z#f+e${s^SD#O#KJ8Q{}a(PMAeN(U8+(S1Fc1sT)b8tq;I22r<7OU7RCOqd3L^BC3d zb-o~@k!Nv(Jo{69@6|2Q%FtK^-WSQAXmUdD2*9?XrKP?L1W{VG=(Wl?9Cvvbp(s(%|e_okfiI_%C+bh2eZ;L$? zGHWw&jbP`WzBbq!x`0%fPD7p#J%yY*VW_#$%&q+CH=Zc6T{x9296*drEY(TxWzabX zQ|{cwCym*d)LRUvjQJy*S@UsJyeSqZ`$leT&eXr`jB77D`*1Vt5!~CDabp7Rfvhnjtpnb zXzM7td*{}IDIaWtb-h3-{0T}%`c(&CK4ELaShNn|GX9?AL!-*a`Spz^r-2MMGrRM> zgFs}D-da9hDD7mzrTW>ibgMgFC$$NPtEE&zLvDdw*xvR8ErK*Z-@>h#=0KgXlqxcA zdz`*mm&H%ZFauHd;pxa={m!*8eXDO9F$&BUwDt^Im5f78a>mbQepj|Au? zZDE!jPSw(Yp#u$~R}xT&P8>3FLP75H`=5rP9Oqkc&v;dJV9OVXuQ=@-iVFE?99sBs zmooG`JN}G&@b>Rr(oW==exBiKW$ITQnIL0*57Wh&SqN(Yn`-W%qu%Z=VMpNAhCU6M ziXj?8HGB9@kGPl=9nS~&%cr)HM9sn?x(!n3J9%F<_h-++vPg8O=X7|sJyzJ4Mv&E%rzj6tXn7Q&t9m(=;* z{AW{4w6)9lyCui$6|#l+wI7ypqe_jONv)PCVA%;dq6&y{HA8S_=I13#YwpaVo5Dsz z1@pwZ(^ZpQ)n~0<_2-TE^+Iu>f*MA|2YhkC+^Xl&Bl+S~P8i$#Fk^}>4kDa%w``&B z&V)x=lvzW)@dq-&p_e;+qh)?R(MSVj=Qs2UzlFEiQi&KU|Fl{UN18rtOK*-palyO^ zBf}y(^gb-qW!&6YiZ&sGHPG z!XIFAcRyJ+j*dULb=;~;+`T$obmcB?=bSk`TyK(x0-Dk&=wo74_1;pnruSRkY@>MP z+2Zo5a7?ep_EJi_TJ9QSMzxinRs&(LU$8id+VA<_IAG!ogpy<-lgEZ!t zxfIv;*fHR}{O6S}8P_kJNh2dNA)nY$8lgeoDhsODkdW2(bK5n1u~xCahWFo*d(H=Y z#FjyLPj1HTc=A@#X>(ka7{Pp{?)JC!bs(==qBw=3Jdq@nE0+ggTp2H2w;4IAFt!*n z@Y<>I*@;j#oz~m#mMD%mf251OQj*+pt9CVFBBvdJdC}9K?#kn(1krbDw-@SbiFeqm zstLg2sh!TKZ=@30G*h&=)hkNK1xjn45L1WSlx=)2JIm5sOhCuMs1P=9?E59`J=2rk zE8|?F)mhY3oUMbg)QC>&%DWJVzWNvS#2u{?@*wx~NjL3rT6% zu3aF$qz*!(reOD*d+dReJeeJv)0y1ZxBLz`husx^+Lm)a4^u#rE@e;}rsFhXDtJPJ zSvY=m+5K_jSmpa8k<YS_V*7! z_TKjh(w0zQqrSJ9k@v9Bxo7vF#&PEj`ys#Ov!t zXy&Ok`Zr#s$~m#h1T?d6bm(d@VbNh^NiWPMY5vh7FvujltfUmP(RqhC-{CwWB_a{B zYFDy8t+u_mcTP2*MY;VtiSvu4+MDAMkx_N_=_s3%jPzZd#q#>c>WQ%J_1;WCdpRTt z=a-x#dJSTljPiFOB}TS0CFARzo<6#^mxR?rZT^O9C~q{^(oW+@8+}hk;cN#M7@3<9 zk-*PSgu^yIWowYuBn-?ajLx3y#4SRB3pQ3zvg*T8#$P5R%U;7=!@I%at;beezOoS2 zi(nqVV*WGz^v@frd^u>pw?*Kd4JI;+b$rkjvoqG2;boO|>cEXeqWd+^4E@?N9So_~ zZM`zGfFs`vmFdDw9ap9M?7q+W8;C5nfnSnZwhT2kKf6CLHj-liJ_;7bDHtWdXvrWc zqTZqS2j+vsD?7F*#gQ4MU`%1#ef^$dL0peo&pc*Bk6OD;_GoP11O_feZ(cQc{igj& zLUKiocx5W|QgRor9Jtro!o3|3(%b{DY$?EUN$!0mN)~qKjF!na8>=zolEiWUWII2Q z7i3}tNP@;i_`_e${B(p|o5A5IKKZfG10oL>v-5?~mVcxmNc|8N(#~x4X*qjxitY$T zz%{oFdv?C6Mw3g1@WT!SntYT$u0dLE7n{JZ&u}&hVlP=799G?qzu^~5J75X;=pdDd z6HyX8#vGUZPMi&C*04vuavVScWk*nf5L!r^)$`jMH0_JCi!&uXucJtXOC^ZQD9 zOk6Cmt7IxW5D?-rJ}`Hf94cxwKZAnAGu!tSiqj8?iT^AGLh>bt^8_rK<#P6GrBDW* zLwEWPdN9J+z=dH>i3yM`Lue5Kr3BVTqk2IaDcf3>k>Z>2p=r>#u9pZ9#5MB8ZVAi| z-LgEAY|qFrhl*h5hL$#;@x_lQ54_}yXUwe5%xF9+=+0@S5X@n5ziwQ8uDql6+yRcV zsRJq9PKc6sz@k2F{XE(C(J)f1s7cd-M{&AN6{+ozm#%55-upZ>erzd~<6EwE{n_J~ ze{necCf=+WRLV^cG#L;)4XfhRVik_gDg~PqP{qqNjT&MCxRzc8!6QxOSJ_R4S9P${ zf^8vZUBUuSI<42oT*_}Av+J? zJB|iS#kFj4DH9>2?&nHhUbB5i%OG`JxC{f|A6Q#GnB#!oGjR8p3bN70Wz0FkI7osEcdht0~NybtLM;rjT zO&oSCt7gP@*mBH$uFDha$FZh@ZO{$Ez_XL|O|Ju(i#Pf%VJhMo+-a@xOP}`>*NX4XQmB$K{HwnS3XkOk?EdPS zp=GKw!KJLJgvG=igGj>|02~njnhZeJI#=c?iqiRdzjLyJN@Q2~y`_-hy`Alv2NdoR3Z|1n> ztS9EAiGD;&>;d=LMq9%AHOA2($VME}aVg!WVb91394SyzW}%b$$X{@B$O8#=Z8QBv zR+x~$SBk}U_0N(dExiyj_6;eUtJ9WOo|u`cDP?W0mYcSHwV-Z|Zj%F*RuuCsWH7tH zEj)HoJVhh)3)9Pl|R=y8*t64Iwm_V;L;auz6??w>5e^8J6s79<)qH&kEJ zA2`x)Tj@RG)4`Hmxs_Cl4DyiqHW3$>qIi*p>yQxuS14~U&LAvz6 zD%4%uwrO+2|4z>G?{mf`%CBbJ=X#9dKi|q7fnee;dZZXPd=1m<=2l5NfPOP|v!zqc zGQn2;&THw;{Ata^54vV{Z8cutn?1_YvmxtU{7WO6qh)>buDuy=VcZRxcXdVDcnp=} z*)%tPaU4h)$qXc+8a^MDIW&;0vdatqBSdjN6xfQ}0^xg<#7x`ec|`d0;~LBNvo-wM z4!L^I6yzBO-Bd(Vjwi(b43J+&j_u}>;ZVby$3hMzpo$`oGgotPa;wRSZ>bY$^UR6& zCaE|~hjDA7!ozyoj~&E7ZjRR6mkbY66x+rn2TZa%v?MxgD1G6xTB_|0E&WLCx2CDM zb|&_j%;QTLxQ5`g`Sv7~KXlQ#{Y~R)WOKp6UugWFS{EL&y*G6f9n9uG))<6uckNDn zw?G+Aszs7wReJ_XBnUT7_VtFX!%HziK9V-0p_H+-@ZHT4sSBnbdo6DWU*7Zo+N0~> z*_`44-AxxUP33|}BbsaIlh)fH9@lKw-P?ZHvgLwHX-ZueP(abHL41m6t5 zIn`#cL!uB?`y8mL`sj@x_(Lf^7HF%Hh2N`Hfj_r_Wz%@S76t-9TtcT~8y*%F(tOpI zQN09Ll9ZE|%2`yPzIHKN)U`gHDNl#0m_g1O%`&kv{>mbsT;uxezIm)nq(`H36(C`d zUDIf1ypR!SiJaxd9j5hle#}zgZRRjf>ICW)0GZ|(0(%E zyM2RYS0!lk(%71CxZd|{1swsNIL#*P4QbjQTZC-9;m2UN$0-`QMw#PyW#9+^eT&Tq zUU&!GDCNtCW0)-+NOM7wShH2%=;%Eb_a+b)E>Hn&<+t7CncGufr%Zd>?6o* zekxnn&scq$`zHA%b+Mdcrl0zlzwxV%cI&!d`NMA+;4rid=cV~vlqob-jPSuON;ufVDxkyX0fSE zVswzhR<2+C_%{opEpaj5KzCRI?&?fhK&kvXzhKl36^n#fzE@}3xUKq-rUr5>>gVI@ z&yQB#AlR|C4c4vF2sysHxka9(#a@nAOi-HkLcQv~<<8Gf_A3+`f{+fX_Svw=gQ%WX zt%yFS6<1PT#4D{9W=$N9i4pwQVCtBdSY zYBJ)YA2JeX3cG-Oe!G8xq=_zkk?=0C4W7{qlG@eP$IoyUC{y$0I*co~P6I0>9MG@G z(VoLr820e^nP0U>mBcZeX~VKy_xHPNGC0F0&g%y-Cz%Zn_`CEYe0$ z(!dw2Uah^P1Hi8#jqj=X3?ycbMGA=5aBJLrSB61^Eb4}=t(8j_<_E?-oaQ6=9qJC8 zPHzXndfhRH{@Gbb?g9(8pM(3Vi%EUrd)G(@C`G%K6a!Ldri-U}xcgyHw`Co=vmbaN zrCyd)DYN3*#a>Ptr;g3{%Y^X-PXs@LQIq#Jrq#C15!WfNjxUwvVSXbtPHCkfo8Q`o zn;swQsH%$ZerR9eJ7XnqUIB8*hY&;^%!tr6Nj9W?JlB?d8TECesvi? z=h@i}+^fRCUmV}1hj4iMvn?GXZfso{yD@QY`%#h^Jy$68yW_4JX5|~Vynj!cvDie- zEuOBhEqx82tsylEV!U;-`FJ_yu4p^bfhw~@;csdiqnPbglPAUNoTS=<2drG5phiXv zHD}aVWqnb#Qt%1xIkpDjjg#K&E$9HZotjse?yd-`{OUxi)lL<2y4el?Zo$(uiLUUh zIs1|05R0pK)AmBsYFwm^#aNq)!yZ>SHOim%$zFX23fJzUQTU1Kj$5O9yA)S%Z|~sk zQDm<(2#HBld8B1jP{xB-2irBOq{wcQ9F7O%ky)Hc(@h@)6Xs?!f;t}Y8U z6Qo$2aNxQ>fb}W@;19uq$nzccCguD2Bb#?OrT2^G9@$_td>P6!j-W)w7ckpiJ(_zvS06cqZGN%!AM*vlJrG?1=4()WXWza<*g0*N@n@)} zE}R%aGb|ZZZE0sIiC_G>CVf6iY7vL?D9oqrc|xZwh+Nr+c9z$yub^K~zUf`ABJ3(Y zQYM|nq2R%J+od*5kxgMO^_$g?GRLDX>IN+!GrQ)7RA){ZYMrb~55uEe=0bRU%VPpm zTAF&wBgZ0IBHJPCf}$g=J032W`cPgPuD~Q%1c|>Y41SQC;|(Z}QoDD1_GF>bjdwB| z2ihWyZ?WJnx0tX(sP?N0XPPHTO`Bl=0`} zSps5_O?f6i8-ym^Y-amfLCPItbeY|*t(>JsAi&>cwa?Yt52Fw=ZTTK&p=Mt zXMM(bkgc34mp;9`h^87X$~$9vgc| z;&?gGQLE&YgE_91-;}qR5s>wlU8P}ePf^L*t=(7*=h$(G)N?w%N^gubqIsna#A>6- z>O?4ae!k$9iW@LQV3*AXu#W2+7wsu^{M>;ZS|^M2c>2nLOb&a6Tb8(HM3llxMhxA) zwegXd%)#xrV8e(dDwv0pi!GN*l^Rl`HP_B24u?~>I2$&pnF0g+fVW_eZs-IiuE+Z~ zWPhCU@Cgb%VA7@2`X(lT+goHw(2+|u_b{h{Zrz>Xeu=T%ngo2q#X=f>C5LZg_xH?| zn7qc=cC~5qwBS!ifu-&CW_x!cbN9_u#-0)Ym9-$swrkqedu-G_OoG-Aq*y$Rsl^ZI z1u~aOs-8>EQk7y^VkefMHoFg8ym@boecnd0fR2{2{u-=Vm#|3vdU2t0_FTrwpzd;6 zv~cq+U9slK?fI6SKx8$-e?-K8Wwi+XrD&qzX?ah5jPI-xhmdP^@cbLi9j#lNm)x## zIQoeLrvc3b;@8&#md-VXN_ktmWQ^<{u#phYg}$9#*)`LlWm`;&>_CA|*{d1(p!4(O zISv1DU+zp*$~g`Hh4@#}(>PT3oj{~jqu*GKWyf3?+HP};e>c&}p88TxKH`v;`eS8Y zWzNAzAMPd#MfyWCyKoC3Y{Q?lUu+nzG@d*_I|kPCN&KFJo9Aa#j6!#oZC6@&B}%Wm zRa|X3fBNR2@`AEa;Ar;7M*ReK;o&-|y4rUe9ai`Niu*-;atNy$xeX6m@?l>>WAo&Z z$ZbjC6XGteq>V*Er^R}WWP;9C}qxT}O`CCZtS?#p_4MNIyz%o=Lxglb` zIPyH&+jcPyBs2F?Q3QkAc`?eu%0kNdOYcYE$Vo|h2*HFcRn?!1HvfRmDNz<|Tio-F z7S8a`6v9NSRFT(ohN8z{g&R9Nsn_`vHe}tt;<&Eb((YWj<*I3%p*q1#%{xk8w=TeBl5On0^HIJoTxAY#lKDJV)s)0-bq(1wEnZrVEn8-h!w z?$ysQ>Oz=oiOFAyKVy**dGaf28yZXRPUBC}&atOr@cJpPQ0!UZt%%8q7q5KCz7QUm zt|_~{C>*aN6>zAo{KI+ov#m`oB+sOSDk|-G&8V9XW9kdk^PrAf%G_~9Kc{C%`{Jk!CS;2P>+_Q)q(|;zdQZw^YPW(VJ;t*{4SE4$; zbT{Hja~B`+5-}YWh~!nQ8dMtMvV9u0ReuS6PChSKaH0v`tysX-NCstU9OfY1YX3N;G zmiCE#|08Fi$`cnIAehIfqKhS$Y4RvXqn{sSf#(dQUId(|oF1*gF1(-SXF|NgmD-~6 zF{isNXT-EyOR+x@e|3b(W)348>vv!Ov;l%*MK5gW%6#jh5t}URwJI>K`mw_2S!JNM z2O3o1=;EA?gzF?b?1L>vEl%O(UXZ^*b0Ce0)5+ zAdI4(4)*fv6c{UX$P8}MSUitm?Vou|XyVW8(|)^PlY%K&-@Bsz9Q*A;B;Q@avluZ^f-f-fPy0UV|1769f zR7rWHcjsM;7p~|Cxg9z^23#*c!36V5F>k@5Wf&gRew zr`ttgp$pWl2BA$svm$VY5<!Hb{t zx&`NheJd8fg)z#G3nVT*KOGmiO5&WgDR=44Hx)YK8;=u)VR6ru06z3Uh$0yMh;QsY zc784o#8_Ptz-eXEVO1jjqjJ{{m@Ef_>OUI$pXtD2u`w8c;kpF?V8KUY@mJ*&M-WZv z;7VM;&5Fe@Vbj0S<%Eb6K+~MMQs&rtJ1Xl(7Z5`Q|A+v%JsudZ`3W)_cO7hBu~m5# z@cv@!HWt91+U_|Y#E8$(lEE&UD_@*6Fn_7Dn!JUu8k}4IsC}?k^2S)LIcac4G;aEA zzr&tIdSb>1hP4DgL-3KGGWhHoTY$G3pxEc7Bjcc5;1jfPu}!ttm-P|EH26(m?40(S zHk@-g2{n*xcb{G!w)2OdDX4Pm%k{;3^y!mus1}XwIaA+5A^;#}H{)EiSEZUp&}!peA2cZb3^5gOy^DIU4d7G2rexEK@u(+v=2T12edD( z(u1&q*jq<5%$TDmms+@80Rcuq{E_xEPtFxB`!ijl?hME^SKg};B{L;U&oSbk(D*W1 zW9?_!$3W(?TT5)~GMCEdAJHLC&b0m)p0RXO9*UtW{4>Mql+l(~$@ZPJADG`E0RcyV zPQ<0Dl+S#`ztR8+Y$P~102pMM3Vh_gTm#ycU3yN6OL~44fYetC5=q{JFP}_&{th(1 zJPgR69zM^f;r%xEYG}zB^m$bTTYMnv>Z1SL({_vLk?;7T^P$Cslv0cIMdz@I+w2Z^ z2i(XWLvsN<)I&}ueXAx_{APR4RZe4`#0e?+jCfWcMeI8_g;gjl59%F%uD-G$jv-Y@g%45FR5-*GXV9TN& zicb@w?k@W5Y{gdP&9H|1)L6NsGd8Qj^{cK~%NfCvP3!Z!8FSH;K6eixYDn^M$<{As zG|oO!(Ee}LC|Z%bt`;f4JwLJ@d2I98~Ab#zNBFiI8Bi~|+aidc3X(l#E zINRuzL03&;LQKj;&pCQpX2zFQwsBes>%hEyopb`VS&M7XfLC*PK1L}4Bs?S<%71XZ zNpp#`ofd4PUZ-R_r_31S?dvJJf z2d>0mp&GU^eU73=l#`M)WT;A4h(ttJ#0Yz+?jKd7>fl6OC!Hp4ouk;B;~6EtGV&20 z9$Ve>voN_w+Xp>$u;<#X3PjAk1)J|RWHhL^M7~sw>gBK7R)18RKICxRw={DuSZayN zun3P7@B^yW#wd#_*S-(6aPhhkEXSZ^{vmVz7c<(e(pXpaF^>rUWbfiHiKD)5`s@re zILM%fo)l+NzWe0i;x5u}d%otB+SLX^0K%9Sc6j!s@|nP%#055@?@IP2w$7psN*7#Le&KEFDza zzR@%AIi;k7IT}B~9ziktE+M{RI0J6F?7mKAVE1Q2!L-E1StC~?hd9S55F_AMS(q@& z%tPNma=EozpJ0_DH#x^CW6B?@|h z6J8dh{NsjnnX486By6HKOa8dW4d$C4Gsw0f3Ub%}OftQ=)xrYCkh`?nCGl(i#Gf2* zeadR;#lc4$=%mx56;-usx1IxVbY^z$MiscG@mbm~0yLRADE9gZhKyv7%m#O4;1ibL z@BF}=k^uvy6RQ!re4<+vTnLhd&QS?Db+nCowkfcbb8bQ`azeYc=$PF23{EY0#+flF z0OuRquCh345;c#jjK@WM@3bLBJV@uAgum8yp11%?!XBaPSn01SoI(F*{W${pkN74P zS$Hy6#eQ%Y2cpl;-!=ZLM}MH=aA5srsm)-(Dk4{{>dZv04MKSJb>VouwJ#=lGqg`C zgwr~&AMIQ|Ww1t5Mney#2TL^B<0zvxo0SL(<+^4=riInWmFarC*AtZ$p|k(3M424_ zXE9Lfi~m-l{1zlm>8t$Pz9Rge%n8bC@3mB@Ykln@A@jqP#kRFR!NK1vfSrNlG*&~u z@SV0mITZyfWU!fsQ*C?=gS@r{D@=+Uy57YqJ;{{IHF33?2p;q9`ERrJv-Sin97;ad z%pktwy``TrWWYs3hRpk4G(Dje>7?d=uU#(e`l{zaD0QYI;Q;v61!yc@3$u(#D+}-jA5&;0V5PMr!FXWG z6n4?Ar^shgHf=ptlbzJ{R60eQ4EG4O6&GBj#Zk+ zpLJKDMrFhr;3kY_#Mqb@ODh-<0$T3@gcaKkzbaEQD489|Vw0G}KbWC^r2q|N&SRV{ zOG6!_Kws~pcPW%nVEng*`2SR`_h;7~S{s5bE$vg8pG);^tYheBhF3PixE|ChLh0W% z|F1&()dK>RUH{SqLZFg{N^xo08kRG3pugg)_)on_{`b9qXvd(07pBXs`)GNI_{&I> zd)lX-9$dfF33|{nN5ciu7i`EhKHj>XDLnle!Th}}&fv-4m4Bv)CZZNQa5p}*bDC>1 z+Dg!xiu}KekAH1Tbn`sue?-ITpLWy-De9-g9%n4V7aIo%VUBurO zNUr}@EDEfUe=KcP1Y$cD`-Dub(v6zQiobAw2uZz?e>tBNm7#9K2|A3x2i7Qv2l&c|W<{qbow%Zd0oOH56JC4w^8%Ojw!CUPK=Yqc+Y5QftRG3kYXaF3X z#hUP}(7q#W8%|k`2yI;Eb#?kI3VI;9{-Z_jG0s0Z?F2GSG0ewfw2o2+cG@_WT>d(O z!L@uqda0CFWr^=y7kJz0=*NPKfTUL|+|CIdlk2$wy;PKYSY+hoEynhqp*1u>>V6wh zxPR-kq4!M&MfRa1zHw&T&C7fIG=<9`4kj8myd`T(z0&Dp1j<0j_Y&d?{NhBKUMzgL zUphN(tm5xlB|uTNqJ!37GnDRUU1VnfcxPO}Y^F-j1*7$1UsLwAF;H8eXUjK{Cdmp) zW~;z9?{IkMrSE2?+XEV@5bX1B69uL)s)znUstw;P>U+IdW81lPrNm-JR}4j36=^cx zFeN_b?(sULHjX*|)a`J@e{Dy`6O<UBa%>A!sw{7h1Djo87Ft(%wfy7fsYW@7|gmfyw%iJiHWtp zD!77VPzuRtHuN~hnDeg+PU+M?852mDy>HwfAxY2`P3K>Nl0v%Fk^s@O)0z4i+hb_d zYH45Z2fqPAQM+I-Xe~4nq^38c`-i2@b)2@NGyfMX^}j2`|BpgbZ?C%fyH)h| zDOOUyW>anFxm*yn6_J7`o7vr^*_R+pt%UBuHJ_X{-dpj1klhgjKh|_=A zP4X12(;@w&Cj_Nh{%M>4XZFkg#dei6Q|O4>N%3o^%k~8vE;}v@DP~!Kgt6_Mf-9L@ zX5p=2w={`29331_dvh}~BhNa@l|insjqbNap~7#Hm+@BO z*90rk)GT~YtzASnKFnfAqH-0xr`gZ<1Z7S*tY`2)B5&BY*mn8V?L=h@$?+o~gd$eq zC~OYQ0Jc<(=m+xfW4GoYhS1&n!`zRvhd6BaH^AZHikkqsC4rD+58U5( zzdN%tJG-;Hv&(;(VeyQJ$6we^Q32t92l=I`&4C ziKM-`K$E382e3F-5}Q7L%Typ_>JChQBk3!9f7+iN95fuYsIWu6oBh2z$g5|6w&`wm zgry0gBgzI1(#5&Zau8zW3YrP=p5WXdbBDWgQuHCBIzQhjh1*!l>wYBOY>nH)*$myo zDP8718}__%-kxa5xk0f5wV1{;m8#w2(aI*LSS!> z1F5eHniV|cFT)(bqOQDiY13iv=;3d&zSt<9_h<)=zD?GaElMb5rd$w_on(k0Z-{sH zor02P0BE6q))TR;E^R=s*8k|-gK=RU@DuCz!^I|~ljCJ-&riy=sv#!yI@j)$a?!EA z7OZ1lZ*r+I^;i@nylrnPO6dcCGQk$9G4@gGj@hA$stLR}Brk9e2d@H)I24{}F$mIX z>Q^FrXKj?>_*g^l-ofty^PDS|qiMiV{7$Zk2sKeh4Bmaqm7pO=cf_Skbr8vK-%SNh zg6Ck>E{zVeck^QTko>im=}&)3Q`;J1WEGp>2&Gf+r+Db^F0qEfjxV&)_o<+hHw?W> zx{7P8P3Iz^X3wL^n8WkvF~qHkALlGM2T3~u!hm%81<&oMTdL}u_(V}#^d;U#dkQQc zY?>&*F@h5x&4~c0V-Rs#XQPC&*>`bmxwT4k0Gbu^b}Dz=Q=gY?G#qDMHS9nb}j< zfG`2VrQa3iAh^b32+yauLV#t-7krh2WLw$CT`QTrbP=FFUIu0Xx)Mpnh2y8_|QoCA_I5&V@Jct_dhW3~`-wmnXDH_Yf$+B}DOU~EavpE%5p=o2(^Lb& z5FxnDo_hO-0~K&UR8}#R(N1O~84~Yge@Exph95^wrl}V<%frPgS3mDt@MLa$v7s)S z^v_bn<}MZDXA>*fY|M+-9PQ4f2ls;q*o1F|mB(wiSMAmPAjo0_nms2rLZsHsXF?1B{RZu# z-J|rurp%ojz^u8ZPB@^<;gfnncLcKN&#>&n%#V~!8a4qI% zN=sRK1j936LMMHgXK-!;3PO6dlD)%33Z>A_@ALu+vs6>Y$kVkvKY%g%8aE^D5EDo`jm?+x{|ZS)$+8d|5a9 zw?2GQ7WO7`&s2Fl#V3>bD%6wqic|%k9K|*a3X#<-^ZQo*?#0E*)s9H&=glq+{0*P{ zomxo=ilpx)x7;1x```{5Q99PO;Ra0U#>$hxiqwBOtLRxj*f)+URW0^Pn>>pHt_FdY z+CFZuYO*jDWv2N_FbjHW=jeV|?~`dQDrRds)@=O!vQRy;N?_dMS5Y>kG7~%VI6*!t z&0mm+#ePt&Uiia2-aKY$ztz$&`eehL$3Gy~5`*IJ@A5t`va}}jb)5>mr#+^|4bt(q zdR&DHWMN7g>jrbXL1-E0Y$;Srq{_%naAzndc#ncju9&mrc#xnooOh20Ad8H?hmWR+-4fsHfnTQj-@01btB<>^}Ekb?WT`s^ik;hd5z z$d6L&Aey151XZRPEaDP!{9Aj(KrGKPK+2M z($XNQU}|rPuI*Z-ZpT4#zQ(KZh&{^ujCi^Vd3cArbx7Ig-joS@@w|HbW;p#M?G3f< zFAupjIkCd9ZVKb#uc5h6F!h%c9dm$|1m$@aajdN?6&etQ&}%r{VAc}5Y=fMEWglWn zkvIpEM&@HI+ocfie&~t~m;%;~gIxwT7jaXA5tibZ*I1x~;7=HQKOZqNfQuEYEx&$n zOdR#aG8_Nt;g(hP9eIbR^$5?11|#jmq$$?s)M{rWH$K&!b2iqGSDPx+2WxDVpz6Ed z*mCGZ#PSku9lR(f5c~q%JmQuQl6MrL`H7PJJUVLdDu$cZ=0DgsyYMi(WU}j6#-+VN=_Q$b@ z`VhX`Y`m2vYT$YyK25jx`5M&Y1yRAjUKbO+lqDhUwAmdJ;lBC=J0y+rF5>*8jE?@i zlG>a1JerujPQVviwsX;8j`^odd=bnQF#RN4GANry%+Ka53+2hfFK$#SUI!~^33bqI zea{DeUW6QTh$6r6__4YRv5oG5S@bCF;W`!KtMpfXn6s8(_M7MD?@dZ=F001QPHGQ<)l?Htw9-TNVtq(Eyi5jZnH5?2TNln+W5EdsGqjiQR%!6i^o9?2s~y zKd7PLdVqSW5>@-(>7K*a2hBN)$1*OR8H##Hc=IW3(z7l4`n2bKG=0dN!vayzM=k1+ ze=(bT#yehMHvP=OY~?5`4+a||GssZ3ZH`YVbYRfE8GZlDVY6qru<}1&KKBq0Nxt^; zI#|`3oB_%)L29VJ$T$U{Qey`$J2~<(r3OTl)4tkL<w2ENIhK;98{0CuHW7R? zIVu;xmcPG}lgot<2*nJi4Ur>*=LEQ~#*7pLb+?v*UCiA#M_m3NtpCHDqT)jVYoY zG6AZ9{?A1-uw1Zgj*6=`5u5q}G+Uh-S~VP<+WSY>I5{J`a4)Q9RPx6+TSYqs4dC}q-4Dc4zED3W)>zqag_ zRl6wKWp9(F@G~4P-wjoO-)v!@rP*Q@%^)rp@V`arY(1P8Jd1wNEdWS)@kl~rM_T-K z%Y>uwjQS`AM@tG8%M$1~6aWo(JzwtF3<&GvO0am_$TaK_lx!CE8mOiFM{RZG;B$RCW%@6o8)*NWoC2;{@AKLef6lF^d4BRu!bN~RxvIo zS?5Q{X933vl|T>~nWebMHu2A@GoMiMzio;0nLbauvaC*2i4X)UAW1t%wROT8k}w~N z0Hc@)-6ikWkQ@0|lTO}CtU=DOW|JG9tIMA!@daJ{=^m0JJ+GppzB;B5k<;q73=u^f z&iLsAd%hKWkF|z8>TvBXUJ0(TkvBNz@4&$nHLP{sLEIXxJ~MV4t|+vZe~zz@TmN`8 za~&QOEYoU+NH%<*h=edi#?LMsOFJxw=J?Z<)ZDUNHQ`yvX{VPKA}O-68yS3to>6?u zkB3ezAeRp$8HS0#duY_=ptoLL$Z*Nr-%)TXudcK{yCo zf(G#;YP;^@Vrl!yMvvYw;BO_b*s8C5!+a-(@2!sqZ78T9zp@}n0ZKp}gavtr7M_y0 zB^!Pdc@bq~otT*2og$j~i>Vh>@1dS*VsWYf*U`-$uz*kckS6paLxk2oQ6|Uy^A0Ru zmMDyOwQRY4-=+A%SFro0K9#QmQTS^4?oUNBvp_W2ShD62IcG>y+P4xBWNLfO)M)UM zR;Q^m7bW^u8GWF~HR$?^1}QV5&R7cd%!Rl@Sh^@^(?45m&&iH78HASj(*F)AJu{Fe z@Ut!_L__#u$l5tHr9~I6Xtf}d*U_wU!Dq$OwN4Odk1U;AC!%sU9#SSJAI#VtNjT8a zzd^ni0t3lf0#ZEKJ~s#y50IgEurrR$9Onpu$o(8Xqv}$8pU0W})25WjZ zr3bfOPufZ4c`7m<@QSWx|nj2Fy!!sZ~AF$IE;U!Fm0c$fD(dT=UX;tJodbZ@tr^i)TQXo2mG01w3hH=+YRE zJlsUt^T!l-$(Hh>c_sMdy{%TmKH57YMO43L0zhY?3Pmc*lTS&rQ5fJE0|qvr zK;Y8>>U~76iSQI>_PR_FO&OI)__JtU3$`>{2F_YI`cLiFAvXgnKohq8c~dO>h6RU3 zO3NPtE9ME>Mk+xLu}o6!ymZH^od{tB&&FLw@~X??0KWW&*O6+w)_F26_NPJf*$0;a zV-;A2^jT%n?axSeOqx?qqf^g>l&_TLqsC zQc$24I7WyWx5<{iZtb@@1-%gxK&&hS=VJh`*A^GkacMZUS0CcETWkjw~^(?@O{d;&k@G#xP(zfr5OQt0hHHBt=w^C8%fb zHp@!G#m)<@dG3%G9aY*7S$r8SKCR?S@09r(dx&|+3s!AheejAfsPI*V3`FEHC(Su6 zcnKL8O8^uHL}eT}SECfdMZ=-Y5z1z_N-6<8JQ(TR%KZb2azutI_yg;MD=1w6ck`pT z9iOQl_u$?`9-`WaIj(pLAfNrYCZ7D6j%CA{y|~7pOxJN?6S*g*UA$oUqy>qCxb*?0 zo8`zc2Oj(xM{!i=@M0Xzkwiw^;`(L{_GV>r!~w)O%!|41e9?Y3aQ;b18*wkgI|vj*q0ARXL%*OHZ=N0dpz zmR2@+n%*l+VJ`t@H1Kdo1791N7Ll z*(KE<5twL4mmJIXMUqs?8<}_7 zRVW3sC8Fv4INUP2 zkz`M;OjtiwJJ@=)ql)|;rWLkl>g)rZqxW&f6F?Zm3&STb1Ubf~xb-;5}9crd%NJey-A9CPvrUgXUv*gD#6g3Z-1Nn(s%lmGnU1 zZCwsJSxPv#X(IpXp_>1>bBma$J~T30rPh=zz3W6xy+CrPH$Q&g@L+b6(>&L+DA6l_ zi&V5d(a!V7+Uv}4lRPu^8gld8yx@9d^ znsa_%9dXTljmj;a1T1g7&eqC{SK?NuhlCxgVL*~hG29ph{}JyiHzTCU0*8PXnHVf;&(ECVE6C$;W!`F7$2D%w0iJTt7m9k6)ae$Go4Sv-j3y& zbBb>)YKU}j&qyAat8cjGI-4}4G8m2K+^+`Lq#c-%`8s zAAWQt?PbtyWKb;{uFd@2%kTqZR%cp^6r=VgfI+~lN!l)I+VsENRyLzj-NYBJk`l{) z^A+R?-8iwbFqn~oSk9uf976C75!ui+p=*Nf9z!daXodOm1a@3yfiLcXithF{gNORk z6*61=R+Y<3{ea;`j#+NbI($T7zLH`hjoQszZTp~EFlzwQ5;th*<1XW!)NK0O|G8`y zLuR@Kz4Ts2=kjBHuNzp>f_N>g8x0R-DX;wpUgy|W*OL>c1!7b z((p;apqRZuz?3iD@VqGaRQOM!7=AvMZEb0Sd@ca+K*FcoX9*^V`m8$cZvajnvc`m#?d+d_~Fexn7|4h3Y+DtH4&`zK@_sZfo%$lQG z!-NjahIE9~16xvB9cB!oqAr-8H985uwsTXEUEXG(l>gUz_+*g;QFt{5vHW=F*JCyg zAW8mZ2ZDk}DhR$QoDMK9XVBUTC?xmx78wCb5vDuYt1-=}=JyO&*AS&1@4sZWrV#PT zklnCy{1`p%aP3G$HvJ1OmM4_GL^0=7s0Ke&3GCWtY~!JRbfSQr*9ykJDEds{7*Hb{ z_cb(jyb1wyKmpItS#WQPdiOnIez76dqA$`k^HXA=Re5Lc!n7n&7cBq~&PDZYm5R;l z?eL0Pq`lls;-)yFmF&fpqUqMOchq9&LJqFGyL|WlMvd;{s@1(}=ra($i2KL(XM+nQ zfX=|hx*``(v>*6o>BUc714#AMor}k99J<@v`3$gjI6RvB@*qoK5tN{Fg-Nco&>)wt z&Ekw&Tli!?^^aXZEK3BXO4YSOsJx)O&`+xcu_q8`XtF@1NM(rk`?1b9nnD-L^all4 zJrt*&EM=Si%iD7_@Em;dJ;_e4Y5YEpQBn5nFf8J?@O`x>MQL2zda)R24gja$Y#APH zuUZzI34vWJwUsqGgcx+bvIn*lOT({-AO8g_vB9*u(swoi z5dsQ?n9yli7(*{j0tARkAV3I_K!PCz?%MwKpL@=^|GjUFH{N^qj(eYDI70T`S$nOy z)|&I1-<)&(aoWLp(+@j;fWcs!5T|~3g2C2!!(gj-uU`kQ1ke6r3qDLDPF#p^4)c$| z_=Nkx?0q7x1cyZg2VUGAFYh;v6S z14!^qo5QK9sA(=Lh^V=#Dy#WfFH+uPwOv_yl?8qAC{qr|wlk_1X{McfJ6v%uU#>b{wirB8k9{)+F6-yp>NOlPi<;hEIAr(%xlLJ4;qLHOM5+z0J=d7~ zYE_bcgK!pFfLG#iuHPS$8hz~@nI2KD9fwEn9nF-(2M<#X-D-ogd*dewnS%jx<32nF5%Hzrm*oBk z_EYcelr^BFnZO;}Mk1O@qIzTF<8eADdjc$zTS#Inz zVR4;fRuIK;X(mf;nV0$==0y9ftf{qg__i1#Xj<$9^pUD!&EvS zU5c*lEct_%Kk{O)v7hLw=0g2)^QEgZG(4N>q8dGq8)P_#Fb7ge61BVS37DK0??)AN zs>?C2;!1b{7HEm;A=$x(*md^gVV|?w6;>*cI2HE%@2;hSn~OFUnGn+7;fKuV!?<1f zmpqVQ{>gBIMmw2yP@11-~Yb+h^=pcLvw|dBVt-Gy`kRI-dYpNi{ zlimt}w7f4-)&#y@ipCkku};*DUO3JJcvB9i$wn(|KcPJ2#bNNw_H(PUK3=uPDdVS4 zIh9t3WwAJUkCG={qta_+&yY^ut9 z2dv=|)-COA;PK@A1K!E4*0?2KTxa{Cm!ZT+7u;Q(`&k<-3YVryaban9>JpYBi6Zp< z^BckQd;i>_fBYF^@gb6zFdy#n6v=xOxM`@aWVDnkCVlT7*iKF;*?C;TxrdL#V13}!DER!DDGd)Xp&xm}*673Odugf(XKf^EB*w;_7ih%1`S^bU!%Xbg0aYVY8ma`DL>As zEg3=DNSH`oqa`t*)jdO5+3fuo1ckH%b&vi^V&vkN{H^DT;g0U#g1}YpZ_sdneC)#Q zt>=8#oq=Xi0az&ha-Hv@>grO^DOmD7SaK9O!md)~)K>6S|I!-$8dm>-xZ%I2WN+dg zgp-YH21nC0C*kmIB-C#IdhhL+^SHfcgO(<@ze@*PN&a2SPh_59@hd&oG$+hXIEmf5 z7Ca39>5T%Ond!9Lm>%Bgx$QT?kQK3rfTn<@xiC*O1y5}E*}u*M7FYc%T~A#?aB5M) z$LiA)iX%~z%0a})*!NB?vk28Z4i|53h+jx@)RgenKHmX;ck%|z|8Tygcssc%J=_+D zyL22kW}qz{fIC)@W>7CJp{BKTkM7*mirbaJ>9SL69LnIVz_G!^NbagWsBe4U?$CcX z`Uqi}$=QHVE#Qm<=Fc9)%K|-~%qt?6A2Xsuo^FLE?)J}b6#hW+z{0Q+g9X_I*HkyL zq@O`B2wOi49TdO*mF|Kf)~!3O3);N*i(K1-7o?Fq&X-s6;Jf~j_laO5z%FOOAnf{| zO^(L@b~pR)0O;EAnlmUvh>fVJf|0Xa_v1XuNQTd6`-PYs^G*)i)wngPCyXrX$1H}u z@Qu|9;d)MaO;tu}+~t$(T^y}=dHlJ+iz_`W^zb>0%9uU#PRxr#X(fX*n4x+dve(>w zxw8h!z zV5+R~Ad5Fe9i?xAMvuQ@$vQG0?g^*lHAfG3HR;LWE2_x=^3K@aU@S z1-V))SoW7tamX);UX;6>J~ElGR76T6J4@1mqoS~00qkI+|*GY*;Vbyj+dA7-=)l`F}avPdfPa8I;9vO?KjG;H70 z5S--6sQx@(*iMbHs3G4_`p_Y8&*OW=uH3BEU@M!o*H%o%houxsaOI}qJ@u$*vIW!Q zk+RitErNe{R(A1$znLnYu!0@9Ia$j(%OYfAV?5Y)i;@8|HiB zMLO1E1_R#n_>W@)ae*6 zW^WVuoOLL&h86MK-8rb2mjCGGy;3r(_j@nh{$npy6DByL`*!q&Fu`Qq9iCW5K<3Z; z`g(8h0o(aMagqOjV;P9sBjgU1q*s~J5r~;w&Ih^gPI%k-+yJsW|YPV~`C++oQ+CESllC6Wp}P;nTd zs0GOzncWdYbYUV_4$@_~*yp~I7)`9?&qol-dFKc-Ft2bB+9GCLP_jBCFLhwsb5f|I za1OCtJzC2CjZhv~bP_zc9kD8F&l!}g49TktB1)s(XT)$l-eu2R&egd65ZpYO2TO)a z(%{AsHTkBA5MJ@#2Vf~NG6A>WEGJF%cO7u)AJB8#{c|ckk-N`w zruCzBdFzYg+t>+K;-N-eo}>9yr_LPI^lZ9huTxxnCt+!>F# z7HfWCR}?uZbL>$X%PMf>%urP8LT|k=ehax-IM#~pJLl3~#y(REy!l118j|_lKghwnCkMzELn<2CVg3t?b(%3j8G2qBo0N?>pOO?i zruxdDpJ}^MOo>}6l`@gcV%0tg`NO65JKYnvvs3uC3{`^z&yl41OR`4#=X0*Ff;hFq zN2PdtTHb3Npxq6c98A1vsVbDksWS9J9ou7lbs~hV(}fdoN8V*iGJ_h^-3Ev?pJU%} zxN|A0_!u=4iPhMfFI6!Mp?lg^ZFzI=rBllu#K^$M`n`1C&I`;FX!i)T*G4&5QDywK zLZw~ny~4k~a%~Kco9Q=`Xs3>$6m4AxuIf zH}_6$Sszo!8eVty^W~r3+Y$or9ENRcB9w(hc^Wz)dbel6l3 zo2)gMX()SUStz%a?5{y6c|)%!(GEgSLmPo7G0 z5giqgIoo5Fbs^UWUoHsP1B1=eh}P!MMekmS5QvOS&Zl#TEhWyYWmms_PT?jihh|+d z=_Zm#a^7t=3r;+Df1O<0Z7qVi1->&}auiosK^p1pjYbjO#;b<<`w!@Pt%6;+B-ZoF z_edG4GkirgoS0cWr%r(9XQsmLT5GBEn3bJ6DfMjBx;a@Ck?inZ7E*w?Ty&yysLaw*7yeRWOuEx zXYiE*PFL-?8Ez>UxazC5AUFHuJ(&OPM^34Z!av~FwT1#);YJc8LHX1fNLSrhqksHG zdU!C-QNyHyNYTLh5JhoGrQd&thxxC6@c? zOUVqRmt`X4XD1%`?(xlKz#XSAOXG22=}z$>n`I%Dq(;oL9|VqohyKxI+!i;aL-%Tb z)l`Oqtn}sH4iy$!LT)_pQp7Y9UlI_R4na+0U4E{tPb(RrgC5l(*7;qvAs%#+8Sfd^ zWmmn9-`I;rqVz9mUS1^6c%EogWCk&Q-rl+^3}az^NxsR%!ILc%rC4*tYhB0JdAx^r zt`J?uhPesXoTPWQ?iYrwh{NWlq^_OO=R3><^;(5xk>L}DGX{kcJJq=?n%(6(VatB> z(k!jBO!XxG0O%y_LXCK4I5#a}X%y|boZ%KqPqCbTYm*m)lz*iQHr?uOfi=X`~u@PKvcm&*YDqkeef z?ENrKE_=z}!Hi#1kZHlqLoa&$*mfOOY_By|1J|<^&f8g^8ykqHVb&B>sa9Q0)1^tEGQ$_MR)*e862qbs1V@u(;zgAnV zd`+=x)3luZqau??8n+N&JFid142`N{|Jtkik-1o;v{&s79BYfyL`lEByOjUqt%*|*2aJ19HEMv;&?qy`hPB|BvUJ={j%t4#ET}s+D z4>x<;XertWt-`rG^rX1@Lv#d*!7d{e6mpa;B1|j~VGvb>^s1_`W&kYjW%;I>Xeh>h zTCHg3b>;2a0pP;8v_>TxcE-)1rAX!Zpl7SI;FTTunLGO6onYaYGN@sxvN#H#+lQF3 zXcB^zbdu#7kRSPp>rodqwlvpS7@4hN7R6bu_U+B}$rP)l&lIkOVBl07kk=;AGkJAM zY0Zty9L^0SZzVr~oOw@X7JsrmZhSxujP6v-w69uj(q_|%H}e#3ff?^0w=f|-YGw%Q zy*ORn8T!m7f+MXgZxZ2m58pR5=xN9*>327=hcrL;~;^Qi(4O< z3PO7)TlQDB1z1T3R`)o@4EBV7*pqSysTSdzzB+3!E54Dypq4nwDn8_A?!GS+1N1j> zx5vRu2h+gIPdHwvm1wdsgmy+*=<|hpx7|IndHvw(%yY5{fE9n;(iu&8tR{or1swZ~ zF|cVF*C$)`n#{ev{n@K@peF4n^z!+_fgSNr2%PULYqR+d&P&&j72j2XytmS5}rDkof#`@X8BIT#M-o!TfF5a`8AJ{>R zCn|m)VSUUn&(lOU-=k<(h#A2SG{h|2#CDi)^EUK!#*EBudva)^WuLshlIFl8Ga4$J z`OwwIwSdzcBJ*%Y$!1~~0RaExz#pXyg}3TRx>777yrY3ZGFe%gQ?#?1II7s9Rgza# zBzkLi(kO9PPUpoi{RoOyM+@DnydG6m0lU*{p<8pDY-au;mN`yCx>YgARsF>B@M6QZE$gj<)-+pAUtv; zyx%8fjP4jOygmu3Gs$dPLf)k>W7hj!D%z?GWz)BG75xu&%h?e{)re(7V%IYbiW^Sv zkyCs_dR0{In5|YxhSRb-J-25tV^o&sjB2QQ^(VA-&q}j(0G?@$VM|ArKFM2sD6tfk zuI7Waem?c^m)l3v<$I6^CRS~`&efD$#Etg+;JDd-|7q-Tt+9=W^W;u<8!Q&L1!sPc zZb8dMa|iS?;gb9B%Yb&=e)PNA?LA{XHi)XU$JI-R1s!}cyFeygP|Ko?S!Yx`{ zsYftBKyNCET-SE(L6s3+5Ofw-50=8ccB{s=F+{<6)G!pmSPX5xvlZKcF2EZ%Oxh!G zkPf7b=b$#5H(eclpBKq+P5Mo-*svR{R6rr`r8Ijz5>b%^!Uu}(zM zEZBeBY5|d(_z^u-S~9-8(6o}?Nm;tx9K_K(I&~H$yM%Mha55@3>}Z`tq&kFO!<|Ef z+J$@IDm5rC8H-7BkAO|ruj=?)wdQw40f8HrZYR!@Pr=It-5J$`%x?!4p-jp<_OR8yDSwvb_ZZ({bS}h;;Q8*>2xi>$lSIWou>DTvf`h@Y^u|n;X2r zuuhDK`Oh|Ak@>yh0jKhUQwMeq z72c8Sg@+HkDjCmZ3ItM9$Po4p$`bliWfRs5hpgHzU09t(`Awy#S|zZdLlLxeugGV! zx9Piwp%Lr$Rf|6ImcZi9-6BATcQkw&UG0|8;Oi(JiRM2O+o$NZ4z61vNA%3(gqUe8 zW4=6(RALOG?J_k0GxK^@9zW=4 zdeN0p88jm(WAcXz%R;la*A9t3;napqO%h$P-#nbp69db)dHX2%!GN|%3+3uS7~vya zQ56M3KCSqAGQ-sO0buI$IZF6?h@Do=IS)ghCyL&iq<3iRQlb)Y4RbTxI%E4~?>fNF zI@hjTmiEqxpQhf@A4aJ}$k44Z>IM}U@=!Lu4!^u*cpKAv}=ugTH07QNmgs( zmNL+^c2>C5kUc?7yA=Wc)*u;5M*4ZwVrhd8gH`0P;hZ5cmeY0` zUbN9t8tY(#Nw?^yMxtti2XJu8#FM+>?{@^u69o{;sf-=lkB4g(PUE)aV7d{SkYk#p z>$`7ykhI^$!*Dk3$6TY3mO&DVJG_vg;}$;Ktd!>H>5dvcC~!#m3*=?!dpA8aTR|s; z;0$6mdEqVvXB+gG#uUR%_R=dC%1(jx<{hXkf{kO|FB1`-R)wv}?~i3M)9f?gE2shz zw6c$11RB_>B?-5S$(V1njEIS6b<#4NRQppb!REUij%(i)fELI^Kkkz@zXR*Q^1Jk9uiO^d5ccA zJ;gD^q>6-!$jG{(i*ds}ftBRy4o@-Dv({$|3(RZ@51Ls*edRr#OGAE~07U!|RsQYy zn31g3tJExVK-K(|hXQ~K+3+|k)s!=OQkN~#FMzbxT&j=cF$$zvGnhJ%69H_aIXz#n z#ab;c0;0Qt(&dU-g8IT{$bSzW_#$Ho0AcFR{T7YykLo!*QIxcEQfV7x60K`xL!F-` zEUD>>G+Amr@cVad|9vwoab#Xv*LybU8elzq!403^tyVI;kmo+^Lx+b9=um^HGCLDlFsmna$pfrs6RB{rZw5r?ns@IzvRsZ*X2C;x)4T(EKNLwOO>A0d_x&>MhV4Ik)-xSzzI zjH~3@vW%Zaid(O0yCo1I$rQu*G&BP*F4tq>WUt|lU%1y6dH?i{PzO<*iK64Jd+ zp_VddzjEXdmd-YkXq+M;QhBlrH)Lwmxd&mSj`c=fZ;wLh&%KrQ^F zJPGq}>(rE7##wVaX5DOX&fPz__;;(;-*a5C#s!GTzeA5{DT{0$IH4MJ=&pznOU9vi zMEAEH2MjDsxiQwb>L6m>wy)~oIoREy5Aynm{Q&ni9;W-^VlS3c=Bl1_SWC?d?SxrK z-rbve;He{;nY&4)q0v2^1`Y$`XM%20qT+@xMFD7OjgJV++O?=r+;)S zAsyJt5Tb~^?b&JYq&MQ~1}7YtVy6~1T#{MyXC}@pDbuulZ^&GJ0cS5Ey$KM1o*xFG zUDh1N=}gojXgdm*;f|;F+f6E5@ONUx>moeUIiZB~5(aW%q3%4?Xy8r>Oas7}odSZv zHOfJ{JZ_2G-$}k_fjNbXft%>id+>Q(wZS`hymmyq@18pl{kg5_yStE!uRpvcDN~>> zAtHHAZ7(M>ae0}XgiNP`yz`&J(}s5ZS^qj+NG36Cm4Azahp-zhlsqCn9$_^69p{iQ zb_8*bauAl?K4x#uRO4$gSlZCX-E6rDrle{iw02~{l_5yP*LW)>1ssh2Cn)f zH5sS{4SG$dP6pp*VO$uKD-c9e$MB5C*Ztz$_-Pg)9onvl@UEls>IjIUlSyODeu@RH zNEVq*Bcde|V>Pd^9l#Vc2wz3?aSJSBaPYG8#Hkm8=1|d8pM1PkesOe~$8<&0^5PQk z=>jSMHX4eZYp3vQSyiCP;Qp6;0BrYw++*%vTO8X!WAYxq*`dCBK6a>Gd9yHo6i$M~OOYR~;QZ(lOxHX_VedLPy% zJ9axQZrT_7=avHfF|;6>StnfLS|xPERUf`Ly}KS#9IAz8ok+=5Ty^vYulIdOvL>L^ zLq>-+vB$LhdRK!`abiI}y2FIRY?yhJ*)Z+raZ3*oF6EQCwqA=);5B5<2FGj1R2N>; z4Bf|m7sM~r$y3emvcEfNIhTOLZAe!GqRaMK-qD8)a}X{LnOqT2;diahL(uE@cquu- z41#0fbM3~ClBE~t%Blcr-O(9}z{N?$hNp+;8<)oP9I>Ur#-Wt440qt9%5-j@2mpSV zJ@AF(HPj}K7Yfv*n5BA>eNQ{&RQw(mJE}Z4Zrp@D#)8<9A+Dvd+VaP#RhoW zh@a>2f)u)+FqCnr{TE)5;>hwdLkj!M#$093aQ<@r#9lmGLD6bX-8GW)rM3T`DO4IH z#fvP%$s1w(_QH6d&YQJ9G55RfHb>aaBU>W`)x+QZkvmlr;3PG0@O-!s=PO7zjHqs$ z#VYkg5n|AihV})FMW3+8RY7{%O3bgiYM-I@F(;w@4ugFt%`<`9IZPevqVwDpSeGxf zaV?@A$E`@&on>CKsSP0u^<0a&$BJ?LfHiJOZ!HJQsiYt}$iYI?TmSWzUK_;LK1_>+ zhsDq3DsdFD`pR4adW!8OVscYHT#}OTEv5eRrm(Ci7-zD^E4~Jp&|O&0LkeJ&q{#hA z#w~Jo(w{zd?yZ)wJj1M2s;TTVtyN>88yi?N`Bjl1m#k{>HD2j;21U887S*B7bg&Fg z;Ts;)+tzd7V~mb-Q)krZx8o(tX(I=eHOOq&chfxOLd<~>7pie4n-!bDbk?!TZ+@vL z@@0>Zd`?s#4Wr8M`>CT`GRi#$++fvFch2c1ha55=UskQ_T3)PNF|CW@o9Z`!__h1B z&$lQviweGKJ8*hh{QT3{FIx&fWCs)s zh=hP)!q$ckM|&@F<7c>#w^1sHJb3NtuKiw2@!TR{*ys3H%G1Q?k&4_C8Y2d40)0BpC(NJ=%|0{68a5ZlfW2y$1w5Q;>6vYJy8xD8M%Y=sQj9Aw z6brt6WKy6Qj_EYJDWQ8fRaNrAdh_#~ks;X(rtk`CYNPJ!-Q(ol(&Y|=eW)F)qT)>-WH`d%;q?!(y}G-WRM5guRvv@I?y zAqLkDRDuMgq%alcfc1P}x5LyO-QGLfC$vm z0JF2LZ4MV(pB{dUtdj0DbH+w;0mmha@bIt+-+VCS&h+M1i?cn6xTWL(;r9eiPI_Qn~{^`d!7P#d7wdfxsQt@89;6 zWXa3b?acdd`MFTA7vh5$e|Mh?nVk;PMt9(WxW%OvvwE)1;a~je9%=Eco_ydhxS&(QOB4c0nsZ(Pcty47|STt15n(GKPugV!{Dw zI|j(CT}K}E>?d$2fZ4k~xxeD|uZ1g1+>*Qn$P`%uUtcvVez2z?^n}hB8{i zqI+_M@h$zyMDT($=mfihWhqer^UihFH+I2Zt3!& z_1^D$kqaP7dKjcA*6RcG65RTif#;wa>;1T742~%yEhC7LE4zcf4;m!R(iucIbY;wN zS($_!!peXYc9U>_6AmIYo91ns($xTIv&qGz0doDJ9GdHsbDdUJK&clT@{*1rXuHsM z@lz%7K}V;2U#`(t0SFXD6qJ)JpE9D|aZho1>6cpjN1ws18OSu{T&@R+LAQqRu7#PX z*BL$FmpN9sJXIMEmTetxEVPbaThH?lv;f+)aA}w=^GMp-Q7+TYato!XD*>Z*lWVm= z=^U$L%YgK`=3Eok##qU*M>N<5Z-2mVG=lUwBu|N^$_7y({Rt{~PjVjsB6DAqnhvuU z8Dz-6<0>wHQWV~{HvbinGK zQ8>8RZ|~GPG_54`)gg{R_oF!vt#z-3D0FnC$~oImF1}IkMe;1<-GR1y^Wj@QTav%j z&R%N2Su2R;nyoG!18j6h;fkbCEi3-*M$ZSpi*&qJ@bO@coE>fy-wXFU!t?sJaT7rB zC;J;_A}gVw^p^Lol+Vd;hyD@+jx0g-Yoa`-X+_$k*$2f7^}c<2qcF11{l;1OuVBKw zVT=k7GoNUX!oK>&rNwIZ0@-vxK;MgV8{Ic08BNRCxxRt$`|1b?icxtLxm{6!Pu%o8 zhhx%-bvR$175n***0hs7`d&BY(HmNF*6L~3d^v~Smq`>IJV0G`$OF(1hNQ{s!j zTUJm{3Lyv}Ke*X!`lb1|w~DG4?14BShjYPrAVF*F1H7q_ujKvsw7Z7ORWd-vbNnHM+*8hcL;yqZs6Gh{hbwRuE29Ir0Y>v+W2p z)v-c+VVh5iN=AwKm=?DGooa?b$Kky+wS=Xca*>q)6#~-5iw-U6yqFmZNDE-zj5i7$ z$}$Gwtc@O)cD68syG|`F@TLsT7$UXhuTSc}J!~5xNb-I&7QUDlBe*nO47)yuTZL}Lgsf&F&yQlTmSXX z*u|d04zAUTBvw@B)jrZtUAQSSvu;S;F=4T14uD5Jplw|fgC1j6Wf>9Dt8w+Z^wkF= zzlJ;Gtn7tKBnE3+<1w)BKSu&DZ4A;D1;X?aI0BkF_jt(|@MIeCpu%E|;TF~o{IemE7R_D(0%G{O}}pHqpO|2R+^!g_{f zR%A5@#FA}wrJl56`&`Id)Z(&%vOAW(HsZpsVOlbfsTqc|vJ>{s7TYO!iZx6$>7ZDL zsqo|^FwJX~8I`RWJ*xWrIWuwZu5ekKTpPl=W>r>)#3*Diy{v*7)LqYWdE_b$Eev~= zI~AE1(T#9zikBt+wyOA`lboH_m}=lKX~Gqe;TH~&3&?;6>BxF&XCdVD=E{ygpi$Z( z$@pbLjVD?Ps~fdSI7}BXk@uq@LTSG5v^=1z?<3;%=y&nubj~LNTyhqd$wW3CdH^N- zCq6a?>Pl;@4-s!hU$j%Hk3B#%@U%31uuA#7jTXYePWS-uw$cU7?jN3PeooT8ztIi+ z%6o(IAWVa3n>t8~+M(zPcowkB(iC3So=kUQltjMoOroi#Y_8c2da#>*MP7e&@OV}T zLf1z4ms87@T+c9~D8Z;c)xjE$m8`0S64SGq_pQ+<@YzrCOH-?e)f%o7FknVt7jB;1 zlUSC|C;Ha+Je&J_GpO)Ig zdd%HG#9?CKj#(`m+tND1!o$tP3NLpTp+NOjR&VXa=B^Pq_T$T2u0Ime?~cEo7;oIR z3Ub&bjOJSRE);0-JSPa$jV?nIx*<`G>$ z2X}eNd#!j8Vg8+|&luCT^!sFWW#(q;L2px#4Kw=8_Q7V$Z=dAO86bam2gA+_e!W~q zY<(aWpz_z^IfEXC#?vpHHw!l?KSB^vJGl_&6tn-+3DY>3;DLZ#V{+q!j>}EUff^3JLo^8!aPfrG3vravUr0!CfkA{N}(q| z#rG*!>8X>4JkhQEIbH&Ld4M}$gf;&NC)(;1=U^mNrYJUlPAAH_Ar&YbKah*I(M!R$ zCC3mI4y?sy>b-}1eg<+Iho+7qRS%=*sJLjB(#2}(#iIoq-vjc z>wFs(ZWDo@INuH?g8Nb}PB%q1fgWgzjTBd0D3tAI`X{)Wlq@%jM;owwc}(Zw&X|Hm z4TMJfLW@h|cSEh&UN@<`-=AL>*Vr8Tf;n5TP_GH12s=-o(B1~}e_H`auDaO#L()OL zRkoulKu{OLGx&8DW#Zw2q4;-s(&xt0?PHmO9KQk*;$UDSE3{4rO2&W`rN@DPlROdy zL6@RCiKVySK>O-{oU>YcC|OjDBBb}=@{HPh+={bSnEn$p4I~CXP7A6YJ(&gb{|4Nx zvgoXp%7AM#zKHMeP~Y)C)3;fBoKbO+MAH1^Y{SteC|J0C-G8i``#&Ogw(IUcAP9_M zG5|7XkX@!!Ll2Ln{wVv!8T&{l*2{*I6U`S*HKK`r)P;f2Siv}*XMi|?7c1PX{b#WPFk~4|$W1an_Tybokj%60;){hd7uG}GTeFgvxh3{0u`l)Tf2Yng z>1)*`V9>R&G_#Qd4ww%PH8tPu`@g0AHv7XY6Je`s#NzvL+G!_v*|0ZmRxQ zsFta{*tPZuxfYbQ0^wio@@pITyTF5UbBn_V5MtA)A)yi)hXIZW2g$NJr--)n2$nwm zx_Gj7@?!Tgq@VNQuGRkS*SP^Pn;NEVzgHKWXtE!SY;5dcUyYGQRF+kzQG&T=7_FA; zHkPVM{VU_zF!}seN7B$)0d^y{eAAa}YV$E|5v+P^^)dtsDl>Hmz4muGY=Kzv&$5IKAz=wls3Tq38K^>xR!EwL`&VLv zq(^UhGHB#oXDs5p3Z?Y(ooL`vm4%Pe*hZt~)v}6`!Of$sZMA9|cQGt@Pgvn(YT;z9 zv$6D!Z&0)W-&g;Xtlv0>?_0sko0MIvU6{UQKC9-XlF? z%-*a zrx>a(SXUO<5!}Qpt$ml@+E%ZoVT)$jVV3O3`P2cIUCuqZShb|LleESGlp5NLJ1?MB zHw5T$W{qx-#=R6A=n0#9<}hG+&O;Wn$zHI!IzeyyUs7poQ|o-I(~`qq-Jd(5Fw*}y zHT-TMdFQgHmY*w5UY+6v7$1-s0S94EJqA*+-5U!;p+65&L(9{vEa@u_LR8>dJ!c9i zhjqX)naE1F=Y!x0@ATJe0#AY5B-?OToQwbBS9Qc@gfJJ;=xT#qI@Sx}*ry<0_9n=e z)s#fw>Kte1`H0ZmC$g__HC=)^f&RUbUqFs5z~LX@YIaqPL~Iz&Sd-$2BOw@2{T>fD z9h@6>0=JOiG}G`S33zXBp8xnpp%7G#7Kbc~;f|H0b6DgF}GFNo*`QUuL%I)5VsPolpMwhELwjFIu> zLAi|w>4ojJr->ppd~D-W2}XJWI_2TsAyBuAMbhTkOyoioWttsjw-TY{$6vCA7$zX8 zC+Wg1*|E&0bI}uDWPzI)W-~=_D+~5_a4pxSFQKPSqEN(4Ox+czhPN{6kX2M8NH&Jj zXnn%jsgrsgkJCV|y<>qdM9v;jmTUWZxYT#?j>-H-id{eSl9l&+I_6cmc2l3xRrB0|KK^c zMHwkHyEWX`m-adqB&;Q>x2vBzUc`uY!^Nqzqqtq?e7{$q-#Z8f{F>ssIAJn>)lLb@ z5QJHPuBC-x?AURec@{uC2{4D_)q45>~0Wpw-YXaWv5HXGaLprJ1KBUYwCCc$V6*oi@@Z+c@BnH=%PNr zFlx}CuO*}#KXRHm2i#958%m7Um|@_ik(g_&B{Td|$BlDPvwMqJvqvxd3cTzLr+5we zEd=AfAqeZc`KGaKwWRo6hnl~DL~b%;v3>16C@g<6*}yv)tmP+gt89+Evw4H5bBdm^ zIyQ(XO0s8s-=U{q2G78TunA|w6S6qe9sUi5bT@n+NL`!wdk+JMa=e^DwVYI1MdqZT){;MEAenfhdqNEmDF? z{OBV`Ge45fH}pt9iU;Qo;<+BCtfw15b)e8FuLB^`pqDrbuef3$p6MKtGV4bjDA~rV zMHE+tERF_D@q61Fzhvc%*h#l5I9Z9?J*8PZ(|~BJ0=`Aeo;32&U4o?8wCup#1*d;) zQgs;A3Hwr#bfx~yvHb-1bk5_X3f}|Qq%3xUQp3av5S<6~XXxKJwSMvSKXa~1pWI@m zg>>E2ajWHgxvQsISpS+q{dQ;9kNlJz9-I1Z)8{+_q(WyMLz!zLOZ&zlkr;IfRRztz!`?kP zZ!=JENmj%IJFY0eEQqMQ&f~9(pMM5CA2>7xDk#*yi^AEKuuo;(1I*+1$+EjmS}72AUd>CWAzwW=A6fZnSf+EKQ+IUIC+ zNZH&4^5{lfx|3xFr%kJXYE2ZypeH(Ck=C#GhV2BYc!ivW?jPL25f5Dh8I#s912Hps z4sZUV^@LbM0`h{?IzcHWZV8L?68OAI05zQ?Mn2kA^J!_i&;YS~h;G5{Z%)B2*$_ok zlXBM8=C1vOC3~Dw`_)+)V%d}q%Hryx-7CNMAbt-$rUXmn!rs3kUHiw|0T6uDpl!g0 zDBs_4of;Z!l|A(&p2KB78KEp z>R$4_JDY8V?Qm;*16xPAd%mcv2iEJod9cH*dA=cg>8SKGT+uNcnAcBmW&BbQlKH7t z%JxmbPBshtgz|iK-w;q}m7?LA3)1HryAH5HLfnZoUQ!ebpO?##1bTQizFt)S(EQF3l7(AdyFhg0spL%(%`fbk&#v+aGhVfK;$9E%_LvUV!sKE>l2gz$U(5^m6&%_6ZqLvqs zyt7H|Z`TmgR~~X|;oMp^TMuorDV^IxoqPA%17XD2;ed&9KZ>!@T0o!*z4{nSsYqUB z$IE-)=lDD!()tzteNH2elb|X2omTGQaf8#3%yYl7rt2k=2ehpc6UlZ(H9X5)(eHTc zd~i0Yw)n1GR}hhHk9t`XrKh6&dkE4uiP})~RI(7TSft{{RzbvEnK<67uG%{MQ)n5T6k@lm5Jmn+coYV_}J4fczvS)91c_(og>r@o-{YV2D*g5kHB*O?tr0}e_K_kB@ketrW}NRvj{v2!@N z7UGW*W@^lR*;YT0JUZM>t+uPv1M8X2aZ^9WPIt;6M3OV2SpFCOy1KRj>qMI`3CLp3 zHlI?iJM&JNm17AK@ z@UD%2?4_DX_xicX{M}OO{t2glcft{P!)){O7&PwP`}h$&r#Wtl`HFN36ifV@Z|0o; zTYRA|lN^^?(iT;LrizYQMZ{Bvv$I!@0qMDdNYNuF&s@4sD~Oh5w!eHndiUWu8&HE# z&B__MfZ~g^A5DV2T)HDneeD}uH0+c ztxtDS&F2KFv~z*N>wK9t`U6noo>_lLzfRSNnbML-|AGH{x|07S1IPp<*xC!3A@(jB z*f=$p&wtCrH;xwJvbWRmbFE$d?@z1Q8VqCSCjBr%`f*%+^;4`hZeTfS;`NOTGGQyV z?NS&xe+uhCB<0Sk?eeYia%62{tn0kAS>@{<-Dwd$;w58$)&HnY0PO} zlF5#`W^Ua<$)G5+51DYN^$U>g2m=mkU;A{qV3mf0;=NNsvk-f==cM?W&a$ijTu-NI z^93eiv{YNRIVeuwrP5wFpIWP^Dai%e#{KAmYWDelte@6cphuhF+r?-1hW`+Rp}CLt zfr4BA)n(y-w*nSW@;UT$$=ok3sh_M7)Z1;)!>pzB+uR>YV`FDkb{i@K4Cejv5B#m2VbxidZ|>)0K%dhM2vpB zcq!c}we_#-Q6Rvsw-cG~i+cEd19T{slAxB4SF;pZl!~wkt68O+ZW-a#uX({=3g}|DF5X|9QT> zcfRji>s{|!@4F%OGR)F&rt)VB3bj5|!LE0|_?z_1fTH=CLv_550*bYvd}8F^8-7J; zu6Cb$x|ck1nq(R?(UK_NlHy$|&#PzpH$t(pQJA|Y(XDQrI9zw0vX&{(QGFBpg- zA4Nl1#@Nqd%=sGeG&A#Sd%d83Ko6(TdT=A_v1xMRZ{E;AomZ(z0`XsJy??Kmx)h~( z+bEuX^PAGHJqrsR7*S(H?%6QY8of%{b2IfR?b4o#X>@Hom2Zgm5fnFjVS;{8|833G zg(>G1_Z(o^El+b?zra7PVLFNp@!HyPc~_yz)%vb;A^RIoWVwYif5?*NfiX_Ll3vyq z4TA&NrRbsQ7zLMC+ougOua_SVtYJzS*zMV%6;zfN^mtuK2Di`nH1k^i?4>st4lF+ z5e2Xow=q)P8ap~4rx(%xa|0ltFSQ6BnQ>>QC6Y(3pgE0f)fi(T%)YzSf2tCv)86*) zJVW~uo!|9s)dA1o5US)+uOq{`NjR%bh6Siuk1U{q4ntk{v**)8_a=IhyRn6@-IY4E z7a=%)s9N-HTXQXB&Q~GUk8kiu-Y1^F*S6aXBeA8IN%pxcP@!ebIJ^!#a zO!QVf2`*8cz}k6ACD`Bfc`EKd2@Ld9;UtqEY$E!otJnS=NAm^cLU{01P?4K;7enO2m-S@95tAQD0)!y@l$86Y;xcJC?vfcgcQ;`OHa4a{CV7PxYR$;I5WjvX#(U$B|Fdqvcle2+7ldV8xxVXARP8L8&~~Z=XFb<<1#p z9u)>8vbp@WVR=fZ<&p3RPu0ACmrGVKjACjyKb71c2?v)!>$_i|u&191i5CYb(h;Qp zcmJ-~vV~H0QV8bUaAoqYb?V#@MgB&YIQZc6_0mRtKiX~_3WXTf zl=-<({^R3SlQSoT8?FFTIZ6^Z*s1Pv%p%|RaT0dX_QlWlbv~8|4a$kfpZfL6{`}ar zLRK67M@mxoJ{0K|eB7E5PT4vXz>7Lgi~4oUwMte?Ww*@!3z%ES(vsw**W=j2 z^u}=EhJ6i&xf2{W_G10-w$-$~N%VgC%E`xS@DV&kqTBZF{N?CNou_jqnRhJc{Z`I;LFb~-wO+}` zSUM32UJ5$q+${df2#VPN_@XoLMIMlm%4Gv+jstEVeIYH8V%S?1NL}pAyfF<}Q3LWD z&Qyk_h+Goksj%ik{X|LcNvpWouNQPSH;6S`Di`=sk-h}q(%~wZDVQ#1X0vDgqPN^k z+3~OQ_WA_8_USKco|)iz*fU$ajwEJeo8SZ>*^h5bn}HM%ObO)=)Yr!!q5MPC_SRu} z-rf_Tmap#J%im4LUjrXN6`00`eS5+SB=S%fwqE}+&52*VtdRZpbsgF=*{0`zZVdhZ zvH0A%3Sz7mnL7zx?7^?S$RjiUT}#cwz3W#T5JDy?62HulZhML3^0cZ~hWEOBo5hTo z>Imxq)0&-Lic+-ekf=6}t+6{ziDQcyZO!ZndoQwy&Tku$@nU2Jl3TXmiLK=NXr`)H z=_*@LzSA3J7m04!K|!ji+IBe$V(2B=LBJm{aXVK162&NTeRPS#o=Ad(ySB~%84rBN z&>)A;lWUH9D9Q~sIym9`#4G_r)|Yi?DFVCUnH&`&hH_1sHJ;}RWo{KI@fh{w0T5v4 z)y0I8qtama^qh#9yA=7pm_zR)D}0gVNDtiAa?V#5k~I6Nw3)+om$zz(d!8e{F48CB z8rStiu944oo~k@=J)O}WP$}uat6V7XItK$pT~Bz{*(fElVW%;@>S%;`W_nHCC&+ld z_Is}_1d97^GuqPkqyd0bcakR4BOOKh=aEA_TJFRfrsNJ`^Bh}R_N$HkXL?ooBJ<5) zG1-4g%Y-1+IFgL$=Xb#L29!a%WfE zkL0iezX#_!^pfm4DD`HkE-o42!kPI|Q80O=9yK^8n?l$6%ALmRN%qDd!A&G6bmw2{ z-I&C5>Z8XqDV}6O5LIH|X@yvP0{d}z|K`=(Pqwc1Cr90pydTGoy0Q42L_a4B5~+3Q zwJ>|GvDZ=ugT4XXRED!P{*qiW3{&<^S~f)D5>R0MFy{m|?5kKE`+>8lH$4h#F;ShN z$m3IjgNo92feT0BXywY5T{Snnfk83%P8OJr3U&ag4z`l&t&$s%HsJ`qAQkCR9qrg1 zlgf5tHR7Wsy(|tuPm83YV4C7Sz6v&IOHYOk>Xnzs{@Fay&02LW8c6>bOyjF4NnOW3 z`-B;INLdn26w5^VqL|q~oROU$!bP^QffCk3agDQN?N-l9o9M43Z{&d&Y>7WnuuB76 zivzp6;NIkxkQ~it-EPjfQ`Xf6rvH)!&WjC;dl(x*DIM`n%M?kl1$XjDqoh8uFsC)L zwYCZMByVA4+u^CpUrc!<)3BV1UQzIH8SUjJL=E@|S;L@vQuT^S4(1?Byiw0Js66Te ztwYrwq1BwXf(|d`tC%8ni$1TC6)~Q<;a$xmRp*E@{mC3Ka0&<`<^d zKT`hm5H*ZJl$i?{M?=JIa;)6mj^!EPJAGk^Qq-;4Pbqz`KwKpYLvyU~?Ld2Q2}3HI z%FtuVevGY9DSqTMXEiv|x6YZ@+^E>+hV)adIg02=y&dF|Z?tRMJ@ebUUM?aa>x{E$+9@Tk7sj~B8>*$Bn)7J0> z-iilhD`s}p=$)7SMAD&BmyWS#QEPT$y(ihtJJ~x#1TR%3PkwR`t?&Mam!Q&8H2qto zsuMB`@l;eTs9{2d*2REB(Y0RqQUO;qO3E(d3F$ljID>3 z+^|py!Led@7eml{f)N>((t6bN{g*(|-Q%oy6+dSxF&oa4IB`V0Hz!t%qx{7tM-~tzk|&+5m<-HCn|<8sgd?|; zjtggizG@G+@4&s}#G)U0)lB%v+q?qCmitT%9>nyveMvZDBrbcM5p5Wq5C&w zQkNShs6SOwDTA8u(pd89SHY=o2IUpM@{469%@Yi4A)Xx7X``I;8hhTpIAn%cB$`H7 zeSjRs|A>1Ny!P>cB`s}oa_aqxZa-bCvyvAl0WWrr?#}Oi*<4j>#OwYnt;cFcs-#8JUmFYtw44Dn+fYP$hDbGG zF!+eKpokrH+JR0Sn`amd+E#QjvRnLUM!3GdC`DEOwvLmaS?p+T&J(RVlH-8xs>wFQ z2@)tM!Kt=r>}KT;rcNxUh?U5iatP(Qmj|S}I~g0NL$E8`ozNabB_Hp`HnXNKy zpKxY-$^;xO=yG0hlS+@ObE%maRF8ih`XpsWFEH~2{OKv<%aPBGz*yy2V9e&5 zCb)t^o#>QW)9Se?Ybx?Q$rUXnQxx{B0y^w*>^VPRVQY;tBe649uM2&p2Bos~DVsTS zS}I2ysBE+ut^bDHkriablW$u!aYu*oY-r$zmYmd3YI`H0zD$ur*p4ANSmCR&(5IGi zG8#@q&Q*w&zdDqBgcNAujLlWzWTf%%E%o`lt5J5)-$(hxKz8yzc`J`N>5El)D9rW! zG)Gz%nf`%B4vkk-rpoJU5w)N8(QSU9FH6~WFm()0<@B(uD~_GTSV(YJzIF-CPfa!+ z`e34!LN#wOT$%g9cI`(G%{#KHz_aA9Jv+)I^i=PvW<=hW{A+VsnAv=F;Y8p4rj5w4 zpCO}Ge-A_TUJ8xlMNJ|(N*X%9vu0*~t%r2UWqsn-MX;ainutyS&p%6HTIEB_c5{aC ze%bvGzr#sglVf z3&1=8BImk+bxp7*&yPG(R>N#D0@B;hDAN-g`M6|$sBoh82i~(=BtP%u@3qH>?hrWN z{iEU>h6{}5gi3x>!UArU3J?}l@L*EExPNy9i3n`TC|Gd~ZQRO3FQyC5>S@g3m7|gOy#!5!j z&p8P~mtp~%8{R+SSP9oOp~&{Vj-QfNtU)^7s{^$YCX9LcV+@1Pr`bi1{x#A{8l2&A zmIFO!qN;6X0Y=o3M-K#74xf{q_SWaabJNfq_NC9gwDoOvh$sym8t%P?SbPX5BKHG` zfL~)-CVQIEI|Ip85!#8nRmq;u4ZDv(?Dz`c#eXN@UOaduCpaE_j&8aXyNMRiFu|+`BAs z&5`3k{s>qbQb~9`Iz@pa|I5fA$-x14Vdzy5x)w(G8OAM~5s{{xdfQ4d#7Z~rlJ?+a%-bb3Ch(#;_ zOUys`{AYXc8sdOQRZ6Z=4O3A`6706i^rFNFp4oI@$l<_yaMBMO3oa2AXTW-XzW`t_ zSi+IDv}3qo_OiodEvU8t4SYIdAvQ!b4YoCF>nb@b-s%fZ%|$^u{thNiRl0U diff --git a/demo_apps/reallocation-example/reallocation.py b/demo_apps/reallocation-example/reallocation.py index 0844e68..751e210 100644 --- a/demo_apps/reallocation-example/reallocation.py +++ b/demo_apps/reallocation-example/reallocation.py @@ -1,8 +1,8 @@ """Perform GPR Active Learning where we periodicially dedicate resources to re-prioritizing a list of simulations to run""" from colmena.models import Result -from colmena.thinker import BaseThinker, agent, result_processor, task_submitter -from colmena.method_server import ParslMethodServer +from colmena.thinker import BaseThinker, agent, result_processor +from colmena.task_server import ParslTaskServer from colmena.thinker.resources import ResourceCounter from colmena.redis.queue import ClientQueues, make_queue_pairs @@ -101,7 +101,7 @@ def __init__(self, queues: ClientQueues, search_space_size: int = 1000): """ Args: - queues: Queues to use to communicate with the method server + queues: Queues to use to communicate with the task server output_dir: Output path for the result data dim: Dimensionality of optimization space batch_size: Number of simulations to run in parallel @@ -132,11 +132,17 @@ def __init__(self, queues: ClientQueues, # Start by allocating all of the resources to the simulation task self.rec.reallocate(None, "sim", self.rec.unallocated_slots) - @task_submitter(task_type="sim", n_slots=1) + @agent def simulation_dispatcher(self): """Dispatch tasks""" - with self.queue_lock: - self.queues.send_inputs(self.task_queue.pop(), method='ackley', topic='doer') + + # Until done, request resources and then submit task once available + while not self.done.is_set(): + while not self.rec.acquire("sim", 1, timeout=1): + if self.done.is_set(): + return + with self.queue_lock: + self.queues.send_inputs(self.task_queue.pop(), method='ackley', topic='doer') @result_processor(topic="doer") def simulation_receiver(self, result: Result): @@ -279,17 +285,17 @@ def thinker_worker(self): ) config.run_dir = os.path.join(out_dir, 'run-info') - # Create the method server and task generator + # Create the task server and task generator my_ackley = partial(ackley, mean_rt=args.runtime, std_rt=args.runtime_var) update_wrapper(my_ackley, ackley) my_rep = partial(reprioritize_queue, opt_delay=args.opt_delay) update_wrapper(my_rep, reprioritize_queue) - doer = ParslMethodServer([my_ackley, my_rep], - server_queues, config) + doer = ParslTaskServer([my_ackley, my_rep], + server_queues, config) thinker = Thinker(client_queues, out_dir, dim=args.dim, n_guesses=args.num_guesses, batch_size=args.num_parallel) - logging.info('Created the method server and task generator') + logging.info('Created the task server and task generator') try: # Launch the servers @@ -303,5 +309,5 @@ def thinker_worker(self): finally: client_queues.send_kill_signal() - # Wait for the method server to complete + # Wait for the task server to complete doer.join() diff --git a/demo_apps/synthetic-data/synthetic.py b/demo_apps/synthetic-data/synthetic.py index da10ba3..1f7b41c 100644 --- a/demo_apps/synthetic-data/synthetic.py +++ b/demo_apps/synthetic-data/synthetic.py @@ -16,7 +16,7 @@ from parsl.launchers import AprunLauncher, SimpleLauncher from parsl.providers import LocalProvider, CobaltProvider -from colmena.method_server import ParslMethodServer +from colmena.task_server import ParslTaskServer from colmena.redis.queue import make_queue_pairs, ClientQueues from colmena.thinker import BaseThinker, agent @@ -183,7 +183,7 @@ def producer(self): config = Config(executors=executors, run_dir=out_dir) - doer = ParslMethodServer([target_function], server_queues, config) + doer = ParslTaskServer([target_function], server_queues, config) thinker = Thinker( queue=client_queues, @@ -195,7 +195,7 @@ def producer(self): reuse_data=args.reuse_data, ) - logging.info('Created the method server and task generator') + logging.info('Created the task server and task generator') logging.info(thinker) start_time = time.time() @@ -212,7 +212,7 @@ def producer(self): finally: client_queues.send_kill_signal() - # Wait for the method server to complete + # Wait for the task server to complete doer.join() # Print the output result diff --git a/docs/_static/implementation.svg b/docs/_static/implementation.svg index 79d1930..f7b4598 100644 --- a/docs/_static/implementation.svg +++ b/docs/_static/implementation.svg @@ -1,205 +1,284 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Client - - - - think - - - - - - - - - - - - - Method Server - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 75 px - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Workers - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/design.rst b/docs/design.rst index 3313af2..d739dc1 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -25,7 +25,7 @@ surrogate model). "Thinker": Active Learning Agent ++++++++++++++++++++++++++++++++ -The "Thinker" process is responsible for generating tasks to send to the Method server. +The "Thinker" process is responsible for generating tasks to send to the task server. Colmena supports many different kinds of task generation methods each with different concurrency and optimization performance tradeoffs. For example, one could develop a batch optimization algorithm @@ -33,8 +33,8 @@ that waits for every simulation in a batch to complete before sending deciding new simulations or a streaming optimization tool that continuously maintains a queue of new computations. -"Doer": Method Server -+++++++++++++++++++++ +"Doer": task server ++++++++++++++++++++ The "Doer" server accepts tasks specification, deploys tasks on remote services and sends results back to the Thinker agent(s). @@ -48,11 +48,9 @@ Communication Communication between the "Thinker" and "Doer" is asynchronous and follows a very specific pattern. -"Thinker" applications make requests to the method server for computations +"Thinker" applications make requests to the task server for computations and receive the results in no particular order. -.. TODO (wardlt): Need to check my nomenclature with a distributed computing person - Implementation -------------- @@ -73,19 +71,19 @@ The client communicates by either writing *task requests* to or reading *results Redis queues. Tasks and results are communicated as JSON objects and contain the inputs to a task, the outputs of the task, and a variety of profiling data (e.g., task runtime, -time inputs received by method server). +time inputs received by task server). We provide a Python API for the message format, :class:`colmena.models.Result`, which provides utility operations for tasks that include accessing the positional or keyword arguments for a task and serializing the inputs and results. -Method Server -+++++++++++++ +Task Server ++++++++++++ -We implement a method server based on `Parsl `_. +We implement a task server based on `Parsl `_. Parsl provides a model of distributed computing in Python that meshes well with Python's native :mod:`concurrent.futures` module and allows for users to express complex workflows in Python. -We create :class:`parsl.app.PythonApp` for each of the methods available in the method server, +We create :class:`parsl.app.PythonApp` for each of the methods available in the task server, which allows us to use them as part of Parsl workflows and execute them on distributed resources. The :class:`colmena.method_server.ParslMethodServer` itself is a multi-process, multi-threaded Python application: @@ -105,7 +103,7 @@ The :class:`colmena.method_server.ParslMethodServer` itself is a multi-process, Communication +++++++++++++ -Communication between the client and method server occurs using Redis queues. +Communication between the client and task server occurs using Redis queues. Each Colmena application has at least two queues: an "inputs" queue for task requests and a "results" queue for results. By default, operations will pull from these two default queues. @@ -123,7 +121,7 @@ and deserializes the result. Each of these operations can be supplied with a topic to either send inputs with a designated topic or to receive only a result with a certain topic. -There is a corresponding queue wrapper for the method server, :class:`colmena.redis.queue.MethodServerQueues`, +There is a corresponding queue wrapper for the task server, :class:`colmena.redis.queue.MethodServerQueues`, that provides the matching operations to the ``ClientQueues``. Both need to be created to point to the same Redis server and have the same list of topic names, and Colmena provides a :meth:`colmena.redis.queue.make_queue_pairs` to generate a matched @@ -162,12 +160,12 @@ by illustrating a typical :class:`colmena.models.Result` object. specification (``method`` and ``inputs``) to an "outbound" Redis queue. The task request is formatted in the JSON format defined above with only the ``method``, ``inputs`` and ``time_created`` fields populated. The task inputs are then serialized (``time_serialize_inputs``) and send using -the Redis Queue to the Method server. +the Redis Queue to the task server. The serialization method is communicated along with the inputs. -**Task Routing**: The method server reads the task request from the outbound queue at ``time_input_received`` +**Task Routing**: The task server reads the task request from the outbound queue at ``time_input_received`` and submits the task to the distributed workflow engine. -The method definitions in the Method Server denote on which resources they can run, +The method definitions in the task server denote on which resources they can run, and Parsl chooses when and to which resource to submit tasks. **Computation**: A Parsl worker starts a task at ``time_compute_started``. @@ -175,7 +173,7 @@ The task inputs are deserialized (``time_deserialize_inputs``), the requested work is executed (``time_running``), and the results serialized (``time_serialize_results``). -**Result Communication**: The method server adds the result to the task specification (``value``) and +**Result Communication**: The task server adds the result to the task specification (``value``) and sends it back to the client in an "inbound" queue at (``time_result_sent``). **Result Retrieval**: The client retrieves the message from the inbound queue. diff --git a/docs/how-to.rst b/docs/how-to.rst index 2ee992e..0ef1843 100644 --- a/docs/how-to.rst +++ b/docs/how-to.rst @@ -1,43 +1,36 @@ Building a Colmena Application ============================== -Creating a new application with Colmena involves building a "method server" to +Creating a new application with Colmena involves defining a "tasks server" to that deploys expensive functions and a "thinker" application that -decides which methods to invoke with what inputs. +decides which tasks to submit. We describe each topic separately. See `Design <./design.html>`_ for details on Colmena architecture. -Configuring a Method Server ---------------------------- +Configuring a Task Server +------------------------- -The method server for Colmena is configured with the list of methods, a -list available computational resources and a mapping between those two. +The task server for Colmena is configured with the list of methods, a +list available computational resources and a mapping of which methods +can use each resource. Defining Methods ++++++++++++++++ Methods in Colmena are defined as Python functions. -Any Python function can be serviced by Colmena, but +Any Python function can be served by Colmena, but there are several limitations in practice: 1. *Functions must be serializable.* We recommend defining functions in Python - modules that are accessible from the Python PATH. Consider creating a Python - module with the functions needed for your application and installing that function - to the Python path with Pip. + in the script that creates the task server or in modules that are accessible + from the Python Path (e.g., part of packages installed with ``pip``) 2. *Inputs must be serializable.* Parsl makes a best effort to serialize function inputs with JSON, Pickle and other serialization libraries but some object types (e.g., thread locks) cannot be serialized. 3. *Functions must be pure.* Colmena is designed with the assumption that the order in which you execute tasks does not change the outcomes. -We recommend creating simple Python wrappers for methods which require calling other executables. -The methods will be responsible for generating any required input files and processing the outputs -generated from this method. -Note that we are considering an improved model where the pre- and post-processing methods can -be separate tasks to avoid holding on to large number of nodes -during pre- or post-processing (see `Issue #4 `_). - Common Example: Launching MPI Applications ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -95,6 +88,13 @@ This basic pattern has many points for further optimization that could be critic consider using separate methods to pre- and postprocessing. We are planning to streamline this process in the future (see `Issue #4 `_). +.. note:: + + We are considering an improved model where the pre- and post-processing methods can + be separate tasks to avoid holding on to large number of nodes + during pre- or post-processing (see `Issue #4 `_). + + Specifying Computational Resources ++++++++++++++++++++++++++++++++++ @@ -105,24 +105,18 @@ We use an complex example that specifies running a mix of single-node and multi- .. code-block:: python + from parsl.addresses import address_by_hostname + from parsl.config import Config + from parsl.executors import HighThroughputExecutor, ThreadPoolExecutor + from parsl.launchers import AprunLauncher, SimpleLauncher + from parsl.providers import LocalProvider + + example_config = Config( executors=[ - HighThroughputExecutor( - address=address_by_hostname(), + ThreadPoolExecutor( label="multi_node", - max_workers=8, - provider=LocalProvider( - nodes_per_block=1, - init_blocks=1, - max_blocks=1, - launcher=SimpleLauncher(), # Places worker on the launch node - worker_init=''' - module load miniconda-3 - export PATH=~/software/psi4/bin:$PATH - conda activate /lus/theta-fs0/projects/CSC249ADCD08/colmena/env - export NODE_COUNT=4 - ''', - ), + max_threads=8 ), HighThroughputExecutor( address=address_by_hostname(), @@ -148,16 +142,16 @@ The overall configuration is broken into two types of "executors:" ``multi_node`` The ``multi_node`` executor provides resources for applications that use multiple nodes. - Note that the executor is deployed using the :class:`parsl.launcher.SimpleLauncher`, - which means that it will be placed on the same node as the Method Server. + We use the ``ThreadPoolExecutor`` to run the pre- and post-processing Python code + on the same Python process as the task server, which can save significant computational resources. The maximum number of tasks being run on this resource is defined by ``max_workers``. - Colmena users are responsible for providing the appropriate ``aprun`` invocation in methods + Colmena users are responsible for providing the appropriate ``mpirun`` invocation in methods deployed on this resource and for controlling the number of nodes used for each task. ``single_node`` The ``single_node`` executor handles tasks that do not require inter-node communication. Parsl places workers on two nodes (see the ``nodes_per_block`` setting) with the ``aprun`` - launcher, as required by Theta. Each node spawns 2 workers and can therefore perform + launcher, as required by Theta. Each node spawns 2 workers and can perform two tasks concurrently. @@ -170,9 +164,9 @@ on behalf of the application (e.g., from an HPC job scheduler). Mapping Methods to Resources ++++++++++++++++++++++++++++ -The constructor of :class:`colmena.method_server.ParslMethodServer` takes a list of +The constructor of :class:`colmena.task_server.ParslTaskServer` takes a list of Python function objects as an input. -Internally, the method server converts these to Parsl "apps" by calling +Internally, the task server converts these to Parsl "apps" by calling :py:func:`python_app` function from Parsl. You can pass the keyword arguments for this function along with each function to map functions to specific resources. @@ -182,7 +176,7 @@ method to the "multi_node" resource and the ML task to the "single_node" resourc .. code-block:: python - server = ParslMethodServer([ + server = ParslTaskServer([ (launch_mpi_application, {'executor': 'multi_node'}), (generate_designs_with_ml, {'executor': 'single_node'}) ]) @@ -193,18 +187,15 @@ Creating a "Thinker" Application Colmena is designed to support many different algorithms for creating tasks and responding to results. Such "thinking" applications take the form of threads that send and receive results -to/from the method server(s) using the Redis queues. +to/from the task server(s) using the Redis queues. Colmena provides as :class:`colmena.thinker.BaseThinker` class to simplify creating multi-threaded applications. -This part of the guide describes the high-level features of the ``BaseThinker`` class. -Advanced features are described in `the next section <./thinker.html>`_. - Working with ``BaseThinker`` ++++++++++++++++++++++++++++ Creating a new ``BaseThinker`` subclass involves defining different "agents" -that interact with each other and the method server. +that interact with each other and the task server. The class itself provides a template for defining information shared between agents and a mechanism for launching them as separate threads. @@ -226,7 +217,7 @@ A minimal Thinker is as follows: The example shows us a few key concepts: -1. You communicate with the method server using ``self.queues``, which provides +1. You communicate with the task server using ``self.queues``, which provides `a wrapper over the Redis queues `_. 2. Operations within the a Thinker are marked with the ``@agent`` decorator. 3. Calling ``thinker.run()`` launches all agent threads within that class @@ -235,14 +226,14 @@ The example shows us a few key concepts: Submitting Tasks ~~~~~~~~~~~~~~~~ -:class:`colmena.redis.queue.ClientQueues` provides communication to the method server +:class:`colmena.redis.queue.ClientQueues` provides communication to the task server and is available as the ``self.queues`` class attribute. -Submit requests to the method server with the ``send_inputs`` function. +Submit requests to the task server with the ``send_inputs`` function. Besides the input arguments and method name, the function also accepts a "topic" for the method queue used when filtering the output results. -The ``get_result`` function retrieves the next result from the method server +The ``get_result`` function retrieves the next result from the task server as a :class:`colmena.models.Result` object. The ``Result`` object contains the output task and the performance information (e.g., how long communication to the client required). @@ -447,7 +438,7 @@ Creating a ``main.py`` ---------------------- The script used to launch a Colmena application must create the Redis queues and -launch the method server and thinking application. +launch the task server and thinking application. A common pattern is as follows: @@ -462,7 +453,7 @@ A common pattern is as follows: # Generate the queue pairs client_queues, server_queues = make_queue_pairs('localhost', serialization_method='json') - # Instantiate the method server and thinker + # Instantiate the task server and thinker method_server = ParslMethodServer(functions, server_queues, config) thinker = Thinker(client_queues) @@ -474,20 +465,20 @@ A common pattern is as follows: # Wait for the thinking application to complete thinker.join() finally: - # Send a shutdown signal to the method server + # Send a shutdown signal to the task server client_queues.send_kill_signal() - # Wait for the method server to complete + # Wait for the task server to complete doer.join() The above script can be run as any other python code (e.g., ``python run.py``) once you have started Redis (e.g., calling ``redis-server``). -We have described configuration options for method server and thinker applications earlier. +We have described configuration options for task server and thinker applications earlier. The key options to discuss here are those of the communication queues. The :meth:`colmena.redis.queue.create_queue_pairs` function creates Redis queues with matching options -for the thinking application (client) and method server. +for the thinking application (client) and task server. These options include the network address of the Redis server, a list of "topics" that define separate queues for certain types of tasks, and a few communication options, such as: diff --git a/docs/index.rst b/docs/index.rst index 19f466f..e975919 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,9 +24,9 @@ model or selecting a new simulation with Bayesian optimization. Colmena provides a few main components to enable building thinking applications: - 1. A "Method Server" that provides a simplified interface to HPC-ready workflow systems - 2. A high-performance queuing system for interacting with method server(s) from thinking applications - 3. An extensible base class for building thinking applications with a dataflow programming model + #. An extensible base class for building thinking applications with a dataflow-like programming model + #. A "Tasks Server" that provides a simplified interface to HPC-ready workflow systems + #. A high-performance queuing system communicating between tasks server(s) from thinking applications The `demo applications `_ illustrate how to implement different thinking applications that solve optimization problems. @@ -40,7 +40,6 @@ illustrate how to implement different thinking applications that solve optimizat quickstart design how-to - thinker source/modules Why the name "Colmena?" diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 633fdee..22c6306 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -58,10 +58,10 @@ Any input/output object of a target function larger than :code:`value_server_thr By default, the value server uses the Redis server passed to :code:`make_queue_pairs`. An alternative Redis server for the value server can be specified via the :code:`value_server_hostname` and :code:`value_server_port` parameters of :code:`make_queue_pairs`. -2. Build a method server ------------------------- +2. Build a task server +---------------------- -The "method server" in Colmena distributes request to run functions across distributed resources. +The "task server" in Colmena distributes request to run functions across distributed resources. We create one by defining a list of functions and the resources to run them across. Colmena uses `Parsl `_ to manage executing tasks. @@ -72,7 +72,7 @@ it to only use up to 4 processes on a single machine: config = Config(executors=[HighThroughputExecutor(max_workers=4)]) -The list of methods and resources are used to define the "method server": +The list of methods and resources are used to define the "task server": .. code-block:: python @@ -83,7 +83,7 @@ The list of methods and resources are used to define the "method server": Colmena provides a "BaseThinker" class to create steering applications. These applications run multiple operations (called agents) that send tasks and receive results -from the method server. +from the task server. Our thinker has two agents that each are class methods marked with the ``@agent`` decorator: @@ -118,7 +118,7 @@ Our thinker has two agents that each are class methods marked with the ``@agent` self.logger.info(f'Created a new guess: {result.value:.2f}') self.queues.send_inputs(result.value, method='target_function', topic='simulate') -"Producer" creates new tasks by calling the "task_generator" method (defined with the method server) +"Producer" creates new tasks by calling the "task_generator" method (defined with the task server) and then using that new task as input to the "target_function." "Consumer" retrieves completed tasks and determines whether to update the best result so far. @@ -133,7 +133,7 @@ A few things to note: 4. Launching the application ---------------------------- -The method server and thinker objects are run asynchronously. +The task server and thinker objects are run asynchronously. Accordingly, we call their ``.start()`` methods to launch them. .. code-block:: python @@ -150,7 +150,7 @@ Accordingly, we call their ``.start()`` methods to launch them. finally: client_queues.send_kill_signal() - # Wait for the method server to complete + # Wait for the task server to complete doer.join() 5. Running the application @@ -164,7 +164,7 @@ The application will produce a prolific about of log messages, including: ``... - thinker.producer - INFO - Created a new guess: 9.51`` -2. Messages from the Colmena queue or method server +2. Messages from the Colmena queue or task server ``... - colmena.redis.queue - INFO - Client received a task_generator result with topic generate``` diff --git a/docs/source/colmena.thinker.rst b/docs/source/colmena.thinker.rst index 3418c06..337a028 100644 --- a/docs/source/colmena.thinker.rst +++ b/docs/source/colmena.thinker.rst @@ -1,20 +1,7 @@ colmena.thinker =============== -Utility functions associated with building applications that steer ensemble simulations. - -Base Class ----------- - .. automodule:: colmena.thinker :members: :undoc-members: :show-inheritance: - -Resource Tracking ------------------ - -.. automodule:: colmena.thinker.resources - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/thinker.rst b/docs/thinker.rst index 2b868dc..436940a 100644 --- a/docs/thinker.rst +++ b/docs/thinker.rst @@ -14,8 +14,8 @@ Several objects are available to all "agent" threads available in a Thinker appl Queues ++++++ -The ``self.queue`` attribute of a Thinker class manages communication to the method server. -Each agent can use it to submit tasks or wait for results from the method server. +The ``self.queue`` attribute of a Thinker class manages communication to the task server. +Each agent can use it to submit tasks or wait for results from the task server. The :class:`colmena.redis.queue.ClientQueues` object must be provided to the constructor. @@ -71,40 +71,5 @@ must decorate a function that takes Result object as an input. class Thinker(BaseThinker): @result_processor(topic='simulation') def process(self, result: Result): - # Do some compute that that result - self.database.append((result.args, results.value)) + # Process results -Task Submission Agents -++++++++++++++++++++++ - -The task submission agents react to the availability of computational resources. -The :func:`colmena.thinker.task_submitter` decorator tasks the pool of resources -to draw from and the number of slots needed for this agent to begin processing. -Agent functions have no arguments. - -.. code-block:: python - - class Thinker(BaseThinker) - @task_submitter(n_slots=4, task_type="simulation") - def submit_new_simulation(self): - self.queues.submit_task(self.task_queue.pop(), method='simulate') - - -Event Responder Agent -+++++++++++++++++++++ - -The :func:`colmena.thinker.event_responder` waits for an event associated with an thinker being set. -The ``event_name`` is the name of a class attribute of the thinker class. - -.. code-block:: python - - class Thinker(BaseThinker): - - def __init__(self, queues): - super().__init__(queues) - self.flag = Event() - - @event_responder(event_name="flag") - def responder(self): - self.flag.clear() # Mark that we saw the event - # Do something about it diff --git a/setup.py b/setup.py index 95fc518..783a75f 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ include_package_data=True, description='colmena: Intelligent Steerable Pipelines on HPC', long_description=long_desc, + long_description_content_type='text/markdown', install_requires=install_requires, python_requires=">=3.6.*", classifiers=[ @@ -32,7 +33,6 @@ ], keywords=[ "parsl", - "pipeline", "HPC", ], author="Globus Labs", From 607283590842e2ed04def7c7d40640f3237a2260 Mon Sep 17 00:00:00 2001 From: Logan Ward Date: Tue, 6 Jul 2021 15:08:07 -0400 Subject: [PATCH 2/2] Some further method->task changes in docs --- docs/_static/implementation.svg | 285 +----------------- docs/design.rst | 2 +- docs/how-to.rst | 4 +- docs/index.rst | 1 + ...hod_server.rst => colmena.task_server.rst} | 8 +- docs/source/modules.rst | 2 +- 6 files changed, 10 insertions(+), 292 deletions(-) rename docs/source/{colmena.method_server.rst => colmena.task_server.rst} (55%) diff --git a/docs/_static/implementation.svg b/docs/_static/implementation.svg index f7b4598..234d89e 100644 --- a/docs/_static/implementation.svg +++ b/docs/_static/implementation.svg @@ -1,284 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +75 pxWorkResultRequestResultThinkerAssayServerWorkers \ No newline at end of file diff --git a/docs/design.rst b/docs/design.rst index d739dc1..9758b4c 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -86,7 +86,7 @@ workflows in Python. We create :class:`parsl.app.PythonApp` for each of the methods available in the task server, which allows us to use them as part of Parsl workflows and execute them on distributed resources. -The :class:`colmena.method_server.ParslMethodServer` itself is a multi-process, multi-threaded Python application: +The :class:`colmena.task_server.ParslMethodServer` itself is a multi-process, multi-threaded Python application: 1. *Intake Thread*: The intake thread reads task requests from the input Redis queue(s), deserializes them and submits the appropriate tasks to Parsl. Submitting a task to Parsl involves calling diff --git a/docs/how-to.rst b/docs/how-to.rst index 0ef1843..a51dc94 100644 --- a/docs/how-to.rst +++ b/docs/how-to.rst @@ -444,7 +444,7 @@ A common pattern is as follows: .. code-block:: python - from colmena.method_server import ParslMethodServer + from colmena.task_server import ParslTaskServer from colmena.redis.queue import make_queue_pairs if __name__ == "__main__": @@ -454,7 +454,7 @@ A common pattern is as follows: client_queues, server_queues = make_queue_pairs('localhost', serialization_method='json') # Instantiate the task server and thinker - method_server = ParslMethodServer(functions, server_queues, config) + task_server = ParslTaskServer(functions, server_queues, config) thinker = Thinker(client_queues) try: diff --git a/docs/index.rst b/docs/index.rst index e975919..26994c9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,6 +40,7 @@ illustrate how to implement different thinking applications that solve optimizat quickstart design how-to + thinker source/modules Why the name "Colmena?" diff --git a/docs/source/colmena.method_server.rst b/docs/source/colmena.task_server.rst similarity index 55% rename from docs/source/colmena.method_server.rst rename to docs/source/colmena.task_server.rst index 5a208a8..633a3ce 100644 --- a/docs/source/colmena.method_server.rst +++ b/docs/source/colmena.task_server.rst @@ -1,15 +1,15 @@ -colmena.method\_server +colmena.task\_server ====================== -.. automodule:: colmena.method_server +.. automodule:: colmena.task_server :members: :undoc-members: :show-inheritance: -colmena.method\_server.base +colmena.task\_server.base --------------------------- -.. automodule:: colmena.method_server.base +.. automodule:: colmena.task_server.base :members: :undoc-members: :show-inheritance: diff --git a/docs/source/modules.rst b/docs/source/modules.rst index cd0f0ed..17773b7 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -5,7 +5,7 @@ colmena .. toctree:: - colmena.method_server + colmena.task_server colmena.redis colmena.models colmena.thinker