From 38b44e0ef2f9eac3c9f1fe9f50f9028787a2d389 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Thu, 26 Sep 2024 14:46:59 +0100 Subject: [PATCH 01/12] Allow reading model metadata from local folder --- aeon/io/reader.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/aeon/io/reader.py b/aeon/io/reader.py index abb6b97e..fda0c8af 100644 --- a/aeon/io/reader.py +++ b/aeon/io/reader.py @@ -313,9 +313,12 @@ def read(self, file: Path) -> pd.DataFrame: """Reads data from the Harp-binarized tracking file.""" # Get config file from `file`, then bodyparts from config file. model_dir = Path(*Path(file.stem.replace("_", "/")).parent.parts[-4:]) - config_file_dir = Path(self._model_root) / model_dir + config_file_dir = file.parent / model_dir if not config_file_dir.exists(): - raise FileNotFoundError(f"Cannot find model dir {config_file_dir}") + config_file_dir = Path(self._model_root) / model_dir + if not config_file_dir.exists(): + raise FileNotFoundError(f"Cannot find model dir {config_file_dir}") + config_file = self.get_config_file(config_file_dir) identities = self.get_class_names(config_file) parts = self.get_bodyparts(config_file) From 028ffc571b00323354e78beb4d79ca25b79a59dc Mon Sep 17 00:00:00 2001 From: glopesdev Date: Thu, 26 Sep 2024 14:50:16 +0100 Subject: [PATCH 02/12] Avoid iterating over None --- aeon/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aeon/util.py b/aeon/util.py index 2251eaad..f3e91b7a 100644 --- a/aeon/util.py +++ b/aeon/util.py @@ -14,7 +14,7 @@ def find_nested_key(obj: dict | list, key: str) -> Any: found = find_nested_key(v, key) if found: return found - else: + elif obj is not None: for item in obj: found = find_nested_key(item, key) if found: From 25b7195c7afb05612164e610ead21bdf71982b7d Mon Sep 17 00:00:00 2001 From: glopesdev Date: Thu, 26 Sep 2024 14:53:47 +0100 Subject: [PATCH 03/12] Avoid iterating over the config file twice --- aeon/io/reader.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/aeon/io/reader.py b/aeon/io/reader.py index fda0c8af..53927bf4 100644 --- a/aeon/io/reader.py +++ b/aeon/io/reader.py @@ -353,7 +353,7 @@ def read(self, file: Path) -> pd.DataFrame: parts = unique_parts # Set new columns, and reformat `data`. - data = self.class_int2str(data, config_file) + data = self.class_int2str(data, identities) n_parts = len(parts) part_data_list = [pd.DataFrame()] * n_parts new_columns = pd.Series(["identity", "identity_likelihood", "part", "x", "y", "part_likelihood"]) @@ -410,18 +410,10 @@ def get_bodyparts(config_file: Path) -> list[str]: return parts @staticmethod - def class_int2str(data: pd.DataFrame, config_file: Path) -> pd.DataFrame: + def class_int2str(data: pd.DataFrame, classes: list[str]) -> pd.DataFrame: """Converts a class integer in a tracking data dataframe to its associated string (subject id).""" - if config_file.stem == "confmap_config": # SLEAP - with open(config_file) as f: - config = json.load(f) - try: - heads = config["model"]["heads"] - classes = util.find_nested_key(heads, "classes") - except KeyError as err: - raise KeyError(f"Cannot find classes in {config_file}.") from err - for i, subj in enumerate(classes): - data.loc[data["identity"] == i, "identity"] = subj + for i, subj in enumerate(classes): + data.loc[data["identity"] == i, "identity"] = subj return data @classmethod From f77ac1dd8748ff53e46bfa2ddefb99f9c8747791 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Thu, 26 Sep 2024 15:43:18 +0100 Subject: [PATCH 04/12] Avoid mixing dtypes with conditional assignment --- aeon/io/reader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aeon/io/reader.py b/aeon/io/reader.py index 53927bf4..1f3f995d 100644 --- a/aeon/io/reader.py +++ b/aeon/io/reader.py @@ -412,8 +412,10 @@ def get_bodyparts(config_file: Path) -> list[str]: @staticmethod def class_int2str(data: pd.DataFrame, classes: list[str]) -> pd.DataFrame: """Converts a class integer in a tracking data dataframe to its associated string (subject id).""" + identity = data["identity"].astype("string") for i, subj in enumerate(classes): - data.loc[data["identity"] == i, "identity"] = subj + identity.loc[data[identity.name] == i] = subj + data[identity.name] = identity return data @classmethod From ac2aa137454538ff7c05dff4476ed09491ac4dcb Mon Sep 17 00:00:00 2001 From: glopesdev Date: Thu, 26 Sep 2024 15:46:29 +0100 Subject: [PATCH 05/12] Remove whitespace on blank line --- aeon/io/reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aeon/io/reader.py b/aeon/io/reader.py index 1f3f995d..45ce8594 100644 --- a/aeon/io/reader.py +++ b/aeon/io/reader.py @@ -318,7 +318,7 @@ def read(self, file: Path) -> pd.DataFrame: config_file_dir = Path(self._model_root) / model_dir if not config_file_dir.exists(): raise FileNotFoundError(f"Cannot find model dir {config_file_dir}") - + config_file = self.get_config_file(config_file_dir) identities = self.get_class_names(config_file) parts = self.get_bodyparts(config_file) From caf3ce11e6e83eda263b33f97f616107d99aa0f1 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Fri, 27 Sep 2024 09:10:12 +0100 Subject: [PATCH 06/12] Use replace function instead of explicit loop --- aeon/io/reader.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/aeon/io/reader.py b/aeon/io/reader.py index 45ce8594..459b5ba7 100644 --- a/aeon/io/reader.py +++ b/aeon/io/reader.py @@ -412,10 +412,8 @@ def get_bodyparts(config_file: Path) -> list[str]: @staticmethod def class_int2str(data: pd.DataFrame, classes: list[str]) -> pd.DataFrame: """Converts a class integer in a tracking data dataframe to its associated string (subject id).""" - identity = data["identity"].astype("string") - for i, subj in enumerate(classes): - identity.loc[data[identity.name] == i] = subj - data[identity.name] = identity + identity_mapping = dict(enumerate(classes)) + data["identity"] = data["identity"].replace(identity_mapping) return data @classmethod From 93428c8fcbd558f91e8954d8cc639434b7d30572 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Fri, 27 Sep 2024 09:31:57 +0100 Subject: [PATCH 07/12] Improve error logic when model metadata not found --- aeon/io/reader.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/aeon/io/reader.py b/aeon/io/reader.py index 459b5ba7..e99db336 100644 --- a/aeon/io/reader.py +++ b/aeon/io/reader.py @@ -313,11 +313,20 @@ def read(self, file: Path) -> pd.DataFrame: """Reads data from the Harp-binarized tracking file.""" # Get config file from `file`, then bodyparts from config file. model_dir = Path(*Path(file.stem.replace("_", "/")).parent.parts[-4:]) - config_file_dir = file.parent / model_dir - if not config_file_dir.exists(): - config_file_dir = Path(self._model_root) / model_dir - if not config_file_dir.exists(): - raise FileNotFoundError(f"Cannot find model dir {config_file_dir}") + + # Check if model directory exists in local or shared directories. + # Local directory is prioritized over shared directory. + local_config_file_dir = file.parent / model_dir + shared_config_file_dir = Path(self._model_root) / model_dir + if local_config_file_dir.exists(): + config_file_dir = local_config_file_dir + elif shared_config_file_dir.exists(): + config_file_dir = shared_config_file_dir + else: + raise FileNotFoundError( + f"""Cannot find model dir in either local ({local_config_file_dir}) \ + or shared ({shared_config_file_dir}) directories""" + ) config_file = self.get_config_file(config_file_dir) identities = self.get_class_names(config_file) @@ -412,6 +421,8 @@ def get_bodyparts(config_file: Path) -> list[str]: @staticmethod def class_int2str(data: pd.DataFrame, classes: list[str]) -> pd.DataFrame: """Converts a class integer in a tracking data dataframe to its associated string (subject id).""" + if not classes: + raise ValueError("Classes list cannot be None or empty.") identity_mapping = dict(enumerate(classes)) data["identity"] = data["identity"].replace(identity_mapping) return data From 00c1ccab3b54c28c3ab63fbb1dd3477a242cf2b5 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Fri, 27 Sep 2024 17:09:24 +0100 Subject: [PATCH 08/12] Test loading poses with local model metadata --- ...multianimal-id-133_2024-03-02T12-00-00.bin | Bin 0 -> 440 bytes .../confmap_config.json | 202 ++++++++++++++++++ tests/io/test_reader.py | 19 ++ 3 files changed, 221 insertions(+) create mode 100644 tests/data/pose/2024-03-01T16-46-12/CameraTop/CameraTop_test-node1_topdown-multianimal-id-133_2024-03-02T12-00-00.bin create mode 100644 tests/data/pose/2024-03-01T16-46-12/CameraTop/test-node1/topdown-multianimal-id-133/confmap_config.json create mode 100644 tests/io/test_reader.py diff --git a/tests/data/pose/2024-03-01T16-46-12/CameraTop/CameraTop_test-node1_topdown-multianimal-id-133_2024-03-02T12-00-00.bin b/tests/data/pose/2024-03-01T16-46-12/CameraTop/CameraTop_test-node1_topdown-multianimal-id-133_2024-03-02T12-00-00.bin new file mode 100644 index 0000000000000000000000000000000000000000..806424a832c41b631b6065ed8769d1b7613d15bd GIT binary patch literal 440 zcmYk&ODKd<7{>9bk!Tbpmqtn!#6lJ>L-DBhB*e_PL?I=&Dc|@8xuhXv;Zn-N&OWp+0T`J%t)KjCg>qoN0zh}yP(iM4e^GZj8^t+x(_kZ&bA6V zPx9c($(gM}&_aqEkA@9cmA#xBz*-|`PcA`6)jBi>XCF>M#~T`k44aXxtUu93ktlt@om_#G-Z=-y?AKgjCTmiFZ949?RdGMd+2yJ>~*HHu01b&vb)|a z=t=8_U)s$QK?%E_e;ZP5%I1^~V_tSGErNVZ$DN(CsWL&Eyazi*cL>TZzj+aB<*Y?+ um=5p4K_q9p^4~c0qSmk%#mbJ1_hVby7xz1zI&_$#IIDNQQ(pQ2PJRJ6r@oZ{ literal 0 HcmV?d00001 diff --git a/tests/data/pose/2024-03-01T16-46-12/CameraTop/test-node1/topdown-multianimal-id-133/confmap_config.json b/tests/data/pose/2024-03-01T16-46-12/CameraTop/test-node1/topdown-multianimal-id-133/confmap_config.json new file mode 100644 index 00000000..5a2084b0 --- /dev/null +++ b/tests/data/pose/2024-03-01T16-46-12/CameraTop/test-node1/topdown-multianimal-id-133/confmap_config.json @@ -0,0 +1,202 @@ +{ + "data": { + "labels": { + "training_labels": "social_dev_b5350ff/aeon3_social_dev_b5350ff_ceph.slp", + "validation_labels": null, + "validation_fraction": 0.1, + "test_labels": null, + "split_by_inds": false, + "training_inds": null, + "validation_inds": null, + "test_inds": null, + "search_path_hints": [], + "skeletons": [ + { + "directed": true, + "graph": { + "name": "Skeleton-1", + "num_edges_inserted": 0 + }, + "links": [], + "multigraph": true, + "nodes": [ + { + "id": { + "py/object": "sleap.skeleton.Node", + "py/state": { + "py/tuple": [ + "centroid", + 1.0 + ] + } + } + } + ] + } + ] + }, + "preprocessing": { + "ensure_rgb": false, + "ensure_grayscale": false, + "imagenet_mode": null, + "input_scaling": 1.0, + "pad_to_stride": 16, + "resize_and_pad_to_target": true, + "target_height": 1080, + "target_width": 1440 + }, + "instance_cropping": { + "center_on_part": "centroid", + "crop_size": 96, + "crop_size_detection_padding": 16 + } + }, + "model": { + "backbone": { + "leap": null, + "unet": { + "stem_stride": null, + "max_stride": 16, + "output_stride": 2, + "filters": 16, + "filters_rate": 1.5, + "middle_block": true, + "up_interpolate": false, + "stacks": 1 + }, + "hourglass": null, + "resnet": null, + "pretrained_encoder": null + }, + "heads": { + "single_instance": null, + "centroid": null, + "centered_instance": null, + "multi_instance": null, + "multi_class_bottomup": null, + "multi_class_topdown": { + "confmaps": { + "anchor_part": "centroid", + "part_names": [ + "centroid" + ], + "sigma": 1.5, + "output_stride": 2, + "loss_weight": 1.0, + "offset_refinement": false + }, + "class_vectors": { + "classes": [ + "BAA-1104045", + "BAA-1104047" + ], + "num_fc_layers": 3, + "num_fc_units": 256, + "global_pool": true, + "output_stride": 2, + "loss_weight": 0.01 + } + } + }, + "base_checkpoint": null + }, + "optimization": { + "preload_data": true, + "augmentation_config": { + "rotate": true, + "rotation_min_angle": -180.0, + "rotation_max_angle": 180.0, + "translate": false, + "translate_min": -5, + "translate_max": 5, + "scale": false, + "scale_min": 0.9, + "scale_max": 1.1, + "uniform_noise": false, + "uniform_noise_min_val": 0.0, + "uniform_noise_max_val": 10.0, + "gaussian_noise": false, + "gaussian_noise_mean": 5.0, + "gaussian_noise_stddev": 1.0, + "contrast": false, + "contrast_min_gamma": 0.5, + "contrast_max_gamma": 2.0, + "brightness": false, + "brightness_min_val": 0.0, + "brightness_max_val": 10.0, + "random_crop": false, + "random_crop_height": 256, + "random_crop_width": 256, + "random_flip": false, + "flip_horizontal": true + }, + "online_shuffling": true, + "shuffle_buffer_size": 128, + "prefetch": true, + "batch_size": 4, + "batches_per_epoch": 469, + "min_batches_per_epoch": 200, + "val_batches_per_epoch": 54, + "min_val_batches_per_epoch": 10, + "epochs": 200, + "optimizer": "adam", + "initial_learning_rate": 0.0001, + "learning_rate_schedule": { + "reduce_on_plateau": true, + "reduction_factor": 0.1, + "plateau_min_delta": 1e-08, + "plateau_patience": 20, + "plateau_cooldown": 3, + "min_learning_rate": 1e-08 + }, + "hard_keypoint_mining": { + "online_mining": false, + "hard_to_easy_ratio": 2.0, + "min_hard_keypoints": 2, + "max_hard_keypoints": null, + "loss_scale": 5.0 + }, + "early_stopping": { + "stop_training_on_plateau": true, + "plateau_min_delta": 1e-08, + "plateau_patience": 20 + } + }, + "outputs": { + "save_outputs": true, + "run_name": "aeon3_social_dev_b5350ff_ceph_topdown_top.centered_instance_multiclass", + "run_name_prefix": "", + "run_name_suffix": "", + "runs_folder": "social_dev_b5350ff/models", + "tags": [], + "save_visualizations": true, + "delete_viz_images": true, + "zip_outputs": false, + "log_to_csv": true, + "checkpointing": { + "initial_model": true, + "best_model": true, + "every_epoch": false, + "latest_model": false, + "final_model": false + }, + "tensorboard": { + "write_logs": false, + "loss_frequency": "epoch", + "architecture_graph": false, + "profile_graph": false, + "visualizations": true + }, + "zmq": { + "subscribe_to_controller": false, + "controller_address": "tcp://127.0.0.1:9000", + "controller_polling_timeout": 10, + "publish_updates": false, + "publish_address": "tcp://127.0.0.1:9001" + } + }, + "name": "", + "description": "", + "sleap_version": "1.3.1", + "filename": "Z:/aeon/data/processed/test-node1/4310907/2024-01-12T19-00-00/topdown-multianimal-id-133/confmap_config.json" +} \ No newline at end of file diff --git a/tests/io/test_reader.py b/tests/io/test_reader.py new file mode 100644 index 00000000..ffbe8efb --- /dev/null +++ b/tests/io/test_reader.py @@ -0,0 +1,19 @@ +from pathlib import Path + +import pytest +from pytest import mark + +import aeon +from aeon.schema.schemas import social02 + +pose_path = Path(__file__).parent.parent / "data" / "pose" + + +@mark.api +def test_Pose_read_local_model_dir(): + data = aeon.load(pose_path, social02.CameraTop.Pose) + assert len(data) > 0 + + +if __name__ == "__main__": + pytest.main() From 6b32583f40753edc082637943750e831d0bd1c71 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Fri, 27 Sep 2024 17:11:08 +0100 Subject: [PATCH 09/12] Use all components other than time and device name --- aeon/io/reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aeon/io/reader.py b/aeon/io/reader.py index e99db336..fbfbe1fe 100644 --- a/aeon/io/reader.py +++ b/aeon/io/reader.py @@ -312,7 +312,7 @@ def __init__(self, pattern: str, model_root: str = "/ceph/aeon/aeon/data/process def read(self, file: Path) -> pd.DataFrame: """Reads data from the Harp-binarized tracking file.""" # Get config file from `file`, then bodyparts from config file. - model_dir = Path(*Path(file.stem.replace("_", "/")).parent.parts[-4:]) + model_dir = Path(*Path(file.stem.replace("_", "/")).parent.parts[1:]) # Check if model directory exists in local or shared directories. # Local directory is prioritized over shared directory. From 010fdb9c8b18a5e25fa3f7a2d746a5282619b70f Mon Sep 17 00:00:00 2001 From: glopesdev Date: Wed, 2 Oct 2024 09:18:25 +0100 Subject: [PATCH 10/12] Add regression test for poses with register prefix --- ...own-multianimal-id-133_2024-03-02T12-00-00.bin | Bin 0 -> 440 bytes tests/io/test_reader.py | 8 +++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 tests/data/pose/2024-03-01T16-46-12/CameraTop/CameraTop_202_test-node1_topdown-multianimal-id-133_2024-03-02T12-00-00.bin diff --git a/tests/data/pose/2024-03-01T16-46-12/CameraTop/CameraTop_202_test-node1_topdown-multianimal-id-133_2024-03-02T12-00-00.bin b/tests/data/pose/2024-03-01T16-46-12/CameraTop/CameraTop_202_test-node1_topdown-multianimal-id-133_2024-03-02T12-00-00.bin new file mode 100644 index 0000000000000000000000000000000000000000..806424a832c41b631b6065ed8769d1b7613d15bd GIT binary patch literal 440 zcmYk&ODKd<7{>9bk!Tbpmqtn!#6lJ>L-DBhB*e_PL?I=&Dc|@8xuhXv;Zn-N&OWp+0T`J%t)KjCg>qoN0zh}yP(iM4e^GZj8^t+x(_kZ&bA6V zPx9c($(gM}&_aqEkA@9cmA#xBz*-|`PcA`6)jBi>XCF>M#~T`k44aXxtUu93ktlt@om_#G-Z=-y?AKgjCTmiFZ949?RdGMd+2yJ>~*HHu01b&vb)|a z=t=8_U)s$QK?%E_e;ZP5%I1^~V_tSGErNVZ$DN(CsWL&Eyazi*cL>TZzj+aB<*Y?+ um=5p4K_q9p^4~c0qSmk%#mbJ1_hVby7xz1zI&_$#IIDNQQ(pQ2PJRJ6r@oZ{ literal 0 HcmV?d00001 diff --git a/tests/io/test_reader.py b/tests/io/test_reader.py index ffbe8efb..640768ab 100644 --- a/tests/io/test_reader.py +++ b/tests/io/test_reader.py @@ -4,7 +4,7 @@ from pytest import mark import aeon -from aeon.schema.schemas import social02 +from aeon.schema.schemas import social02, social03 pose_path = Path(__file__).parent.parent / "data" / "pose" @@ -15,5 +15,11 @@ def test_Pose_read_local_model_dir(): assert len(data) > 0 +@mark.api +def test_Pose_read_local_model_dir_with_register_prefix(): + data = aeon.load(pose_path, social03.CameraTop.Pose) + assert len(data) > 0 + + if __name__ == "__main__": pytest.main() From 0a88b79c34eb019dd8b371648442cf7d43dcbbe2 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Wed, 2 Oct 2024 09:37:10 +0100 Subject: [PATCH 11/12] Infer base prefix from stream search pattern --- aeon/io/reader.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/aeon/io/reader.py b/aeon/io/reader.py index fbfbe1fe..d44c2995 100644 --- a/aeon/io/reader.py +++ b/aeon/io/reader.py @@ -304,15 +304,22 @@ class (int): Int ID of a subject in the environment. """ def __init__(self, pattern: str, model_root: str = "/ceph/aeon/aeon/data/processed"): - """Pose reader constructor.""" - # `pattern` for this reader should typically be '_*' + """Pose reader constructor. + + The pattern for this reader should typically be `__*`. + If a register prefix is required, the pattern should end with a trailing + underscore, e.g. `Camera_202_*`. Otherwise, the pattern should include a + common prefix for the pose model folder excluding the trailing underscore, + e.g. `Camera_model-dir*`. + """ super().__init__(pattern, columns=None) self._model_root = model_root + self._pattern_offset = pattern.rfind("_") + 1 def read(self, file: Path) -> pd.DataFrame: """Reads data from the Harp-binarized tracking file.""" # Get config file from `file`, then bodyparts from config file. - model_dir = Path(*Path(file.stem.replace("_", "/")).parent.parts[1:]) + model_dir = Path(file.stem[self._pattern_offset :].replace("_", "/")).parent # Check if model directory exists in local or shared directories. # Local directory is prioritized over shared directory. From f925d750cd1278c5d902410ec342ebf4248dfd86 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Wed, 2 Oct 2024 09:54:17 +0100 Subject: [PATCH 12/12] Use full identity likelihood vectors in test data --- ...n-multianimal-id-133_2024-03-02T12-00-00.bin | Bin 440 -> 480 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/data/pose/2024-03-01T16-46-12/CameraTop/CameraTop_202_test-node1_topdown-multianimal-id-133_2024-03-02T12-00-00.bin b/tests/data/pose/2024-03-01T16-46-12/CameraTop/CameraTop_202_test-node1_topdown-multianimal-id-133_2024-03-02T12-00-00.bin index 806424a832c41b631b6065ed8769d1b7613d15bd..55f13c0f3d99fd8e7eccd071bad7fb0c1c8bcf35 100644 GIT binary patch literal 480 zcmX}oTPVX}7{~Et$~%W?F(OYfsR4i|15%awAal+yecrNw)J8%ZvmF8os% z&731ST{x7xl|(U7xR5ki)I9Hn|8xE6+tW|)yofgYP05csxErVi@!bsMCB?obNTbeIPl)k|j;MdZ`CK!@m_-3X^XVq(cZT=)e1#RdXwMbe^YghEF3Cq} z&$(X#Va<=GNN8(tGse4zg3r+_8RN6gZDBHO%LpATQu-z9HN Pa>D?53-yn~0&4yMWxKJ5 literal 440 zcmYk&ODKd<7{>9bk!Tbpmqtn!#6lJ>L-DBhB*e_PL?I=&Dc|@8xuhXv;Zn-N&OWp+0T`J%t)KjCg>qoN0zh}yP(iM4e^GZj8^t+x(_kZ&bA6V zPx9c($(gM}&_aqEkA@9cmA#xBz*-|`PcA`6)jBi>XCF>M#~T`k44aXxtUu93ktlt@om_#G-Z=-y?AKgjCTmiFZ949?RdGMd+2yJ>~*HHu01b&vb)|a z=t=8_U)s$QK?%E_e;ZP5%I1^~V_tSGErNVZ$DN(CsWL&Eyazi*cL>TZzj+aB<*Y?+ um=5p4K_q9p^4~c0qSmk%#mbJ1_hVby7xz1zI&_$#IIDNQQ(pQ2PJRJ6r@oZ{