From ad7082b7d66afdd9217f405eb9b10337538cbaed Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Mon, 26 Jun 2023 19:48:31 +0200 Subject: [PATCH 01/36] Fix timeline plot data loading --- muscle3/profiling.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/muscle3/profiling.py b/muscle3/profiling.py index 116bb465..5a78ba7c 100644 --- a/muscle3/profiling.py +++ b/muscle3/profiling.py @@ -114,14 +114,14 @@ def plot_timeline(performance_file: Path) -> None: min_time = cur.fetchall()[0][0] cur.execute( - "SELECT instance, (start_time - ?)" + "SELECT instance_oid, (start_time - ?)" " FROM events AS e" " JOIN event_types AS et ON (e.event_type_oid = et.oid)" " WHERE et.name = 'REGISTER'", (min_time,)) begin_times = dict(cur.fetchall()) cur.execute( - "SELECT instance, (stop_time - ?)" + "SELECT instance_oid, (stop_time - ?)" " FROM events AS e" " JOIN event_types AS et ON (e.event_type_oid = et.oid)" " WHERE et.name = 'DEREGISTER'", (min_time,)) @@ -141,7 +141,7 @@ def plot_timeline(performance_file: Path) -> None: for event_type in _EVENT_TYPES: cur.execute( "SELECT" - " instance, (start_time - ?) * 1e-9," + " instance_oid, (start_time - ?) * 1e-9," " (stop_time - start_time) * 1e-9" " FROM events AS e" " JOIN event_types AS et ON (e.event_type_oid = et.oid)" From 2965415b2c646e9e888786d6161a7708238dd1f8 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Mon, 26 Jun 2023 21:14:03 +0200 Subject: [PATCH 02/36] Hack private namespaces out of C++ API docs --- docs/source/conf.py | 7 ++++++ docs/source/cpp_api.rst | 48 ++++++++++++++++++----------------------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 36445d26..cd2c6483 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -136,6 +136,13 @@ def patch_installation_version(): import subprocess subprocess.call('cd ../.. ; doxygen', shell=True) +# -- Remove impl namespaces for exported symbols -- +for p in pathlib.Path('..', 'doxygen', 'xml').iterdir(): + contents = p.read_text() + contents = contents.replace('::_MUSCLE_IMPL_NS', '') + contents = contents.replace('::impl', '') + p.write_text(contents) + # -- Run apidoc plug-in manually, as readthedocs doesn't support it ------- # See https://github.com/rtfd/readthedocs.org/issues/1139 def run_apidoc(_): diff --git a/docs/source/cpp_api.rst b/docs/source/cpp_api.rst index ba6e5b12..30d2d929 100644 --- a/docs/source/cpp_api.rst +++ b/docs/source/cpp_api.rst @@ -3,39 +3,33 @@ API Documentation for C++ This page provides full documentation for the C++ API of MUSCLE3. -Note that in a few places, classes are referred to as -``libmuscle::_MUSCLE_IMPL_NS::`` or ``ymmsl::impl::``. This -is a bug in the documentation rendering process, the class is actually -available as ``libmuscle::`` and should be used as such. - - Namespace libmuscle ------------------- -.. doxygenclass:: libmuscle::_MUSCLE_IMPL_NS::Data -.. doxygenclass:: libmuscle::_MUSCLE_IMPL_NS::DataConstRef -.. doxygenclass:: libmuscle::_MUSCLE_IMPL_NS::Instance -.. doxygenenum:: libmuscle::_MUSCLE_IMPL_NS::InstanceFlags -.. doxygenclass:: libmuscle::_MUSCLE_IMPL_NS::Message -.. doxygentypedef:: libmuscle::_MUSCLE_IMPL_NS::PortsDescription +.. doxygenclass:: libmuscle::Data +.. doxygenclass:: libmuscle::DataConstRef +.. doxygenclass:: libmuscle::Instance +.. doxygenenum:: libmuscle::InstanceFlags +.. doxygenclass:: libmuscle::Message +.. doxygentypedef:: libmuscle::PortsDescription Namespace ymmsl --------------- -.. doxygenfunction:: ymmsl::impl::allows_sending -.. doxygenfunction:: ymmsl::impl::allows_receiving - -.. doxygenclass:: ymmsl::impl::Conduit -.. doxygenclass:: ymmsl::impl::Identifier -.. doxygenfunction:: ymmsl::impl::operator<<(std::ostream&, Identifier const&) -.. doxygenenum:: ymmsl::impl::Operator -.. doxygenstruct:: ymmsl::impl::Port -.. doxygenclass:: ymmsl::impl::Reference -.. doxygenfunction:: ymmsl::impl::operator<<(std::ostream&, Reference const&) -.. doxygenclass:: ymmsl::impl::ReferencePart -.. doxygenclass:: ymmsl::impl::Settings -.. doxygenfunction:: ymmsl::impl::operator<<(std::ostream&, ymmsl::impl::Settings const&) -.. doxygenclass:: ymmsl::impl::SettingValue -.. doxygenfunction:: ymmsl::impl::operator<<(std::ostream&, ymmsl::impl::SettingValue const&) +.. doxygenfunction:: ymmsl::allows_sending +.. doxygenfunction:: ymmsl::allows_receiving + +.. doxygenclass:: ymmsl::Conduit +.. doxygenclass:: ymmsl::Identifier +.. doxygenfunction:: ymmsl::operator<<(std::ostream&, Identifier const&) +.. doxygenenum:: ymmsl::Operator +.. doxygenstruct:: ymmsl::Port +.. doxygenclass:: ymmsl::Reference +.. doxygenfunction:: ymmsl::operator<<(std::ostream&, Reference const&) +.. doxygenclass:: ymmsl::ReferencePart +.. doxygenclass:: ymmsl::Settings +.. doxygenfunction:: ymmsl::operator<<(std::ostream&, ymmsl::Settings const&) +.. doxygenclass:: ymmsl::SettingValue +.. doxygenfunction:: ymmsl::operator<<(std::ostream&, ymmsl::SettingValue const&) From e9fb1c0d69d7571581d9aac09516ffeec3d6a8b9 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Mon, 26 Jun 2023 21:14:43 +0200 Subject: [PATCH 03/36] Add menu headings for C++ API docs --- docs/source/cpp_api.rst | 42 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/source/cpp_api.rst b/docs/source/cpp_api.rst index 30d2d929..8141e21f 100644 --- a/docs/source/cpp_api.rst +++ b/docs/source/cpp_api.rst @@ -6,30 +6,72 @@ This page provides full documentation for the C++ API of MUSCLE3. Namespace libmuscle ------------------- +Data +```` .. doxygenclass:: libmuscle::Data + +DataConstRef +```````````` .. doxygenclass:: libmuscle::DataConstRef + +Instance +```````` .. doxygenclass:: libmuscle::Instance + +InstanceFlags +````````````` .. doxygenenum:: libmuscle::InstanceFlags + +Message +``````` .. doxygenclass:: libmuscle::Message + +PortsDescription +```````````````` .. doxygentypedef:: libmuscle::PortsDescription Namespace ymmsl --------------- +allows_sending +`````````````` .. doxygenfunction:: ymmsl::allows_sending + +allows_receiving +```````````````` .. doxygenfunction:: ymmsl::allows_receiving +Conduit +``````` .. doxygenclass:: ymmsl::Conduit + +Identifier +`````````` .. doxygenclass:: ymmsl::Identifier .. doxygenfunction:: ymmsl::operator<<(std::ostream&, Identifier const&) + +Operator +```````` .. doxygenenum:: ymmsl::Operator + +Port +```` .. doxygenstruct:: ymmsl::Port + +Reference +````````` .. doxygenclass:: ymmsl::Reference .. doxygenfunction:: ymmsl::operator<<(std::ostream&, Reference const&) .. doxygenclass:: ymmsl::ReferencePart + +Settings +```````` .. doxygenclass:: ymmsl::Settings .. doxygenfunction:: ymmsl::operator<<(std::ostream&, ymmsl::Settings const&) + +SettingValue +```````````` .. doxygenclass:: ymmsl::SettingValue .. doxygenfunction:: ymmsl::operator<<(std::ostream&, ymmsl::SettingValue const&) From 08441f0b2498b2d552d3b54535c64850e9c20678 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Wed, 28 Jun 2023 21:43:38 +0200 Subject: [PATCH 04/36] Protect manager connection from race conditions in MMPClient --- libmuscle/cpp/src/libmuscle/mmp_client.cpp | 2 ++ libmuscle/cpp/src/libmuscle/mmp_client.hpp | 6 +++++- libmuscle/python/libmuscle/mmp_client.py | 12 +++++++++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/libmuscle/cpp/src/libmuscle/mmp_client.cpp b/libmuscle/cpp/src/libmuscle/mmp_client.cpp index 5b82c9de..aa2d17f4 100644 --- a/libmuscle/cpp/src/libmuscle/mmp_client.cpp +++ b/libmuscle/cpp/src/libmuscle/mmp_client.cpp @@ -325,6 +325,8 @@ void MMPClient::deregister_instance() { } DataConstRef MMPClient::call_manager_(DataConstRef const & request) { + std::lock_guard lock(mutex_); + msgpack::sbuffer sbuf; msgpack::pack(sbuf, request); diff --git a/libmuscle/cpp/src/libmuscle/mmp_client.hpp b/libmuscle/cpp/src/libmuscle/mmp_client.hpp index 0ba9245e..c082809b 100644 --- a/libmuscle/cpp/src/libmuscle/mmp_client.hpp +++ b/libmuscle/cpp/src/libmuscle/mmp_client.hpp @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -28,7 +29,9 @@ namespace libmuscle { namespace _MUSCLE_IMPL_NS { * This class connects to the Manager and communicates with it on behalf of the * rest of libmuscle. * - * It manages the connection, and encodes and decodes MsgPack. + * It manages the connection, and encodes and decodes MsgPack. Communication is + * protected by an internal mutex, so this class can be called simultaneously + * from different threads. */ class MMPClient { public: @@ -123,6 +126,7 @@ class MMPClient { private: ymmsl::Reference instance_id_; mcp::TcpTransportClient transport_client_; + mutable std::mutex mutex_; /* Helper function that encodes/decodes and calls the manager. */ diff --git a/libmuscle/python/libmuscle/mmp_client.py b/libmuscle/python/libmuscle/mmp_client.py index d9ebe698..2298fadd 100644 --- a/libmuscle/python/libmuscle/mmp_client.py +++ b/libmuscle/python/libmuscle/mmp_client.py @@ -1,6 +1,7 @@ import dataclasses from pathlib import Path from random import uniform +from threading import Lock from time import perf_counter, sleep from typing import Any, Dict, Iterable, List, Optional, Tuple @@ -106,6 +107,9 @@ class MMPClient(): It manages the connection, and converts between our native types and the gRPC generated types. + + Communication is protected by an internal lock, so this class can + be called simultaneously from different threads. """ def __init__(self, instance_id: Reference, location: str) -> None: """Create an MMPClient @@ -115,6 +119,7 @@ def __init__(self, instance_id: Reference, location: str) -> None: """ self._instance_id = instance_id self._transport_client = TcpTransportClient(location) + self._mutex = Lock() def close(self) -> None: """Close the connection @@ -280,6 +285,7 @@ def _call_manager(self, request: Any) -> Any: Returns: The decoded response """ - encoded_request = msgpack.packb(request, use_bin_type=True) - response, _ = self._transport_client.call(encoded_request) - return msgpack.unpackb(response, raw=False) + with self._mutex: + encoded_request = msgpack.packb(request, use_bin_type=True) + response, _ = self._transport_client.call(encoded_request) + return msgpack.unpackb(response, raw=False) From 35ec94d8bdf5434211053f368e23858c979cf774 Mon Sep 17 00:00:00 2001 From: David Coster Date: Thu, 6 Jul 2023 10:55:29 +0200 Subject: [PATCH 05/36] rotate x-axis labels by 45 degrees to decrease the probability of labels covering each other --- muscle3/profiling.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/muscle3/profiling.py b/muscle3/profiling.py index 5a78ba7c..38f7f488 100644 --- a/muscle3/profiling.py +++ b/muscle3/profiling.py @@ -33,8 +33,12 @@ def plot_instances(performance_file: Path) -> None: ax.bar(instances, wait, width, label='Wait', bottom=bottom) ax.set_title('Simulation component time breakdown') ax.set_xlabel('Instance') + ax.tick_params(axis='x', labelrotation = 45) + for label in ax.xaxis.get_ticklabels(): + label.set_horizontalalignment('right') ax.set_ylabel('Total time (s)') ax.legend(loc='upper right') + plt.subplots_adjust(bottom=0.30) def plot_resources(performance_file: Path) -> None: @@ -81,8 +85,12 @@ def plot_resources(performance_file: Path) -> None: ax.set_title('Per-core time breakdown') ax.set_xlabel('Core') + ax.tick_params(axis='x', labelrotation = 45) + for label in ax.xaxis.get_ticklabels(): + label.set_horizontalalignment('right') ax.set_ylabel('Total time (s)') ax.legend(loc='upper right') + plt.subplots_adjust(bottom=0.30) _EVENT_TYPES = ( From 04d36fac1fd4b13b0922db6cd4ddc5ce8b1cf623 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Thu, 6 Jul 2023 21:14:30 +0200 Subject: [PATCH 06/36] Work around click issue with the latest mypy --- scripts/convert_fortran_source.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/convert_fortran_source.py b/scripts/convert_fortran_source.py index dd937b49..8e021b93 100644 --- a/scripts/convert_fortran_source.py +++ b/scripts/convert_fortran_source.py @@ -5,7 +5,7 @@ import click -@click.command(no_args_is_help=True) +@click.command(no_args_is_help=True) # type: ignore @click.argument("fortran_files", nargs=-1, required=True, type=click.Path( exists=True, file_okay=True, dir_okay=False, readable=True, allow_dash=True, resolve_path=True, path_type=pathlib.Path)) @@ -42,4 +42,4 @@ def convert(fortran_files: List[pathlib.Path]) -> None: if __name__ == "__main__": - convert() + convert() # type: ignore From 200360b9bc644b2110a604a159febd26abbb7c6b Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Fri, 1 Sep 2023 13:37:12 +0200 Subject: [PATCH 07/36] Dynamically load data for profiling timeline plot --- .../python/libmuscle/manager/profile_store.py | 2 + muscle3/profiling.py | 183 ++++++++++++++---- setup.cfg | 5 +- 3 files changed, 146 insertions(+), 44 deletions(-) diff --git a/libmuscle/python/libmuscle/manager/profile_store.py b/libmuscle/python/libmuscle/manager/profile_store.py index 5efa69bd..f4c78ac8 100644 --- a/libmuscle/python/libmuscle/manager/profile_store.py +++ b/libmuscle/python/libmuscle/manager/profile_store.py @@ -241,6 +241,8 @@ def _init_database(self) -> None: cur.execute("CREATE INDEX instances_oid_idx ON instances(oid)") + cur.execute("CREATE INDEX events_start_time_idx ON events(start_time)") + cur.execute( "CREATE VIEW all_events" " AS SELECT" diff --git a/muscle3/profiling.py b/muscle3/profiling.py index 5a78ba7c..98f04a9b 100644 --- a/muscle3/profiling.py +++ b/muscle3/profiling.py @@ -1,8 +1,11 @@ import sqlite3 from pathlib import Path +from typing import List, Optional, Tuple import numpy as np +from matplotlib.axes import Axes from matplotlib import pyplot as plt +from matplotlib.patches import Rectangle from libmuscle import ProfileDatabase @@ -63,7 +66,7 @@ def plot_resources(performance_file: Path) -> None: seen_instances = set() for i, core in enumerate(sorted(stats.keys())): - bottom = 0 + bottom = 0.0 for instance, time in sorted(stats[core].items(), key=lambda x: -x[1]): if instance not in seen_instances: label: Optional[str] = instance @@ -72,7 +75,7 @@ def plot_resources(performance_file: Path) -> None: label = '_' ax.bar( - i, time, 0.8, + i, time, _BAR_WIDTH, label=label, bottom=bottom, color=palette[instance]) bottom += time @@ -100,75 +103,169 @@ def plot_resources(performance_file: Path) -> None: 'SEND': '#0095bf'} -_MAX_EVENTS = 2000 +_MAX_EVENTS = 1000 -def plot_timeline(performance_file: Path) -> None: - with sqlite3.connect(performance_file) as conn: - cur = conn.cursor() +_BAR_WIDTH = 0.8 + + +class TimelinePlot: + """Manages an interactive timeline + + This implements on-demand loading of events as the user pans and + zooms. + """ + def __init__(self, performance_file: Path) -> None: + """Create a TimelinePlot + + This plots the dark gray background bars, and then plots the + rest on top on demand. + + Args: + performance_file: The database to plot + """ + _, ax = plt.subplots() + self._ax = ax + + # Y axis + self._cur = sqlite3.connect(performance_file).cursor() + self._cur.execute("SELECT oid, name FROM instances ORDER BY oid") + instance_ids, instance_names = zip(*self._cur.fetchall()) - cur.execute("SELECT oid, name FROM instances ORDER BY oid") - instance_ids, instance_names = zip(*cur.fetchall()) + ax.set_yticks(instance_ids) + ax.set_yticklabels(instance_names) - cur.execute("SELECT MIN(start_time) FROM events") - min_time = cur.fetchall()[0][0] + # Instances + self._cur.execute("SELECT MIN(start_time) FROM events") + self._min_time = self._cur.fetchall()[0][0] - cur.execute( + self._cur.execute( "SELECT instance_oid, (start_time - ?)" " FROM events AS e" " JOIN event_types AS et ON (e.event_type_oid = et.oid)" - " WHERE et.name = 'REGISTER'", (min_time,)) - begin_times = dict(cur.fetchall()) + " WHERE et.name = 'REGISTER'", (self._min_time,)) + begin_times = dict(self._cur.fetchall()) - cur.execute( + self._cur.execute( "SELECT instance_oid, (stop_time - ?)" " FROM events AS e" " JOIN event_types AS et ON (e.event_type_oid = et.oid)" - " WHERE et.name = 'DEREGISTER'", (min_time,)) - end_times = dict(cur.fetchall()) - - fig, ax = plt.subplots() + " WHERE et.name = 'DEREGISTER'", (self._min_time,)) + end_times = dict(self._cur.fetchall()) instances = sorted(begin_times.keys()) + self._instances = instances + + # Rest of plot + ax.set_title('Execution timeline') + ax.set_xlabel('Wallclock time (s)') + + # Background ax.barh( instances, [(end_times[i] - begin_times[i]) * 1e-9 for i in instances], - 0.8, + _BAR_WIDTH, left=[begin_times[i] * 1e-9 for i in instances], label='RUNNING', color='#444444' ) + # Initial events plot + xmin = min(begin_times.values()) + xmax = max(end_times.values()) + + self._bars = dict() for event_type in _EVENT_TYPES: - cur.execute( - "SELECT" - " instance_oid, (start_time - ?) * 1e-9," - " (stop_time - start_time) * 1e-9" - " FROM events AS e" - " JOIN event_types AS et ON (e.event_type_oid = et.oid)" - " WHERE et.name = ?" - " ORDER BY start_time ASC" - " LIMIT ?", - (min_time, event_type, _MAX_EVENTS)) - instances, start_times, durations = zip(*cur.fetchall()) - - if len(instances) == _MAX_EVENTS: - print( - 'Warning: event data truncated. Sorry, we cannot yet show' - ' this amount of data efficiently enough.') - ax.barh( - instances, durations, 0.8, - label=event_type, left=start_times, + instances, start_times, durations = self.get_data(event_type, xmin, xmax) + self._bars[event_type] = ax.barh( + instances[0:_MAX_EVENTS], durations[0:_MAX_EVENTS], _BAR_WIDTH, + label=event_type, left=start_times[0:_MAX_EVENTS], color=_EVENT_PALETTE[event_type]) - ax.set_yticks(instance_ids) - ax.set_yticklabels(instance_names) - - ax.set_title('Execution timeline') - ax.set_xlabel('Wallclock time (s)') + ax.set_autoscale_on(True) + ax.callbacks.connect('xlim_changed', self.update_data) ax.legend(loc='upper right') + ax.figure.canvas.draw_idle() + + def close(self) -> None: + """Closes the database connection""" + self._cur.close() + + def get_data( + self, event_type: str, xmin: float, xmax: float + ) -> Tuple[List[int], List[float], List[float]]: + """Get events from the database + + Returns three lists with instance oid, start time and duration. + + Args: + event_type: Type of events to get + xmin: Time point after which the event must have stopped + xmax: Time point before which the event must have started + """ + self._cur.execute( + "SELECT" + " instance_oid, (start_time - ?) * 1e-9," + " (stop_time - start_time) * 1e-9" + " FROM events AS e" + " JOIN event_types AS et ON (e.event_type_oid = et.oid)" + " WHERE et.name = ?" + " AND start_time <= ?" + " AND ? <= stop_time" + " ORDER BY start_time ASC" + " LIMIT ?", + ( + self._min_time, event_type, self._min_time + xmax * 1e9, + self._min_time + xmin * 1e9, _MAX_EVENTS)) + results = self._cur.fetchall() + if not results: + return list(), list(), list() + + if len(results) == _MAX_EVENTS: + print('Too much data, please zoom in to see events.') + + return zip(*results) # type: ignore + + def update_data(self, ax: Axes) -> None: + """Update the plot after the axes have changed + + This is called after the user has panned or zoomed, and refreshes the + plot. + + Args: + ax: The Axes object we are drawing in + """ + xmin, xmax = ax.viewLim.intervalx + + for event_type in _EVENT_TYPES: + instances, start_times, durations = self.get_data(event_type, xmin, xmax) + if instances: + # update existing rectangles + bars = self._bars[event_type].patches + n_cur = len(instances) + n_avail = len(bars) + + for i in range(min(n_cur, n_avail)): + bars[i].set_y(instances[i] - _BAR_WIDTH * 0.5) + bars[i].set_x(start_times[i]) + bars[i].set_width(durations[i]) + bars[i].set_visible(True) + + # set any superfluous ones invisible + for i in range(n_cur, n_avail): + bars[i].set_visible(False) + + +tplot = None # type: Optional[TimelinePlot] + + +def plot_timeline(performance_file: Path) -> None: + global tplot + tplot = TimelinePlot(performance_file) def show_plots() -> None: """Actually show the plots on screen""" plt.show() + if tplot: + tplot.close() diff --git a/setup.cfg b/setup.cfg index 547e9306..5b1cf6aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ addopts = --cov --cov-report xml --cov-report term-missing -s # -vv --log-cli-level=DEBUG [mypy] -files = libmuscle/python/**/*.py, scripts/*.py +files = libmuscle/python/**/*.py, scripts/*.py, muscle3/*.py mypy_path = libmuscle/python warn_unused_configs = True disallow_subclassing_any = True @@ -31,6 +31,9 @@ ignore_missing_imports = True [mypy-pytest] ignore_missing_imports = True +[mypy-matplotlib.*] +ignore_missing_imports = True + [mypy-msgpack.*] ignore_missing_imports = True From a5657a77f5abdfb2f6dd87f2a4400d17df605750 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Fri, 1 Sep 2023 15:55:47 +0200 Subject: [PATCH 08/36] Plot area where no data is shown in the timeline plot --- muscle3/profiling.py | 63 +++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/muscle3/profiling.py b/muscle3/profiling.py index 98f04a9b..1fba0316 100644 --- a/muscle3/profiling.py +++ b/muscle3/profiling.py @@ -137,20 +137,20 @@ def __init__(self, performance_file: Path) -> None: # Instances self._cur.execute("SELECT MIN(start_time) FROM events") - self._min_time = self._cur.fetchall()[0][0] + self._min_db_time = self._cur.fetchall()[0][0] self._cur.execute( - "SELECT instance_oid, (start_time - ?)" + "SELECT instance_oid, (start_time - ?) * 1e-9" " FROM events AS e" " JOIN event_types AS et ON (e.event_type_oid = et.oid)" - " WHERE et.name = 'REGISTER'", (self._min_time,)) + " WHERE et.name = 'REGISTER'", (self._min_db_time,)) begin_times = dict(self._cur.fetchall()) self._cur.execute( - "SELECT instance_oid, (stop_time - ?)" + "SELECT instance_oid, (stop_time - ?) * 1e-9" " FROM events AS e" " JOIN event_types AS et ON (e.event_type_oid = et.oid)" - " WHERE et.name = 'DEREGISTER'", (self._min_time,)) + " WHERE et.name = 'DEREGISTER'", (self._min_db_time,)) end_times = dict(self._cur.fetchall()) instances = sorted(begin_times.keys()) @@ -163,23 +163,34 @@ def __init__(self, performance_file: Path) -> None: # Background ax.barh( instances, - [(end_times[i] - begin_times[i]) * 1e-9 for i in instances], + [end_times[i] - begin_times[i] for i in instances], _BAR_WIDTH, - left=[begin_times[i] * 1e-9 for i in instances], + left=[begin_times[i] for i in instances], label='RUNNING', color='#444444' ) # Initial events plot xmin = min(begin_times.values()) - xmax = max(end_times.values()) + self._global_xmax = max(end_times.values()) + first_cutoff = float('inf') self._bars = dict() for event_type in _EVENT_TYPES: - instances, start_times, durations = self.get_data(event_type, xmin, xmax) + instances, start_times, durations, cutoff = self.get_data( + event_type, xmin, self._global_xmax) self._bars[event_type] = ax.barh( instances[0:_MAX_EVENTS], durations[0:_MAX_EVENTS], _BAR_WIDTH, label=event_type, left=start_times[0:_MAX_EVENTS], color=_EVENT_PALETTE[event_type]) + if cutoff: + first_cutoff = min(first_cutoff, cutoff) + + # Plot cut-off area + if first_cutoff != float('inf'): + self._bars['_CUTOFF'] = ax.barh( + self._instances, self._global_xmax - first_cutoff, _BAR_WIDTH, + label='Not shown', left=first_cutoff, + color='#FFFFFF', hatch='x') ax.set_autoscale_on(True) ax.callbacks.connect('xlim_changed', self.update_data) @@ -193,10 +204,12 @@ def close(self) -> None: def get_data( self, event_type: str, xmin: float, xmax: float - ) -> Tuple[List[int], List[float], List[float]]: + ) -> Tuple[List[int], List[float], List[float], Optional[float]]: """Get events from the database - Returns three lists with instance oid, start time and duration. + Returns three lists with instance oid, start time and duration, and + the last timepoint returned in case we had too much data to show and + data got cut off, or None if all matching data was returned. Args: event_type: Type of events to get @@ -210,21 +223,21 @@ def get_data( " FROM events AS e" " JOIN event_types AS et ON (e.event_type_oid = et.oid)" " WHERE et.name = ?" - " AND start_time <= ?" - " AND ? <= stop_time" + " AND (start_time - ?) * 1e-9 <= ?" + " AND ? <= (stop_time - ?) * 1e-9" " ORDER BY start_time ASC" " LIMIT ?", ( - self._min_time, event_type, self._min_time + xmax * 1e9, - self._min_time + xmin * 1e9, _MAX_EVENTS)) + self._min_db_time, event_type, self._min_db_time, xmax, + xmin, self._min_db_time, _MAX_EVENTS)) results = self._cur.fetchall() if not results: - return list(), list(), list() + return list(), list(), list(), None if len(results) == _MAX_EVENTS: - print('Too much data, please zoom in to see events.') + return tuple(zip(*results)) + (results[-1][1],) # type: ignore - return zip(*results) # type: ignore + return tuple(zip(*results)) + (None,) # type: ignore def update_data(self, ax: Axes) -> None: """Update the plot after the axes have changed @@ -238,7 +251,8 @@ def update_data(self, ax: Axes) -> None: xmin, xmax = ax.viewLim.intervalx for event_type in _EVENT_TYPES: - instances, start_times, durations = self.get_data(event_type, xmin, xmax) + instances, start_times, durations, cutoff = self.get_data( + event_type, xmin, xmax) if instances: # update existing rectangles bars = self._bars[event_type].patches @@ -255,6 +269,17 @@ def update_data(self, ax: Axes) -> None: for i in range(n_cur, n_avail): bars[i].set_visible(False) + # update cutoff bars + bars = self._bars['_CUTOFF'].patches + if cutoff: + for bar in bars: + bar.set_x(cutoff) + bar.set_width(self._global_xmax - cutoff) + bar.set_visible(True) + else: + for bar in bars: + bar.set_visible(False) + tplot = None # type: Optional[TimelinePlot] From 0642a4adf1f84a2b8fddea375ddb5de169cb8528 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Fri, 1 Sep 2023 16:55:23 +0200 Subject: [PATCH 09/36] Warn the user if events were droppod from the profile timeline --- muscle3/profiling.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/muscle3/profiling.py b/muscle3/profiling.py index 1fba0316..c226f8b6 100644 --- a/muscle3/profiling.py +++ b/muscle3/profiling.py @@ -106,6 +106,13 @@ def plot_resources(performance_file: Path) -> None: _MAX_EVENTS = 1000 +_CUTOFF_TEXT = ( + 'Warning: data was omitted from the plot in the\n crosshatched' + ' areas to improve performance.\n Please zoom or pan using the' + ' tools at the bottom\n of the window to see the missing events.' + ) + + _BAR_WIDTH = 0.8 @@ -185,12 +192,17 @@ def __init__(self, performance_file: Path) -> None: if cutoff: first_cutoff = min(first_cutoff, cutoff) - # Plot cut-off area + # Initial cut-off area if first_cutoff != float('inf'): self._bars['_CUTOFF'] = ax.barh( self._instances, self._global_xmax - first_cutoff, _BAR_WIDTH, label='Not shown', left=first_cutoff, color='#FFFFFF', hatch='x') + self._cutoff_warning = ax.text( + 0.02, 0.02, _CUTOFF_TEXT, transform=ax.transAxes, fontsize=12, + verticalalignment='bottom', horizontalalignment='left', wrap=True, + bbox={ + 'facecolor': '#ffcccc', 'alpha': 0.75}) ax.set_autoscale_on(True) ax.callbacks.connect('xlim_changed', self.update_data) @@ -276,9 +288,11 @@ def update_data(self, ax: Axes) -> None: bar.set_x(cutoff) bar.set_width(self._global_xmax - cutoff) bar.set_visible(True) + self._cutoff_warning.set_visible(True) else: for bar in bars: bar.set_visible(False) + self._cutoff_warning.set_visible(False) tplot = None # type: Optional[TimelinePlot] From 3c920f555e05828c70befd02d7711bf655aa5825 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Fri, 1 Sep 2023 21:27:28 +0200 Subject: [PATCH 10/36] Add DISCONNECT_WAIT profiling event --- docs/source/examples/python/Makefile | 1 + docs/source/examples/python/dispatch.py | 54 +++++++++++++++++++ libmuscle/cpp/src/libmuscle/communicator.cpp | 4 ++ libmuscle/cpp/src/libmuscle/profiling.hpp | 5 +- libmuscle/python/libmuscle/communicator.py | 4 ++ libmuscle/python/libmuscle/instance.py | 14 +++-- .../libmuscle/manager/profile_database.py | 17 +++--- .../python/libmuscle/manager/profile_store.py | 2 +- .../manager/test/test_profile_store.py | 2 +- libmuscle/python/libmuscle/profiling.py | 3 +- muscle3/profiling.py | 14 +++-- 11 files changed, 95 insertions(+), 25 deletions(-) create mode 100644 docs/source/examples/python/dispatch.py diff --git a/docs/source/examples/python/Makefile b/docs/source/examples/python/Makefile index 48f27607..55f3a218 100644 --- a/docs/source/examples/python/Makefile +++ b/docs/source/examples/python/Makefile @@ -7,6 +7,7 @@ test: . build/venv/bin/activate && DONTPLOT=1 python3 reaction_diffusion.py . build/venv/bin/activate && DONTPLOT=1 python3 reaction_diffusion_qmc.py . build/venv/bin/activate && DONTPLOT=1 python3 interact_coupling.py + . build/venv/bin/activate && DONTPLOT=1 python3 dispatch.py .PHONY: clean clean: diff --git a/docs/source/examples/python/dispatch.py b/docs/source/examples/python/dispatch.py new file mode 100644 index 00000000..ea74e245 --- /dev/null +++ b/docs/source/examples/python/dispatch.py @@ -0,0 +1,54 @@ +import logging +import time + +from libmuscle import Instance, Message +from libmuscle.runner import run_simulation +from ymmsl import ( + Component, Conduit, Configuration, Model, Operator, Ports, Settings) + + +def buffer() -> None: + """A component that passes on its input to its output. + + If the input is not connected, it'll generate a message. + """ + instance = Instance({ + Operator.F_INIT: ['in'], + Operator.O_F: ['out']}) + + while instance.reuse_instance(): + # F_INIT + msg = instance.receive('in', default=Message(0.0, data='Testing')) + + # S + time.sleep(0.25) + + # O_F + instance.send('out', msg) + + +if __name__ == '__main__': + logging.basicConfig() + logging.getLogger().setLevel(logging.INFO) + + components = [ + Component( + 'component1', 'buffer', None, + Ports(o_f=['out'])), + Component( + 'component2', 'buffer', None, + Ports(f_init=['in'], o_f=['out'])), + Component( + 'component3', 'buffer', None, + Ports(f_init=['in']))] + + conduits = [ + Conduit('component1.out', 'component2.in'), + Conduit('component2.out', 'component3.in')] + + model = Model('dispatch', components, conduits) + settings = Settings({}) + configuration = Configuration(model, settings) + + implementations = {'buffer': buffer} + run_simulation(configuration, implementations) diff --git a/libmuscle/cpp/src/libmuscle/communicator.cpp b/libmuscle/cpp/src/libmuscle/communicator.cpp index 254d0290..38827022 100644 --- a/libmuscle/cpp/src/libmuscle/communicator.cpp +++ b/libmuscle/cpp/src/libmuscle/communicator.cpp @@ -314,8 +314,12 @@ void Communicator::shutdown() { for (auto & client : clients_) client.second->close(); + ProfileEvent wait_event(ProfileEventType::disconnect_wait, ProfileTimestamp()); + post_office_.wait_for_receivers(); + profiler_.record_event(std::move(wait_event)); + for (auto & server : servers_) server->close(); } diff --git a/libmuscle/cpp/src/libmuscle/profiling.hpp b/libmuscle/cpp/src/libmuscle/profiling.hpp index f46b8db9..f8552d32 100644 --- a/libmuscle/cpp/src/libmuscle/profiling.hpp +++ b/libmuscle/cpp/src/libmuscle/profiling.hpp @@ -20,12 +20,13 @@ namespace libmuscle { namespace _MUSCLE_IMPL_NS { enum class ProfileEventType { register_ = 0, connect = 4, - deregister = 1, send = 2, receive = 3, receive_wait = 5, receive_transfer = 6, - receive_decode = 7 + receive_decode = 7, + disconnect_wait = 8, + deregister = 1 }; diff --git a/libmuscle/python/libmuscle/communicator.py b/libmuscle/python/libmuscle/communicator.py index 8be5cb88..88604002 100644 --- a/libmuscle/python/libmuscle/communicator.py +++ b/libmuscle/python/libmuscle/communicator.py @@ -413,8 +413,12 @@ def shutdown(self) -> None: for client in self._clients.values(): client.close() + wait_event = ProfileEvent(ProfileEventType.DISCONNECT_WAIT, ProfileTimestamp()) + self._post_office.wait_for_receivers() + self._profiler.record_event(wait_event) + for server in self._servers: server.close() diff --git a/libmuscle/python/libmuscle/instance.py b/libmuscle/python/libmuscle/instance.py index db0f2f6b..7b4d2f71 100644 --- a/libmuscle/python/libmuscle/instance.py +++ b/libmuscle/python/libmuscle/instance.py @@ -246,10 +246,7 @@ def reuse_instance(self) -> bool: self._save_snapshot(None, True, self.__f_init_max_timestamp) if not do_reuse: - self.__close_ports() - self._communicator.shutdown() - self._deregister() - self.__manager.close() + self.__shutdown() self._api_guard.reuse_instance_done(do_reuse) return do_reuse @@ -1251,14 +1248,15 @@ def __close_ports(self) -> None: self.__close_outgoing_ports() self.__close_incoming_ports() - def __shutdown(self, message: str) -> None: + def __shutdown(self, message: Optional[str] = None) -> None: """Shuts down simulation. - This logs the given error message, communicates to the peers - that we're shutting down, and deregisters from the manager. + This logs the given error message, if any, communicates to the + peers that we're shutting down, and deregisters from the manager. """ if not self.__is_shut_down: - _logger.critical(message) + if message is not None: + _logger.critical(message) self.__close_ports() self._communicator.shutdown() self._deregister() diff --git a/libmuscle/python/libmuscle/manager/profile_database.py b/libmuscle/python/libmuscle/manager/profile_database.py index 9ee9618d..d82df918 100644 --- a/libmuscle/python/libmuscle/manager/profile_database.py +++ b/libmuscle/python/libmuscle/manager/profile_database.py @@ -1,5 +1,4 @@ from collections import defaultdict -import logging from pathlib import Path import sqlite3 import threading @@ -7,9 +6,6 @@ from typing import Any, cast, Dict, List, Optional, Tuple, Type, Union -_logger = logging.getLogger(__name__) - - class ProfileDatabase: """Accesses a profiling database. @@ -107,9 +103,16 @@ def instance_stats( cur.execute( "SELECT instance, start_time" " FROM all_events" - " WHERE type = 'DEREGISTER'") + " WHERE type = 'DISCONNECT_WAIT'") stop_run = dict(cur.fetchall()) + if not stop_run: + cur.execute( + "SELECT instance, start_time" + " FROM all_events" + " WHERE type = 'DEREGISTER'") + stop_run = dict(cur.fetchall()) + cur.execute( "SELECT instance, SUM(stop_time - start_time)" " FROM all_events" @@ -164,8 +167,6 @@ def resource_stats(self) -> Dict[str, Dict[str, float]]: i: r + c for i, r, c in zip(instances, run_times, comm_times)} - _logger.info(active_times) - cur = self._get_cursor() cur.execute("BEGIN TRANSACTION") cur.execute( @@ -176,8 +177,6 @@ def resource_stats(self) -> Dict[str, Dict[str, float]]: for name, node, core in cur.fetchall(): instances_by_core[':'.join([node, str(core)])].append(name) - _logger.info(instances_by_core) - cur.execute("COMMIT") cur.close() diff --git a/libmuscle/python/libmuscle/manager/profile_store.py b/libmuscle/python/libmuscle/manager/profile_store.py index f4c78ac8..036dea85 100644 --- a/libmuscle/python/libmuscle/manager/profile_store.py +++ b/libmuscle/python/libmuscle/manager/profile_store.py @@ -194,7 +194,7 @@ def _init_database(self) -> None: " minor_version INTEGER NOT NULL)") cur.execute( "INSERT INTO muscle3_format(major_version, minor_version)" - " VALUES (1, 0)") + " VALUES (1, 1)") cur.execute( "CREATE TABLE instances (" diff --git a/libmuscle/python/libmuscle/manager/test/test_profile_store.py b/libmuscle/python/libmuscle/manager/test/test_profile_store.py index 63795d90..2961b6d9 100644 --- a/libmuscle/python/libmuscle/manager/test/test_profile_store.py +++ b/libmuscle/python/libmuscle/manager/test/test_profile_store.py @@ -18,7 +18,7 @@ def test_create_profile_store(tmp_path): cur.execute("SELECT major_version, minor_version FROM muscle3_format") major, minor = cur.fetchone() assert major == 1 - assert minor == 0 + assert minor == 1 cur.execute("SELECT oid, name FROM event_types") etypes = cur.fetchall() diff --git a/libmuscle/python/libmuscle/profiling.py b/libmuscle/python/libmuscle/profiling.py index 9398ec95..fd06bb19 100644 --- a/libmuscle/python/libmuscle/profiling.py +++ b/libmuscle/python/libmuscle/profiling.py @@ -9,12 +9,13 @@ class ProfileEventType(Enum): """Profiling event types for MUSCLE3.""" REGISTER = 0 CONNECT = 4 - DEREGISTER = 1 SEND = 2 RECEIVE = 3 RECEIVE_WAIT = 5 RECEIVE_TRANSFER = 6 RECEIVE_DECODE = 7 + DISCONNECT_WAIT = 8 + DEREGISTER = 1 class ProfileTimestamp: diff --git a/muscle3/profiling.py b/muscle3/profiling.py index 75ac8e66..cbf0d368 100644 --- a/muscle3/profiling.py +++ b/muscle3/profiling.py @@ -97,13 +97,14 @@ def plot_resources(performance_file: Path) -> None: _EVENT_TYPES = ( - 'REGISTER', 'CONNECT', 'DEREGISTER', + 'REGISTER', 'CONNECT', 'DISCONNECT_WAIT', 'DEREGISTER', 'SEND', 'RECEIVE_WAIT', 'RECEIVE_TRANSFER', 'RECEIVE_DECODE') _EVENT_PALETTE = { 'REGISTER': '#910f33', 'CONNECT': '#c85172', + 'DISCONNECT_WAIT': '#eedddd', 'DEREGISTER': '#910f33', 'RECEIVE_WAIT': '#cccccc', 'RECEIVE_TRANSFER': '#ff7d00', @@ -193,9 +194,16 @@ def __init__(self, performance_file: Path) -> None: for event_type in _EVENT_TYPES: instances, start_times, durations, cutoff = self.get_data( event_type, xmin, self._global_xmax) + + if not instances: + # Work around https://github.com/matplotlib/matplotlib/issues/21506 + instances = [''] + start_times = [float('NaN')] + durations = [float('NaN')] + self._bars[event_type] = ax.barh( - instances[0:_MAX_EVENTS], durations[0:_MAX_EVENTS], _BAR_WIDTH, - label=event_type, left=start_times[0:_MAX_EVENTS], + instances, durations, _BAR_WIDTH, + label=event_type, left=start_times, color=_EVENT_PALETTE[event_type]) if cutoff: first_cutoff = min(first_cutoff, cutoff) From f0e4d00bbeb5104a29587ddb71fd8e00d9628b5f Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Fri, 1 Sep 2023 21:30:26 +0200 Subject: [PATCH 11/36] Fix error when we have too little data to have a cutoff --- muscle3/profiling.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/muscle3/profiling.py b/muscle3/profiling.py index cbf0d368..242dad97 100644 --- a/muscle3/profiling.py +++ b/muscle3/profiling.py @@ -297,18 +297,19 @@ def update_data(self, ax: Axes) -> None: for i in range(n_cur, n_avail): bars[i].set_visible(False) - # update cutoff bars - bars = self._bars['_CUTOFF'].patches - if cutoff: - for bar in bars: - bar.set_x(cutoff) - bar.set_width(self._global_xmax - cutoff) - bar.set_visible(True) - self._cutoff_warning.set_visible(True) - else: - for bar in bars: - bar.set_visible(False) - self._cutoff_warning.set_visible(False) + # update cutoff bars, if any + if '_CUTOFF' in self._bars: + bars = self._bars['_CUTOFF'].patches + if cutoff: + for bar in bars: + bar.set_x(cutoff) + bar.set_width(self._global_xmax - cutoff) + bar.set_visible(True) + self._cutoff_warning.set_visible(True) + else: + for bar in bars: + bar.set_visible(False) + self._cutoff_warning.set_visible(False) tplot = None # type: Optional[TimelinePlot] From 4ad318ae890783354c2a374d1e1e3cc3b0fa5b4a Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Mon, 4 Sep 2023 22:14:49 +0200 Subject: [PATCH 12/36] Update contributors, collaboration and support info --- CITATION.cff | 3 +++ README.rst | 14 +++++++++-- docs/source/contributor_logos.png | Bin 0 -> 93978 bytes docs/source/index.rst | 38 ++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 docs/source/contributor_logos.png diff --git a/CITATION.cff b/CITATION.cff index 1eb23089..3b3ef8f7 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -12,6 +12,9 @@ authors: family-names: Veen given-names: Lourens orcid: "https://orcid.org/0000-0002-6311-1168" + - + family-names: Sebregts + given-names: Maarten keywords: - multiscale diff --git a/README.rst b/README.rst index cdbfe884..b1f0c294 100644 --- a/README.rst +++ b/README.rst @@ -18,12 +18,22 @@ | MUSCLE3 is the third incarnation of the MUSCLE Multiscale Coupling Library and -Environment. It is developed by the e-MUSC project of the University of -Amsterdam and the Netherlands eScience Center. +Environment. With MUSCLE3, you can connect multiple simulation models together into +a multiscale simulation. Browse to `the MUSCLE3 documentation`_ to get started. +Collaboration +============= + +For academic collaboration, please contact `prof. Alfons Hoekstra (UvA CSL) `_ +and/or `Lourens Veen (NLeSC) `_. + +Commercial support for MUSCLE3 is provided by +`Ignition Computing `_. + + Legal ===== diff --git a/docs/source/contributor_logos.png b/docs/source/contributor_logos.png new file mode 100644 index 0000000000000000000000000000000000000000..57e2fe672bc1dcaa45cf569484f4ef4e7a99301b GIT binary patch literal 93978 zcmcG#WmHtr+c!K2(g=cdH$!)qBOnavfPkccba!`&l)?~0*H9AD(j_3>AdMj1jpTd$ z-_P^hYd!D%>0Rp`z8qM?*=L`#_jUd1L}{qK#>1w-27y3$N{VtY5C}~U1VZ_U`2_e& z-$fo32t)@`l6$G;HNU^)?Ps9lFY<6Cw&hS`B{lK*o+5JCQ5rzPm(4gJsg2_R$^|M$WFFwXxl z;D3+v{~qvv80UW&@W03T|2nb%9_Rm;iT$7Z_J0okd%*uQAD={lV@6R|JzL1ncl)A} zl75f(_y(@S)6~?|*L-cyQu>p4SNk764WQZlNHGjW?eWXqjwG6*sI&QK9%=#tG0x|4 zI(!q&2Zt7_PFPUZm?}iSTA-StF;f**B_q3j{_lzN4&9BgzrdF8*e9c=PUp+N&x_{v zsl%VSy*hdvj}OpdPcoeC!9Np$8+|f3;ygaAmi)7eYL38y7a2Vq7Tb0!zKnThT^?nf z+OPMqr;Zx6*PuJ&8?~tfJPoc98a7-=exQg#Xy%U?Zr5$}>a0dgjvl3YtNGf-nq?d2 z$p2@7ky#k&Kui2@FYOoVr7-=FTKf8XU*}t4C3e5QnG7bE!=NDl2w$nI#k2MGb>(Ev zED29;qY&_S1LeTAMOM^WP_(r@w_-!PI` zCUhn>t?5@xv5FJ(x}yKr#k!Lyy$ftm2@H}6SC#@kQTF3b+To*^y@LZiZ)gc zQKZ7*5rw})@oE%X5y&K>-fXe@&g%($Dyt8#JLMBWUfnZ!wxn9}*!1L=U_M+ivn=~Q zFJsbdvZs}KPmB>P1lVwb-cDhXKv@IA*Z(I&fS2&)P z6*Lp;v)7BBAQ8!JtK9CZaR0V^hwRz6|J0){-ZOm>`}x{*hbHh%lLlMM{1Q+5-RU4t zyX1{HO?;+F{z#UTuaA!p=)5Z&2bif^lPUq)LW84mt52QzeOoO$6&2Urmg^EeEfsBL z*bl)NT@_TT?x`Wrz<2ALDp8^ktd~e!du8T?dh_OB4t9Bo<1p=~5adA+F=HX~u6{?H zSg2H|BCQuUnl#7?A}D~i^(m6Cyk@A_+A7_F52QFN5>Iugt#@mN_@>KCq*PNs0`=x6 znGLgo4948wDITBYHzt3-&lMY{MNEoHIjkA~S80quJeru0r4@RUpnf-gTQj6|C7pX1{>-Ts1TQ4YuSXpcR8^jH z)?3&<<{gUfcR_&{NP_4FYT>ZuYsD58sf;pv!31IE*m42ckHslDgp$YXCiIR>gxXEZ z#U;%3VL3#ARCar%=lQ|po{VUbv;2In=v&Z#+DHUqR?*gwljQdHHo9l+Yi69? z>B|xEi43LG6aT=gL#c86oqfp-+>x-)BO@b4*#Vou7y<0an|zs@ZV|JX=lD|1_qH;6 z?L}9`+Ss*EAgRQRY8eITa$w~}$=nwzl!QvAeI2|Vym$+I`(&t)u#eB3>?9IZ602Um z$rua90R7;NvVPvp6j72vh8%JsB$T}+Cci-oLd$|dpEI!ZwyeqZx>)Ec|)65P-=qCe>=<5wvzXBD!}70Qi)SzwM^rC|-$ zD&9L)C-)>W9An}mbhL-Y!h}as1uQrIs`2;WCc?0x7SBB6FR6zt(fu0=us*u<51I1| z?Xc9K*P{2vg_CoY5Q0EP#i;SO4!T2$UXlZEJPfJbsd{h+LZJcs&qw{;tj;HrBh+~^ zJYK${Vs%saSCqSWU1y>o?a75n+hmo$BeMgp+S=Nz20!qNU3jI=%#eV_C?X3P~~p8>7l-rr&#}qwd(||8lU4!XW>@piK9WBP(X+#o?O-vEhgOd<5Ij z)YKGNuhuLxLPf!H{Wb|!a?XM&fI8Pj+WxqzE@J;|i9dFh#d5?^zlopEUri}teCV5W ziw<$078!`1JsC8~R4C7e3Ssdt)w3;6LK~CYv*NZ-oVd~Qm;S;~{|dCLXIb`rEz_aQ zL&GXJj3q~q41i4rSm?5h`B84&U2ju{j10v5zH{h+Eg|j0DuopKOTQU&UW_LwD#1;2 ziwe*{!(_jMW0;d(u#;56p%w(G+TnB0T&XCWc7A0YNP4;b*)jNAPaYdMF|OgbPD}m2 z7OM_I;oZ-2kkDhSy*ayWV{YNpK~JH+64 z?wBcIzY)G8hSM9Jbn?53PND#!qx7s$-ae85TNacT$YzDC^ZLt3V(_K7Uv6+c+e2CM zjmiwHJHz!*K3cmKS^SRWkJ%!A-9#U%dICbp0vp0lxGTs67OlGOlIJ+vfHWL!+Vy>8 zMhQ|>gEQ!buIhc<#(qwZxixhKCrUJdghzE$yYpKs_ICyO5O+s~5+zpY#$l|i{fH3V-&TLzlpTQ zMU&aV9koFOyttw?#JB?#w!4Hl$=n(3f*eZPKl3op+WI!C{IO(v1Pupmc&QV|G8igC z1X0sJH>I6df6w@wHa*2RsBIFC`4Y|`S(X*_CpS5P0zH;N6Kh;cu!-nv~uS#~>+Q2!2#66Zg2%w4(SB|^S{?(Ecy4X*dCpUli?gU!V= zr5AWnA5pnp^-()Eh}}QOrxJCVHT1mS$$ohEx^Gcr$_lUK9KYEm(0`ODufh;rx`wD` z*=4$GZn8K0($CnX0?SOT>zf_;6*Ht9PMAwVRU2XxL62LTaVJJ#!DjJNGV8;%Y%!@y zx^{PAEY44CF|zMsm3gQun5cR1LM^@eq8BL%9K>c3Pu|N`!-dEcq!_V!*Ip1#7S1kR zzY+iP1=+$ziagqUc9=yb%GpKNQ~||gMhgq>d_{sTc&xR0_5s?vX2DreKoIQsEAF`$ z6KFk^5#m;UwX1mehA-Hhlz<#z1IG4(kF_t~?VnVBqMYu;YFTf5yZa9g-;_aSbIPPGp`OT1|q2Zf#~OICQ}Zp`fd;GYU6=C+&;v3 zhgEAQY={iqks(Xa49j<(3XEU_yY$Cvjdhud)H$nlhV=hH=XOgI1zSVzl-rw|5V{PX z14XE{y?wbg#fjG)X8-|x7_3w%*RuKjPehr3re3_|0tfc97+SE>oD- zVdK^coyjJmt;;f-!a4W{6(y+FI7`^9H!lv+9lXtRjf8gi(~1`9QJ|qoEq6fJ*i3%K zX(tmJL}_TEFJeIA%*o5KKvia+%SviEb6+qPMC^!$W1)!U_Puf}`|OI{sJl&cUAJd~ zRe`Fm1vO&$%{rO9uF(YnN3?W(hSmQ$ORU35s3QWsc12)Ss>thNhr9u6<@J`>+I)BN zS?JN>TN%FGAHDQQd5zI{L=K~XXpG2{IgKcBDzM=#5?a|q`wI#}I418(jvNLz+gmM` zpT>eYWBvoK0^+-3w!8sQ4h4f!eRMIAwS|Qd1THNX>(jY#y<{Xj51#p!B+rsx$o18$ zSLZ>7>`6$triztziP!kJ%A^W1z>+2e30wI!b>Ya9)Zwv)g#r4Q_)*h+Xr=+dw(5Fw)ADx^8s8)LVz)Sn`db4Emn&H%=lmdN8QN9UF z@qS&^`KVbI+fSjdp*;qTsk5-}F|kS|Nv2_^5cSKx?4ro8~;ADE&! z6Ksp?;GB)}%4-}yTq{RVgGDvcnYK1WDKRN{CQPF|371_`qPT_sxxrbG-1@`1&=9X| zM3dGr?jYM1k&x2YDho+85ZM(6u)+qO{%(f4AYWJ^^XshHqUM)_Kq!#=Pej)(g)A|w zwbm#|$+wk;z0f&RDSq0gMBcAP#sK*dPR&)}`(A`b#Zd49M(_jqDXHs8A0~{vnD^5n*WgK&d*^;6E z*^Q15yhIVfGdXo?EQv+2o)ZiajY!*j~@ju&KTM2w|Be=5?$=a1CQG@2nnbbloy=8<)48DMN@j8r8NE z__?{c^~R7FiLd_r=>%V6VL5GBz6n^l%HITc^QNVx&6K_KznIn;%@8|_O|>p00wl5%`#)L#u|^)5~l7iN7m zwfhQe9zy`!4#D@5-{jvvH6l>{==B6wlgUP^+wC-eip#o^Dm7UrYfPx*M*^{HN_oZk zmCE`48EbE?wj4HrNu%x7!$a1I`POi1dszvK&{d0ao)#zaV_~T%XXx{GT)pSrupX>| zS0q}80VbolRr{V5x|6G`oR*lmA`B~6?I*F0i~d(vbu+WZf!#&Y7bReaw~lClZjfSF zuWf7p?oy}yrVb#Dcb-?>k3BpAC&P{bX7AtAkwp7H-g@#FwU7nUKflSu7s%Rrtx8_B zYlEyKfxrjNXWg;r$73vGV`JmiGlcKHS1ioVZrwioIW&Qm_yw4nGR*$^CF<#bVP-kC z7sE#q-={K3MN6A4WIHooXS=tzheglG$f#d$H`nHWTVd!sQ@L^+rS9i_E_%j-;BVAw7+MhqH&4+AL<>JCT3?JYqF)zkGuCOTTTaL?z-dKo^xvk zF8f{?2e?1x8kzQ{CXT&)3WCE=95`)1ULRr*+aMBJZGNBt_ z{v4u+#h!*9YXuQ9OW-q||G|ri%(xaT{uNA%xcuu^-wkvVGLmTd(X-l_ zLTkCe>zFDp$$)v5XqQI6>*L>G>}a5T(P5Hu>AB5I=GUQ*!C{kvI^}wGR^xfQ>eGdC0#EBK@bO3lV6|=MNS<=ts_KRYQjo{dvTHwcO2)b2<0{4#fWHVp3H^WC?*<=% z@>m&g)lK^_mZ&c6-ABE0>K}N2b75#`D6;I;?RdpcJrrrY2>ZtE<4G+kJl;&o{`7#P zc7`MzDX{nYiI`Eb7i0GT*Uy|hBuHc259d!3MxFIj4_4Y1VjQR+B9Cctc}DTJ@+A^ay&t zl}jmstdF-@F|2o5ne}#+Z9Ta|KjT9a2+-u57~SAH;J2SP7S*fcFYc{S{?+9#(g>BYld`@`6glTWVf?`TQ2xCksU5AFC7rwn(9UH#7U%M4C zoSXaHyY}zl9s9Qfa^m2IOJj@O9RHo{KYci0-C%YjLla6#nXX25p9g0riLThQDaY$p zESij|`u#gsK+=UQ;{M(rG&VH_S1Yn#prNIe@j2hdN+-tt+Z~DT>E@dpG7^vka5oO&hRur#fF4YyANA}hQr<;lFSk}tLe;JUL4F! z@tDU%npCZtThUfUm&++{zWq&Nf`=cq>~m-bDl#}o8>*a}gw9;|LJ!=_r8TeFz*Y)O|E|ssry|uKh90M7T35g-(SfDC4Qm%Cl*`CC-TqE&i1WW zH8tJXRDG@6%urWRQ3QkN5GsW*Fw~kXVj54)_s^Fv&mr3_-qh2aZ=?dmGd&DNFz>e8 zySux&xVVIbgfywjo13FyHt*^Qs8;_pL=tmoml-s=EYu%lZB5S(_z zrnk(EWJ(0yt-Fsq#TOP9dbaKvR!Y90|IqV^_$ctPeC6>Wu)8YD%ksSUv9Hxh)OFBM zzf0GQKdDSrG|sG!$H=5%WQ6J$A}*Z^uRn9WLPVN{MWJq07P!lNxP;L)x?FtJkYa7v zAB@@=Jtq)^W8=?6w8ikj;%&EMw3)YpZMoZ2kOJ5Qy%H|x@nLgOP1VY1J4D~<_Sw%b z0pj8XPmFP%L9P8;jKj2fct8cRX9S=eSeRe3xU)^#4;MG-=LNgnX?g-*YW+u{Ox<#^ zPHVL+_Ki;u8}$@Ri4~Qo?AgKH8s5Y_S)x?RA5^{~{Vx~A&PYp?Sy*~mdn4WUV#2H& z`y?!^$~mCF!h}=niIro?Hbt4mv&~>129og07LG%&3|ezaXmG$`V~%hF)6py`o|bQ! ze3v&XoCJ_AIc^(WM(h{VmyuQ{I=!&Bh6cmo-{rCOO{4-Z{+iB#UUw>W$~|38k=vjSxjEiml~2aorLQGaH{ z=3iG*PbOFmr)7k-e7h_~PM*9`_&r0hdD*1eP?_V~9rTSKLi7lije7U^^UYIT#s8RS z_i7R3^goXOeVho;rhgDdm6YDmnrYJIq{e66(B+&N8x!@u1xL)NsNBsbZvXoAtAxFEa|kgx&es44 zcey^K6mn9u)d87>)xKtnSl!(=NmKOy7brsh0@?O zk0}>qU9w5IjYAnAFrVG~=?T32*X1rKY&B(Ov5E<|rLb|6pJ*a}@o(7+KI1Iq?fHfVw~xBU zM+A>ygKSdy25VL6&^plaPF`0{iL8)mIZ1 zpRxw~=EB0wBrZz5Jy?kr^+$#Q$GCGz%l z{3U60Pg>fa(zjcg->S^t4G#~)Cv-X0FdOeL7ia^#?wj2A9@cmsOi$K&goOGlDjYN6zI!Y~@}#ci1S9eKJ1Px8ReL0Inyhn7rtE9K<0r zAPV?I8V)mh)1{9rso`x~#N3&u)Wc_VRX785w*$3bA~^$9_NKQluI)I6@5>|uT6xKV zRcD<^*PmcfJ!^37i#MqyZMHTPVH(gVZ77(yT#zZWgGug;fL%UG9Zsl}pzJQ{x(}oK#WyzrSYQ-AfhadB?wh@H_ z!!dKQlRkKiWRw;?QJs0W;}|rL<6N^dJ=C1#1U{M+@8lvFuTNeEcn!ccW)m^igsxjg83G*88NdRe#%PFQ|_tc}f}3KqG(0 z#x&|AnZlR|qe=qZq_b(MsD>;S+95UC&5?_SsY>_W$`qW7_|rnnI7I;ZsI!>ZBeAvb0&Cw@^JhV8Y&G z|JNITrp=7(TDTGthKCnBU13UU;ACQNfAGR_roQ|GKlxLx-d_?c-uJ{h25t5Wm#-q9 z-L%Q<-0v3eXNjXd{H6^!?+89ru@LmxDE(T|w2^%Eq~isnA82*_H7#J_-G$0rht>=V zgu84cj|S#j(!Jus00cT&4h@)6C*~K)V)fMF^72=0Ka(Ym*uV`x+3d@d=Y&ci{aoe| z-HQZTr*a&hpVUskL##sxo$Ma?@ZjJZ)x*F3ahr7D zRf(Q>mn=1;6po|r#0O}!{M`(-!?l#DN|VE_aBs9!ow@A6!1FK0OSdBpy|@uiNg$5W zD>pOL8LoZK(pQb`9)?!{kDWdnm1*!iS{@&NgVf?jWbm2w4kT?=7&dzVB|=jr_Z@qZ z2Vg$kosAwXx7s>6jkynk1T!l#B|Hzkw=z9XdQyfE0+y$T82B_TM{T|+vY3sID>S0g zHs|RpcNQ1V-ji(tqz7;u{}7q^WHq?&C0l0uUm{bCsDNVTzfxTW)HncXw{38d8Uq5w z$u-y#p#!Cs@^L%c90r_M3Ab$x?vx~$c--XteA9>i3BWD4+89V;%`BZdYq{8)J({ao zaL%}DyGnkFLqa9y@?mQvb7^S_V9FVh$|>CUs~9rpr8JAm$O!-dHrOvTIj*#Ic6LH? zi{anwn{#uWms`BQ>Qpu!ci}9|&#U=ZN&;Li6kl4GlZ1rF$mMKUBtAA)>UO_Izg{x% zcs1C-fx#yR0Zjkzmhm@_17wZGa2CVt#l;1XE!YIY2N#!@f6ly-ZtO|B+uNd7i*HGd z{hZ0N0&I35xySo8qjrG(|M+m9pP&DdKx1HHXZ~G{1s6c-httl0{^`Mjzy>limD5;f zi$oBWNy)dZAK}&FbU%1w0Nc!aMZf^N@FsL~G~0MsYZttWpN}gT>wSoX^T<=u zl!5{6@0-=F(ul0d-KUFJ-JK5)bP{Fk@)}R_oK^YJo)dzOQQ)8SEP_A;dEv!J3luQ6u{I&A%KwQqEpdwn6qeFl#ZxOk)1Hr zn1zw+VKaZnMkV*^at+si5g-r8X@ToZSQaYby;|TadGS0%(b+Cbs2{-^MZo@N;ObQ$ z%H$JBu?g>_DFZy$@du$rUPLe*GUQLLB9lK7e&AHz$6T4KkZDqzMiL|qFMIp9YzX(f zZsZ$QiAb5ag~ybz2NCs$*Lanu2A%i&mlc6hfqdQI2m+tU#;avtKNSd2D z^}w4+BE5^|rc1}=mcPKNzPav`X|otkOQiV#e7Ykpu2upAJw0{m%!EO>xE)zi&Dnk~ z>&aaD($}5%nSl>~rx(_KcSi;uhc>HYk^$DC?_r}aj4PcQty8Hx=E#>)#9^^^r+=y0 z^KD8pdJsS<*?#-&o6)vE-0Z3A&9Nf=PI^d>8n-air5pKdsMFjkCC`Dq>NsD!Q<(1H zvnAmuYBHe)I`)4|XSh4(8`z1yyCp>x&4)Moj;-#8i;b0wt?liB0RIK#EJLr~ z^3fz*Hz)CdwE_3tiIFW&I+j~~vjT2P?r*Q|0T%57;KoWx%&ohB_ox&9FaU^-)Xbd_ zUETxN03>4SkVrA{R!wqB%G+gNFRzB;<34+sw_47B9e@8U1Gp)`9S`OJl&jjed6+NO^VMUBVN=)DFXd3%17(!czB5UrKQ_!XDeJs#I_17 z$%gf1!SAX+oG6LBkJ&YgF4?ZC8W#}jpP8xOo%Q$k*L2*!eUN!P(+Q=!Ufh!QBvDeZ zy&qk9+!@VYeirdG>@!=p66$o+rlF#1ijb zsYY6>!U<2jF!eYNC>6eZO5mr4q~p1!kxT6Ua0m0$AYQ53M`Nk-GY$_imlSead?zvk zIUT+C2pIjD?NPf1FTrdDJC7VS?NwmVd|$b0-I-k28bX*YsFi3@k%@})^RG2+00kZ( zYbx2&_eqB6)pPqz?lNsaYsjZDb=@2~*xv`}$gEy$T-@PYjY!I1h4JI@_VzE*w&QRh ztMa->kslG0HIf7ihtTWEcQ|L%*QXp&C!33ih-9CKH8(c{LYb7P=w~RBgi}}7*q8^6 zmYjTNZx5&YTXXZ?!b+PNur@h#DkOjRmewsdIIgrm?6pe*<@?R#WS+dT#gJ74Cx_mj z@}jM=&QOfQroDDQnd`AgX#muDm8#p#Ru}@!V^pSHY`2|2O01<6Is`>l1G2lsQA6vjZS+)6bf% z0Uge6e}8kn1F!azo}HXb9NhxqEU)nIIg?87hb*Pd$a*=XhYpVdu4F_U{1`sf0jwEl zsSf-I%#hWeU%G+L>UTbl2e4k&4+TW0zdhS;m;QYjC`3J`K)Of3_tI{#x{H^Joc!N> z0dVR%Dg{tK)2d$pVhRp)2H=?^Z@)?fOgwR-y{XkYtx|v8T2O``M24Qe7OCL>rxfos zpdD{Y0@?}EBqKG@$JZB>)7EA(p4Y=FrKOejx?J}5@*v&k%ci3Ey-TI==vf!9)CeW`(y0m9Pni$D0yV zK(;gb*5~GRw~J9SE7MioA6d3kVS+p)9RphbOWQFDOlCQ2h-_#wRo;eG9tu0C0M3WaMC_Q54lKE^ah?S=*n&7a6mHRudtC4S?Ckv5yL}7FxM9fjz za%>Eu8@c>k!>Oh!h~!!bIeN=l1o3Pft17EPW-2|BFr<%I7FUIX$E(%k*{9DaxmDje zH{8Lacs0#&oOMLP=0ZaT30B({3c(oY%q!xO0Re_Ks;C}53fsfv76F$v*)B9jK*1JS zJg)FQ+w3u|g5-X4Tv?6;nAu?R7Dv)R95s*K^4-^T$ABX&^(vtF-}S2p@QaFCEnN1H z8lT2_8h8SwOi)me4XL1@0Lc12qf%Gr=TwVr{(ID(cenqH2`k*8Jo)H{Qyv){kO`Ue z%{C*Ya2acuO1dz>0c8wZ+j)B}Ml!r7h@}0lc|m{;WaN8LXFHPttel@>v=kJ6`&E53 z-XC6-XzlLocwhfE<}~`8 zpvEgIdfkC4vvYJ*V}HBosEv9OS(SGUnX+G(`|s2~CRRj*we7bEZ}M9K1t_mQ01^cw8x1@F1PCm()e*oQ~l}7v9U3yIdjM6%X!-`esRplGm%6w@bQoX+ol$U*968(y4-N`4wa_g&hDG(PPrV1p28C@@ zo1{rq;oL4rZkj9WOhVq%X+!gE5)vPmUs zlcZ`wNGItP-s9EoJJq--W>SFjRP0sgxi1&V+#>*eSj*U;-tL{ap$ISVaS}}I-=DAB zD@x zoB-?j)#gvRb+=QLeKS?`@Fy&NyiiqL`tKiwa(DKmo9)+++xJI-f$qGP;+YPPj@e?a z$pmYi7_|F)d%kN?tRp}lO1wOJ5(F&#WOHDk3z6mP3SeRdlh)83gp9l*7%0dd`(DpM z^Yewk7Hw!-r#>n^)YsQ<_PMZuS_cNMY!0X6(;gli9vT7MrKESwH|sTK<3_Un0H^Wvg!hAp~p-QC^&;NT!5I`Dor@WlVIBN)~9 za$n4ZULq`@sI{ulBFi*wJ&_Qi)t;5tJp0H=FIVTmL=&%Wxc#s1fA+u*?^5PzJ-0Q< z+4>ACt*JRH2&HCByv4(zzdTq7yveZ1u!i{qbqi(H-m%%vWxjvIWmNKPAF=;1R>;*; zO?tt`7-rRlY5s#33p7qqK)0AZ%|{734W^>%=U`3}6`p_46?g^;Vo4wZ%i4~8k~17+ z!Z<|5U!5hvg!P#w0B*cC45i;PLLvSf*W*a6$+Pd*m(aht52x3Px@3{3VehypjKZDO zlJcNw^dez&dPE?0F~yQl4Y*-v<7&p41~zxd@ikjowKj}{{CiK=FDI9oyJVr1xB2V@ zPa<;n*Q3AMsCp|F(CGE-eTGu2_TH_@)rFUEcw#7BlMCVWfU{jZu9#HU>pO=@_uKa?yxVZFXRlI7tu>EQDn}!Ou!0kL@qt0WV(2@t?STI&^0JM5Mjg|p+vH{~)UP!?Gh^g2XTFsk z*qOkrys)rf{Fam~Z25XwmP^2dtgW;zk|PP~?%U^VbRZdH$(7hIXUwZml!_?OTWr5S z(2;(;bDguRzgfG!{-qvJ4XA3uA`L+EwQwdkEMBcLwBuc);^FQRT{PJ%%!Ic%B{C}=-Vie(!s#e&q~Iw*rI&abVw(D1uE z1i2uXSE!)(g`I;vFB`UJXH;)jA7@i35Bv|KAkKiUWy02-vpXB$js+t6^z)M-Uik|R z_9w=vg(6>sA~4Fd=Ap>9EecGDoNrS;&f-!@!A9Z+J1`(+U2=AkHJ%UEXZrJ8?+Fz- zYQBOWvzr9uxuyAwp5yR@Eq^d6%+EhdQ0DgEXt1%h1*|31J|ELtzzI&Hl^m6D%8^JU zM+pLWD34z4c0ogeF6X~>29S|tS3owLft9#Ho`~Vt+fQnjiysw~Ujc-*+Mczk2M$H{ z-{%P+h5&sCrKA3!q3xnNF$=J!0JdWI-D!M0{~i#NFrHBnKr(85Dk>@f(okXi-RbvN z3RZ!?7=VGlhGTqfdQPA1CaEGx$Ao9vvQD`e(z$fpwn50mwLbp1>9Wu#t>Y&TSam?ZECJpWCs0ro4Z=J#ov5;*n6(LdPDlKd?UhLlB(f{=nxawvjuknln{ z?<=flF;Tn(ZKTQ3+Wh84lrMsaDo-SJuuwuMiBptI&@j69m8Y^a^>qm6?=Dsr953Ut zRzB;(KbiauA4^9^y6sFL6H`c1{fJ7QJ^2DtJM7{W78d4Dh!O`6rSjziD@m)IJ2Uev zk5aqQU>9ic2PPK1F_!?_%sj71Q)J4scFB`C{lhysJx$EDWI?w14`902muQvN)jb7; z(2-G60zN4o9v-Xj596`9Iq#Wnb1+g>RW(r{23`#-`Cg%4qBXGLf`oxnl&AYU;V?~| z#)@|}n+57m;S(SDQTlW|Q}4>{gcnv;Xs7$@8yd20*l^73es{?O2vp0&0=WKosmtOS zpN0BzUWB2K4U{_e%*_BBf{#ZW&2>UuM1%P+a^YWhJ8FTLq7msl!K~2bLDhFhG zukK+bR`0Z5y;`@;V9L?>dD}*s#0j7P%hWiPNrP4&cVpv8;B_1I;@SQW{OXb?*UTvf zHv{sL>Oj+4c{tdTdHmZh<#TSO#Ug$A%M8F?g@D2f$O@T!j46W$EA2Abqks5z6-py97R=H<#c_v;T+d zEV<0crq9FCUr#v~LEeg?kpNW+m#mM&qely3d&*t_N+X{ae5v%@JyDZA%R!NV!iShk zR0AEF_Lq~ih4XD7=Ul+ZUu%o+YhlI#r+-^RuPP9>0-xl`AdYFglM@p@r2}RC^5=iH zcA}!9TpGz*tDV<-fmi1;OKpbS4QIK!zaj`-ZG?pCikj?I$KT{tR`Q7J&vFIsC1UX7t*PeG{Q#4E2YpfpLL2uMWrOdv!Soz|tU?%sQOeb?oz(7#$|&GzG`i3gr6 zP%i;#4v<0s0{Tz#xBPz24y1jI@DpDT%02b3(mj_{w;d*nrqD&gGtO)pX@_^l7Z?3z zqE~IoG0@TDb)dt3Zw2azAQY78fb|LxX0>Z=?iAni@z>nV`}URlTRtIxi&g1=@YQqC zxldqJk}|2>2`;$NlcnYZ3d1Ns9R9SI5!Uq zy#NiFgei|iOW=SKBj2+`5m|aOysqB++>Ogn&*?vlN^U7^;(n z<%Xn$SP(Esk-h*vu4SnguE$Oh5kY_fPg`kpLzo-}oQQFWIz0o);`P+>GySw`s!5@h zzyy7PDCdGF^{^m_gCCf=lIP0a8rW<}Ro}l*HiWH)kOBf6w2<5Q`=SM1Ba8wUiUnf) zpx%Ew%4i0>EE4i>jZs*hzgB^6yIGdQ#0! z2CoD6TfJKh-}+xY8rmNG9`ej`EKL7nK}RpE2nkVnhNSh6k{{%NOea4 z!agl-5*O$|wyDBO9IG&DVN?O12ST{U_tVCg=Ty%jyuARXYHDr!0|J_EG0l~Ku>MxeMlDG~uO5vo2!8UqkA ze)TF8NVw77{^}dy9AIN&dLdAml!{fqx4WyR%Z&Z${mj(VuX-syKBIAg#Kc6`D}t{i zpkOl&v&9=sga4fc=$$|+Z#h6WGmd^$nfdvZ)zs7^SXw;XJ(cjV10=puz4vDU4`-~E zt(Q%WKSntXoBlzS&KzFf6*^$N@ZFB8SQF+&J|EN3{<>{5|^s%OKTbd;k# z07l^<0%M{(yg!A{X60B+ye@yqL8pbPAWnKx_3}lVg9A$McMUnh?r-E2#r~++==3}& zWi~belje?BlAfl1UUqI)c6L9n3_lb4MI@lUcQTl|@#3)x2MyJeP6_V&{NB=n7Ql0r z8^5Y1TB+8tsJ)AcwVS(38 z)YPqe6%zQ=;>C3i*4B-GH?Nl5CRJYsSc)5}BiNBZe!IndFnP9EU1btiTSshbdYVP8 zP>UVRY_T#8JkfueRKR&Y?S(%8eSkN~@o;g^uda^f&Q2=~CB#Sp6ZfAA34F1#?Pm9Q zi-_c$v-wx&(rwf6~FgljrZ*(_2Y%{3Pupn)=x`{veM*3-*Wc>q59V` z;}sP>fhVMaBV^52xvUL`(wN$4#V=E_et<%s4+If>e%Sq5ku`O)xcyOch-es#j$RQ{ zJBl1a@FgY3KvtwO^T|!zsq)VyopVu!9CT2#7Cb~h&v0v8PW6~+-Gj3fO^6F( z8>CUr1dU6u?XNo@ze+j^I*)px^@%R(UEPrw8;Td_sLMrBaf#;CYkWE3ws4tyOtBUn z!TMdV5Q;+W1bz!LVFrpIm5->%E;-w+Jg zqDteJ8ql7z*=97PlDdXJFseAA$wKSQQIG@mB}bZa`38R&QF$aEuZeYx+AHikNAfY? znjY&|)UnTdX)ZEp8)l7WjcA{Jpr?0K0muLck^hJ{!^0GJge8E7)Z8qgs0RS-^>~>Fk-)QX6&3;BvBy7&>S3lVK&jaZB4W5!C&7QquqDV{ z*2yriFYuttX|eESKvsj3#5gHcsc5p3ji?|Rv2Og7W&i(S>MWz8jMle5bci~nfTBY) zba$69bhk8;lF|(#okKTB3kU*&ARyfx64D5YlytwFbIyOQck3q>3iHI?cl@sFHmix3 zqXGJVzm5zg6BBtFf=oU!EgY2jwt(eGVy0iOzIhfe-Ipzy*s9xZvy*oL0x@0f&$E5e zgcp@Oi4}Nw7y+IOmz%JdfV2HGD)S6-P@(&b!xcCtp6_N{x1?03_$Vgzh)f&|*Ucg==4@hTp))i%r}N<2G;1yPw{%q~M(#5a3uVk}*Sn+ZTnC?| zrv7X4_4cjXUpW_#+Yp7o0<|<2=9@k_&T2Dyu7t|g(dCpV5dvU=*GajKcc8mt)z1KL zr*^PPZ;AJ=eLMtt^;K7V5}9(+w7d4NI0lEyE$|VJ7xe___|MPVl7=Dw9jtbO(}633 zj52ipN+N8O>(Mh2AJ&!w$-!Fwt*k51GBH#Y+d^`ok2FDpp=NqBdRBYsre)(`zsLR4 z!_D{QmDi?LiSR&QpPZnGmz5f~E3z;DFz(0wNMGkHnuQ@;o{_Vi580TTzj9?G_{*=nVluP1D6By@paB{lVxPy)g&PCz+_Z+H*j_dJUepe}oU`XmR~(V)+dR5J2wsDq-`;i_ zL7(%(+r+|}6RTK8F;kzzYR)<>lpONudhNujccRd*@S=?Sd!!GCMAvUo4cr!j6xXk5 z64f7@3Y$VEuxQ0`L7hyLm7M{t6Wta9%g3y@2ai{GUu6C6Nj-02tR@2YPqPHVl2#zn zhBidqcD2xelm8`>RJsxpJTockh(xtib*O#$uoR{5cwBx(C0dpBz(MQ_>o?5YMq|a; zj3VKP9+`4&TJ=c&3HG9`jPcUn0>v6XG8Nd27k?6{@Y1U5!SP|;X_oqLV_3l72dTMi zjbal%dHd}Ogw$yDyAR-IR-?RBU3FuHNL9o;@ydEjn*};`b+}$lyg=iXmyq}PS3VY- zqtmm$B}f9q!_H@}2#>#Zw;?=(9W~4}8!_kDe9@pg&`@9h_|c=ki%qVe@C!<<{Bt^F z_#mTZ`{{S~vvvQzh82YMJOe&OquUp-o28>sHbt9izu0mCA3b{Rf1dx?{~)A9G2(PB zYT-@W%_+c9K)mY*j-sod%QpvhH3fgXUO~+UNJWYgCAuht+@Fd1Zqzpi156*Z;(fRC zv69t6A$GpWwbx+zvLUIBm!JQW&smjv8Fxt9oX^#608?llvK4UpLPg&O8+Lno=t|E; zTCD!ve*`M4VCxjZwNd^pi9m`XH#)JZ0c?XxsbBo(vQ)hy5lnjyB{JCK3k!s4`A?X2 z+0Q6n!QX;sEN>X>cAtG+K?w^yY9emiCI>E_o*VXEcvMS%XCmL{^inRG+pbps@Unqg z!PDjQ)TX~%z$Bx2r)nS5=%$G6#%N_uSTLb=qi6Z?zX*IT#1M8G2_+`whnjES{Kq3x zn2PoH=f?fyXDbDjy+L)=OHA-!>IyVDbNAlkKkr_3?4*lMw^ z)n~2uJ0elxpH1i47>oRBkanprk>pj5yMjJ{@hA@hT_zd5GGZo=ma$SejyyVzB>zKV z1{pTSl+#cwIx$BWq)2%ApT^OE$DiCA@l84jr%o2K?$GF7EgCvM1#i|b_FUxaIYol!78V`J}5o z&9zE@ko2k@OZ0rv)gTlK=py60%h~(jMq7v)W=59;nrp9tOp2w*-p+1+@0C06`v+^U zpogKjEGd>ee0*FzACMXv8h}r_hxRH)HR1IjX9p!nPiyOg`exr6-TPhyfHVYr&&Tqm zuwH;R>CuS6^!&X3%?c|KE`T7vg#5_S*xOi(nw@K)P%LP@0j$O^E_yn+OqZMBH;?_C zl`f#S8=XL*`A_^EPg`=mU6+yX-G%d4xI}3fU%J1IlW6OhiO`^t*n0K0S}LOnCRvO`|6IS2XAc1`qYac zD+|A^4MSOkAj#8^@KOOR7UX)cHZwW(6a;}|-cG7Y{b>~{{5wdE_1qwrec6rWjlb>| zm%dZq7GBAa8HeQpV}`1>w6n2vLC2ZLw-$$!cd4YmopY})@%#UV;pES zus@kiUwWOeT)hZj7y5xF!LJ50uPcVEc+EG2_zve0AGWLwF68nJD`;0IEk1qv0u#d0 zU7C1z*-<6-&DZb0Png~(lkcD%1*DAM!3pTF@ap`J&fV>dfujMa8`6Hf28W^X#Xzd8 zY#4q`ef`Njyw-8l%^nRxN~C(_34mbkyDtWBE>wk|zntne>t~PN+uL(ZzK(>tbQ9~e zoK!mc0HT1R_hQduUwVpf`FaVE#@w)^e&ORWspXr!Y{@=P;Tred?k7N#LiPeQdwSLn zCG+!;-Wv(;fnk&6LW7;19Z2?6ncsk?&F8d5SXA`19qsW)-5M8w&KBF)R{AY3w*}k< z+-?N`0EtV24pRA+Hxp=fD}f%PcYRBBB^ww$IeHBmTwY!IE`!GDdzL}(<*TJ-aN=ha zt23q!g2XHPv8ZQ2yu$@jBlX>bC$Ic3lErQhJHtI^jh2Bj5YP)djAcuq(BE#C@5Qcj zK@Fk=GPbzra@f5yVtOSdxzuR(%YNYoLD+=+S108_%hms^UaC#mU3t7O-B?&e@+DgBDdR%k;-@zl)!an&v)!J?J5P4fH^D zs8;Mul0*EevUM%n1ALEPw;Udy_ao>SU{W8j@`9kB3iD_VIQhHh*q*3jPC*8+xlr1b zi?o-|g<@*@@~W|pEEVIRB@7~1$>QBSXLUPyIx`cmCD|#%35dioiUicq3Q`E(Nw303 zCtIZ|v9)mPFky1=8haB(X(~A&DP)vE76hs`nIgRIHY-nw_n~3|s>Cjk(sz$BOw;I& zjEE!3eNJ>eANH0vrvThhPzFG)i zZ}8bkC<$02DQ039H`ZEWl#$v&Wi0jSxtUX(yUm55y= zjb<52&2bL{@Vc#{qN1eaz6P}KbM`af?y-4IBXrR@tD?&LXtwqAPT63_YfqXgq3t|s zG&Uw?ckalgM)&#`-`x!$0(5K4EU6NhNuu6Ae)0VIbLlQQ8t;ouokh@-d2KGmu5<)4 zwjV8CpVj{y_6D4GJTL$-yy*3+fn(FA6l4r=3!ry%a&|_H`;J1WA>fZju3?+MpMpZg zKG;BBzVi3-;LY6K-nO?c1p(xM1?PQr_s5Uz=4HR56QGM$=Ip@W=H-3mI$RF`HSg28 z5`X{!YUgH%OQl-RRl?ldyi5}u_*>(J-@+R`4-)}vnciU~dh=W5g`itrNlEY74Ny)y zg23YL71$`Hq%>5i-=6LL1fOO?zjr-CfwgG^G<0zScqsGJE;co4D6rVxK}%Rn^(eet zU0BzX+Q18!qg(=BL7mw(E?wkA@4$yFUID*6Pyw(-<1o*#dfQRiw0?D~)A}zUSQ_IV zPzAW1>zfDWKWiMO(mil~y`T#hf0#!{PJ&YEP+C|s-yb9~MIIi+AfNXl7(&_|k%-Qn zkN1}*W@fCRyMH=0g?D-DwXBs|g-KdFo|z0bSK9H?agKprxmDWExV^SYY=MS)YeiFa zQ%J|lSrmBMK4)d=MqtJ>rhQa|%KI_wpv75r>pherE2_85Slq$d+XCY0t5-AxYALh? zJaTXtd9cQ4VPc5(N8RIBOXN-GE{hF%Ph7*pCuUovT59o~l`2Z9)GS{XQDnOyy4B(J z_QbA{UGSz95oK7lZe+TQ6*qa9l1+4NZK7o*e|#9vQ^CoIGEB zy#Io9+x6M&tq}@9z5@}PS&gTU&jQPr>yhTAlPV3-5q*7q|NS%nJ3Hg6`5!;5dnp>)n$1P9Q97R}yJN2i>jfquE7}JA+t& z?{zu%{Pgrm5u4v8K%X-UU-~?F*8v(+AOz76>1D?&EGjygP++^cS-v|L&;+Hz{4@bq zY{~`)tO}*?!nbwF9?9y&0h89;oXs&jse^!rE>bT8edz3Tubdp}G68|@NBVq;qZdpi!qm@WSBA8IYLCp* zj?&KIOJIS=41GdUA01_E7~=sz;WnTHRCR5n{`DD?)IMkV$NkH=iaov8Q|vaFS8=#yJU^z+yH-lequ zdRB)U`7Kb}$^nf-k!Z3;q?DKuM?8>#%GWAw>a8YC!QXIhmQRHc@eeRWaN>xZ@kW8t zzfA{55fTg6nbwkbChkaidA}Di1SGYBxx+&5G9K^+TiVe!R%2K`+VtvVhF6TRCnCGk zf*>})Cjw`HoV9VIyvh=b@9u+RFUb@x18oM1Iiy|qG3bjx6(A-}ukRF!y;i?6xjP0F z>oUUr2pNmIEp%6O)>(mKt&9B+C!DR0+M#p=>C>=r(EIGC&4ie24jd_yX=EUZ-Zbx0 zS7=w>j>kP01YDM=Sa%a7P_9e4Nc~>xaoF<|G{8kB*O>_-3Po|Ox36t)X=cHVq{QLU#mezf9VA|2q)>d%(^RyxO<|$24s7#4p!z*B}0gmjK zV&0{yebi=D3HFZrrM~ksGpB%|Z>gx*8Gji7y6$UVpBM1rQ70o;@e5~@43vsyD-Cjof01Sa6_B;7V0Mjz>s+lyC`1hbybh;8N9^wA%<<=? zFK_0d@C$rud?0$U=?r@a8gEj5em4qiyNWc43E_SNTUS6G*m@Wf@C{@^K(u1On|UOx z1IH0(9*pi?2hqEZq4Op%r4&k?7X`P#tYGCNX}sg?W|9t4Dr=WLxEZ{CXL6kNlV5|G zgr1%r1$OW>hZb&{ue!anqd=Dg=r?}zwh6PE^W(`(G}keVCx)K2mUrmS1FjGA^ChGg z8=WWNSj}_IdoS(|U*vk*azTKa`=4v>l|2xH>Ai{tztukA-*9tYEnkdKEHCS{+|=Bb zTJk5MX|RcgY7cD-Kc zZHml#S!~tC1AS+w<)!^QAJg+~Ce3V{5L5g~%*hX7O}{$>%tj3olXYFP+?MeZvNc;n zCSL9Z@8f@uH~^F|4z_xoby1NxZT$w~97bzROlk^Mdyp$*Ze=diD1)T(B!sF_EIh)k zQNzSU>x4{BXiLi!Sd&UKsU|}5Oy`fD?1-=p#_->HL!qy^9EjDSo6xsq*-*77_-`P?rs3>l4m#PEjh5EX>ELafK+1BZP>%hDwQr5`8_(0>rk9PA(YwhT0jz;korH#Zf)Biuba0P~0CQ}x&4^c4#c zu0+nBOOqnJvaAd^*DL{BPL-W@0s>IlHm*xUfs7sjMXeKVbPZ@0>*b)En-^B?paeQB ze74YKO-6w1%+A(UNKkNuPVy1{7ho8{ll&kh7}T+9^_Yb>T`oY;0~*|GRs-w+_*9yw zG?iFWn~z3>asj|)R^v9zGwZuS{UtjIr9yw`x)%iU^>@b|@!?al4$?z=K0OMLNgW2; zV=I4tnt2b}r>ZW7G22aEs-g*^hl7xe0$ke`7{vNvG)j>;c8xiA0_RfzVUmni*T3hz z+g^#=OL^0zKLoL~8oYkDfAOtn1VdDE?n!#ks{nh|CK=@Oua?j(mpBG_WY)<0u+q>t z1Oq5uN|d#eyIu?A)8H;=dr=F|H!br|1TgEW~+V%HQ4L= z_fus0ASt3U!Ys+$m6CLZf`}cg9gG^?`lTDX-X$X7mB?cWg#!L@0%(b4SYqC$+nRYd z3Xfb+mfc>Hy3e@gR^-u>sGK;S4=&VUi^j>NvmMI7Bof8;zg{EYtn{jrmDWV9 z?-5_JRsuFsiY^6jKhens25JwmnA_1i?6r@h#G}^B76n2IWQ!Q6P*9_TkU0l>wJEZo zyIDY!km&vVk~OtM!bp6RQWh=+!D(01kleLUnk|o_^L~6;gldhUkA@N~jzpcBuj8ho zL~gmJd$v%kOgkmT2>ZFKAH?!Gl#7>)9rba`hlC5B3)8WR+eJPBmka)XMkBJ z*?*S1(HC9j4VO6>8yVGxOOwkSK!}9Di-O=BWIjM(7cAhIi zSU?(Vn(+(tOAo(4UaECk?Qm^mG-k1C_`DgyiqRRlkYbr$>=0slk@MMo5);F%d>f}M z6seBO;-{L0oeF^t!?od&FxtSGK-_xBM!VnU)ie3I=;^6xccXt++#e;q4PA4GcVZ1s zmgEfRDOHax=brew=ze(VW`o=KGE}8Wg_d4FUv^9%C$>YYV{$P!g*Hf<(n64foGMV3 zcDRRxSxM{JC?g+%E%9~SvI4m!>AX;H~PA|GY5?WufWYXA$YlB-ztd{8VLRiOnbo?uxJ^_^~8YOB1s;RsEVaD6J zpjY&{B1@Xm9RhL@(KeC{f%`s5333`VR$O5ayFZ1ijsLrI|1ejd)P+Aq2AA1OX{DzQ z3=TX&s^{?SK6am<6QHC?21<3p50YR@tl9zc&b;#X$XY{Ip;)xQ1`2R1vid_5z-ch{ z0=wMT-&XKk(hhR$$EI8T_z&{g@&xHr1K<^_{?c1tL?k9~z)x0I+Chl#YI&z z))lzd)z!;Ki`Lv<{cplXMwBa000;DLCER);0oY`NdVYp-FRE>1k+#$UyZk|E{fkkJe$baH}t|GgO zbf76{u+fw3BbcaNS5)3qs4A)JLC|vcFckR9;HyZ}sxfKeC7Y#~Aqe1F++isXgFCnL z!n@5=TZg@VaqJ>h<;BsbO!NuHfklOtXB7kSMHhoNtDVj3V$&3t!0@4|Vhr`XEpN*O zg%9DDTnIZw0}i6TIh|Qs-*5V77O@@vaO2}=A#bMDA~eLMXYo*lNDDtRFK3Z$XU3!nAzHv|Fg2Q zQ(z`}N=nL{ItZ$@pge$D6dGQm+XCcxV41N2cZ5^GVnz4Hy*D~NI}-!dt_$BKcEG=s zX_`eN7DBo5hh)0wGR%E0o8QhfzT zX#01?%RHjI@>)j*Y{L$5YKT)A2EFrC5%&vWWSn$~*{*h1nocTW)S%o5D=i;i)h>iU ztFLd3=#n*wpk8|UAuF17KJBb}o+b=*cp+gnm3j@HljWOFn0Eyo4}(*_W_+~>&PI$R z8?hNMw=Pb$omnrtvh6R|z7CjkPJqIQR3)bU`q=I%yP7$1N$G~P3%4S-0~N;Q^Nfa1 zAKhrQBV~7nv1vou?bfeEPZve*1eZR)fJ)$+VCK8&kLLY?unVt{_B;sBdE+gxRpl7@ zUdll3zl*_Yq$tg6c(jWh)HT%0kVs$yUH_=uLO(?wF63|o_)gVhkK*8D1iaT7Bm*FX zdtJ6VJ@KD4Jv|L5d7xT1zqB|8Ijm8uuZ)SrKflNLN|Ok>qzGv<)>cyh5&)m71)$oZ zA(#VR)RQWXn2=7BYNLSrx;Rh*P(XYS7TA=N4P!|^?^<*raqDNoz=pG7N>2|{E z&=TZwx6Pe7_4CzB(XBD^AkbvYDQs+{nLKn9L)rtTZ(II3+A~Cc=jl_fkPw3WmB>)|>ocf5x^A80P%tnZrk zT)ypK@Hhd?h-4y%8i&V>LpGd|*stfWMGPqgw;9gF5Bt2}JjvLVdG_%-RrwAPH$tYmq5^piATj|93;z(}DL;vy5MhpMx;)c|ahXvt_CU?N;=*GBfivGixa;^Ey2~ z4UMm=VjXn_qMhKvXZ)W6)_ar=J$+MG=_sY0hNo1Y)58L%vtu%x8U<~$c=44S}#qtuf;$1S79n!*`8UlCgVm*8QvFg@cDlr5-L~K`ihXG!XfTL)t_?{P360z0_Bn={>AZ;@vnKyjJy>-N9OP&JiPPP0LAB@fYI2+(G%&y-onx+ ztcE{;>p@Kq>|V*S_p=$O1pn5SH~Z+(aDL0}@9($g^%+rFt=}uiZTU3nF_>bUf^de5 z06rIz$HPcbN{B>Mv@?fbM`FFP0$+HS7_tFEj0r@6CL|*evogVT_iEm?ejn7dH(SIe zQrW(Gm3@zd!y29K7_ZlT-5!GMF+?(E&I4}92^ z!*A;YPV>)pTecPguCBh!6eZF#=tG}{RwpBK=qa##vFcEz8GU~Vn%(V9ZO-EF=qBP+ zqNp;-K+DvnBW>CcMHXecfA`;VB?X~9Ju}=BoOnCNqVWYUM*&yQ-0TO%bJCMYk=EYb z?V`4uLu=dlZ6X>KYJ}P-?c#gG`CL+sIYoiY+Y4Q~XY-Q(TO6)?WmApUMIqsUIs1KV z(Ovx!2!!WqErB`#z`a_(r+{wT*3nT62pNI9fz`q!3S-U|z#xVC1lb8C%2o#M6%8%^ z<(ES>QU2Fc?%ji7`w;Q|x8PU;2x)j;(|hu|3BM1;3s-}55$-yvJ2W?!>%j%lbUyK1q6ZB)gPMO0rO5kAL#Q760n;2+Gm(QTFR z?zp?!|KCh(@$3h63r>=&!m~D?UO$hPJBrycLW{(ib`ec=SJr1wesUgCP+=SyX|rCe zQ}-)5ikvqUzaUziEYl=iPkxtI|0_|jCgtD=W~LyF!~H(w1~@0{+cXR`kFQ(Ktea7KAQ+t2kD{hba_U`{6NItpUq#)rUhuhke1}W+nO4VUT6Pz zZ(=3;{Z@-^7%-Bbs}Vp9M+QY_^{Gvo;;~RADXN9!Kd2xft9Gk4dj%UA%^A&oevGt? zk0J|K=8sg%)_->dcR3}7K&iO2-F$TzI`AM4oCWVnqP*5p)o|ib8?p3dl-3_Rww(4F z-&F7@7$2>~&-8>j`rm#&yIEeyw6!yKUZz4lQ*~i!I$m6f4%d!uc4=%Yt2sFzRw3iT zbI{M=pwVh;gdr6W%)ex;5?@c=YA&%UUKABLQLOqPa3Tizwui~4oB4$XMk zEnh6H3#oC+%_tUqs4A?r3GiHbm(P*^mjCPjx1WZm6RM->AO@~$frV0%z+U9!(oznh z=-1i@i;Q2jmOl^A-OeNTuRbpr;B{I%?(_3rO#=|FO_HydOX$=mHmX#WsT>^wv7L1l zDTBbsqZo_oB_U(s*x02wwyLN|2(83rDUn=$5pq#xj|udf-XjV`PxpxvWf_6rf>l(` zSnDW+v}hh~vrLHy2FLmsBUydhNy&?=9fRek+4--=?3~ez12!!U_HIv$r~{|246$d`-j6Qdngp{laFM+K?a5GKXL{+j;$ zplrf9C$|Ux4FfOgx1Jz}na8*;iEVsOn62WQ0>c4TDh-QK?mNMw-}D2feG)WyRK=6O zBR%vS9Dq@E#+5S}R|>1qSG!pSeI^L7%LILO5QTI31prqZ;BD~TJ>cHHKD_YRtO-foeBLPtHV9)=)99pB8)!`E$J-1TPJT6+Zz44&V93ApSt_FugcyMRLiHLcYpv8^92zI=eJ zuIGlEN7o*f+3&o|jEuVt)W^3?o96}|7)+?q67drTWw3e(Cn1ssl1H@3Dk0s2-2dm9 zIFD~N_ucPfk)}EWd?Hl0&f$coEtzb4IPNhT#9D?yLRtwvonbZEY9rEccM#T4>=gLF z%Z<}L)D)zyU&$Se;C#&s>}H#*cCK1QwTEh{jAFla{SoB)E|i?+Kl_%chy`v5;_2!J z6kxblRP@%>~v<*i=@kQD)$Np^;_WUzTU`+>5)x?K_4 zpyB~O1bx~If&wWaHpb$7xgC_L%1d@zH%}I+9Jj~9TwV!KluZ+*9CPo^J zXR2w*YwnH|0wxW0u13$Ub;k6$!E~@Z%hoSSyX(F?yTVu z$Ojt5AWNH0nED?>tHD+@&*`37K{4t7#k+k^Wronbiv>{v)&ES>J$dwz14#VzpA07q z1c6%L<}e`9%60E_B*4Q5P`0r!L)JloFRu^0@4x=WHXz{cS+`=IJcHkKRbbARR+l0- zdF7sI9BP~L)S}oov z?KJfausH0c$z<1JMs6}#I;$H}c6%JCq3L-a{yl@C6TKc!8+twFTDBj2U|^fLe06ub zl3Hp}9Kas!x2bX`&2%#Av5XZ;P#Huf4wpce9Al42#O0vlPY#wT2yX(&$VGUk` zY(7`5^a0#{J`a@P<2C%M2tUWIRy5jWl^jh~PB+qr{mhBy@<8Ba8p8sWo0i?~ zSK_E7dM!7QGIkHm5%6bf?Eqtcn+(?}uJG-v)}61(VkN-LzMGUfl%Ys?bUjNYH_a5x zFo8-k{)0n-1K}TrYYR>nJqK9?zzd1PY4W(DENuoZJL%Y#CW!d8xnMJt}Sb-WGCZ>($VpF*=G-!$HZyD3~8 zuDWP+$HY{S4QLlcnL&^B0jdPWvqT%c#vc+@kG5y z6~V4F_BCzr?{m`DIv;2L9?(-AxzP8@Dd~8FE#QF+`oK}|crDm)bT(44(CB@U&N3es z8fs=GE7D1tq^^A&ft+BEZrUb;TXu^uL+QcZG4F&Q)h#^wfSFQ#pscJ6XqO0u=RP2b zD=>Y`l5&j6<)mJb-5v05ysZ{}eRNw}Yip;&HCnD+-sf>xWVy-e7rw1cO&8M* zm0(XH$gi^=KRieT|JRG_6|7$G=SzpNJNE+SB<~fzQ0RS4b)?ha;6Pa#C!?@>Jlba2 zbX<-q$H^-ldz7VM{X!y!s1gI6g9jTz6c2$|;{@tEkF9ZM_mis1Dn6~lgnvR7I%H>D zxiF%&n&)4?k88PFv#c&GKaH+&^gTTK79zFa{tXX76$IOU2Sr7Y4_7K>M^V+F8#F}D z@XQ~5jqb6sov$ob3c`b8|8bnJkskE2TaP9eSFN~qx?52&m=M9jHJ3t%ps2!`iSgj2 zP%15j;F5%yT0_Ri$H%>d^-(&8=TGMtUNy`^3RfJU(7H z<7@!qaVw3#>#}R^w-2uU!Cv_va4z1yyIp(iCF#N?^7`m2v^tFt2+{}*KVegjTi0<4 ziAh|emzFzWbeg;-)Kq0b;}<*e{RSdBq`^BwRz8I-&DkBmyuBX#ba;4(7_IyA;)@mD z!-rZPefMH;1k3A;UzaC4>pUlrz#)hvgqSfX=K%!Myiaiq@xWLvMuL&6vJ_Oga3!#d ztkAI^qS^=N-8C%|n#i`Z2SLXo=hd`^g7!IC1Z|T*>+i<WtsdASHWzkLC=^p1ABQqDUNIl~ z$mu^9(PE&hMQ7-$vZPjQ?sDR9GL599|J#erzGIW}%GhLvL%(J2;khJ~^HyVL%0lEw zqI?tZ9kq=_Z?ynKZ@h?Bd5BH}%b?&>4#F~AIVYntA_eMj(0s>f0F4`^$?f02F#4Wj816FxNdCN_GHuqI7QBe%4SX{#|vpQ*d)7)VqI8ZnH9Y-z>4!?E>) zwe!$r(BxO*ip3R(wEiWMw}Y{k*62Op;;TcXSDzj=%XkN$?-k2bdIYTftR59VecX0c zoFV-$MeKqL3<4{)Ow#Qzti4{db4=9VWwVmkh_ZPJc?-Sc{*xT!s-Hupj03vjFX1Q& zP|&>Q!ZRb~PWb3^_wuXVpPe8IuZ_0LFAlQOP813;L8Thwr0i0ebof~+cqJZf2NUJ> zj;$vFj<>n{o|~u==mUy~1T|?-v(a0&Q0m2M(I&$;z`f@CDcad`o6F|FCl!#s{fb5X zn#z59cze9-dnLs7?Obb^0eqQ*i#l0xIC=XCe3Q8!0|y19qNhalp{#&;M`vh;`QzI>UTn!=pPbZM${ zSa<*~yI^lIP}jYG?#-^7h85`1dUW~6)NZxoUGgqS0zGq{q*ID79$vR5;6PvyO&l4r zJjvCA!&l+7N}b}JsCzD~jjHM{9Y#=As^DXM9)?HZFboDQd1F}!I7EqGc1y?yiXX^X z&=sJ2WCw3*;yq>7JsJ;xC^p#I6AQ-Yg^6ua&uNF=69UF)m|&dZCY11{jmj> zT>evJ{_)|5GFsVPlB2FhcBiimJ^P%x#h?!GS%4dK&#=(TS7=21Xdb2kt*`k2rtEEH6XR4XkYCa2sG z38RdG!O1G7v|=C|JXqPvdO%rz8#+`a{%RVYrT#;cl$;!om((11`bO6vDloCI?u8c! zbD|=OesAE-vvu+Jjlg; zLW@-iC1!Yp2wM4`b>&HAB~2xO?#LO24Au@h!3=TX4uzujVU*d8T`;fj>Ad;sE1~f- z-H2lO49u^q_gHQPe(t@_ik;Th_V;#7^HHpVA?zRY(07;pF5_~RJ;u;%r})U3M)q|- zo%emuo?<6RM1v8FUgrE0iI;SFPPCgZv6;1UN{!Ayi-c-o(kc>2;jE>o;qVdgJpE&^ zyDo2386_w6^+uUQIkEV{6sfxQ;Z zv|+mYK4nZSW^FEyuiyU4MC{?n>C7i7@P4>HLMp!f8)>QcnI86N=2Hp%Of_CpI?iA% zV?|o)zD}3o$ugo0dKjI`@alJsRn9>f)C@T0{s7K!c!t0VUH|F|3(|hnGCn>I#uIAn zc>#w9;5>3$WyFTT27KC<7JS@*Ed6L}wAwwr!p#a`&-Xqno~8i61Cu+&In&RN#O^-- zImldD6gx^+0W+v}n?2g_AYjVsy}d~aoAJ!gpGL;Z(70age8?y8x-WfqE+q)ibdDi^ zNJ!HgQOK$HnluGf-No0^dgZyABvXWba0)E8h*L^0fg?qtkqrV-C4{Wt;-TT8rSnv> zsu7}MLlz7B1=agn&N?t0jc>}M%yB9GFDG-CoO>zaZjMW)1sW@aOu@~3alX6ke|NQP zY+&GX%6C*S5*ycgCUST9xumZ}W{A5d_c8%N2S$;>f{_x#)R=ORnBQ2qC>1|$zXF5jo z`1qB~vMIqUh9jq5{w?luc`@V^{hv$>1@buB;p;8d!eifp`U|{rQShdLAd1#JVp@$Dkj@ewdP0v>5BBvHwd>Cxk1VLf8E>jvf7 z1B@Tk%K+3E1<1~mgqP3#e-|0)?*QU^f8k5prO=DJ^WLkYYWv5KX>}W0RiyviQvr)n zgEG~tMGbv|)xrZ+gr=x927-zz4_#Uu13NH-30=B?M~wiA17VB^w&{d)(rDx1CF!ft z`cnDX>SvdqZ#lMYYZ;MOo&eXi%ehg%d%o^-07x3||6VODINmJ+5$cY^7rkF|Bj=9a zRxjUQy;Qh>FZxVH(j)6^;3~-Vo=Brgb`zvL-ughZ=l zoOTm@3W(`tTRwvq6_eRE#K-HfZUwy=+oWTj4bFww5Lz(saPTK`)(ZcyA%9vT|KimX zuKDm^gKw=w-zs}9W4M#rUFsTnN{kYlbDvDQ{VG0;h;10-zn3&}(e`Z^un7Ff^1U>E zn>VP4!V)BAa1cAQeRYfTE2=41PBg%Gz*r+maCH*6Xq#_Y*VWabF927Jacde?iNZqa zoULIuH^gt?@&;a0Fiq#)CY$gOc&xMRx->mobAB%(*#`{fmj9)Ep|fI5ehWss+=~}i zzDs@1G8!E_hcD4;;-W~K`b+An*~(gY;jYUMP;9Uw9@AhgOR=DE=V4X`qZ{Y(Op~h- zR^q^@U@&VMgsOuLgy^}o61Hgqf0u@0Bq&F|tFO^fJE&1*@C9yKEk{+&uMPvFrSBcS z04mdERs-gd%+7+Hq7|6+fH+d+diBK{;G252_w9^%|4;FJh6hW!$6X;-v}dr4!@#$@ z+;97_aRvIz0iyz33m%g`C86l?bl>FhiE1FYk!IZ$ff!wn+$GxDiSe0&cjv{AgAv5& z33L>wp-{Q~%$!{zuC43aGz&dKw)|%dph{+9%d+Mf z4dJok?H6Rb0%fIxAzU!WaK2f4*_7?-T5sa^b3gazWy>RkaO!@-+9x^i)n+WuLp#YuHD~ zQ-*8$B!=L!n_KLKpHjklObXFF`xNm0)cNm8Q5t$;yD7w^fQFKh60sT2h#t)&gIC+bneiwZ-;yZGoh~ArBkZ8_CzY&Q zEY7tN5fRZMuW3hr61`EiC&0YouGe3f_r^}!f}uwy(R$1&%N~Evfn5r~qNuKhM=W1| zkzdXczWgc{_Y5tXLo#$rAQ`tpGkgobq@9#H8vi>fR5g-RCK@Ff>Pk=h5|1(rDx*vF zOe!$jUAf4N{9!V~!82FE^Mix1F?L8;vhZ|if@Bb!R=Ao9%0tHxUj?kP__AdY^Wry z*$b_)Q1_8CG<`d3&kN?w?3`TY<>j4XHhH&4za(fg{3E);Pqk5%fJN~1*Xo^fTh*&- z4>Yca!0)f}p@wO6G7{O?Dk~l8GgaVnH%*$}E=YWCZf?eWcL0*2hoAO-$t2Q5 zb(?YWSXg8Y4GFW{0pPS9cu4_MSmU7|zYOlJj==~o?(d{0bv9?wWL!N9?=`d*oJ8A* z(9FT$U_8_IC+|*-zn6TI{GtkL*J8pcbFap7xNwe@y<$=O6Zr zb(+46n|CzS47nbTi-HB0Qdz5-ds^E6#XuVNmOUqR*9F$u5$?}QP^G&P^`h)fQl`=9i;DyD&c-P%6 z6Y8|y^YcgBUEpcY{d0ijr)L+B=Seh&hd$K!ddp|%uYP69mRHFnW$v)`N2kp?bOw-9 zYH1}am1BZX@#_oLNHaaX^lLCC)(hzhD(~aJk6n0%zJC1*>W>g&3CZy9ChVkmw6sN4 zF&rEm&d$z3&t3ti)1My8%~OTIg*SJ@@$e=|-V{6vYY z^if(A$fTG3dns6HxNIgqXff$DSYsrF9Qp&bCrG=V^1iXJc{G?=3& zbX{P`S)3n+4n>WRNG==X>V%q+qQNlRp~dYCk^~(@XzoAa)hRJBOflmA@*2rKV;qrr zhQ)}XT|vYMTtU$l`|(kcCg`MVUbHHzAvD_@tuFcz-Dc^x2c5NEYwgAXMwIMaXTM7< zNn+UA&O6YCqRDv}X=tRa*!O`7b@aJN)fz^vU%u@Vu>zD;Sq5)zBeC<09Kq~Uf-x6+ zx2c=i_r5!yzzhE*56#e__-`=Tmi5qIqCfDu3##>YrGeVfuuW4jN zK|(^Z!ZNpe>bf=3)Cx#cFMhsg*+Lk9rhm*KyFV?kI=17*nWaehI(fnqB1TuW73paOuDpxeeW12~QW%1DX z3dslDYAjb}pKNN<*oICJMPJyo3dhm0_%hz2zbT2*cd*_iy}zQx zh;;#@k3X4p<6a!Cd=C*bswt=+4u_HhJ&HlSldhAKq_k&tzchQ#GaG_Og8>4l=v>i3 zC(_frd`m2KnAp?sZ#XzK>Y70nlcYD*W5@Gg}8qxg&*W?c*SazYKp^& zWny7nG{N{Cd0h;#3`&Vz#?!d+-um-msmwOHT??B5+R%1rnZ*nbWom?lp@OS-?*=`3 z{+iRcww%4=QpMLDm~|Yzy#~vyZT(lx0rw`{@6jIRL$slR)S-(zJ>do0PW%}auh=3B z=IovQcu0?zbfmH0y??KexNSyLQC`lOsg#hQwg+E--q&1EP*7Lbh0$EE?*J%S;rK)_ zps38|@0eirk_o$KE~0w&eQ0RkcE_p{$&)8nAJpC$I9Hb?W@c(=X>CqYBMmfDbX4&) z(J=#|Fg)x)+-OrAj!1knYz~yDaLTSjT&$#ao8I7<0roI2@?+A%@Sp&$7m^H|ns?(_ zSMu!)sp^*l71aYq3Ki4-w{x$_hXa0?$KoR*;zV>*<=A^j2}9X?z#RFb&@KF4%1{*Q z^ok5MT&H|{Iv9-xA0K$Nal|bYQE5=n^0Kz`7%{B5pREQ{7s_N39|_ax_8+CKixWY_ zA8D;~prVsUj1x7+SVreM0C#aMjQUO4=l6`lA;I7#)l$)gBH0fNfV zI_Px!2u>MXUX%zmwz=5#ywQ+AH@CJ9TO>;+zf#pgbY*0k zhOJV+y=S%awzjq|QO^FB8b2|iK06IyA-~H!sANQhJgbiUn>TON)hCjbk3imC5tTIm zn}~!(UpU}ac)vVh;p|yrRzp;>8)`UO7dAT;icC04VVY_vO$oL#lN4vlSONsy8jLWsmIU?|wp@_3|@Y3j`UK{^aLzk(&RqRnDq zu?*OB!taTM6Q#+#2EuWm)&hS&>|T)UPO$3itPRT&tT(>;`3*;a%sk+k=%JSOFwXlb z+;!aEYb&o9nN36{`!lf2PpOb(e4Qf3mX86!CUGW`4`Yi|(u zYRU2L_{SSlekp7_56b&HG8lk2F}#V^J9)4nm^)!9KWC);+sw-vSO$Sx#W~#0+E4c(b?`j z(o=GVbY3vBX!cJejXbY^dTc$MeP}bi?%U>Fe=UndY?{Z3xXa|jil$bb(uV&ZO;;IJ zRoAT#ib$7qcXu~Pceiwh(%sSx(v5V35(3iQAl>jvcX!_9H^yc7%Yla@XYak{e9|KN zr(((FhYg(kc~BeHjTVcFY#Ipp5XjmUEJm>Fz?X#_CRwrRX)sRV)PV8kd-US5TS-se zoE0YgyLYHGn>d)b%E35qVr7FPgK1oh|7w;XeF*f-+2u*Mf$)&Q1+J=}LKjZ%C)07h ztt7j9ea{KrwhP~w1-q$y&HSTvCp}#P1$fE2#Yzz4fTvKwBfXL6 z^(NiFqIzva49LmJAs`@ZGXk*$Fq9dzc{Ruh;HP1*T$2e_TvPIi_3dJ>_>LyX0F#3>G}^xR4v{)Vu_L_-xw_oq}YLRN)Vrwucg`8c|9mVsgS$rEgA zph;G%bn@cjqWx|oDkhT)j5tGrV@51j2eY;t!En>X3J6Hodi&S9F@Z+0ySGn2*&j!7 zq2p85u8T7V0N!+Z$lT78=&6e?5u@q-lsM8FM=4&I#~GRGY) zeqmsK-_Mhybdtn()bFT9mgNhwID^9oqweb{V{z#9o`u!SOFU^v=N zHAvuII=;a+@R_u0Y^j7fWzeTf4I%u~@j6qcwmM<-Q@j27q-C~3i8+! z0J;GZsT4Lmg~-V_FPYopKknzhJ_^r&t!aDl($~+Xhre33?+|plqJdCQQ;wyxORe4Q z4=1Omr$ZcodtM=nrAx`4*JJMwCgqEAP;XDN*vvNyWrm~RL|0IJ z9*-Mil9M401ILZPfeJo?f`5)--vMM^Evl{0dEQl zhZX}-yMtL_PV&sbl}(}&ETZUSAI*9>>jcwgH^0RE+eO}bHnY6ETvk>wWwH;r%`AoQ z%`$HdqWUhD4r}~@M!*g#47*W7djVWL#llzd-rOH3tsirIaW>86ul`e`-ap`g`D>OLvYS5uPQr-$we7W^PCVSLW79IV4&{ssK40ns&jvl0qaY7XJ2fh#CNcmI_q)9V-PPYA#I+!4PHj*VS6UxMrD1r(-J zE9JDBUYJ4t9A3NHEhYjrsYGAjniLXfbhcM>{r<_W%@y3c5Eeji7M-?kUefjT^(l?# zJRjC{JbWgGNXyB60yfdoZ{%%ZT&9(_jdqKRi;%OI$5VFMT+bUF_Y83zSW*OR1`m*t zl95S(46fAw#89$%o%?rdz$X86P*DSZ#h{Sb3-ZUNp-)2Mq}dI~ExxJ5%-xcQp#Vs# z!guxeFvWgY0^e6I*s4!a#Jl6|oW}8MBSo(p8S7dL=(Znm2QO;g?Rx*jL|OfBD9EHi zw`1A$?px%R=IV#A^c*81@~>=((v+buU;ZPi~GqkL3ePpv9h%m^;!J}^) zrny>BKgx^!h$I|;>xLzO)67jty<sra=i}r z&o>7Q$7A3EmG$)*Bgsf9czBvtXSTgBnrodSYi9M=_Si>9AM4x)`fzMH@RDV>9eA=7 zOIpZ7HhF+gTeE^WhCrT*S{Y1h$>p`Rs&l%iqJY-^irS|v>&jOpS@12FgpY6O6^Jk$ z%gXfi8psKer2>38=;+D;>C6^>spxgi%$I#qRXy5jqE_Y;I&E%q|;Tgq09 z{n3UmKG!e`h2W%!!$(80K;RMeyhvLDj5@F9D9|$ChehO~Xhd?`6&Nqp;3T(a9M>pwB5v=z7PsWhxbC zxBNSqHI#J@A2AU4XgMPD?(zoRHkYFli4}L6ftDMb7DtQ?!H+rx8 zg|ouLlJ=r?lXfcP$jD&i=AIAc5XLl%5lkE+!j!BU*XeG3G~>(!9Z!k5sf}hs2_3xo z)}Vd-Rz7nF`5nL#6-(GN$G9?)AjkwfEGAonI_#DP&E7mdTZ6q3AIx7%7-Wm4j93;v zb9uT#2UJ#8f)9(?p!F_dfcSP=98`0Gpo|&{8649#g9%4H@cDm6i>I(&d%AyqkJw~) z=WzNKRQ9mf*v7)Xk-yQrUBT+yj#oyFQ1r=q-BzOv<@sieNHz_v7%Z~Q~75P&1aK{+4}9GmCmE=@1*F4ET33J zbkq&Q5i8M9MBH^<>)MUYOq6Z(k|c{nVQI`MlV=~DN9HwY-RrF2bdk1rdii8IH)k5i zH(d7}zmdWq^@p?WVDxSbABeq6&MY4y!kZ09sya3z_#w`+n$KZ?U7-*chls#dnv|-V zFenD#=LYkU9pJdy+1_3{j-}c}Mn*sY9gdFx5|oU=TQyna&P~Y8{TE0CR_Pq!T+n1H zLYtVXRi!zdCpO^s{ICwPDSFQ=w0w+{$T^%Ti_zl96 zo}L6MX6Y(Q`Lam`1&<#N2cn6E*QjF>d;Tu``jUyi=3rr#b!LqTvb;+h!8$=fAt<=Y z-?SNMtd0TFMeBSYe|?~i#l}u;s+=H@A|o#VuL-(|&c2aL-M4bvM;QQ#G<~9|8~WA$ z^g#BTa$v#-xYj@{A;3Fpb;ks1V3Ck~ce9ILCF*D%>}v*hE=Kk6lrnPXnJe%h@d*hR z+{)CiCAt$J`@Tw36xQ6~c%iHqrg0nkg9uK5N<8gQ25MAPFan5crg=x@Y{yBrh!OD4 zNHCpie+Ls3$xya70Pgk6iURjgE{Lqop2J&Lj{>hHY3To)ClhHa(kW9blKG*T{#udKHp93c7 z=oI~a%k|b>^vo@tK&UB7l+I~8EI5tW)rA~pIueQ$wdNxh%JDW7QU7X^8ZZH*o#J`w zG#-AFQOr-Z5Bw04;6w^Eh-!;lrjWPd>!G9#5{#_1%Ezub*H?qK4b!@^^Dlm9UGLc~ zM;-h}la_q$yUBle@oB_^%Em{tPu z9v|Z-|6Ys^I)T=uiCk`GPEJhI&m4GKzBh}YZigyp`KM|B=cW3g0`}dltt(;{ky!e- z0r;R@G~F@>SHKFi!l6Txl9QW)v~WYe>tqtxYB#If(I;kvBJ~7{%++@K%_HH7Xdk^TysS7gKouzpQzW+?=lOI`CMLvr{`a zBA7<=#zsX%w4Js;g7qKIgYZXIRu*`kg3RJE;OR(GU7R{F{x8MW6_J2v4;*hGk$bs= z>=xAOf|`?l-_0+L3jd}M09H(ZW>ug+__FK=0*#VCEj;U9jDU)9pPR*9aGM?)PjcbK z1i>Jk5w)&SImIcG$=#j^{wqTJ`u6?9%&;F&reM+w3AvwSk-!p3;OXyD1dB^ZtUC^D zJ)U-~qtLtwAO_qSpt%2?j$i(I`rTjL4#c)WLEz!xHLG+RtfxaC9=v1{^yWojx3@9t zy)7~{vNN6M_4x1*5PRxxrU2Xa#dVi$#pY8bX47C>9WM(NHS37t-_zGNP)w}nvTGyA z0tZc9^acV+4p4RJDi4D)HNq$j-b!GLF(>&-I=7W--3kQFzvIty=4j2&pGugk6rdY% zc&n4Uf*IQ@Qw{udB-vo|Qb94Ru)omhjz>gXTrMCvKp+yk@57br;r!c95NjzJi{l8| zp@zH%>3@5#J-d=iwLkRZ!;;cPrfm+adsX~w^|_xy7{0xdBJ%avVHgfj9lm_3O0=ut zz!Ubf$Au6GdURT_(l`9>j~$2fgv3&ir=P2xWOP`;Nrgj`s18FmR8@)mZtfl3<A@anA9^28iod3z?{KN8l_0+`bNn`}30kdmfAzT1qWyZWzlmy^XOvsa zB9GLqKRngPZSVmLZ%R{t#-@Sk)6EG8 z-}>_9;`K4B)~vet>8Z7G-Jl87DuY77M=3%G32ZlD(}eH03IYQIcSK#cJT){wzucMb zg6diDe9Z$4)X(Mx-2OBgS)zzPAjK2h={h==DNMe*TSJLs`f{PF&CShm!a|2Y;o^TW zw0NQ~YT*ngUf@tt<)k(Wom-`4VDK_b&kSgsg62E7TW2gxOeVwj&xdo2AQo=}OczgS z(pid=&vx%Vvf#yl5HXN6?D2RU0=~vVj9i|3o7X z`C+kY(W{-v4FlEPNt-SNk3kpD$r6Xh79B|OCOJSB5$7Gc6Cy|HC{khNv%g2Z+3n{)H9r%E7#Q=L>qH!I91OqwVTq zBu19k6c(zziDTW&%Llzg zMFT}-N!Sq$|E&(Ynmo&n7v{E>tUZ@Erhyc^lyu5e((2zin2LI){C%lYO@YW;`{m`) ze?;9XdL%GCWdwIJ# zzH|Jr_@pk+dy~07PmV1_>r87u%y!aV4Bbj&-?LvFq##Fd!p?l!Bn?J{+ug8!``ZL3 z?X4GKlFw51%VvMxLqHulEa}7w+h^zbQ0b%3iQZz-A?zblgZZfg*XM8Fwo2*w$X^XK zUZoMMmqJ+2MkK869$orVVfs14%ml?vxrSY_` z|2F<8W(9{bvwM$b`q9Xrw%%o1Uu+q52m=cXm`YLnZx-pI`Ut*Lk;8U_WIizeT>e|X zRodjKrh&Q`Xi<`KbBUW`DIzz3VvozTzksPcO{5U0mZytjKugoa;2`XlZURNy>&pd5 zErSU(V3JZ20<>KZiqiV8V{T#T{>QcPtf4IK(>?%aHOg``(bKQDdR~Ia|7EbtX+b44 zZx~75tPC-3pV%dmDpbsX(`&_Z^o{!7$jAuDm4qV&A_Ra+0)7V+PW|}t$Bga1gU__` z_isLS_PW|yHX0flaEGw6e%o!%%36tzM$~$WekJ+^hViAPjIFICA>W6>Vz-~Vj6fMJ z`_383NCn8$o<_4~bHhlr{ zGWFQwc~eIdU!FS$BTd-C*4kn3$9m&7s&abMQkgD_UhU=nw+J%Y@b)T6nvm=>L`a6d7h&^q5Fk{)+4$14%%b1^ z`@^?A)z@65DZvj`qVFe*3i(%rwRRcM1)v# z;5F!e0Ms!yb{J}TX=!Oi#pNaiBQrBI6BAXi%YFhDK0Y?kHsRqdfi*Rjkbmj(FF?55 zpF4%;|C2cZE+;%ZoK!57ikezVT%3Z63SeXmyu8g#O=hKM> zz!$%Zts#JZF4;5)oprtiY3H_HUJro$1s%EfJDK)CtO8t$6JPdW4F%GabR(PSGGyhok-#2$(w`z zPYSXoh!h}&i9MetlFSr!1!F5CEs3VtFM&b^qfDH(2u7?R@^hmOKD`YW*X_H|P~>d2 zmg@s@Wja+VRK@S|Nfe8WMMYif8eH_zByU$;%=AkwL^Dj~5+&15ms-IkSg z2s)m56OJ}+bh`|t(aiNmx}qh}ir4N@wq(Ew{E)khA?a5wI*>~I^JlFc0S-3Mi0cy` zp1eg}AGI+R1o3e8dTfu5QaW|ogbtNxHc9>{7(K%24OMg=t(sJ%T`fBS1`X?f;qmP|{CivyMrNC-g_ z)jb972WVp;(lzwEHxDTHadp)IQWjg*pfQV2zm8USysph7C$02aJya|#if{Ena3A0* z2?;;o5)FmlyWQ_#}N z#7;S`dV)%>Au~E==8@sygT=ZMcUR>81u$2DNElCp$pR@Q_b7z^CaE|gfG>l82x4Gy z*vzb)H9z#HOsHAS!g8jC(` zu!_%VYY+@PkWPR?;+i6NMa1)`eElAe8YyDqqQWLlONO(F0JX@MZIG`%PH5)jMqUJ~ zChDMO2@!z}AVn!b6q)!Mg!7li6x|e^CI&!kD=VXDa|?@` z{psCEMHS6;^Qhh(u9*XYRbGvyX) z7rBMHjwfzu2jW*9z23a3Kx3{;&9={s2AY< zl|8pG+^-&VC5^EzVbi^`QmighlSi7EyIwDXsLx2Rg4r^XCbiOHTRm~7&(WHPWqq*+ z^Otj%{KAd4UmX5U5o)h~GHvmsc}|*I`uz~3!+Urs|DN!{0RGauQ6K-inw+A{dZT^K zn8mdLv>0f%%6 zT=%;K5kK!mV~!O+ZsDcDhXn3=M*pOcse&i@J=_)TjqmABA{tI$O~sCPFYssIg)dt% z`kQDTqJXGmwHfp7%Hg%wskIhBa+76SRxLO*OJ{B3SWjK}%$WcteE3BxMGbKN!|%G% zGcz+&Qg$3)hGk84bZ7zZO6_gCKrm2LQUZ`1!|m;DPfyR2pe}vzO91#^^VwOrZOySa z@cMQJsrlb;W2;~wg?{&Y_Suu%?2iG1XWOKe&uQB>0C~P1-Y8~`@@2cW51ByAqvKM2 zpJC}Xb>{5qEV)8+5{C&O(2UROq+tm~D^7C4kqZX-m!$FH;*<`2sHD}s$S@FC_HL-Z z>NeL}as~*j^-zt^gpKP=LFs@;2S?U<4#;(Ua-N%?Z;|vcQB@TQZ7fdmDwrI^saqrz zfhB{^z=3!ebfH>6lDHCzq|qs_x(UR26y5riCAV+oX(`z-w)Pq><>d#mC~kmi!7HCsVU+qRBdNA1}l6U%H9@wxl1w%^>AsEBMhfrz13ib7aXk>v-iY@7QR89}oqp6&8-HhbM}*#el$dYbsF;)WLTSD|;%-^ZHZYqHONF0|lSDT55#PssroHrOL4W?* z)iB93=X84k!WMKqW4Fxl+1;lv_^|460%!L|TagVeC z(rNGhm-={aw$*j@uGN4u1pLJK4<@R z%^M0AgXP-^ue(gGgv)--v>Z){50R)~Ki~+XH%_ABN>fz-h(2Xl#&9b=wmQar!@LsZuUo6+QPxP75`x=3Vjo@q}pyOJhUVd zB{6)ypc)26gGosRm6%3D_R}#i1s@w0Dj0{PGYt)y<$dtmQPRNFjw|uo@u4qMHB|Cx zXHN*sgUI#V*bHeb#-jgi#eKcfpk=YI9oc7aH_D_TErJKQtzj=tav@b^HzdP{fVeq-nnH+Ceii#v1Tr(%zvrX(lcqWCJzgxCrHlsc#VTs>W* z1nZXDqd})}+FMFmw2i+g5ghFQt*OECBB6kOLl+mS@6yLd-UZ+sECEt9W6Vs84rI6U z!x`jlH|{VA4cN=s@lu!Rl-NBMJ&q z8|C5;z7d0vH;m#QJ3c3Yz9IP9fgo*QPYdezwCY`TU9mrH?fdI1mk3l*n7u6gRnb1U zKZ?r2?Twamr4%w!<5}K^9;=#3WWO6I%pKlz8AAV5gbBbvbJdS;kBrOK>-(EL7yt#% zFHfnwv4&tpBg&&JDk=iS(~e)$M@L7=rI5WlVPZEWO1>+!oR2f^Y!!gC@wqu~By|F5dkN*QY#O;7rVU3P#!14L<*8c>& zp6|{6MwFZ!HYVot`M@}!RG|&D)rwSszQT5@aCsma7p$-k#H6cVGF@L~$#I2^`Z-%T zv1KSr7VFC$aZNQV`Whe{jhTlEz?afu2yRpC9P|MIf3Z(&7Xq&zvm z*&H0Cf&5tf+}CcCHDU^O{sJnvvn~{(&s+0HQHGHlX5?S! z+ZE0KfSOQ`rmEapx5vR#Io!lV!+_?jTRx@#U*J^}wQ!9!yve zsf#14N|SQ#d(H-&OCQhwQjGc+`tRC1peT5L8 z`j7O!%(mgyE!JmjOPeoTEsc27qm4<}e)V5U&?K{4jN)+& zx{oUX&IGuKy!h8Zu(gxZ_((sw>^?ZC00X!CQ_0C`IEmJsij$ZiPn{0P=Qq*h`v9tr z_sf1A>py2}jts|5y9{s|a!-XvM5MEs%R2l(!Qwe}Xc|cx9v%jMAdeqENrUZx77TDa z9M}!Lj(WgRPA#9dH+kpu?GK~@`(9vPpL+u+TY+>=R$F_)pE==c$JdNSrNgG=5vw{1 zid$vj+%>pVec2PrK$513IuQ2lHbRpOFAPo2l%=Mxf1HE56R84xGdDpSdP2l;-Sw&5 z6w#8VAx~C*8;?NIV^00q*f>w`=6nEY8DAdDT{{rNrgDZvX6wj85(9msx3xgLYa;Dzjev)})GRYf@{R?2c{{Z?_2lQK+e{vz`lKz0`u}tpwQ;Wb4Iu(xoc;gzGKlaxwp;sIZ z$2HRwUu|IP^~i+Hn;Z45U6IFv1lFf{+%5IOV&d47b!xab)H2XeAEjR=Xp;!K2}B7* zZIDkFTeN;XkO%*EJwAxprIgL2zu73qJtDj~v<%r>QBU)H{Pl&AY&4tSt^Pi1y3N~t zG3h@SK7W+|p1`>aI#BFHAD@@Dh&*ymmu8lhf7#nRzJltFUx(+%WEHCo($}^rCF6Kz zEG}@<(5xVV!lckphxmLlw`4qPqK)8{sGo;c=p^0o*-#Pf;Sc<2IvIQk+Zl{8d zIkietE=!eyXwp!WbtLMPzAvpizMO)-C?`?Up9G4kkw$f+w%q+tGM{h>i)~D=r5}i) zwwzy!-+$)Mxo#0`BD(*jZugE8A{P0%b@itlc5O-}8z^lf@h@{b;1rIaXOYxyy@-*3}UpFyUVFAh#|a$M5Ii z;`*hU|Q3D!V#4_H=4l^?SODfQ)5;C5?#x z2IS^I=XLK94G4;*iy_%Q{&mD;SM%?1P-(3=VmRJF}XT=vY>dNh=-XElXDK*^-E@5 z;5AL?|D?G52a{-RncfEBK5S*|d4P%HBwJwz{V=Ja&2nLlXujrq@C(9BElu_H$}|YLWlzON;xfr%DNW(bN=r+<*SLqObsoS^^2D z+>K%&T}@nk;r_DsE#EIsnG|-*+Jggus4W0T_74m^YDKX2qoI8RhLiO462bp89UWx? z@MZ|z<=gqr@FUrJX z+PIuO-VZDHn({eDa@nO(F>wfEunDQtM|A@)_=Q-2j7$`U9SDC~Zi$D4QmH3?hl6tN(OFJIBjzKam3a09TU{@G3@<sWWAXhVK#(Qd=va9i`UtV#d&M+TVE6wIGrYTzZ*pp^7()=Ey{~|?-GTm@Ng>- z9p-&710JD0c8zMXmJ{ih%X3vimYDqupeiFXiswt}>pK}&O&-dF>Qg!%VgrP1S=@K+ zNIjg|Z7HridfNO=94+=|&Gph^s%F~i9;J#E*QPVJ7M@dj>-<%vFMEfaAjQ{+jx#ZT zcCba6AAG~f)l?n%RG za$1~66`ou4o0&MiEOCTQ-ve$9Ay9-Cqq1p&I4Tcd)_a(JDAu5JX5onWA?#fA25X?a;<8yh0&PVEJ& zq`3>(+cYZbv3CA_-1B;El=;> zcntoIAeS1E4v+^e&mcf55kIYL)!L#2rn@@7NvuKhac0P1}wG50F)z zveMz2e)?tiPp!7~4IdNp8Yta={S^di($O?lQ;;r=Wf$zNaaH2amkM-@RiKD-=#P=1mNSf@RR23ac3_k=% zicb3nHnD}a!=Fw4AiU49DKmyMv{d7t;3nzrNE{ZvhZrpd4*#}MZYceFM@{$_H-;jx zG4zkoFZK7RM^t$hGq%u>_~Cn+Ke(U&OdRC1FEiU@rNvum7+oU*H?mSj%H3TsmaA{< z=2NLEn2Ly+$!gJ#!Kb{~Xm1$By`yH1Es4W}kpI4we$@`&w*bnbr~-h5u^5SjC4n>Cwm}!n zkH`c9O{N3Lw`@IY%w7O(evd-(Tp&?816YDvXXjVlnBw~Sf4N=6K6iPonWHCD<*66`_8mZq zZHIdV%E7e$HF5Kr_5<}SU@E|}2c(-FPP25lw7?zFNzdI_uH^$JB+I%wc6N4LQ-pP| z!xI13$Gk8iJT4b2b@imh&q971y?_*G`SQv1Lz#NHef!H2rq{gY#Q}ZI=k@kyfQCZO zz~n~A>*KLgsY5Om4R~|Fw)2D8aaf}Qu;wn?OL_V)^k&`RAHNL}hU>@>OV9Gu?TTv?aJO&lBv4lSRF{W(kS&CgFSn|5jP}VAyno;yW)(Mev_*^d% zCTL|6xJ53ovK4nbzF)4Bg9Q{h;da45LqkVH!_#`#?4J)Zb8L<*O-5E0zK~3TKh0V{ zZ4tAH4;gi|>3dQOZ>vON5I!cpGby|jYQXd~Ee*8g7NUq<*(Cg3Dwl}3bvEJ4%PsEu{yma*R z*uXI1ac+_uHd1H@Oi zjK^W&OxSOkF49EI_ZgeG{k1Rb^s#m5$B!SNY~9nIn9U3fgyBacLYdjmXTmRkFVBEV zbY@998C(S5JKA0z4lm}x2wJi;e%-t~_>G(!H11J)NqBhN0Tb%Yf@$24ArK<{V^ntB zh`=RBVy^m$9wzATUodhCTqiwoc6|R2Ogpcc%X#|MIkOq4lr|x=aZo%sZbq(19yYJy zX2uP3h=icCaMK9nQ zVrwhqWYIK+)3wp9P;=_uX0m*ZYom`#%x`)*)zEOCXk+‡@ArGz9r@OwW3in0oa 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 literal 0 HcmV?d00001 diff --git a/docs/source/index.rst b/docs/source/index.rst index 3eb13295..1b475e14 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -14,6 +14,44 @@ multiscale simulation. Simulation models can be as simple as a single Python file, or as complex as a combination of multiple separate simulation codes written in C++ or Fortran, and running on an HPC machine. +Contributors +------------ + +.. image:: contributor_logos.png + :alt: Logos of the University of Amsterdam Computational Science Lab, Netherlands eScience Center, and Ignition Computing + +University of Amsterdam Computational Science Lab + Original concept, Multiscale Modelling and Simulation Framework (MMSF) coupling + theory, original MUSCLE, MUSCLE2. + +Netherlands eScience Center + MUSCLE3 implementation, teaching materials. + +Ignition Computing + Checkpointing implementation and additional development. + + +This work was supported by the Netherlands eScience Center and NWO under grant number +27015G01. + +We would like to acknowledge the contribution by The ITER Organization of results of +work carried out within the framework of ITER contract IO/22/CT/4300002587. The views +and opinions expressed herein do not necessarily reflect those of the ITER Organization. + +Academic collaboration +---------------------- + +Please contact `prof. Alfons Hoekstra (UvA CSL) `_ +and/or `Lourens Veen (NLeSC) `_. + +Commercial support +------------------ + +Please contact `Ignition Computing `_. + +Citing MUSCLE3 +-------------- + If you use MUSCLE3 for scientific work, please `cite the version of the MUSCLE3 software you used `_ and the following paper: From 4c3852109b2ff74c1e66a43b47271c22d8b23e2f Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Wed, 27 Sep 2023 13:36:45 +0200 Subject: [PATCH 13/36] Fix mypy complaint about missing matplotlib type annotation --- muscle3/profiling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/muscle3/profiling.py b/muscle3/profiling.py index 242dad97..03065c96 100644 --- a/muscle3/profiling.py +++ b/muscle3/profiling.py @@ -322,6 +322,6 @@ def plot_timeline(performance_file: Path) -> None: def show_plots() -> None: """Actually show the plots on screen""" - plt.show() + plt.show() # type: ignore if tplot: tplot.close() From d7aea12055aa8edcf9c221c3ff3eda21957452fe Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Wed, 27 Sep 2023 14:28:10 +0200 Subject: [PATCH 14/36] Use a slightly less old Intel compiler for the CI --- .github/workflows/ci_ubuntu20.04_intel.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_ubuntu20.04_intel.yaml b/.github/workflows/ci_ubuntu20.04_intel.yaml index 96f24e73..28cba0aa 100644 --- a/.github/workflows/ci_ubuntu20.04_intel.yaml +++ b/.github/workflows/ci_ubuntu20.04_intel.yaml @@ -20,4 +20,4 @@ jobs: - uses: actions/checkout@v2 - name: Run tests on Ubuntu 20.04 with Clang - run: docker run -v "${GITHUB_WORKSPACE}:/workspace" --env LC_ALL=C.UTF-8 --env LANG=C.UTF-8 --env DEBIAN_FRONTEND=noninteractive ubuntu:20.04 /bin/bash -c 'apt-get update && apt-get -y install wget && wget https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB && mv GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB /etc/apt/trusted.gpg.d/intel-sw-products.asc && echo "deb https://apt.repos.intel.com/oneapi all main" >/etc/apt/sources.list.d/oneAPI.list && apt-get update && apt-get -y dist-upgrade && apt-get -y install build-essential cmake git valgrind pkg-config python3 python3-pip python3-venv curl intel-oneapi-compiler-dpcpp-cpp-2021.1.1 intel-oneapi-compiler-fortran-2021.1.1 intel-oneapi-mpi-devel-2021.1.1 && apt-get -y remove libssl-dev zlib1g-dev && useradd -m -d /home/muscle3 muscle3 && su muscle3 -c -- "cp -r --preserve=mode /workspace /home/muscle3/muscle3" && su muscle3 -c -- "pip3 install --user -U \"pip<22\" setuptools wheel" && su muscle3 -c -- "pip3 install --user \"ymmsl>=0.13.0,<0.14\" qcg-pilotjob==0.13.1" && su muscle3 -s /bin/bash -c -- "cd /home/muscle3/muscle3 && . /opt/intel/oneapi/setvars.sh && CXX=icpx MPICXX=\"mpiicpc -cxx=icpx\" FC=ifx MPIFC=\"mpiifort -fc=ifx\" make test_examples"' + run: docker run -v "${GITHUB_WORKSPACE}:/workspace" --env LC_ALL=C.UTF-8 --env LANG=C.UTF-8 --env DEBIAN_FRONTEND=noninteractive ubuntu:20.04 /bin/bash -c 'apt-get update && apt-get -y install wget && wget https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB && mv GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB /etc/apt/trusted.gpg.d/intel-sw-products.asc && echo "deb https://apt.repos.intel.com/oneapi all main" >/etc/apt/sources.list.d/oneAPI.list && apt-get update && apt-get -y dist-upgrade && apt-get -y install build-essential cmake git valgrind pkg-config python3 python3-pip python3-venv curl intel-oneapi-compiler-dpcpp-cpp-2021.2.0 intel-oneapi-compiler-fortran-2021.2.0 intel-oneapi-mpi-devel-2021.2.0 && apt-get -y remove libssl-dev zlib1g-dev && useradd -m -d /home/muscle3 muscle3 && su muscle3 -c -- "cp -r --preserve=mode /workspace /home/muscle3/muscle3" && su muscle3 -c -- "pip3 install --user -U \"pip<22\" setuptools wheel" && su muscle3 -c -- "pip3 install --user \"ymmsl>=0.13.0,<0.14\" qcg-pilotjob==0.13.1" && su muscle3 -s /bin/bash -c -- "cd /home/muscle3/muscle3 && . /opt/intel/oneapi/setvars.sh && CXX=icpx MPICXX=\"mpiicpc -cxx=icpx\" FC=ifx MPIFC=\"mpiifort -fc=ifx\" make test_examples"' From c3ee7f36dd8860f8babea6564a581d434da7b8a1 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Wed, 27 Sep 2023 14:52:06 +0200 Subject: [PATCH 15/36] Update all workflow steps to the latest versions --- .github/workflows/cffconvert.yml | 2 +- .github/workflows/ci.yaml | 4 ++-- .github/workflows/ci_macos11_clang.yaml | 2 +- .github/workflows/ci_macos11_gcc_gfortran.yaml | 2 +- .github/workflows/ci_macos12_clang.yaml | 2 +- .github/workflows/ci_macos12_gcc_gfortran.yaml | 2 +- .github/workflows/ci_macos13_clang.yaml | 2 +- .github/workflows/ci_macos13_gcc_gfortran.yaml | 2 +- .github/workflows/ci_python_compatibility.yaml | 6 +++--- .github/workflows/ci_python_macos.yaml | 6 +++--- .github/workflows/ci_ubuntu20.04.yaml | 2 +- .github/workflows/ci_ubuntu20.04_clang.yaml | 2 +- .github/workflows/ci_ubuntu20.04_intel.yaml | 2 +- .github/workflows/ci_ubuntu22.04.yaml | 2 +- .github/workflows/ci_ubuntu22.04_clang.yaml | 2 +- .github/workflows/ci_ubuntu22.04_intel.yaml | 2 +- 16 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/cffconvert.yml b/.github/workflows/cffconvert.yml index 807c7e63..47c15ee8 100644 --- a/.github/workflows/cffconvert.yml +++ b/.github/workflows/cffconvert.yml @@ -12,7 +12,7 @@ jobs: timeout-minutes: 2 steps: - name: Check out a copy of the repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Check whether the citation metadata from CITATION.cff is valid uses: citation-file-format/cffconvert-github-action@2.0.0 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b2229ac6..b8e01bad 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,10 +7,10 @@ jobs: timeout-minutes: 15 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python 3.8 - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: 3.8 diff --git a/.github/workflows/ci_macos11_clang.yaml b/.github/workflows/ci_macos11_clang.yaml index ac139b58..e1168c93 100644 --- a/.github/workflows/ci_macos11_clang.yaml +++ b/.github/workflows/ci_macos11_clang.yaml @@ -16,7 +16,7 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install dependencies run: | diff --git a/.github/workflows/ci_macos11_gcc_gfortran.yaml b/.github/workflows/ci_macos11_gcc_gfortran.yaml index f83e7242..6dd73eea 100644 --- a/.github/workflows/ci_macos11_gcc_gfortran.yaml +++ b/.github/workflows/ci_macos11_gcc_gfortran.yaml @@ -15,7 +15,7 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install dependencies run: | diff --git a/.github/workflows/ci_macos12_clang.yaml b/.github/workflows/ci_macos12_clang.yaml index 603aa7bc..db96b555 100644 --- a/.github/workflows/ci_macos12_clang.yaml +++ b/.github/workflows/ci_macos12_clang.yaml @@ -16,7 +16,7 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install dependencies run: | diff --git a/.github/workflows/ci_macos12_gcc_gfortran.yaml b/.github/workflows/ci_macos12_gcc_gfortran.yaml index a754fea2..92e2146a 100644 --- a/.github/workflows/ci_macos12_gcc_gfortran.yaml +++ b/.github/workflows/ci_macos12_gcc_gfortran.yaml @@ -15,7 +15,7 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install dependencies run: | diff --git a/.github/workflows/ci_macos13_clang.yaml b/.github/workflows/ci_macos13_clang.yaml index 00f1abc8..8fbe27f1 100644 --- a/.github/workflows/ci_macos13_clang.yaml +++ b/.github/workflows/ci_macos13_clang.yaml @@ -16,7 +16,7 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install dependencies run: | diff --git a/.github/workflows/ci_macos13_gcc_gfortran.yaml b/.github/workflows/ci_macos13_gcc_gfortran.yaml index 36302282..bcbf7c9a 100644 --- a/.github/workflows/ci_macos13_gcc_gfortran.yaml +++ b/.github/workflows/ci_macos13_gcc_gfortran.yaml @@ -15,7 +15,7 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install dependencies run: | diff --git a/.github/workflows/ci_python_compatibility.yaml b/.github/workflows/ci_python_compatibility.yaml index 21013188..5ae8180b 100644 --- a/.github/workflows/ci_python_compatibility.yaml +++ b/.github/workflows/ci_python_compatibility.yaml @@ -11,15 +11,15 @@ jobs: steps: - name: Check out the source code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Cache Tox - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: ${{ github.workspace }}/.tox key: python-compatibility-${{ matrix.python-version }}-tox diff --git a/.github/workflows/ci_python_macos.yaml b/.github/workflows/ci_python_macos.yaml index def3ff7c..cc024f50 100644 --- a/.github/workflows/ci_python_macos.yaml +++ b/.github/workflows/ci_python_macos.yaml @@ -11,15 +11,15 @@ jobs: steps: - name: Check out the source code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Cache Tox - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: ${{ github.workspace }}/.tox key: python-macos-${{ matrix.python-version }}-tox diff --git a/.github/workflows/ci_ubuntu20.04.yaml b/.github/workflows/ci_ubuntu20.04.yaml index 03006993..2c89d255 100644 --- a/.github/workflows/ci_ubuntu20.04.yaml +++ b/.github/workflows/ci_ubuntu20.04.yaml @@ -14,7 +14,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Run tests on Ubuntu 20.04 run: docker run -v "${GITHUB_WORKSPACE}:/workspace" --env LC_ALL=C.UTF-8 --env LANG=C.UTF-8 --env DEBIAN_FRONTEND=noninteractive ubuntu:20.04 /bin/bash -c 'apt-get update && apt-get -y dist-upgrade && apt-get -y install build-essential cmake gfortran git valgrind libopenmpi-dev pkg-config python3 python3-pip python3-venv curl && apt-get -y remove libssl-dev zlib1g-dev && useradd -m -d /home/muscle3 muscle3 && su muscle3 -c -- "cp -r --preserve=mode /workspace /home/muscle3/muscle3" && su muscle3 -c -- "pip3 install --user -U \"pip<22\" setuptools wheel" && su muscle3 -c -- "pip3 install --user \"ymmsl>=0.13.0,<0.14\" qcg-pilotjob==0.13.1" && su muscle3 -c -- "cd /home/muscle3/muscle3 && make test_examples"' diff --git a/.github/workflows/ci_ubuntu20.04_clang.yaml b/.github/workflows/ci_ubuntu20.04_clang.yaml index 1050b45b..f6fde0ad 100644 --- a/.github/workflows/ci_ubuntu20.04_clang.yaml +++ b/.github/workflows/ci_ubuntu20.04_clang.yaml @@ -14,7 +14,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Run tests on Ubuntu 20.04 with Clang run: docker run -v "${GITHUB_WORKSPACE}:/workspace" --env LC_ALL=C.UTF-8 --env LANG=C.UTF-8 --env DEBIAN_FRONTEND=noninteractive ubuntu:20.04 /bin/bash -c 'apt-get update && apt-get -y dist-upgrade && apt-get -y install build-essential clang cmake gfortran git valgrind libopenmpi-dev pkg-config python3 python3-pip python3-venv curl && apt-get -y remove libssl-dev zlib1g-dev && useradd -m -d /home/muscle3 muscle3 && su muscle3 -c -- "cp -r --preserve=mode /workspace /home/muscle3/muscle3" && su muscle3 -c -- "pip3 install --user -U \"pip<22\" setuptools wheel" && su muscle3 -c -- "pip3 install --user \"ymmsl>=0.13.0,<0.14\" qcg-pilotjob==0.13.1" && su muscle3 -c -- "cd /home/muscle3/muscle3 && CXXFLAGS=-fPIE OMPI_CXX=clang++ CXX=clang++ make test_examples"' diff --git a/.github/workflows/ci_ubuntu20.04_intel.yaml b/.github/workflows/ci_ubuntu20.04_intel.yaml index 28cba0aa..8243cbbd 100644 --- a/.github/workflows/ci_ubuntu20.04_intel.yaml +++ b/.github/workflows/ci_ubuntu20.04_intel.yaml @@ -17,7 +17,7 @@ jobs: timeout-minutes: 15 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Run tests on Ubuntu 20.04 with Clang run: docker run -v "${GITHUB_WORKSPACE}:/workspace" --env LC_ALL=C.UTF-8 --env LANG=C.UTF-8 --env DEBIAN_FRONTEND=noninteractive ubuntu:20.04 /bin/bash -c 'apt-get update && apt-get -y install wget && wget https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB && mv GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB /etc/apt/trusted.gpg.d/intel-sw-products.asc && echo "deb https://apt.repos.intel.com/oneapi all main" >/etc/apt/sources.list.d/oneAPI.list && apt-get update && apt-get -y dist-upgrade && apt-get -y install build-essential cmake git valgrind pkg-config python3 python3-pip python3-venv curl intel-oneapi-compiler-dpcpp-cpp-2021.2.0 intel-oneapi-compiler-fortran-2021.2.0 intel-oneapi-mpi-devel-2021.2.0 && apt-get -y remove libssl-dev zlib1g-dev && useradd -m -d /home/muscle3 muscle3 && su muscle3 -c -- "cp -r --preserve=mode /workspace /home/muscle3/muscle3" && su muscle3 -c -- "pip3 install --user -U \"pip<22\" setuptools wheel" && su muscle3 -c -- "pip3 install --user \"ymmsl>=0.13.0,<0.14\" qcg-pilotjob==0.13.1" && su muscle3 -s /bin/bash -c -- "cd /home/muscle3/muscle3 && . /opt/intel/oneapi/setvars.sh && CXX=icpx MPICXX=\"mpiicpc -cxx=icpx\" FC=ifx MPIFC=\"mpiifort -fc=ifx\" make test_examples"' diff --git a/.github/workflows/ci_ubuntu22.04.yaml b/.github/workflows/ci_ubuntu22.04.yaml index 639a8b96..7f65dc8e 100644 --- a/.github/workflows/ci_ubuntu22.04.yaml +++ b/.github/workflows/ci_ubuntu22.04.yaml @@ -14,7 +14,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Run tests on Ubuntu 22.04 run: docker run -v "${GITHUB_WORKSPACE}:/workspace" --env LC_ALL=C.UTF-8 --env LANG=C.UTF-8 --env DEBIAN_FRONTEND=noninteractive ubuntu:22.04 /bin/bash -c 'apt-get update && apt-get -y dist-upgrade && apt-get -y install build-essential cmake gfortran git valgrind libopenmpi-dev pkg-config python3 python3-pip python3-venv curl && apt-get -y remove libssl-dev && useradd -m -d /home/muscle3 muscle3 && su muscle3 -c -- "cp -r --preserve=mode /workspace /home/muscle3/muscle3" && su muscle3 -c -- "pip3 install -U pip setuptools wheel" && su muscle3 -c -- "pip3 install \"ymmsl>=0.13.0,<0.14\" qcg-pilotjob==0.13.1" && su muscle3 -c -- "cd /home/muscle3/muscle3 && make test_examples"' diff --git a/.github/workflows/ci_ubuntu22.04_clang.yaml b/.github/workflows/ci_ubuntu22.04_clang.yaml index c9f40582..343af0a6 100644 --- a/.github/workflows/ci_ubuntu22.04_clang.yaml +++ b/.github/workflows/ci_ubuntu22.04_clang.yaml @@ -14,7 +14,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Run tests on Ubuntu 22.04 with Clang run: docker run -v "${GITHUB_WORKSPACE}:/workspace" --env LC_ALL=C.UTF-8 --env LANG=C.UTF-8 --env DEBIAN_FRONTEND=noninteractive ubuntu:22.04 /bin/bash -c 'apt-get update && apt-get -y dist-upgrade && apt-get -y install build-essential clang cmake gfortran git valgrind libopenmpi-dev pkg-config python3 python3-pip python3-venv curl && apt-get -y remove libssl-dev && useradd -m -d /home/muscle3 muscle3 && su muscle3 -c -- "cp -r --preserve=mode /workspace /home/muscle3/muscle3" && su muscle3 -c -- "pip3 install -U pip setuptools wheel" && su muscle3 -c -- "pip3 install \"ymmsl>=0.13.0,<0.14\" qcg-pilotjob==0.13.1" && su muscle3 -c -- "cd /home/muscle3/muscle3 && CXXFLAGS=-fPIE OMPI_CXX=clang++ CXX=clang++ make test_examples"' diff --git a/.github/workflows/ci_ubuntu22.04_intel.yaml b/.github/workflows/ci_ubuntu22.04_intel.yaml index fecdf10f..2af4a7db 100644 --- a/.github/workflows/ci_ubuntu22.04_intel.yaml +++ b/.github/workflows/ci_ubuntu22.04_intel.yaml @@ -15,7 +15,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Run tests on Ubuntu 22.04 with the Intel compiler run: docker run -v "${GITHUB_WORKSPACE}:/workspace" --env LC_ALL=C.UTF-8 --env LANG=C.UTF-8 --env DEBIAN_FRONTEND=noninteractive ubuntu:22.04 /bin/bash -c 'apt-get update && apt-get -y install wget && wget https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB && mv GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB /etc/apt/trusted.gpg.d/intel-sw-products.asc && echo "deb https://apt.repos.intel.com/oneapi all main" >/etc/apt/sources.list.d/oneAPI.list && apt-get update && apt-get -y dist-upgrade && apt-get -y install build-essential cmake git valgrind pkg-config python3 python3-pip python3-venv curl intel-oneapi-compiler-dpcpp-cpp intel-oneapi-compiler-fortran intel-oneapi-mpi-devel && apt-get -y remove libssl-dev && useradd -m -d /home/muscle3 muscle3 && su muscle3 -c -- "cp -r --preserve=mode /workspace /home/muscle3/muscle3" && su muscle3 -c -- "pip3 install -U pip setuptools wheel" && su muscle3 -c -- "pip3 install \"ymmsl>=0.13.0,<0.14\" qcg-pilotjob==0.13.1" && su muscle3 -c -- "cd /home/muscle3/muscle3 && . /opt/intel/oneapi/setvars.sh && MPICXX=\"mpiicpc -cxx=icpx\" CXX=icpx MPIFC=\"mpiifort -fc=ifx\" FC=ifx make test_examples"' From 5187b0d7623f475d8b842f07e82ab10f97a4b278 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Wed, 27 Sep 2023 15:17:08 +0200 Subject: [PATCH 16/36] Add Python 3.11 to the CI --- .github/workflows/ci_python_compatibility.yaml | 2 +- .github/workflows/ci_python_macos.yaml | 2 +- tox.ini | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci_python_compatibility.yaml b/.github/workflows/ci_python_compatibility.yaml index 5ae8180b..d21765ae 100644 --- a/.github/workflows/ci_python_compatibility.yaml +++ b/.github/workflows/ci_python_compatibility.yaml @@ -7,7 +7,7 @@ jobs: timeout-minutes: 5 strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - name: Check out the source code diff --git a/.github/workflows/ci_python_macos.yaml b/.github/workflows/ci_python_macos.yaml index cc024f50..9107472c 100644 --- a/.github/workflows/ci_python_macos.yaml +++ b/.github/workflows/ci_python_macos.yaml @@ -7,7 +7,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - name: Check out the source code diff --git a/tox.ini b/tox.ini index e7310a7a..9b28adea 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37, py38, py39, py310 +envlist = py37, py38, py39, py310, py311 skip_missing_interpreters = true [testenv] @@ -27,6 +27,7 @@ python = 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 [pycodestyle] max-doc-length = 88 From 558b34c7b5dbeec66b15799523cef2651be77456 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Wed, 27 Sep 2023 15:21:19 +0200 Subject: [PATCH 17/36] Add Ubuntu 23.04 to the native compatibility CI --- .github/workflows/ci_ubuntu23.04.yaml | 20 ++++++++++++++++++++ .github/workflows/ci_ubuntu23.04_clang.yaml | 20 ++++++++++++++++++++ .github/workflows/ci_ubuntu23.04_intel.yaml | 21 +++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 .github/workflows/ci_ubuntu23.04.yaml create mode 100644 .github/workflows/ci_ubuntu23.04_clang.yaml create mode 100644 .github/workflows/ci_ubuntu23.04_intel.yaml diff --git a/.github/workflows/ci_ubuntu23.04.yaml b/.github/workflows/ci_ubuntu23.04.yaml new file mode 100644 index 00000000..5113acf9 --- /dev/null +++ b/.github/workflows/ci_ubuntu23.04.yaml @@ -0,0 +1,20 @@ +# Run Continuous Integration for the latest Ubuntu release +# This mainly checks for issues/regressions in the native build +name: native_compatibility_ubuntu23.04 +on: + schedule: + - cron: '30 7 * * 6' + push: + branches: + - 'release-*' + - fix_native_compatibility_ci +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - name: Run tests on Ubuntu 23.04 + run: docker run -v "${GITHUB_WORKSPACE}:/workspace" --env LC_ALL=C.UTF-8 --env LANG=C.UTF-8 --env DEBIAN_FRONTEND=noninteractive ubuntu:23.04 /bin/bash -c 'apt-get update && apt-get -y dist-upgrade && apt-get -y install build-essential cmake gfortran git valgrind libopenmpi-dev pkg-config python3 python3-pip python3-venv curl && apt-get -y remove libssl-dev && useradd -m -d /home/muscle3 muscle3 && su muscle3 -c -- "cp -r --preserve=mode /workspace /home/muscle3/muscle3" && su muscle3 -c -- "cd /home/muscle3/muscle3 && make test_examples"' diff --git a/.github/workflows/ci_ubuntu23.04_clang.yaml b/.github/workflows/ci_ubuntu23.04_clang.yaml new file mode 100644 index 00000000..922edf5d --- /dev/null +++ b/.github/workflows/ci_ubuntu23.04_clang.yaml @@ -0,0 +1,20 @@ +# Run Continuous Integration for the latest Ubuntu release +# This mainly checks for issues/regressions in the native build +name: native_compatibility_ubuntu23.04_clang +on: + schedule: + - cron: '30 6 * * 6' + push: + branches: + - 'release-*' + - fix_native_compatibility_ci +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - name: Run tests on Ubuntu 23.04 with Clang + run: docker run -v "${GITHUB_WORKSPACE}:/workspace" --env LC_ALL=C.UTF-8 --env LANG=C.UTF-8 --env DEBIAN_FRONTEND=noninteractive ubuntu:23.04 /bin/bash -c 'apt-get update && apt-get -y dist-upgrade && apt-get -y install build-essential clang cmake gfortran git valgrind libopenmpi-dev pkg-config python3 python3-pip python3-venv curl && apt-get -y remove libssl-dev && useradd -m -d /home/muscle3 muscle3 && su muscle3 -c -- "cp -r --preserve=mode /workspace /home/muscle3/muscle3" && su muscle3 -c -- "cd /home/muscle3/muscle3 && CXXFLAGS=-fPIE OMPI_CXX=clang++ CXX=clang++ make test_examples"' diff --git a/.github/workflows/ci_ubuntu23.04_intel.yaml b/.github/workflows/ci_ubuntu23.04_intel.yaml new file mode 100644 index 00000000..8b6051c9 --- /dev/null +++ b/.github/workflows/ci_ubuntu23.04_intel.yaml @@ -0,0 +1,21 @@ +# Run Continuous Integration for the latest Ubuntu release +# This mainly checks for issues/regressions in the native build +name: native_compatibility_ubuntu23.04_intel +on: + schedule: + - cron: '0 7 * * 6' + push: + branches: + - 'release-*' + - fix_native_compatibility_ci + - issue-25-intel-compiler-support +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - name: Run tests on Ubuntu 23.04 with the Intel compiler + run: docker run -v "${GITHUB_WORKSPACE}:/workspace" --env LC_ALL=C.UTF-8 --env LANG=C.UTF-8 --env DEBIAN_FRONTEND=noninteractive ubuntu:23.04 /bin/bash -c 'apt-get update && apt-get -y install wget && wget https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB && mv GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB /etc/apt/trusted.gpg.d/intel-sw-products.asc && echo "deb https://apt.repos.intel.com/oneapi all main" >/etc/apt/sources.list.d/oneAPI.list && apt-get update && apt-get -y dist-upgrade && apt-get -y install build-essential cmake git valgrind pkg-config python3 python3-pip python3-venv curl intel-oneapi-compiler-dpcpp-cpp intel-oneapi-compiler-fortran intel-oneapi-mpi-devel && apt-get -y remove libssl-dev && useradd -m -d /home/muscle3 muscle3 && su muscle3 -c -- "cp -r --preserve=mode /workspace /home/muscle3/muscle3" && su muscle3 -c -- "cd /home/muscle3/muscle3 && . /opt/intel/oneapi/setvars.sh && MPICXX=\"mpiicpc -cxx=icpx\" CXX=icpx MPIFC=\"mpiifort -fc=ifx\" FC=ifx make test_examples"' From ed7a79eb6b1f3274991c9fe688511c0196f883a1 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Wed, 27 Sep 2023 18:41:47 +0200 Subject: [PATCH 18/36] Add missing py.typed marker so that users can type check their code --- MANIFEST.in | 2 ++ libmuscle/python/libmuscle/py.typed | 0 muscle3/py.typed | 0 setup.py | 1 + 4 files changed, 3 insertions(+) create mode 100644 libmuscle/python/libmuscle/py.typed create mode 100644 muscle3/py.typed diff --git a/MANIFEST.in b/MANIFEST.in index d52a8f9e..27a384aa 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,5 @@ include LICENSE include README.rst include NOTICE include VERSION +include muscle3/py.typed +include libmuscle/python/libmuscle/py.typed diff --git a/libmuscle/python/libmuscle/py.typed b/libmuscle/python/libmuscle/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/muscle3/py.typed b/muscle3/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/setup.py b/setup.py index c64ec76f..5ded4c8d 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ 'muscle3': 'muscle3', 'libmuscle': 'libmuscle/python/libmuscle' }, + include_package_data=True, entry_points={ 'console_scripts': [ 'muscle_manager=muscle3.muscle_manager:manage_simulation', From cc77c1ce8ca6d06c7861c8f7644db86695734f11 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Wed, 27 Sep 2023 19:49:13 +0200 Subject: [PATCH 19/36] Fix warning about Data copy constructor with -Wextra (#258) --- libmuscle/cpp/src/libmuscle/data.hpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/libmuscle/cpp/src/libmuscle/data.hpp b/libmuscle/cpp/src/libmuscle/data.hpp index d4e83a3b..30adcebd 100644 --- a/libmuscle/cpp/src/libmuscle/data.hpp +++ b/libmuscle/cpp/src/libmuscle/data.hpp @@ -623,6 +623,18 @@ class Data : public DataConstRef { // create from scalar type using DataConstRef::DataConstRef; + /** Copy-construct a Data object. + * + * Explicit default avoids a compiler warning on some compilers. + */ + Data(Data const &) = default; + + /** Move-construct a Data object. + * + * Explicit default avoids a compiler warning on some compilers. + */ + Data(Data &&) = default; + /** Create a Data object containing a grid object. * * This creates a DataConstRef that represents a grid or array of a From ec53dc4a3619774329311f274e3a082624b9bae2 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Wed, 27 Sep 2023 20:22:17 +0200 Subject: [PATCH 20/36] Fix a few more warnings while we're at it --- .../bindings/libmuscle_fortran_c.cpp | 40 +++++++++---------- .../bindings/libmuscle_mpi_fortran_c.cpp | 40 +++++++++---------- libmuscle/cpp/src/libmuscle/logger.cpp | 2 +- .../src/libmuscle/mcp/transport_client.cpp | 2 +- scripts/api_generator.py | 2 +- scripts/make_libmuscle_api.py | 2 +- 6 files changed, 44 insertions(+), 44 deletions(-) diff --git a/libmuscle/cpp/src/libmuscle/bindings/libmuscle_fortran_c.cpp b/libmuscle/cpp/src/libmuscle/bindings/libmuscle_fortran_c.cpp index fdfd1dbe..f9d7d847 100644 --- a/libmuscle/cpp/src/libmuscle/bindings/libmuscle_fortran_c.cpp +++ b/libmuscle/cpp/src/libmuscle/bindings/libmuscle_fortran_c.cpp @@ -83,7 +83,7 @@ std::intptr_t LIBMUSCLE_DataConstRef_create_copy_(std::intptr_t value) { std::intptr_t LIBMUSCLE_DataConstRef_create_grid_logical_a_(bool * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); DataConstRef * result = new DataConstRef(DataConstRef::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -91,7 +91,7 @@ std::intptr_t LIBMUSCLE_DataConstRef_create_grid_logical_a_(bool * data_array, s std::intptr_t LIBMUSCLE_DataConstRef_create_grid_int4_a_(int32_t * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); DataConstRef * result = new DataConstRef(DataConstRef::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -99,7 +99,7 @@ std::intptr_t LIBMUSCLE_DataConstRef_create_grid_int4_a_(int32_t * data_array, s std::intptr_t LIBMUSCLE_DataConstRef_create_grid_int8_a_(int64_t * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); DataConstRef * result = new DataConstRef(DataConstRef::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -107,7 +107,7 @@ std::intptr_t LIBMUSCLE_DataConstRef_create_grid_int8_a_(int64_t * data_array, s std::intptr_t LIBMUSCLE_DataConstRef_create_grid_real4_a_(float * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); DataConstRef * result = new DataConstRef(DataConstRef::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -115,7 +115,7 @@ std::intptr_t LIBMUSCLE_DataConstRef_create_grid_real4_a_(float * data_array, st std::intptr_t LIBMUSCLE_DataConstRef_create_grid_real8_a_(double * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); DataConstRef * result = new DataConstRef(DataConstRef::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -134,7 +134,7 @@ std::intptr_t LIBMUSCLE_DataConstRef_create_grid_logical_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -171,7 +171,7 @@ std::intptr_t LIBMUSCLE_DataConstRef_create_grid_int4_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -208,7 +208,7 @@ std::intptr_t LIBMUSCLE_DataConstRef_create_grid_int8_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -245,7 +245,7 @@ std::intptr_t LIBMUSCLE_DataConstRef_create_grid_real4_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -282,7 +282,7 @@ std::intptr_t LIBMUSCLE_DataConstRef_create_grid_real8_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -1333,7 +1333,7 @@ std::intptr_t LIBMUSCLE_Data_create_copy_(std::intptr_t value) { std::intptr_t LIBMUSCLE_Data_create_grid_logical_a_(bool * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); Data * result = new Data(Data::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -1341,7 +1341,7 @@ std::intptr_t LIBMUSCLE_Data_create_grid_logical_a_(bool * data_array, std::size std::intptr_t LIBMUSCLE_Data_create_grid_int4_a_(int32_t * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); Data * result = new Data(Data::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -1349,7 +1349,7 @@ std::intptr_t LIBMUSCLE_Data_create_grid_int4_a_(int32_t * data_array, std::size std::intptr_t LIBMUSCLE_Data_create_grid_int8_a_(int64_t * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); Data * result = new Data(Data::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -1357,7 +1357,7 @@ std::intptr_t LIBMUSCLE_Data_create_grid_int8_a_(int64_t * data_array, std::size std::intptr_t LIBMUSCLE_Data_create_grid_real4_a_(float * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); Data * result = new Data(Data::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -1365,7 +1365,7 @@ std::intptr_t LIBMUSCLE_Data_create_grid_real4_a_(float * data_array, std::size_ std::intptr_t LIBMUSCLE_Data_create_grid_real8_a_(double * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); Data * result = new Data(Data::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -1384,7 +1384,7 @@ std::intptr_t LIBMUSCLE_Data_create_grid_logical_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -1421,7 +1421,7 @@ std::intptr_t LIBMUSCLE_Data_create_grid_int4_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -1458,7 +1458,7 @@ std::intptr_t LIBMUSCLE_Data_create_grid_int8_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -1495,7 +1495,7 @@ std::intptr_t LIBMUSCLE_Data_create_grid_real4_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -1532,7 +1532,7 @@ std::intptr_t LIBMUSCLE_Data_create_grid_real8_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); diff --git a/libmuscle/cpp/src/libmuscle/bindings/libmuscle_mpi_fortran_c.cpp b/libmuscle/cpp/src/libmuscle/bindings/libmuscle_mpi_fortran_c.cpp index 7fde7582..7d5f01e8 100644 --- a/libmuscle/cpp/src/libmuscle/bindings/libmuscle_mpi_fortran_c.cpp +++ b/libmuscle/cpp/src/libmuscle/bindings/libmuscle_mpi_fortran_c.cpp @@ -83,7 +83,7 @@ std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_copy_(std::intptr_t value) { std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_grid_logical_a_(bool * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); DataConstRef * result = new DataConstRef(DataConstRef::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -91,7 +91,7 @@ std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_grid_logical_a_(bool * data_arra std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_grid_int4_a_(int32_t * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); DataConstRef * result = new DataConstRef(DataConstRef::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -99,7 +99,7 @@ std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_grid_int4_a_(int32_t * data_arra std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_grid_int8_a_(int64_t * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); DataConstRef * result = new DataConstRef(DataConstRef::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -107,7 +107,7 @@ std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_grid_int8_a_(int64_t * data_arra std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_grid_real4_a_(float * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); DataConstRef * result = new DataConstRef(DataConstRef::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -115,7 +115,7 @@ std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_grid_real4_a_(float * data_array std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_grid_real8_a_(double * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); DataConstRef * result = new DataConstRef(DataConstRef::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -134,7 +134,7 @@ std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_grid_logical_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -171,7 +171,7 @@ std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_grid_int4_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -208,7 +208,7 @@ std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_grid_int8_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -245,7 +245,7 @@ std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_grid_real4_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -282,7 +282,7 @@ std::intptr_t LIBMUSCLE_MPI_DataConstRef_create_grid_real8_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -1333,7 +1333,7 @@ std::intptr_t LIBMUSCLE_MPI_Data_create_copy_(std::intptr_t value) { std::intptr_t LIBMUSCLE_MPI_Data_create_grid_logical_a_(bool * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); Data * result = new Data(Data::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -1341,7 +1341,7 @@ std::intptr_t LIBMUSCLE_MPI_Data_create_grid_logical_a_(bool * data_array, std:: std::intptr_t LIBMUSCLE_MPI_Data_create_grid_int4_a_(int32_t * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); Data * result = new Data(Data::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -1349,7 +1349,7 @@ std::intptr_t LIBMUSCLE_MPI_Data_create_grid_int4_a_(int32_t * data_array, std:: std::intptr_t LIBMUSCLE_MPI_Data_create_grid_int8_a_(int64_t * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); Data * result = new Data(Data::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -1357,7 +1357,7 @@ std::intptr_t LIBMUSCLE_MPI_Data_create_grid_int8_a_(int64_t * data_array, std:: std::intptr_t LIBMUSCLE_MPI_Data_create_grid_real4_a_(float * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); Data * result = new Data(Data::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -1365,7 +1365,7 @@ std::intptr_t LIBMUSCLE_MPI_Data_create_grid_real4_a_(float * data_array, std::s std::intptr_t LIBMUSCLE_MPI_Data_create_grid_real8_a_(double * data_array, std::size_t * data_array_shape, std::size_t data_array_ndims) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); Data * result = new Data(Data::grid(data_array_p, data_array_shape_v, {}, libmuscle::StorageOrder::first_adjacent)); return reinterpret_cast(result); } @@ -1384,7 +1384,7 @@ std::intptr_t LIBMUSCLE_MPI_Data_create_grid_logical_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -1421,7 +1421,7 @@ std::intptr_t LIBMUSCLE_MPI_Data_create_grid_int4_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -1458,7 +1458,7 @@ std::intptr_t LIBMUSCLE_MPI_Data_create_grid_int8_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -1495,7 +1495,7 @@ std::intptr_t LIBMUSCLE_MPI_Data_create_grid_real4_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); @@ -1532,7 +1532,7 @@ std::intptr_t LIBMUSCLE_MPI_Data_create_grid_real8_n_( ) { std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast(data_array); + auto data_array_p = const_cast(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); diff --git a/libmuscle/cpp/src/libmuscle/logger.cpp b/libmuscle/cpp/src/libmuscle/logger.cpp index c23b1a0a..5f1d9795 100644 --- a/libmuscle/cpp/src/libmuscle/logger.cpp +++ b/libmuscle/cpp/src/libmuscle/logger.cpp @@ -37,7 +37,7 @@ void Logger::set_local_level(LogLevel level) { local_level_ = level; } -void Logger::append_args_(std::ostringstream & s) {} +void Logger::append_args_(std::ostringstream &) {} } } diff --git a/libmuscle/cpp/src/libmuscle/mcp/transport_client.cpp b/libmuscle/cpp/src/libmuscle/mcp/transport_client.cpp index 3ffbd62b..bacc1c15 100644 --- a/libmuscle/cpp/src/libmuscle/mcp/transport_client.cpp +++ b/libmuscle/cpp/src/libmuscle/mcp/transport_client.cpp @@ -5,7 +5,7 @@ namespace libmuscle { namespace _MUSCLE_IMPL_NS { namespace mcp { -bool TransportClient::can_connect_to(std::string const & location) { +bool TransportClient::can_connect_to(std::string const &) { return false; } diff --git a/scripts/api_generator.py b/scripts/api_generator.py index dac141b9..3746ad96 100644 --- a/scripts/api_generator.py +++ b/scripts/api_generator.py @@ -755,7 +755,7 @@ def fc_convert_input(self) -> str: result = ( 'std::vector {0}_shape_v(\n' ' {0}_shape, {0}_shape + {0}_ndims);\n' - 'auto {0}_p = const_cast<{1} const * const>({0});\n' + 'auto {0}_p = const_cast<{1} const *>({0});\n' ).format( self.name, self.elem_type.fc_cpp_type()) diff --git a/scripts/make_libmuscle_api.py b/scripts/make_libmuscle_api.py index 9cf47d34..e49ead74 100755 --- a/scripts/make_libmuscle_api.py +++ b/scripts/make_libmuscle_api.py @@ -81,7 +81,7 @@ def __init__(self, with_names: bool) -> None: ) {{ std::vector data_array_shape_v( data_array_shape, data_array_shape + data_array_ndims); - auto data_array_p = const_cast<{1} const * const>(data_array); + auto data_array_p = const_cast<{1} const *>(data_array); std::vector names_v; names_v.emplace_back(index_name_1, index_name_1_size); From f862fbef51dfff9c704cdca0612c995943767163 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Wed, 27 Sep 2023 20:38:07 +0200 Subject: [PATCH 21/36] Fix race condition in C++ Profiler initialisation --- libmuscle/cpp/src/libmuscle/profiler.cpp | 2 +- libmuscle/cpp/src/libmuscle/profiler.hpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libmuscle/cpp/src/libmuscle/profiler.cpp b/libmuscle/cpp/src/libmuscle/profiler.cpp index 0ced4c19..5869bf62 100644 --- a/libmuscle/cpp/src/libmuscle/profiler.cpp +++ b/libmuscle/cpp/src/libmuscle/profiler.cpp @@ -31,8 +31,8 @@ Profiler::Profiler(MMPClient & manager) : manager_(manager) , enabled_(true) , events_() - , thread_(communicate_, this) , done_(false) + , thread_(communicate_, this) {} Profiler::~Profiler() { diff --git a/libmuscle/cpp/src/libmuscle/profiler.hpp b/libmuscle/cpp/src/libmuscle/profiler.hpp index 96570f65..618fb034 100644 --- a/libmuscle/cpp/src/libmuscle/profiler.hpp +++ b/libmuscle/cpp/src/libmuscle/profiler.hpp @@ -72,9 +72,9 @@ class Profiler { std::vector events_; std::chrono::steady_clock::time_point next_send_; - std::thread thread_; - std::condition_variable done_cv_; bool done_; + std::condition_variable done_cv_; + std::thread thread_; /* Background thread that ensures regular communication. * From 531c90a530dd1725eded664a55cfac9bc4b62ef8 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Wed, 27 Sep 2023 21:47:04 +0200 Subject: [PATCH 22/36] Remove unused constant from the gRPC days --- libmuscle/cpp/src/libmuscle/mmp_client.cpp | 1 - libmuscle/python/libmuscle/mmp_client.py | 1 - libmuscle/python/libmuscle/test/test_mmp_client.py | 9 ++++----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/libmuscle/cpp/src/libmuscle/mmp_client.cpp b/libmuscle/cpp/src/libmuscle/mmp_client.cpp index aa2d17f4..b5a99fe1 100644 --- a/libmuscle/cpp/src/libmuscle/mmp_client.cpp +++ b/libmuscle/cpp/src/libmuscle/mmp_client.cpp @@ -33,7 +33,6 @@ using ymmsl::SettingValue; namespace { - const float connection_timeout = 300.0f; const std::chrono::milliseconds peer_timeout(600000); const int peer_interval_min = 5000; // milliseconds const int peer_interval_max = 10000; // milliseconds diff --git a/libmuscle/python/libmuscle/mmp_client.py b/libmuscle/python/libmuscle/mmp_client.py index 2298fadd..34238a87 100644 --- a/libmuscle/python/libmuscle/mmp_client.py +++ b/libmuscle/python/libmuscle/mmp_client.py @@ -18,7 +18,6 @@ from libmuscle.snapshot import SnapshotMetadata -CONNECTION_TIMEOUT = 300 PEER_TIMEOUT = 600 PEER_INTERVAL_MIN = 5.0 PEER_INTERVAL_MAX = 10.0 diff --git a/libmuscle/python/libmuscle/test/test_mmp_client.py b/libmuscle/python/libmuscle/test/test_mmp_client.py index 138efa0a..721e26bd 100644 --- a/libmuscle/python/libmuscle/test/test_mmp_client.py +++ b/libmuscle/python/libmuscle/test/test_mmp_client.py @@ -18,11 +18,10 @@ def test_init() -> None: def test_connection_fail() -> None: - with patch('libmuscle.mmp_client.CONNECTION_TIMEOUT', 1): - with pytest.raises(RuntimeError): - # Port 255 is reserved and privileged, so there's probably - # nothing there. - MMPClient(Reference([]), 'tcp:localhost:255') + with pytest.raises(RuntimeError): + # Port 255 is reserved and privileged, so there's probably + # nothing there. + MMPClient(Reference([]), 'tcp:localhost:255') def test_submit_log_message(mocked_mmp_client, profile_data) -> None: From 17334616148817840f0be4d51c27f337eab39b62 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Wed, 27 Sep 2023 21:50:34 +0200 Subject: [PATCH 23/36] Remove obsolete checkpointing API warning --- libmuscle/python/libmuscle/instance.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/libmuscle/python/libmuscle/instance.py b/libmuscle/python/libmuscle/instance.py index 7b4d2f71..de9589ee 100644 --- a/libmuscle/python/libmuscle/instance.py +++ b/libmuscle/python/libmuscle/instance.py @@ -6,7 +6,6 @@ from typing import cast, Dict, List, Optional, Tuple, overload # TODO: import from typing module when dropping support for python 3.7 from typing_extensions import Literal -import warnings from ymmsl import (Identifier, Operator, SettingValue, Port, Reference, Settings) @@ -116,11 +115,6 @@ def __init__( self.__is_shut_down = False self._flags = InstanceFlags(flags) - if InstanceFlags.USES_CHECKPOINT_API in self._flags: - warnings.warn( - 'Checkpointing in MUSCLE3 version 0.6.0 is still in' - ' development: the API may change in a future MUSCLE3' - ' release.') # Note that these are accessed by Muscle3, but otherwise private. self._name, self._index = self.__make_full_name() From 1bb85f3c9a5c4d576ed8c2c790284118b98a87e3 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Wed, 27 Sep 2023 21:58:26 +0200 Subject: [PATCH 24/36] Add Python 3.11 support to package metadata --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5ded4c8d..a997e6ff 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,8 @@ 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10'], + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11'], packages=_muscle3_packages, package_dir={ From b68dd26e0d879e1a25f884f7dc96ab82229b0e94 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sat, 30 Sep 2023 18:56:24 +0200 Subject: [PATCH 25/36] Add SHUTDOWN_WAIT and SHUTDOWN profiling events and record them --- libmuscle/cpp/src/libmuscle/communicator.cpp | 4 ++-- libmuscle/cpp/src/libmuscle/instance.cpp | 5 +++++ libmuscle/cpp/src/libmuscle/profiling.hpp | 2 ++ libmuscle/python/libmuscle/communicator.py | 4 ++-- libmuscle/python/libmuscle/instance.py | 6 ++++++ .../python/libmuscle/manager/profile_database.py | 2 +- libmuscle/python/libmuscle/profiling.py | 2 ++ muscle3/profiling.py | 11 +++++++++-- 8 files changed, 29 insertions(+), 7 deletions(-) diff --git a/libmuscle/cpp/src/libmuscle/communicator.cpp b/libmuscle/cpp/src/libmuscle/communicator.cpp index 38827022..03907899 100644 --- a/libmuscle/cpp/src/libmuscle/communicator.cpp +++ b/libmuscle/cpp/src/libmuscle/communicator.cpp @@ -315,13 +315,13 @@ void Communicator::shutdown() { client.second->close(); ProfileEvent wait_event(ProfileEventType::disconnect_wait, ProfileTimestamp()); - post_office_.wait_for_receivers(); - profiler_.record_event(std::move(wait_event)); + ProfileEvent shutdown_event(ProfileEventType::shutdown, ProfileTimestamp()); for (auto & server : servers_) server->close(); + profiler_.record_event(std::move(shutdown_event)); } Communicator::PortMessageCounts Communicator::get_message_counts() { diff --git a/libmuscle/cpp/src/libmuscle/instance.cpp b/libmuscle/cpp/src/libmuscle/instance.cpp index 19de8f47..a3d4f5f5 100644 --- a/libmuscle/cpp/src/libmuscle/instance.cpp +++ b/libmuscle/cpp/src/libmuscle/instance.cpp @@ -849,11 +849,16 @@ bool Instance::Impl::have_f_init_connections_() { * @return true iff no ClosePort messages were received. */ bool Instance::Impl::pre_receive_() { + ProfileEvent sw_event(ProfileEventType::shutdown_wait, ProfileTimestamp()); + bool all_ports_open = receive_settings_(); pre_receive_f_init_(); for (auto const & ref_msg : f_init_cache_) if (is_close_port(ref_msg.second.data())) all_ports_open = false; + + if (!all_ports_open) + profiler_->record_event(std::move(sw_event)); return all_ports_open; } diff --git a/libmuscle/cpp/src/libmuscle/profiling.hpp b/libmuscle/cpp/src/libmuscle/profiling.hpp index f8552d32..91a7f6df 100644 --- a/libmuscle/cpp/src/libmuscle/profiling.hpp +++ b/libmuscle/cpp/src/libmuscle/profiling.hpp @@ -25,7 +25,9 @@ enum class ProfileEventType { receive_wait = 5, receive_transfer = 6, receive_decode = 7, + shutdown_wait = 9, disconnect_wait = 8, + shutdown = 10, deregister = 1 }; diff --git a/libmuscle/python/libmuscle/communicator.py b/libmuscle/python/libmuscle/communicator.py index 88604002..4ca3c436 100644 --- a/libmuscle/python/libmuscle/communicator.py +++ b/libmuscle/python/libmuscle/communicator.py @@ -414,13 +414,13 @@ def shutdown(self) -> None: client.close() wait_event = ProfileEvent(ProfileEventType.DISCONNECT_WAIT, ProfileTimestamp()) - self._post_office.wait_for_receivers() - self._profiler.record_event(wait_event) + shutdown_event = ProfileEvent(ProfileEventType.SHUTDOWN, ProfileTimestamp()) for server in self._servers: server.close() + self._profiler.record_event(shutdown_event) def restore_message_counts(self, port_message_counts: Dict[str, List[int]] ) -> None: diff --git a/libmuscle/python/libmuscle/instance.py b/libmuscle/python/libmuscle/instance.py index de9589ee..f4aa85fb 100644 --- a/libmuscle/python/libmuscle/instance.py +++ b/libmuscle/python/libmuscle/instance.py @@ -1005,11 +1005,17 @@ def _pre_receive(self) -> bool: Returns: True iff no ClosePort messages were received. """ + sw_event = ProfileEvent(ProfileEventType.SHUTDOWN_WAIT, ProfileTimestamp()) + all_ports_open = self.__receive_settings() self.__pre_receive_f_init() for message in self._f_init_cache.values(): if isinstance(message.data, ClosePort): all_ports_open = False + + if not all_ports_open: + self._profiler.record_event(sw_event) + return all_ports_open def __receive_settings(self) -> bool: diff --git a/libmuscle/python/libmuscle/manager/profile_database.py b/libmuscle/python/libmuscle/manager/profile_database.py index d82df918..5c2d67d5 100644 --- a/libmuscle/python/libmuscle/manager/profile_database.py +++ b/libmuscle/python/libmuscle/manager/profile_database.py @@ -103,7 +103,7 @@ def instance_stats( cur.execute( "SELECT instance, start_time" " FROM all_events" - " WHERE type = 'DISCONNECT_WAIT'") + " WHERE type = 'SHUTDOWN_WAIT'") stop_run = dict(cur.fetchall()) if not stop_run: diff --git a/libmuscle/python/libmuscle/profiling.py b/libmuscle/python/libmuscle/profiling.py index fd06bb19..d3c1f9a5 100644 --- a/libmuscle/python/libmuscle/profiling.py +++ b/libmuscle/python/libmuscle/profiling.py @@ -14,7 +14,9 @@ class ProfileEventType(Enum): RECEIVE_WAIT = 5 RECEIVE_TRANSFER = 6 RECEIVE_DECODE = 7 + SHUTDOWN_WAIT = 9 DISCONNECT_WAIT = 8 + SHUTDOWN = 10 DEREGISTER = 1 diff --git a/muscle3/profiling.py b/muscle3/profiling.py index 03065c96..87c88242 100644 --- a/muscle3/profiling.py +++ b/muscle3/profiling.py @@ -97,14 +97,16 @@ def plot_resources(performance_file: Path) -> None: _EVENT_TYPES = ( - 'REGISTER', 'CONNECT', 'DISCONNECT_WAIT', 'DEREGISTER', - 'SEND', 'RECEIVE_WAIT', 'RECEIVE_TRANSFER', 'RECEIVE_DECODE') + 'REGISTER', 'CONNECT', 'SHUTDOWN_WAIT', 'DISCONNECT_WAIT', 'SHUTDOWN', + 'DEREGISTER', 'SEND', 'RECEIVE_WAIT', 'RECEIVE_TRANSFER', 'RECEIVE_DECODE') _EVENT_PALETTE = { 'REGISTER': '#910f33', 'CONNECT': '#c85172', + 'SHUTDOWN_WAIT': '#ffdddd', 'DISCONNECT_WAIT': '#eedddd', + 'SHUTDOWN': '#c85172', 'DEREGISTER': '#910f33', 'RECEIVE_WAIT': '#cccccc', 'RECEIVE_TRANSFER': '#ff7d00', @@ -172,6 +174,11 @@ def __init__(self, performance_file: Path) -> None: instances = sorted(begin_times.keys()) self._instances = instances + if not begin_times: + raise RuntimeError( + 'This database appears to be empty. Did the simulation crash' + ' before any data were generated?') + # Rest of plot ax.set_title('Execution timeline') ax.set_xlabel('Wallclock time (s)') From 39be340eab492dce435015b2d35db437e9aae38c Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sat, 30 Sep 2023 18:57:01 +0200 Subject: [PATCH 26/36] Add C++ dispatch example --- docs/source/examples/.gitignore | 1 + docs/source/examples/Makefile | 4 ++- docs/source/examples/cpp/buffer.cpp | 31 +++++++++++++++++ docs/source/examples/cpp/build/Makefile | 4 ++- docs/source/examples/dispatch_cpp.ymmsl | 34 +++++++++++++++++++ .../dispatch_implementations.ymmsl.in | 8 +++++ 6 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 docs/source/examples/cpp/buffer.cpp create mode 100644 docs/source/examples/dispatch_cpp.ymmsl create mode 100644 docs/source/examples/dispatch_implementations.ymmsl.in diff --git a/docs/source/examples/.gitignore b/docs/source/examples/.gitignore index 5d3ee3e1..18937208 100644 --- a/docs/source/examples/.gitignore +++ b/docs/source/examples/.gitignore @@ -1,3 +1,4 @@ rd_implementations.ymmsl benchmark_implementations.ymmsl +dispatch_implementations.ymmsl run_* diff --git a/docs/source/examples/Makefile b/docs/source/examples/Makefile index d308b42b..1f948a6e 100644 --- a/docs/source/examples/Makefile +++ b/docs/source/examples/Makefile @@ -69,7 +69,7 @@ endif .PHONY: base -base: python rd_implementations.ymmsl benchmark_implementations.ymmsl +base: python rd_implementations.ymmsl benchmark_implementations.ymmsl dispatch_implementations.ymmsl @@ -106,6 +106,7 @@ clean: $(MAKE) -C python clean rm -f rd_implementations.ymmsl rm -f benchmark_implementations.ymmsl + rm -f dispatch_implementations.ymmsl rm -rf run_*/ @@ -129,6 +130,7 @@ test_cpp: base cpp $$(ls $$(ls -d run_checkpointing_reaction_diffusion_cpp* | tail -n1)/snapshots/*.ymmsl | head -n1) . python/build/venv/bin/activate && muscle_manager --start-all rd_implementations.ymmsl rd_python_cpp.ymmsl rd_settings.ymmsl . python/build/venv/bin/activate && muscle_manager --start-all rd_implementations.ymmsl rdmc_cpp.ymmsl rdmc_settings.ymmsl + . python/build/venv/bin/activate && muscle_manager --start-all dispatch_implementations.ymmsl dispatch_cpp.ymmsl .PHONY: test_cpp_mpi test_cpp_mpi: base cpp_mpi diff --git a/docs/source/examples/cpp/buffer.cpp b/docs/source/examples/cpp/buffer.cpp new file mode 100644 index 00000000..8cd2d4da --- /dev/null +++ b/docs/source/examples/cpp/buffer.cpp @@ -0,0 +1,31 @@ +#include "libmuscle/libmuscle.hpp" +#include "ymmsl/ymmsl.hpp" + +#include "unistd.h" + + +using libmuscle::Data; +using libmuscle::Instance; +using libmuscle::Message; +using ymmsl::Operator; + + +int main(int argc, char * argv[]) { + Instance instance(argc, argv, { + {Operator::F_INIT, {"in"}}, + {Operator::O_F, {"out"}}}); + + while (instance.reuse_instance()) { + // F_INIT + Message msg = instance.receive("in", Message(0.0, Data("Testing"))); + + // S + usleep(250000); + + // O_F + instance.send("out", msg); + } + + return 0; +} + diff --git a/docs/source/examples/cpp/build/Makefile b/docs/source/examples/cpp/build/Makefile index e6fd6332..a52fd13f 100644 --- a/docs/source/examples/cpp/build/Makefile +++ b/docs/source/examples/cpp/build/Makefile @@ -5,7 +5,9 @@ MPI_CXXFLAGS := -std=c++14 -g $(shell pkg-config --cflags libmuscle_mpi ymmsl) MPI_LDFLAGS := $(shell pkg-config --libs libmuscle_mpi ymmsl) -binaries := reaction diffusion mc_driver load_balancer checkpointing_reaction checkpointing_diffusion benchmark +binaries := reaction diffusion mc_driver load_balancer +binaries += checkpointing_reaction checkpointing_diffusion +binaries += benchmark buffer mpi_binaries := reaction_mpi diff --git a/docs/source/examples/dispatch_cpp.ymmsl b/docs/source/examples/dispatch_cpp.ymmsl new file mode 100644 index 00000000..304de8c8 --- /dev/null +++ b/docs/source/examples/dispatch_cpp.ymmsl @@ -0,0 +1,34 @@ +ymmsl_version: v0.1 + +model: + name: dispatch_cpp + + components: + component1: + implementation: buffer_cpp + ports: + o_f: out + + component2: + implementation: buffer_cpp + ports: + f_init: in + o_f: out + + component3: + implementation: buffer_cpp + ports: + f_init: in + + conduits: + component1.out: component2.in + component2.out: component3.in + +resources: + component1: + threads: 1 + component2: + threads: 1 + component3: + threads: 1 + diff --git a/docs/source/examples/dispatch_implementations.ymmsl.in b/docs/source/examples/dispatch_implementations.ymmsl.in new file mode 100644 index 00000000..553db472 --- /dev/null +++ b/docs/source/examples/dispatch_implementations.ymmsl.in @@ -0,0 +1,8 @@ +ymmsl_version: v0.1 + +implementations: + buffer_cpp: + env: + +LD_LIBRARY_PATH: :MUSCLE3_HOME/lib + executable: MUSCLE3_EXAMPLES/cpp/build/buffer + From b929e4e3f9f66788e8187ed53566ba226eb6f8b3 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sat, 30 Sep 2023 21:26:46 +0200 Subject: [PATCH 27/36] Detect crashed component more accurately --- .../libmuscle/manager/instance_manager.py | 126 +++++++++++++----- 1 file changed, 90 insertions(+), 36 deletions(-) diff --git a/libmuscle/python/libmuscle/manager/instance_manager.py b/libmuscle/python/libmuscle/manager/instance_manager.py index 2dffec06..7b619db0 100644 --- a/libmuscle/python/libmuscle/manager/instance_manager.py +++ b/libmuscle/python/libmuscle/manager/instance_manager.py @@ -1,7 +1,8 @@ import logging +from pathlib import Path from textwrap import indent from threading import Thread -from typing import Dict, Optional, Union +from typing import Dict, List, Optional, Tuple, Union from multiprocessing import Queue import queue @@ -156,6 +157,9 @@ def cancel_all() -> None: self._requests_out.put(CancelAllRequest()) all_seemingly_okay = False + # Get all results + results: List[Process] = list() + while self._num_running > 0: result = self._results_in.get() @@ -165,48 +169,98 @@ def cancel_all() -> None: ' a bug report.') return False - if result.exit_code != 0: - if result.status == ProcessStatus.CANCELED: - _logger.info( - f'Instance {result.instance} was shut down by' - f' MUSCLE3 because an error occurred elsewhere') - else: - _logger.error( - f'Instance {result.instance} quit with error' - f' {result.exit_code}') - - stderr_file = ( - self._run_dir.instance_dir(result.instance) / - 'stderr.txt') - _logger.error( - 'The last error output of this instance was:') - _logger.error( - '\n' + indent(last_lines(stderr_file, 20), ' ')) - _logger.error( - 'More output may be found in' - f' {self._run_dir.instance_dir(result.instance)}\n' - ) + results.append(result) + if result.status != ProcessStatus.CANCELED: + registered = self._instance_registry.did_register(result.instance) + if result.exit_code != 0 or not registered: cancel_all() + self._num_running -= 1 - elif not self._instance_registry.did_register(result.instance): - _logger.error( - f'Instance {result.instance} quit with no error' - ' (exit code 0), but it never registered with the' - ' manager. Maybe it never created an Instance' - ' object?') - cancel_all() - else: - if result.status == ProcessStatus.CANCELED: + # Summarise outcome + crashes: List[Tuple[Process, Path]] = list() + indirect_crashes: List[Tuple[Process, Path]] = list() + + for result in results: + if result.status == ProcessStatus.CANCELED: + if result.exit_code == 0: _logger.info( f'Instance {result.instance} was not started' f' because of an error elsewhere') else: - _logger.debug(f'Instance {result.instance} finished') - _logger.debug(f'States: {result.status}') - _logger.debug(f'Exit code: {result.exit_code}') - _logger.debug(f'Error msg: {result.error_msg}') + _logger.info( + f'Instance {result.instance} was shut down by' + f' MUSCLE3 because an error occurred elsewhere') + else: + stderr_file = ( + self._run_dir.instance_dir(result.instance) / + 'stderr.txt') + if result.exit_code == 0: + if self._instance_registry.did_register(result.instance): + _logger.info( + f'Instance {result.instance} finished with' + ' exit code 0') + else: + _logger.error( + f'Instance {result.instance} quit with no error' + ' (exit code 0), but it never registered with the' + ' manager. Maybe it never created an Instance' + ' object?') + crashes.append((result, stderr_file)) + else: + with stderr_file.open() as f: + peer_crash = any(['peer crash?' in line for line in f]) + + if peer_crash: + _logger.warning( + f'Instance {result.instance} crashed, likely because' + f' an error occurred elsewhere.') + indirect_crashes.append((result, stderr_file)) + else: + _logger.error( + f'Instance {result.instance} quit with exit code' + f' {result.exit_code}') + crashes.append((result, stderr_file)) + + _logger.debug(f'Status: {result.status}') + _logger.debug(f'Exit code: {result.exit_code}') + _logger.debug(f'Error msg: {result.error_msg}') + + # Show errors from crashed components + if crashes: + for result, stderr_file in crashes: + _logger.error( + f'The last error output of {result.instance} was:') + _logger.error( + '\n' + indent(last_lines(stderr_file, 20), ' ')) + _logger.error( + 'More output may be found in' + f' {self._run_dir.instance_dir(result.instance)}\n' + ) + else: + # Possibly a component exited without error, but prematurely. If this + # caused ancillary crashes due to dropped connections, then the logs + # of those will give a hint as to what the problem may be, so print + # those instead. + _logger.error( + 'At this point, one or more instances crashed because they' + ' lost their connection to another instance, but no other' + ' crashing instance was found that could have caused this.') + _logger.error( + 'This means that either another instance quit before it was' + ' supposed to, but with exit code 0, or there was an actual' + ' network problem that caused the connection to drop.') + _logger.error( + 'Here is the output of the instances that lost connection:') + for result, stderr_file in indirect_crashes: + _logger.error( + f'The last error output of {result.instance} was:') + _logger.error( + '\n' + indent(last_lines(stderr_file, 20), ' ')) + _logger.error( + 'More output may be found in' + f' {self._run_dir.instance_dir(result.instance)}\n' + ) - self._num_running -= 1 return all_seemingly_okay def shutdown(self) -> None: From 6b904de3e8eb1793a4543fed2fa67d63cc38ec7a Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sat, 30 Sep 2023 21:47:29 +0200 Subject: [PATCH 28/36] Print output dir on successful run --- muscle3/muscle_manager.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/muscle3/muscle_manager.py b/muscle3/muscle_manager.py index 8463bc27..746be301 100644 --- a/muscle3/muscle_manager.py +++ b/muscle3/muscle_manager.py @@ -151,6 +151,11 @@ def manage_simulation( print() else: print('Simulation completed successfully.') + try: + rel_run_dir = run_dir_path.relative_to(Path.cwd()) + print(f'Output may be found in {rel_run_dir}') + except ValueError: + print(f'Output may be found in {run_dir_path}') sys.exit(0 if success else 1) From d6a14336938158163726d8dcd30843515c2332a6 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sat, 30 Sep 2023 21:47:53 +0200 Subject: [PATCH 29/36] Print a bit more manager log in case of a crash --- muscle3/muscle_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/muscle3/muscle_manager.py b/muscle3/muscle_manager.py index 746be301..e7453f2e 100644 --- a/muscle3/muscle_manager.py +++ b/muscle3/muscle_manager.py @@ -143,7 +143,7 @@ def manage_simulation( print('Here are the final lines of the manager log:') print() print('-' * 80) - print(last_lines(log_file, 30), ' ') + print(last_lines(log_file, 50), ' ') print('-' * 80) print() print('You can find the full log at') From 039e8d64c1b4e8cf05a38d5e625f362fa7832be0 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sun, 1 Oct 2023 12:33:24 +0200 Subject: [PATCH 30/36] Make order of events more intuitive in profiling timeline plot --- muscle3/profiling.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/muscle3/profiling.py b/muscle3/profiling.py index 87c88242..b705a8c5 100644 --- a/muscle3/profiling.py +++ b/muscle3/profiling.py @@ -184,7 +184,7 @@ def __init__(self, performance_file: Path) -> None: ax.set_xlabel('Wallclock time (s)') # Background - ax.barh( + running_artist = ax.barh( instances, [end_times[i] - begin_times[i] for i in instances], _BAR_WIDTH, @@ -230,7 +230,13 @@ def __init__(self, performance_file: Path) -> None: ax.set_autoscale_on(True) ax.callbacks.connect('xlim_changed', self.update_data) - ax.legend(loc='upper right') + ordered_artists = [self._bars[event_type] for event_type in _EVENT_TYPES] + ordered_names = list(_EVENT_TYPES) + + ordered_artists.insert(6, running_artist) + ordered_names.insert(6, 'RUNNING') + + ax.legend(ordered_artists, ordered_names, loc='upper right') ax.figure.canvas.draw_idle() def close(self) -> None: From d9edc33c4ac425845f05bbab0637c6e010683a1c Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sun, 1 Oct 2023 13:03:07 +0200 Subject: [PATCH 31/36] Widen timeout in test so we have fewer CI failures --- .../cpp/src/libmuscle/tests/test_profiler.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/libmuscle/cpp/src/libmuscle/tests/test_profiler.cpp b/libmuscle/cpp/src/libmuscle/tests/test_profiler.cpp index 64c56cbe..1824c653 100644 --- a/libmuscle/cpp/src/libmuscle/tests/test_profiler.cpp +++ b/libmuscle/cpp/src/libmuscle/tests/test_profiler.cpp @@ -29,6 +29,7 @@ std::chrono::steady_clock::duration communication_interval_() { #include #include +#include #include #include #include @@ -211,7 +212,17 @@ TEST(libmuscle_profiler, test_send_to_mock_mmp_client) { TEST(libmuscle_profiler, test_send_timeout) { reset_mocks(); - communication_interval = 40ms; + + std::chrono::steady_clock::duration wait_time; + + if (getenv("CI")) { + communication_interval = 40ms; + wait_time = 500ms; + } + else { + communication_interval = 40ms; + wait_time = 60ms; + } MockMMPClient mock_mmp_client(Reference("test_instance"), ""); Profiler profiler(mock_mmp_client); @@ -220,7 +231,7 @@ TEST(libmuscle_profiler, test_send_timeout) { ProfileEventType::receive, ProfileTimestamp(), ProfileTimestamp()); profiler.record_event(ProfileEvent(e1)); - std::this_thread::sleep_for(50ms); + std::this_thread::sleep_for(wait_time); ASSERT_EQ(mock_mmp_client.last_submitted_profile_events.size(), 1u); ASSERT_EQ(mock_mmp_client.last_submitted_profile_events.at(0), e1); From 9e2e72b63a391aa51222549e4e51930a5f874678 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sun, 1 Oct 2023 14:16:15 +0200 Subject: [PATCH 32/36] Update profiling documentation to reflect recent changes --- docs/source/profiling.rst | 77 ++++++++++++------- .../libmuscle/manager/profile_database.py | 9 ++- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/docs/source/profiling.rst b/docs/source/profiling.rst index 6e7b678a..a79f288e 100644 --- a/docs/source/profiling.rst +++ b/docs/source/profiling.rst @@ -62,11 +62,11 @@ are described here. Plotting statistics from the command line ----------------------------------------- -The most simplest way of examining performance data gathered by MUSCLE3 is -through the ``muscle3 profile`` command from the shell. If you have done a run, -then you should have a run directory containing a ``performance.sqlite`` file. -If you have MUSCLE3 available in your environment (only the Python installation -is needed) then you have the ``muscle3 profile`` command available to show +The simplest way of examining performance data gathered by MUSCLE3 is through +the ``muscle3 profile`` command from the shell. If you have done a run, then +you should have a run directory containing a ``performance.sqlite`` file. If +you have MUSCLE3 available in your environment (only the Python installation is +needed) then you have the ``muscle3 profile`` command available to show per-instance and per-core statistics as well as a timeline of events. Per-instance time spent @@ -80,10 +80,10 @@ Per-instance time spent muscle3 profile --instances /path/to/performance.sqlite -With ``--instances``, the plot will show for each instance how much time it -spent in total on computing, communicating and waiting. This plot gives an idea -of where most of the computing is done, and which components you need to -optimise to get an answer sooner. +With ``--instances`` or ``-i``, the plot will show for each instance how much +time it spent in total on computing, communicating and waiting. This plot gives +an idea of where most of the computing is done, and which components you need +to optimise to get an answer sooner. In many models, you will find that there's one component that takes up most of the compute time, and others that spend most of their time waiting and then do a @@ -128,9 +128,10 @@ Resource usage If you are running on a large computer, then it may be interesting to see how you are using the resources allocated to you. The command ``muscle3 profile --resources performance.sqlite`` will produce a plot showing for each core how -much time it spent running the various instances. This gives an idea of which -component used the most resources, and tells you what you should optimise if -you're trying to reduce the number of core hours spent. +much time it spent running the various instances (``-r`` for short also works). +This gives an idea of which component used the most resources, and tells you +what you should optimise if you're trying to reduce the number of core hours +spent. The total time shown per core doesn't necessarily match the total run time, as cores may be idle during the simulation. This can happen for example if @@ -150,14 +151,11 @@ Event timeline muscle3 profile --timeline /path/to/performance.sqlite -If you really want to get into the details, ``--timeline`` shows a timeline of -profiling events. This visualises the raw data from the database, showing -exactly when each instance sent and received data, when it was waiting for -input, and when it computed. The meaning of the event types shown is as follows: - -RUNNING - The instance was running, meaning that it was actively computing or doing - non-MUSCLE3 communication. +If you really want to get into the details, ``--timeline`` or ``-t`` shows a +timeline of profiling events. This visualises the raw data from the database, +showing exactly when each instance sent and received data, when it was waiting +for input, and when it computed. The meaning of the event types shown is as +follows: REGISTER The instance contacted the manager to share its location on the network, so @@ -167,6 +165,22 @@ CONNECT The instance asked the manager who to communicate with, and set up connections to these other instances. +RUNNING + The instance was running, meaning that it was actively computing or doing + non-MUSCLE3 communication. + +SHUTDOWN_WAIT + The instance was waiting to receive the information it needed to determine + that it should shut down, rather than run the reuse loop again. + +DISCONNECT_WAIT + The instance was waiting for the instances it communicates with to + acknowledge that it would be shutting down. This may take a while if those + other instances are busy doing calculations or talking to someone else. + +SHUTDOWN + The instance was shutting down its MUSCLE3 communications. + DEREGISTER The instance contacted the manager to say that it was ending it run. @@ -204,7 +218,7 @@ Analysis with Python If you want to get quantitative data, or just want to make your own plots, then you can use MUSCLE3's Python API. It contains several useful functions for extracting information and statistics from a profiling database. They are -collected in the :py:class:`libmuscle.ProfileDatabasa` class. +collected in the :py:class:`libmuscle.ProfileDatabase` class. Per-instance statistics ``````````````````````` @@ -420,12 +434,21 @@ Database format version +----------------+-------------------+ This table stores a single row containing the version of the database format -used in this file. The current version is 1.0. This uses semantic versioning, so -incompatible future formats will have a higher major version. Compatible -changes, including addition of columns to existing tables, will increment the -minor version number. Note that this means that ``SELECT * FROM ...`` may give a -different result for different minor versions. If that's not acceptable, specify -the columns you want explicitly. +used in this file. This uses semantic versioning, so incompatible future formats +will have a higher major version. Compatible changes, including addition of +columns to existing tables, will increment the minor version number. Note that +this means that ``SELECT * FROM ...`` may give a different result for different +minor versions. To make your code compatible with future minor versions, it's a +good idea to specify the columns you want explicitly. + +Here is a brief version history: + +Version 1.0 + Initial release. + +Version 1.1 + Added new ``SHUTDOWN_WAIT``, ``DISCONNECT_WAIT`` and ``SHUTDOWN`` events. + No changes to the tables. Formatted events ```````````````` diff --git a/libmuscle/python/libmuscle/manager/profile_database.py b/libmuscle/python/libmuscle/manager/profile_database.py index 5c2d67d5..a1c7a535 100644 --- a/libmuscle/python/libmuscle/manager/profile_database.py +++ b/libmuscle/python/libmuscle/manager/profile_database.py @@ -289,10 +289,11 @@ def time_taken( Args: etype: Type of event to get the starting point from. - Possible values: `'REGISTER'`, `'CONNECT'`, - `'DEREGISTER'`, `'SEND'`, `'RECEIVE'`, `'RECEIVE_WAIT'`, - `'RECEIVE_TRANSFER'`, `'RECEIVE_DECODE'`. See the - documentation for a description of each. + Possible values: `'REGISTER'`, `'CONNECT'`, `'SHUTDOWN_WAIT'`, + `'DISCONNECT_WAIT'`, `'SHUTDOWN'`, `'DEREGISTER'`, `'SEND'`, + `'RECEIVE'`, `'RECEIVE_WAIT'`, `'RECEIVE_TRANSFER'`, + `'RECEIVE_DECODE'`. See the documentation for a description + of each. instance: Name of the instance to get the event from. You can use `%` as a wildcard matching anything. For example, `'macro[%'` will match all instances of the From 1a81aff56cadd657ccf6e3131ba2345bd39fd2f7 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sun, 1 Oct 2023 17:20:32 +0200 Subject: [PATCH 33/36] Add M3 version information to profile database version history --- docs/source/profiling.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/profiling.rst b/docs/source/profiling.rst index a79f288e..93bc35f3 100644 --- a/docs/source/profiling.rst +++ b/docs/source/profiling.rst @@ -443,10 +443,10 @@ good idea to specify the columns you want explicitly. Here is a brief version history: -Version 1.0 +Version 1.0 (written by MUSCLE3 0.7.0) Initial release. -Version 1.1 +Version 1.1 (written by MUSCLE3 0.7.1) Added new ``SHUTDOWN_WAIT``, ``DISCONNECT_WAIT`` and ``SHUTDOWN`` events. No changes to the tables. From d9bcbf402da38cb37eb5740529dadac7fdd5813b Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sun, 1 Oct 2023 18:00:04 +0200 Subject: [PATCH 34/36] Add version 0.7.1 to change log --- CHANGELOG.rst | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cc2b8393..a00c6907 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,34 @@ Change Log All notable changes to this project will be documented in this file. This project adheres to `Semantic Versioning `_. +0.7.1 +***** + +Added +----- + +* Support for Python 3.11 (working already, now official) +* Enabled type checking support for the libmuscle Python API + +Improved +-------- + +* Easier crash debugging due to improved root cause detection +* Fixed crash in profiling timeline plot +* Better performance of timeline plot +* Better visual quality of timeline plot +* Improved profiling of shutdown process +* Fixed crash in profiler for large simulations +* Fixed several (harmless) compiler warnings +* Small documentation rendering improvements + + +Thanks +------ + +* David for reporting many of these and submitting a fix too! + + 0.7.0 ***** From e6db2bc9da119c04b836393ddaca35203961174a Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sun, 1 Oct 2023 18:13:27 +0200 Subject: [PATCH 35/36] Set release version to 0.7.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index c0ab4272..39e898a4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.1-dev +0.7.1 From 75da84da2ab86337965db34155a76b849986f067 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sun, 1 Oct 2023 18:15:57 +0200 Subject: [PATCH 36/36] Update badges to point to master --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index b1f0c294..2b0f3e2c 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,11 @@ .. image:: https://github.com/multiscale/muscle3/raw/develop/docs/source/muscle3_logo_readme.png :alt: MUSCLE3 -.. image:: https://readthedocs.org/projects/muscle3/badge/?version=develop - :target: https://muscle3.readthedocs.io/en/develop/?badge=develop +.. image:: https://readthedocs.org/projects/muscle3/badge/?version=master + :target: https://muscle3.readthedocs.io/en/develop/?badge=master :alt: Documentation Build Status -.. image:: https://github.com/multiscale/muscle3/workflows/continuous_integration/badge.svg?branch=develop +.. image:: https://github.com/multiscale/muscle3/workflows/continuous_integration/badge.svg?branch=master :target: https://github.com/multiscale/muscle3/actions :alt: Build Status