From b188f4720e8bf4eab1ef065548fa11d96dda24fe Mon Sep 17 00:00:00 2001 From: yangbin-xijiao <13345886+yangbin-xijiao@user.noreply.gitee.com> Date: Mon, 19 Feb 2024 03:29:10 +0000 Subject: [PATCH 01/25] =?UTF-8?q?=E6=96=B0=E5=BB=BA=20subject2-pre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- subject2-pre/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 subject2-pre/.keep diff --git a/subject2-pre/.keep b/subject2-pre/.keep new file mode 100644 index 0000000..e69de29 -- Gitee From f522192630dfc0984787f327d197f2fcfd4cefda Mon Sep 17 00:00:00 2001 From: yangbin-xijiao <13345886+yangbin-xijiao@user.noreply.gitee.com> Date: Mon, 19 Feb 2024 03:42:07 +0000 Subject: [PATCH 02/25] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=EF=BC=88=E8=99=9A=E6=8B=9F=E6=95=B0=E6=8D=AE=E7=94=9F=E6=88=90?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: yangbin-xijiao <13345886+yangbin-xijiao@user.noreply.gitee.com> --- subject2-pre/tvhl.py | 200 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 subject2-pre/tvhl.py diff --git a/subject2-pre/tvhl.py b/subject2-pre/tvhl.py new file mode 100644 index 0000000..421a73c --- /dev/null +++ b/subject2-pre/tvhl.py @@ -0,0 +1,200 @@ +import numpy as np +import pandas as pd +from tensorflow.keras.models import Sequential +from sklearn.preprocessing import MinMaxScaler +from tensorflow.keras.layers import Dense, LeakyReLU +import tensorflow as tf +from scipy.spatial.distance import cdist +import requests +import csv +from io import StringIO +import datetime + +def get_data_from_api(api_url): + """ + 从API获取数据的函数接口 + 参数: + - api_url: API的地址 + 返回值: + - 如果成功,返回API响应的JSON数据 + - 如果失败,返回None + """ + try: + # 发起GET请求 + response = requests.get(api_url) + + # 检查响应状态码 + if response.status_code == 200: + # 解析JSON数据并返回 + return response.json() + else: + # 打印错误信息,并返回None + print(f"Error: {response.status_code} - {response.text}") + return None + except requests.RequestException as e: + # 打印异常信息,并返回None + print(f"Request Error: {e}") + return None + +def json_data_to_csv(json_data): + try: + # 提取表头(CSV文件的列名) + header = list(json_data['pageData'][0].keys()) + # 创建一个内存中的文件对象,用于写入CSV数据 + csv_output = StringIO() + # 创建CSV写入器 + csv_writer = csv.DictWriter(csv_output, fieldnames=header) + # 写入表头 + csv_writer.writeheader() + # 写入数据 + csv_writer.writerows(json_data['pageData']) + # 获取CSV数据作为字符串 + csv_data = csv_output.getvalue() + print("转换成功:JSON 数据到 CSV 字符串") + return csv_data + except Exception as e: + print(f"转换失败:{e}") + return None + +#创建生成器 +def build_generator(): + model = Sequential() + model.add(Dense(64, input_dim=7)) + model.add(LeakyReLU(alpha=0.2)) + model.add(Dense(32)) + model.add(LeakyReLU(alpha=0.2)) + model.add(Dense(7, activation='sigmoid')) + return model +generator = build_generator() + +#创建鉴别器 +def build_discriminator(): + model = Sequential() + model.add(Dense(32, input_dim=7)) + model.add(LeakyReLU(alpha=0.2)) + model.add(Dense(16)) + model.add(LeakyReLU(alpha=0.2)) + model.add(Dense(1, activation='sigmoid')) + return model +discriminator = build_discriminator() + + +#建立损失函数 +def compute_joint_distribution_loss(real_samples, fake_samples): + real_relative_time = real_samples[:, 0] + fake_relative_time = fake_samples[:, 0] + real_samples = real_samples[:, 1:] + fake_samples = fake_samples[:, 1:] + distance =10 * cdist(real_samples, fake_samples, metric='euclidean') + conditional_distance = np.abs(real_relative_time - fake_relative_time) + joint_distribution_loss = np.mean(distance * conditional_distance) + return joint_distribution_loss + + +#训练模型 +def train_gan(noise_data, real_data, epochs, batch_size): + for epoch in range(epochs): + noise = noise_data[np.random.randint(0, noise_data.shape[0], size=batch_size)] + real_samples = real_data[np.random.randint(0, real_data.shape[0], size=batch_size)] + generated_samples = generator.predict(noise) + # print("Real Samples Shape:", real_samples.shape) + # print("Generated Samples Shape:", generated_samples.shape) + X = np.concatenate((real_samples, generated_samples)) + y = np.ones(2 * batch_size) + y[batch_size:] = 0 + discriminator.trainable = True + discriminator_loss = discriminator.train_on_batch(X, y) + noise = noise_data[np.random.randint(0, noise_data.shape[0], size=batch_size)] + y_gen = np.ones(batch_size) + discriminator.trainable = False + generator_loss = gan.train_on_batch(noise, y_gen) + # 计算联合分布差异项 + joint_distribution_loss = compute_joint_distribution_loss(real_samples, generated_samples) + # 更新判别器的损失函数 + discriminator_loss = discriminator_loss +1*joint_distribution_loss + # 打印损失和精度 + if epoch % 100 == 0: + print( + f'Epoch: {epoch}/{epochs}, Discriminator Loss: {discriminator_loss}, Generator Loss: {generator_loss}') + + +def gene_data(generated_data): + # 将时间恢复到24小时制 + generated_data['time'] = (generated_data['time'] * 24).astype(int) + + data_by_hour = {} + # 将generated_data按时间分组,放入字典中对应的键中 + for i in range(24): + data_by_hour[i] = generated_data[generated_data['time'] == i] + empty_df = pd.DataFrame() + + # 遍历1到23小时,检查每个小时的数据是否为空 + for i in range(0, 24): + if data_by_hour[i].empty: + next_i = (i + 1) % 24 # 计算下一个小时的索引,使用取模运算确保最后一个小时的数据获取方式是循环到小时1的数据 + data_by_hour[i] = data_by_hour[next_i].head(1) + data_length = len(data_by_hour[i]) + if data_length < 31: + data_by_hour[i] = pd.concat([data_by_hour[i], data_by_hour[5].head(30)]) + for i in range(0, 24): + empty_df = pd.concat([empty_df, data_by_hour[i].head(30)]) + + # 复制empty_df的第一行数据,并赋值给time_series_data + time_series_data = empty_df.iloc[:1].copy() + for i in range(1, 60): + for j in range(0, 24): + time_series_data = pd.concat([time_series_data, empty_df.iloc[j * 24 + i:j * 24 + i + 1].copy()]) + time_series_data = time_series_data.copy() + time_series_data['time'] = range(len(time_series_data)) + return time_series_data + +def set_noise(): + uniform_noise = np.random.uniform(0, 1, size=(10000, 1)) + normal_noise = np.random.normal(0, 1, size=(10000, 6)) + noise = np.hstack((uniform_noise, normal_noise)) + return noise + + +#从API获取历史数据 +# api_url = "http://202.117.43.38:28081/jcce/v1.0/job/list" +# data = get_data_from_api(api_url) +data = pd.read_csv('pre_microsoft.csv') + +# 使用提取的数据进行CSV转换 +# data = json_data_to_csv(data) +# data = pd.read_csv(StringIO(data), parse_dates=["startTime", "endTime", "arriveTime"]) +current_time = datetime.datetime.now() +# print(current_time) +# data["startTime"] = np.round((data["startTime"] - current_time) / np.timedelta64(1, "h") + 100) +#提取特征 +features = ['time', 'cpu_use', 'gpu_use', 'mem_use', 'cpu_plan', 'gpu_plan','mem_plan'] +data = data[features] +# 将相同时间的数据合并 +# data = data.groupby("startTime").sum().reset_index() +data['time'] = data['time'] % 24 +#根据接口进行修改 + + + +#归一化处理 +data = data[features].values +scaler = MinMaxScaler(feature_range=(0, 1)) +data_scaled = scaler.fit_transform(data) + +#设置数据生成参数 +discriminator.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) +discriminator.trainable = False +gan_input = tf.keras.Input(shape=(7,)) +gan_output = discriminator(generator(gan_input)) +gan = tf.keras.Model(gan_input, gan_output) +gan.compile(loss='binary_crossentropy', optimizer='adam') + +noise = set_noise() +#训练模型 +train_gan(data, noise, epochs=1000, batch_size=128) +# 数据生成和处理 +generated_data = pd.DataFrame(generator.predict(noise), columns=features) +generated_data = gene_data(generated_data) + +# 保存数据 +generated_data.to_csv('tvhl.csv', index=False) \ No newline at end of file -- Gitee From 74f775c9fdb50b81f9b58086554da4607102f2ac Mon Sep 17 00:00:00 2001 From: yangbin-xijiao <13345886+yangbin-xijiao@user.noreply.gitee.com> Date: Mon, 19 Feb 2024 03:43:00 +0000 Subject: [PATCH 03/25] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=EF=BC=88=E8=81=94=E9=82=A6=E5=AD=A6=E4=B9=A0=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: yangbin-xijiao <13345886+yangbin-xijiao@user.noreply.gitee.com> --- subject2-pre/client.py | 185 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 subject2-pre/client.py diff --git a/subject2-pre/client.py b/subject2-pre/client.py new file mode 100644 index 0000000..37a03f8 --- /dev/null +++ b/subject2-pre/client.py @@ -0,0 +1,185 @@ +import socket +import pickle +import numpy as np +import pandas as pd +import torch.optim as optim + +import torch +import torch.nn as nn +import argparse + +# 定义网络 +class GRU(nn.Module): + def __init__(self, feature_size, hidden_size, num_layers, output_size): + super(GRU, self).__init__() + self.hidden_size = hidden_size + self.num_layers = num_layers + self.gru = nn.GRU(feature_size, hidden_size, num_layers, batch_first=True) + self.fc = nn.Linear(hidden_size, output_size) + self.activation = nn.ReLU() # 添加激活函数 + + def forward(self, x, hidden=None): + batch_size = x.shape[0] + if hidden is None: + h_0 = x.data.new(self.num_layers, batch_size, self.hidden_size).fill_(0).float() + else: + h_0 = hidden + output, h_0 = self.gru(x, h_0) + output = self.fc(output[:, -1, :]) + output = self.activation(output) # 添加激活函数 + return output, h_0 + +# 定义学习任务的设置 +def parser_args(): + parser = argparse.ArgumentParser(description='GRU Model Parameters') + parser.add_argument('--timestep', type=int, default=4, help='Time step size') + parser.add_argument('--batch_size', type=int, default=32, help='Batch size') + parser.add_argument('--feature_size', type=int, default=6, help='Number of features') + parser.add_argument('--hidden_size', type=int, default=512, help='Size of the hidden layer') + parser.add_argument('--output_size', type=int, default=3, help='Size of the output layer') + parser.add_argument('--num_layers', type=int, default=2, help='Number of GRU layers') + parser.add_argument('--epochs', type=int, default=3, help='Number of training epochs') + parser.add_argument('--learning_rate', type=float, default=0.0001, help='Learning rate') + parser.add_argument('--model_name', type=str, default='gru', help='Name of the model') + parser.add_argument('--save_path', type=str, default='./{}.pth'.format('gru_a'), help='Path to save the best model') + parser.add_argument('--no_cuda', action='store_true', default=False, help='Disable CUDA') + args = parser.parse_args() + return args + +def train_model(model, x_train, y_train, config): + # 设置优化器和损失函数 + optimizer = optim.Adam(model.parameters(), lr=config.learning_rate) + # optimizer = optim.SGD(model.parameters(), lr=config.learning_rate) + criterion = nn.MSELoss() + # 设置初始隐藏状态(可根据需要调整) + # hidden = None + + # 进行本地训练 + for epoch in range(config.epochs): + optimizer.zero_grad() + + # 手动清除隐藏状态 + hidden = None + + # 前向传播 + output, hidden = model(x_train, hidden) + loss = criterion(output, y_train) + + # 反向传播和参数更新 + loss.backward(retain_graph=True) + optimizer.step() + + print(f"Epoch [{epoch + 1}/{config.epochs}], Loss: {loss.item()}") + + # 返回更新后的客户端模型参数 + return model + +# 划分数据 +def split_data(data, timestep,feature_size, target_size): + dataX = [] + dataY = [] + for index in range(len(data) - timestep): + dataX.append(data[index: index + timestep]) + dataY.append(data[index + timestep, :target_size]) + dataX = np.array(dataX) + dataY = np.array(dataY) + train_size = int(np.round(dataX.shape[0])) + x_train = dataX[:train_size, :] + y_train = dataY[:train_size, :] + x_test = dataX[train_size:, :] + y_test = dataY[train_size:, :] + + return x_train, y_train, x_test, y_test + + +# 数据加载 +def load_data(data_path): + dataset = pd.read_csv(data_path) + return dataset + +args = parser_args() + +data_path ='tvhl_a.csv' +dataset = load_data(data_path) + +selected_features = ['cpu_use', 'gpu_use', 'mem_use', 'cpu_plan', 'gpu_plan', 'mem_plan'] +target_size = 3 # 三个目标列的大小 +selected_data = dataset[selected_features].values + +x_train,y_train, x_test, y_test = split_data(selected_data, args.timestep, args.feature_size,target_size) +x_train_tensor = torch.Tensor(x_train) +y_train_tensor = torch.Tensor(y_train) + +# 开始训练 +model = GRU(args.feature_size, args.hidden_size, args.num_layers, args.output_size).to('cpu') +# 创建优化器 +optimizer = optim.SGD(model.parameters(), lr=args.learning_rate) + +# 创建TCP客户端套接字 +client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + +# 连接到服务器 +# server_address = ('127.0.0.1', 4321) +server_address = ('202.117.43.11', 8080) +client.connect(server_address) + +ROUNDS = 20 +length = len(pickle.dumps(model.state_dict())) +chunk_size = 1024 + +for round in range(ROUNDS): + print("第 %d 轮" % (round), end='\n') + + size_message = client.recv(4096) + length = int.from_bytes(size_message, byteorder='big') + + # 从服务器接收模型权重 + received_weights = bytearray() + + while len(received_weights) < length: + # print('从服务器接收模型权重') + chunk = client.recv(4096) + received_weights += chunk + + print('全部接受完毕') + if len(received_weights) == length: + try: + received_weights = pickle.loads(received_weights) + print('解析成功') + # 设置模型权重 + model.load_state_dict(received_weights) + except EOFError: + print('解析失败: EOFError') + else: + print('接收到的数据长度不符合预期,可能存在问题') + + + # 在客户端本地训练模型 + print("开始训练") + train_model(model, x_train_tensor, y_train_tensor, args) + + # 获取更新后的模型权重 + updated_weights = model.state_dict() + print('获取更新后的模型权重') + # 序列化并将更新后的权重发送到服务器 + print('将更新后的权重发送到服务器') + updated_weights_serialized = pickle.dumps(updated_weights) + print(f"Serialized client model weights size: {len(updated_weights_serialized)} bytes") + + # size_message = str(len(updated_weights_serialized)).encode('utf-8') + # client.send(size_message) + size_message = len( updated_weights_serialized).to_bytes(4096, byteorder='big') + client.send(size_message) + + total_sent = 0 + for i in range(0, len(updated_weights_serialized), chunk_size): + chunk =updated_weights_serialized [i:i + chunk_size] + sent = client.send(chunk) + total_sent += sent + + + print('发送完毕') + +# 关闭客户端套接字 +client.close() +torch.save(model, args.save_path) \ No newline at end of file -- Gitee From 9bf3fcb4c7522f4511d74db44ed78487886a20eb Mon Sep 17 00:00:00 2001 From: yangbin-xijiao <13345886+yangbin-xijiao@user.noreply.gitee.com> Date: Mon, 19 Feb 2024 03:43:46 +0000 Subject: [PATCH 04/25] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=EF=BC=88=E8=81=94=E9=82=A6=E5=AD=A6=E4=B9=A0=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8=E7=AB=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: yangbin-xijiao <13345886+yangbin-xijiao@user.noreply.gitee.com> --- subject2-pre/server.py | 164 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 subject2-pre/server.py diff --git a/subject2-pre/server.py b/subject2-pre/server.py new file mode 100644 index 0000000..bcb74aa --- /dev/null +++ b/subject2-pre/server.py @@ -0,0 +1,164 @@ +import socket +import pickle +import torch.nn as nn +import argparse +from concurrent.futures import ThreadPoolExecutor + + + +# 定义神经网络 +class GRU(nn.Module): + def __init__(self, feature_size, hidden_size, num_layers, output_size): + super(GRU, self).__init__() + self.hidden_size = hidden_size + self.num_layers = num_layers + self.gru = nn.GRU(feature_size, hidden_size, num_layers, batch_first=True) + self.fc = nn.Linear(hidden_size, output_size) + self.activation = nn.ReLU() + + def forward(self, x, hidden=None): + batch_size = x.shape[0] + if hidden is None: + h_0 = x.data.new(self.num_layers, batch_size, self.hidden_size).fill_(0).float() + else: + h_0 = hidden + output, h_0 = self.gru(x, h_0) + output = self.fc(output[:, -1, :]) + output = self.activation(output) + return output, h_0 + +# 定义学习任务的设置 +def parser_args(): + parser = argparse.ArgumentParser(description='GRU Model Parameters') + parser.add_argument('--timestep', type=int, default=4, help='Time step size') + parser.add_argument('--batch_size', type=int, default=32, help='Batch size') + parser.add_argument('--feature_size', type=int, default=6, help='Number of features') + parser.add_argument('--hidden_size', type=int, default=512, help='Size of the hidden layer') + parser.add_argument('--output_size', type=int, default=3, help='Size of the output layer') + parser.add_argument('--num_layers', type=int, default=2, help='Number of GRU layers') + parser.add_argument('--epochs', type=int, default=3, help='Number of training epochs') + parser.add_argument('--learning_rate', type=float, default=0.0001, help='Learning rate') + parser.add_argument('--model_name', type=str, default='gru', help='Name of the model') + parser.add_argument('--save_path', type=str, default='./{}.pth'.format('gru'), help='Path to save the best model') + parser.add_argument('--no_cuda', action='store_true', default=False, help='Disable CUDA') + args = parser.parse_args() + return args + +args = parser_args() +model = GRU(args.feature_size, args.hidden_size, args.num_layers, args.output_size) + +# 创建服务器套接字 +server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +# 绑定服务器地址和端口 +# server_address = ('127.0.0.1', 4321) +server_address = ('202.117.43.11', 8080) +server.bind(server_address) +# 开始监听,允许最多客户端连接 +server.listen(5) +clients = [] +c_weights = {} # 用于保存每个客户端的权重数据 +c_length= {} + + +while True: + print("等待客户端连接...") + client, addr = server.accept() + print(f"接受来自 {addr} 的连接") + clients.append(client) + if len(clients) == 2: + break + +ROUNDS = 20 +length = len(pickle.dumps(model.state_dict())) +chunk_size = 1024 + + +for round in range(ROUNDS): + + print("ROUND %d" % (round), end='\n') + # 获取当前模型的权重 + model_weights = model.state_dict() + model_weights_serialized = pickle.dumps(model_weights) + print(f"Serialized model weights size: {len(model_weights_serialized)} bytes") + + for client in clients: + print('接受客户端数据长度:') + print(client) + size_message = len(model_weights_serialized).to_bytes(4096, byteorder='big') # 将整数转换为字节数组 + client.send(size_message) + + # 向所有客户端发送当前模型的权重 + print('向客户端发送当前模型的权重') + for client in clients: + total_sent = 0 + for i in range(0, len(model_weights_serialized), chunk_size): + chunk = model_weights_serialized[i:i + chunk_size] + sent = client.send(chunk) + total_sent += sent + + + + def receive_data_from_client(client, c_length): + + print(f'接受客户端数据长度:{client}') + size_message = client.recv(4096) + length = int.from_bytes(size_message, byteorder='big') + c_length[client] = length + + print(f'接受客户端数据:{client}') + received_weights = b"" + while len(received_weights) < c_length[client]: + chunk = client.recv(4096) + received_weights += chunk + + if len(received_weights) == length: + try: + received_weights = pickle.loads(received_weights) + print('解析成功') + c_weights[client] = received_weights + except EOFError: + print('解析失败: EOFError') + c_weights[client] = c_weights[client] + else: + print('接收到的数据长度不符合预期,可能存在问题') + + + + # 创建ThreadPoolExecutor + with ThreadPoolExecutor(max_workers=len(clients)) as executor: + # 提交任务给线程池 + futures = [executor.submit(receive_data_from_client, client, c_length) for client in clients] + + # 等待所有任务完成 + for future in futures: + future.result() + + + # 计算平均权重 + aggregated_weights = model.state_dict() + # print(aggregated_weights) + + + # 将所有值清零 + for key in aggregated_weights.keys(): + aggregated_weights[key].zero_() + + # 累积权重 + for weights in c_weights.values(): + for key, value in weights.items(): + if key not in aggregated_weights: + aggregated_weights[key] = value.clone() + else: + aggregated_weights[key] += value + + # 计算平均值 + for key in aggregated_weights.keys(): + aggregated_weights[key] /= len(c_weights) + + # 将聚合后的权重应用到模型中 + model.load_state_dict(aggregated_weights) + print("权重聚合完毕") + +# 关闭服务器套接字 +server.close() + -- Gitee From 02f46d2fbdca7b2602f224b01d6dff90d645d326 Mon Sep 17 00:00:00 2001 From: yangbin-xijiao <13345886+yangbin-xijiao@user.noreply.gitee.com> Date: Mon, 19 Feb 2024 03:44:30 +0000 Subject: [PATCH 05/25] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=EF=BC=88=E4=B8=AA=E6=80=A7=E5=8C=96=E7=9F=A5=E8=AF=86=E8=92=B8?= =?UTF-8?q?=E9=A6=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: yangbin-xijiao <13345886+yangbin-xijiao@user.noreply.gitee.com> --- subject2-pre/post_fl.py | 230 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 subject2-pre/post_fl.py diff --git a/subject2-pre/post_fl.py b/subject2-pre/post_fl.py new file mode 100644 index 0000000..fab6ad9 --- /dev/null +++ b/subject2-pre/post_fl.py @@ -0,0 +1,230 @@ +import numpy as np +import pandas as pd +import torch +import torch.nn as nn +from sklearn.preprocessing import StandardScaler, MinMaxScaler +import argparse + + +# 数据加载 +def load_data(data_path): + dataset = pd.read_csv(data_path) + return dataset +# 加载数据 +data_path = 'processed_alibaba.csv' +data = load_data(data_path) +# 选择需要训练和测试的特征列和目标列 +data = data[['cpu_use', 'gpu_use', 'mem_use', 'cpu_plan', 'gpu_plan', 'mem_plan']] + + +# 标准化 +scaler = MinMaxScaler(feature_range=(0, 1)) +data = scaler.fit_transform(data.values) + +u = 0.1 +# 定义迁移学习的训练轮数 +num_transfer_epochs = 100 + +# 定义学习任务的设置 +def parser_args(): + parser = argparse.ArgumentParser(description='GRU Model Parameters') + parser.add_argument('--timestep', type=int, default=4, help='Time step size') + parser.add_argument('--batch_size', type=int, default=32, help='Batch size') + parser.add_argument('--feature_size', type=int, default=6, help='Number of features') + parser.add_argument('--hidden_size', type=int, default=512, help='Size of the hidden layer') + parser.add_argument('--output_size', type=int, default=3, help='Size of the output layer') + parser.add_argument('--num_layers', type=int, default=2, help='Number of GRU layers') + parser.add_argument('--epochs', type=int, default=10, help='Number of training epochs') + parser.add_argument('--learning_rate', type=float, default=0.001, help='Learning rate') + parser.add_argument('--model_name', type=str, default='gru', help='Name of the model') + parser.add_argument('--save_path', type=str, default='./{}.pth'.format('gru'), help='Path to save the best model') + parser.add_argument('--no_cuda', action='store_true', default=False, help='Disable CUDA') + args = parser.parse_args() + return args +args = parser_args() + + +#新模型 +class Config(): + + timestep = 4 # 时间步长,就是利用多少时间窗口 + batch_size = 32 # 批次大小 + feature_size = 6 # 每个步长对应的特征数量,这里使用8维特征 + hidden_size = 512 # 隐层大小 + output_size = 3 # 由于是单输出任务 + num_layers = 4 # gru的层数 + epochs = 32 # 迭代轮数 + best_loss = float('inf') # 记录损失,初始化为正无穷大 + learning_rate = 0.001 # 学习率 + model_name = 'gru' # 模型名称 + save_path = './{}.pth'.format(model_name) # 最优模型保存路径 + +config = Config() + +# 划分数据 +def split_data(data, timestep,target_size): + dataX = [] + dataY = [] + for index in range(len(data) - timestep): + dataX.append(data[index: index + timestep]) + dataY.append(data[index + timestep, :target_size]) + dataX = np.array(dataX) + dataY = np.array(dataY) + train_size = int(np.round(0.7*dataX.shape[0])) + x_train = dataX[:train_size, :] + y_train = dataY[:train_size, :] + x_test = dataX[train_size:, :] + y_test = dataY[train_size:, :] + + return x_train, y_train, x_test, y_test + +# 定义网络 +class GRU(nn.Module): + def __init__(self, feature_size, hidden_size, num_layers, output_size): + super(GRU, self).__init__() + self.hidden_size = hidden_size + self.num_layers = num_layers + self.gru = nn.GRU(feature_size, hidden_size, num_layers, batch_first=True) + self.fc = nn.Linear(hidden_size, output_size) + self.activation = nn.ReLU() # 添加激活函数 + + def forward(self, x, hidden=None): + batch_size = x.shape[0] + if hidden is None: + h_0 = x.data.new(self.num_layers, batch_size, self.hidden_size).fill_(0).float() + else: + h_0 = hidden + output, h_0 = self.gru(x, h_0) + output = self.fc(output[:, -1, :]) + output = self.activation(output) # 添加激活函数 + return output, h_0 + + + + +# 获取训练数据 +x_train, y_train, x_test, y_test = split_data(data, config.timestep, config.feature_size) +target_size = 3 # 三个目标列的大小 +x_train, y_train, x_test, y_test = split_data(data, args.timestep,target_size) + +#形成训练数据集 +x_train_tensor = torch.Tensor(x_train) +y_train_tensor = torch.Tensor(y_train) +x_test_tensor = torch.Tensor(x_test) +y_test_tensor = torch.Tensor(y_test) + +# 假设您的模型类是GRU,config中包含相关的模型配置信息 +pre_trained_model = GRU(args.feature_size, args.hidden_size, args.num_layers, args.output_size) +# 加载预训练模型的参数 +# 加载预训练模型的参数 +model_path = "gru_a.pth" +pre_trained_model = torch.load(model_path, map_location='cpu') + +transfer_model = GRU(config.feature_size, config.hidden_size, config.num_layers,config.output_size) # 定义GRU网络 + +loss_function = nn.MSELoss() # 定义损失函数 +optimizer_transfer = torch.optim.AdamW(transfer_model.parameters(), lr= config.learning_rate) # 定义优化器 + + +# 将公共模型的参数设为不可训练 +for param in pre_trained_model.parameters(): + param.requires_grad = False +print(y_train_tensor) + +# train_dataset = TensorDataset(x_train_tensor, y_train_tensor) +# # 创建 DataLoader +# train_loader = DataLoader(train_dataset, batch_size=config.batch_size, shuffle=True) + +for epoch in range(num_transfer_epochs): + + transfer_model.train() + running_loss = 0 + + optimizer_transfer.zero_grad() + # 手动清除隐藏状态 + hidden = None + + # 从数据中获取输入和目标张量 + inputs = x_train_tensor + targets = y_train_tensor + + y_train_pred,hidden = transfer_model(inputs) + y_pretrained_pred,hidden = pre_trained_model(inputs) + + loss = (1 - u) * loss_function(y_train_pred, targets) + u * loss_function(y_train_pred, y_pretrained_pred) + # 反向传播和参数更新 + loss.backward() + optimizer_transfer.step() + + running_loss += loss.item() + + # 将运行中的损失打印出来,以便监控训练过程 + print(f"Epoch {epoch+1}/{num_transfer_epochs}, Loss: {running_loss}") + + +# 保存迁移模型 +transfer_model_save_path = './ta_model.pth' # 定义迁移模型保存路径 +torch.save(transfer_model.state_dict(), transfer_model_save_path) + +print('Finished Transfer Learning') + +# 测试循环 +transfer_model.eval() # 将模型设置为评估模式 + +# with torch.no_grad(): # 在测试期间禁用梯度计算 +# test_inputs = x_test_tensor +# test_targets = y_test_tensor +# +# test_predictions, _ = transfer_model(test_inputs) +# test_loss = loss_function(test_predictions, test_targets) +# + + +with torch.no_grad(): # 在测试期间禁用梯度计算 + test_inputs = x_test_tensor + test_targets = y_test_tensor + + test_predictions, _ = transfer_model(test_inputs) + test_loss = loss_function(test_predictions, test_targets) + + + # 创建一个文件保存输入数据 + with open('input_data.txt', 'w') as input_file: + # 打印每次的数据输入和预测结果 + for i in range(len(test_inputs)): + input_data = test_inputs[i].numpy() + target_data = test_targets[i].numpy() + predicted_data = test_predictions[i].numpy() + + print(f"样本 {i + 1}:") + print("输入数据:") + for row in input_data: + print(" ".join(map(str, row))) + + print("目标数据:", target_data) + print("预测数据:", predicted_data) + print("----------") + + # 将输入数据写入文件 + input_file.write(f"样本 {i + 1}:\n") + input_file.write("输入数据:\n") + for row in input_data: + input_file.write(" ".join(map(str, row)) + "\n") + + input_file.write("目标数据:" + repr(target_data.tolist()) + "\n") + input_file.write("预测数据:" + repr(predicted_data.tolist()) + "\n") + input_file.write("----------\n") + +# print(f"测试损失: {test_loss.item()}") + +# # 将测试结果保存到CSV文件 +# result_df = pd.DataFrame({ +# 'Actual_cpu': test_targets[:, 0].numpy(), +# 'Actual_gpu': test_targets[:, 1].numpy(), +# 'Actual_mem': test_targets[:, 2].numpy(), +# 'Predicted_cpu': test_predictions[:, 0].numpy(), +# 'Predicted_gpu': test_predictions[:, 1].numpy(), +# 'Predicted_mem': test_predictions[:, 2].numpy(), +# }) +# +# result_df.to_csv('pa_results.csv', index=False) \ No newline at end of file -- Gitee From 811d062e52536c2ad4454e8cba7933ccc95cd23a Mon Sep 17 00:00:00 2001 From: yangbin-xijiao <13345886+yangbin-xijiao@user.noreply.gitee.com> Date: Mon, 19 Feb 2024 04:16:43 +0000 Subject: [PATCH 06/25] =?UTF-8?q?=E6=96=B0=E5=BB=BA=20all-process?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- subject2-pre/all-process/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 subject2-pre/all-process/.keep diff --git a/subject2-pre/all-process/.keep b/subject2-pre/all-process/.keep new file mode 100644 index 0000000..e69de29 -- Gitee From 26fa90a611604f23fc7589ea3672413ad13a247d Mon Sep 17 00:00:00 2001 From: yangbin-xijiao <13345886+yangbin-xijiao@user.noreply.gitee.com> Date: Mon, 19 Feb 2024 04:20:47 +0000 Subject: [PATCH 07/25] =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: yangbin-xijiao <13345886+yangbin-xijiao@user.noreply.gitee.com> --- subject2-pre/README.md | 88 +++++++ subject2-pre/environment.md | 478 ++++++++++++++++++++++++++++++++++ subject2-pre/environment.yaml | 478 ++++++++++++++++++++++++++++++++++ 3 files changed, 1044 insertions(+) create mode 100644 subject2-pre/README.md create mode 100644 subject2-pre/environment.md create mode 100644 subject2-pre/environment.yaml diff --git a/subject2-pre/README.md b/subject2-pre/README.md new file mode 100644 index 0000000..89186e6 --- /dev/null +++ b/subject2-pre/README.md @@ -0,0 +1,88 @@ +# JC-workload-predictor + + + +This repository provides the implementation of the paper "Joint Privacy-Preserving Cloud Workload Prediction Based on Federated Learning", which is published in XXX. +In this paper, we propose a joint privacy-preserving cloud workload prediction framework based on Federated Learning (FL), which enables the collaborative training of a workload prediction model across cloud providers with heterogeneous workloads and model preferences without exposing private workload data. +we first design a Generative Adversarial Network (GAN) based virtual workload dataset generation method that incorporates workloads' temporal and resource-usage-plan-related features via temporal-aware feature calibration. +Then, we design a FL approach based on a hybrid local model composed of a homogeneous public model and a heterogeneous personal model. +Finally, we design a Knowledge Distillation (KD) based approach to post-train the personal model with both private workload knowledge and shared workload knowledge learned from the trained global public model. +Extensive experiments on various real-world workloads demonstrate that compared with the state-of-the-art, our framework improves workload prediction accuracy by 45.5\% in average over all cloud providers. + +
![]() |
+ ![]() |
+
| MSEs of memory utilization prediction results (lower is better). | +MSEs of CPU utilization prediction results (lower is better). | +
+
+
+
+
gu{7p)%0vBJh3cz<6rSg9l$%``*RY3}P_w?<4-X^im=O6m_I7&;N%OxAE788!{1!*4wtO`9_eT!}V7 zWvcVjI%2>qPu0XazP^#Hg`UQ5`#H{gaTnR!YiZrw{p-_e@Qmk(fInY=QU8MH%VlDf zv&;5 x zsbCemakH| _>ql~fh`s~PT`Yt!) zD`_q7LLb~z{@|XBPx>23QaNR6&_0csWpm^bZlS!1E|?|d^ HmFQgC4f531(HmyGc>+T&6% z&)^;G8dZ|8G|x%1_hPDa_EIW&?=j;a28248-OJezf16KRKcex@QDyCRk4jqfGmo@b zNle(r@I^q<>9nWaw1H& u^4b^hz82flgtXFZH|$}h1s>7z$aKfd zvCcV_57;2B#T=(>EYM3)Fm*w2nT%2vk|Bx=`IVQ-F1nsh!|?K6Ld{G}XH33SOh;O# zTSfI>bxy}CPy*f5F#>~>fB$6T1^0oRr$$EpzPZeny+}*4_9qDo8bITRV$roC>{vl^ zc|e(1reg6X_xPgUQlHuYJx${aS`CK7ghYrpnW074aZ{x|;!OVV@3HyE1k(ipuFVld zZ 8!ned<5!h;k5B*kVo3TPnRI?GV4Us7)M#|nyb-;Ioen6cQ0*ck!r%u>vA z@PeeS^LX5n7B`xVhjWOKl7OuWJpRe9UJ|mM9q#&6c>Vb0=$_@xPmEV}+%h 1L%bDuz>%VlWX{lsC3IkBj8aX{c0RZ=Mil;<7JYquI|9ez+HM3t zb#V^m*B9VdGTjzUFF585qwn$(x2T}O@xxzVewX2m0o%DwA|3~|m^et98d8xq?nVC& z>>FVF@+F%bS)#;6%lC~vmQtN?jw~$H&Qgx{zSGB$ofh$43tj8**rEjKt<=w`Oy^5( zx<@-*Q_PP_e}_M^WcJ*iS071TZK)LG6Rt!OwRO#~zG;ZjN%4dvXTXdp7p5w(=4rRZ z$)#>kVHXR{u0AwcibwHUnyzFZa^5hQqfC3u``wx$G dr(krrjRmyM;usce z-08Hi9|?y#uC3^B^)EBH5QjGsjFiBedte05 0Bim%gbfK}TPqw4({_IwwEcyo-|D+(>DTg`dtKh~&3?zP}MAo4rBz>ytSJg4xe{ zipG4 xh8}{>{Z9^yF*dx5t!1B2jb(AY8L?0w#t#};Wh9HX`nbwb>_d)2IkE~M zihzWE#>+=Nw9LkO;--YFY6qfAVQZ}3Lqs;55nD{y^GQCuf7uOn6;XW;x-bFWyTfRw zt1ap;Ckm8=Q&Ubor(1^8^_FzIw{pa;ojYSyNfJ}TSB3@!UD8`S$#%SrK(76`lW5C; zBM=f5JB`M>2|Imy7Y0+Ur--elQMF)_cB;%cnB5F#c?7>YNt-Ax2Nrqr+p5Rs-VMQ$ zxDc6G?`niMN17)bST?YarBu~Vrw+d6$_LS$0W}SmuPQlEkWzuTX_xvn^B3YAS(KgD zk>bC4=4aIltzv*c5{$8t;=S}S-T7)tcoJrBO~efw=QZdy)I*lf_&b8OCb>saI&k02 z8*p@E*dv2V 8$&UOZpve`-E?xy$tM4PB`^0Cl0OR9)CzC<%hJ(|?+FCFh8dEOr)QYcBP z-unV|>{%}N3j$YYo}pCqyo``ZrxfShG%9CQBt9P<0VwKiLk<2!r)+GHA2aVCfq1WT z!uHJa{>)S}mH0|r>2#V6{0qOvt{T_`g{aKBw}h~dS7z0Jky~VX&jo&EYW!~96JxO< zLjTP8Ev)3IrDe{#Rrlv;3YZ<%M+X!kJts+Zuuq*O^Fyk**o>8luVxpe&z0MF&aK_~ zRs9c*2^6MakvzZq?8M;cV!kJSWS`+)0h+C8kmdrE%C4~6Fr$A6ByQQLN^A;dw|b0t z$icmE(U-xj#&l{AIX==y#X4CC$?NMg*1d9&82VWI=oEap(OSZSpj9646HvP@`vov^ z#`Lg_fcVnFSX&$;HSSubTD}ml1NZqAsQ8Q0HT{&WDHxY>a_3D=a&JL~^PJYGXDil~ z72J0}d|u?YVQ~DU<9K)a7Yzi{-c<|Jr>8OCc}10=p?LOPI_H6?W#W7z$J18ny4C!9 zxwORfJvrpKn)SKVg4ynr!*F_wEe1CtmR4|y1M{tOCP7qrCp5{BXG=^Gl-H*dCI1Y4 z%bQFrzjYS(lY=ph3`i?Ol|GQTg@oPTVp>KUmSyxkRHdi*xIeu-?7q3y5?4>-6&cSA zuG^CHS;``~D9iA_>1Wcj9BQfXEa^(VJBSOug|qPMRYK8#S6QOvp?xqXZ>P>b0?MSY zL4$uwU0)N8Q)$M*YHXSD_kZlh)ia`rg8v{D`EUcGMw4k&%;rh~+s2Vl>pg4Shz@%7 zqF*eT1WYP5;W+W8d01-;cMZN8G4+#p+4G9$A-kofTFZjPo?O=00^gC^5qQop-PMdP zhX 7o3pi~f!ZYR?Mz z;i@&gw7+STkMg8}zQDATvp53;>YNG#B1c)vqr^1$bDa#x=pX0S7J61OPE$*ek1<*I z1Gk7RmMD+Q%PwZR+mt0VF!i}x=Ad+dY?=J_N)o!&&@7xwrGy@){ohLLn`u_UGo*q} zOG|g%_ 1AZz?EUtC3t08Sa(v+i zE7Q~0H%E@DyxcQmEM(r$F8B-IZFqeKUP)~g`k9A=N>fv)J&oTe=O5n#nCprpruot|ne00wxD$Ss)nSNSSV{$c{y zbVpZok>}aJVQ@4`muWYGOJ`_5?CmN%4ri; 980{JTi4;f*S7q`Za37#L4c4CQgv$ajV**LejE1%95%52!NWffkEV9sJFs_~U z0su#{C6U;CfdZ2o6+0UoeT_PtN!C&p{YGxXly-}+&MlheI{W%g4$Pk$6;i-68ym)p zTafC(?bPoFJ3nD+;Zz-mH|{y#>4Btre3rTXZkA^5-x(Ro5z_6+7= 6cXFD-elhOH;~AMA#pgm|X$;T|A+JX{O@Ln2{#$FrCp6H@y>n@F%kLdGxkQaq zrTx{hK|;VCN7Sf-1j0zaC>kOLp(Y--gbXF*4AkiyTPith$%!!=IoljL-R*Vh_~_X8 z@Y+ubldVjHoVPvOFlg{g5Y^v;H!72X7ZPTk?pZW^4olN9kQtpdm^n_T5J&`qXCOI9 zVvI^Gj~kv{5FbEJ0dYv`k y!QzJ~2_mH`hK8Um7!5z@?O^)L0)$txjN%V@EaS1nxDr*cE((LNhTs zqlp90T=8^*)u}gR%9E!6iP7@@pL)e5P| pUOEJVwO-r zrq9!CzuNN)1$`I=zppqAh*KPYgc;q>q4S}Cyk{X<)>F3diQL*V(BMH~OF;^Jt;Td8 zzfNr!UiHJPym-U7&Nr*gx;I*8fuV3T3kYGX-GromL8rmD_k{L3jbDbOia3t)%fINv z|G}ymlAn|*ccj%n8)1wSHYsa2rE_& ddSGX!3B$Umby}A~3cyK1sqM3r9GNU_;3vkUwSJ*Hwd{vnJmdCK^V?EC>tDET z0I2hji3(<}UMhKjZiB6+X?Q;;<@|*k^3O57yB18TX^SyvJ5!s7h-h`YF}+Am+3RbV zOBrG;D;Xasw8d8i7&_NOd0@7|spNW3%<+}f+}`##mSCrNDI3?&zMg`wDunD+?1Zs2 zS<9sH_Bxf2k_ rjmXP_6q4ZelZE5w+iJ5f=Ep=$0 #e>#vV!bqgL&d0=cS`KcSxo{C! zK1da>c9k0gCS81p>r(H_0$H1!O_; |j2UUri!v@W@#2DpAv8VKJ73Y#e53O#&X`xNG_N>88CpY8psWHXH+}m3%n$ zZ1nWiDX*xD=We9 mZpoV)T;&kMCCl4f?Ru z>pk~xIV=Mi=NXp+kO<1o;*;-`kB%kV$ijsO(ns@?q@>b%oIc;vvjmA6tuL-g>}fAh zOak}f?@+J5>m<}QSkjH=Gl#l983x?&Q=jBk$0Y2%LQ|7K67phIk2plJb`((~2mqw^ zDDI()rD)c`QPg_oor-p0SIRvMnp>6XAC0TibELPPBH*!2vcsivLBEM$b ?rFZii6X?cu*aJe~ZK$mMb`oWI(d)vU(Py-T zRLELg#YRg~88Zpx?6KUYaO|{ST*Hi2;&;w~ZujA+v$TsP<6&KGN56k2aMXmmZG$x% zT&2TZ?ZmI80To4ZbLcq&?j{A8=)J*}vB7z#{F#WcHG`NsWLkg`k)9B~QaTJ4`!_)T z(;6BtkhvJqG2V7*k8B>Ybh$+1iY;b%K0)sKhr PYr?})maPO@x1hRJ>y}1r3n400To4U0@^Q+ zjOri1jvH~3sE5@{<2mi=*(z#eCbLN5O~)(fet_!R2kx9f^Hy6iXes}V)LWtgOi~?; z0n{b5cXbgNX&aVp1-}$LF1qu2Jt4+9W`zrL6^4%MLQ6H^-52^<%HUmcvHITGZ{dZb zkT5$wBI4$nIwCE-=rQvlxGzRL)AzFPjxxT`W;u-)IYxl?e|XYUz?l=LSV%-}(0YeR zauSbMF}dC`Wvjj##;`E#5lwcGww|Yjp-PLagc)Ctq;Vui%D;^*E-@Fh5H1uxqA4(D z- 0HDk>KQn)FO$2*z1tRXwPaXk5Gt1@+5?}LwE!4Z+& z%;pdkw(FOIGc&Cm#{Dp=bF Y+Y&sc#H`=c$; z`!;{Al${g@(ppp}8%zZ`phbqk*$X?^QBh3O`%C*OFVjq6@>&-p?EaqJ zy_r;-5C3yeEUg1xOSqbN(S!${n*MR>+}d-O^`89toYuxdu?| k<59^J_ zTG~@_ULWBGX!9b;? I^nm*AYIYhuO*L4_@<72+fILkfb7*sh44%;pN#qt&iUr1xjdKSq+Bl#U@Iv z150cPlW 9DY zPUCevp@_b32!L&3#V=BMI BAWsuEwhC(e Ni>vkbow)+ zlPnlLLXZb8bmG2J>}RkNtIq^fwy-PW3eHX6{Z&{(3KlDXi#>-5z&oo=4R*+^Ax_z8 zb*JAUr_WJ{Pk7Bq2?*vUisC+v5$k*U;r=IEt7jH-oA!zj1}l||v4}o`x?o|m`-p?L z(I4u`KhIFFGD;y50mDLkfEj=ndlEE&g%nrzg=e5zH@e2;_^aE+6umxjG#(2*&WL1@ z)Q;xUGKTZ+vHL0zFP)}mW`Y_UJVU%A `B^fZRe5*K7O26L)c z7I}XlUwP}+yN$(GMa c4L(|D5oZxrvi@ z#vDCPU+9~wh1f&IpTpg>N!ylBN~6KnU1nuwl@Sb5R6{nK`?()&>afK@-zIT8&e+!n zE@Gut4ZeLNOr2zBsdk+oD3xe} {|1&q0;l-@aFn!4`2jltycS%ov6w@0;|aTA3um?!(Pg^izXHVi_1Y zf3!mdoi6i~JPzqNx-4_bs`MG>458lP9(Yvt#{fcpm&o9_oi1VtJtGe^x13bJk1D)z z4Ji+=3F)xbqO^LPMlcJ(0%zv4q8`Qb@9#{f4+9R@D|n
4a6FL6*upGyDzgrd8B$hYEM> zikApd*1-sF^TjqXUwcI10`Up)oyCdep~(?+6bWd+=PS`@-*e5@I?#zctSByxv*& zNX>@v_{4u#ksgfp>I+sVsXXNRifQS<8}|dnShh08FZSeF%Vad-?Qp3GqxB9y0#L`j z>GeBYr;DLvy$lk)o$3c)h6KtiRCWO`3PHSgv|8}*k6YKzol*tURlYno#C!xN$Ob*p zyynY`V>7C&D@||(&+al!>lAFhYfuJ}V B24_0?ya*h#t=GngS?TEsm5S z*qAfY^hlqO6W>&`?cc(O>qW3IF_~cl1jt)=&x`AgITn&1Yn?WP?bq9NUl`6P(@QZQ zzPDw;ZVkN{VfPK~4GN?lA>?mWi+nX^Iuaok+W=!DN!AWdM(BT;<}GoTkgGjyKx=-- zo`0bv$Q%~vXxN9L>kRB`+rfR7@YrJ&>sfjw#)u!6@OLG}QUHtqy}~h)KdN@CTSUSu zhhVmxm@Q7q$djQG01ke4eVbFYUO`VT-F2oGB0n>6SYX$hRZjX!m(cYGo$(}79lyhj zDh|=h(+vG1g4a(T#-ica);^Ez#J$kJm>@;jRlP-Az6yN8WHR5sqNiOAFd`WQ0?DGu z#n}6qGDu}SMsnaJ$STUjos9_+4D+O6rTg(gP*Xrjjns3^bkXKgE&DOI3mGudcM!e< z3{+L-*^cF6^b@&++y9e>pCP9aAa$6R+gt2zSL4A{ck9<<#)rp87~X2h6Zku`zM^_9 z^@-{qcnpn%grvExO`^(k+*MWwV6%;po4XOJ$sq$&%_BaXsiA0no5ztDoa|ZlPjle0 z{k}So` dq)k*7&Vf_ z2C?Dqj(1r_!+K&^r_Ty~2~5sUzHj_+zFwFN)W};Kba_@`ihadnPLDi9!FwW*G~}L5 zP5fIlEMzOf09ZYZQk%HSYSoIwea_Mg>b*3v^Gso#x}7FnQ#mm~M*a`PkJ}`OQHKZ} z%fd4-Ex4bI9)s+ugtx&aXOXrY`{aJDUncsA?@Gg|EY3 #rRD@vVX1ESRs!J>5?UAuyd8!O( zHCx!_52n7E?=uG?>z(VR`#%#nH2#Cf5e7?I2=;_!?x%%iy2wT6PfdHWwtP*D_ssXS zp=-uG&_o&2D5-+r>6$-An>HD!VV<~Z9LGfY#Ta%dC_q8;vCQe;jT%_`GwT?--5dg5 z&I8{unjk<0qWp!Lb|9YD5T--YY)_j8EMtq=2EPyl)%VF1GNfPrPzcp+q8P)UwQOdj ztCY*|z(?`nY;o=`rDPaQYU5ZcRze5>@cNL1QS22_uk3V-OEOc?V)NXXIF!94VFGWS zR0%ZFoGFVyg$`ogpG{U@ bT{Sdn7b9bbT(H!~@+Sr~@8l%$t z6=-3Vav4c?%nEC`jBd?wFSxEwLnVfq7gSBaFx?WP5(Dt(Gt^75=D<0?0OU*l59W}M zl3oNlFL(2%xCNKvhX8-0QwVpLn|OQ>kj?3uPVfx|`a9~U#NU3s$+KCkp5q_f&KHRt zmS2LEg<#B1+b4KIa9<#7{*yg%_`ldkN=r;|Ed=Z?UgWwrisk$T(j !ZN4(eqSF9kI0=a7;cjrs&SngzDV$({Wq-BYfvLr9#8= `a=&f3@+s}x%=Tk4} ze^AV!nL*(98{3!+9vvUL$W9IvrLm~z3$4Zh@zo~Vo7lk$J|)#bH|DOHQl3lj!^(kJ zpc9l{`gtAWGNbROw&h~YtMfE5OlHLgz?1Okeg7TmB~6xJZ{_;lfK_AT{{hLYF^|mq zSAyg|lR~H7&qPTdS%uj*X82IiT%tR-$c@~}7+ZbQ=}sk6it3K {Tt*|cq= zDk$CE-HmiggLH!+-Q6unH%K=KN=kPl-6<*E-5tU=c;C-kd;jqV!dlnNH8W?-5nYsj z)`a_}6HlhGkLjdP!L=oChjbS<^s>M1+q5GxnCiL4l`mgQv{kO=tm>t?NNelFKPdCq z_am22XL6qW 7gM-)7C-|?)LO87*w7+2FO{$*2v~l(t!^U_buSAbjjIQL zOCE8`;)%y~oEK0mjP+@QC$+D) &pKRSg-)M!achHek@AY6Mp&mp(%uau$IId%BJ zF__2%3!#&>__^EQw%o2?&g-ZbYy~eo(dFI;q6s=kitS9CO)aGEbZ1`slO2?U`ASH` zCa-8dK#?GpLPcnPFkK@=2&{>eRVAJ%kr zSu4Uz3x0k2GCHh6x&KUt%smN_Pz?PdI5r?rgtl;Y**S;J=Cx6={6*QaQJ%rUNs#m^ zxlvN(H@zVmBIqx^v`w6hfe{CdbAoc@!x*j!{w8wmWb>+T_lj`lM9-wj$<*uB*qNRj z^(;<%aO7tdphr0R+x~>*bI2Ansj|Aw3F+7sxLq-M&-BdD8+r}s)oXY^KdaN>y~iNb zHM4B_!?LAPZn1j0yCuvvutr(q1}-Yg&{%3^;Y0&l+kZhtpU@7q>&SlM^2-wZ`6rm^ zgyc$^CKR?iEg$J*M-?0YP(JeE1U-7WYvqM#@XBQz@j?q;KyR7w4JjR__R)zb$;GfT z PXQJ3gH7j(yaShd_CYr6B^jsy;o2SRj{%=Bx9G{3)AmnubJS0#R9BH zZl~YcH;2iE&Gw4^ztyQ|Z{ywhAXNFk?@~9iuG^{W% Vj;Wqg@%- zYb0%IfdgOBT(lgew3m QFHdg~H~8pkd-*?J%9`#lQd%$gN#cMb0+rVu4n8Zpyy z&Hxb}g|rK$3rqTf9Wv1_Ni=5~YnY=oEO&)@WwFQPge6>o02WEr+>8Zs9Wai}cD4cI z2wzriJ!*1p;H&$JhjvBS#W^CK&j2~Rrs38{FJies{_driNJT#XU^k!U71 9AOZi5AEU3}fvR!3Ua6frd+`Lb53C+*GDutuWI`OJ3B%uUWz5SiDJIW4{ zLW}X4yLiz*l(J)_c1TOG(Wt)sAJ2$e+=zC)=Ak9D&UV{%WcCyw0^gGR_o$@U*_;e* z1>f;+uXZ^)n_K>g(06NeCC9}YeSvh!m(D>^nP}_3OH~rAfdU!B1!@soQo)t(Z@q2= zTlILjjV%bQ$*<9v%qSPCTr9rX8pLOjdoP?_bV%jTmy@2Z?iwb~KcK!-5&s|jMqvXR zH7Aw7+{?j<$iLJ}cX$1?@u9&Ro0{FX#4TfJL+w?kJbh4gLC0qP7nM9svmrk!II3Y0 z+J&x9PsErzG8C!39zE`EBEKvPq!pm!#`y<`@906 LQh?03;cQm~oTK;mYc&FOZZ7*MeRf6!!6i zf1d*9f!Ju!Y2U3@r*mPsz~}smN{*Q~^sVc1s9=cbQg2bBdyLAGUmi2!(cGTl$dvrr z%ZNISo0cYbBJ0|$@gJIUJ1V2?B!SCz>2XAi$f5T?K9Jf@BtszBAI&NFjHptV0=7bZ z+ST~hySkbmW28-T=n)QzIDg4Pz2d}Vk?H^ =hb{Vob THBW4egbr-iyhFJ;`U1KZg&X{k1b99fg!~Xn5k6nF2 zqeTK#S%v##flO8Qy3YyPd_U^{1nICUDAHR}y}X}I^kP4_24gB`@`Vd@OQ!Y6vZ z8tTs%NclOjO<>!Lv @YaYaH+Y!n7yl5q%)m-elf7i=i+ zE_{9!OEx5EoApa+KrDkO 1HCxH!L`F+?>kny$}=C+nt?+@PvmSV9XfqmRLQlR`y$S{Tv&!uQ=TqgJC1 z;o*g_PJ4{NeQjhfQY}EO*#?tZ4c$d(IM|9J!pum&A1>~#xk_^$!&Rqp!k~k-Xi6kP zUxz^l_QO<>Y9N7`B>kze!B0yaEAqE+oSYlSu4*l7a)Ft^g#~me<(j*FH msn3*q1WcS^qBQ#N atUn|UQjm5_hO zi>>uuKt(b5EY6R-m&b`L)h(sf_z};l8dsc%EvS;j3oZ4#lJ|Wqjwc2refrsV_Qr1# zryH2d&Es6w;*(nRO;k n?}UB2}G zvnqm8>Mx173Vs(YZJj48D33dLDa5)kdmEe;!9A`e&1pDWtToT`u@4}V(F#{(s7%8P z0FWS}C?ruS$HjqrcyE8ZgBCXutsJE%cwUdY_Cb1Yl?W_aoCk^3`l;b_t{XWVqR}X! z1gF@LgtVR?@Ox_g$WJ-3qfqju_jC{S$8R!|1_&fFN0%xCpIp|L#zw~0`+I8p= ryttAk#i6dh0d!R#KL`v}8PJ8VhVl50WF zG?n}+VZIUuq(pR7u%`F@K@fIQr|S+iA$BotPY 5GWH3wVQo69xTyM9??Cr>i@X{ZQfOLfp@ZGK|`}<7?zD_1-WH zx&T}Kt)Y}POkM@AZ7*(R )cYvG|dSV@WD+_%{yJ`pseR zp+&UJmct@k ;=aLIub_~Kge|CiXH~$UH9tvuIHfYKir=u?x`Ge@OG^ZqRie~ zQW`ckY>F(uU9IOX`d%TSv>s@Z)_upc2kAY^uqjp6W9gnkLw3ePsVc`X#nfz{`u$`i zgJ`bBh+lH3It97>ER=N-Jx9s^;x!ye95->C^?&GnxE!~(l(wE$?vzc!#W}2~GgPYL z77brF*d+Hd@NTO-H6xrRNR@xSr=Hg$@W~Ha;onJ?#?%#_l?aZ{tSqE)2 h_fcX(f?$f|BBJnW#)%y;pTSYyRn zB`hoC4`Y$aO+zBLnjdn9{MHhFbha7T8=c2rcX)igIgz$M`utq;Y>KYElh7@s0Id#& zr*g9tW6=!JGQL=1C0t#Nhi1iv@z3gKnex-5*4*`ieuC;cDfMS~{_}pURXc@~30;!m z><#d?Nnhn03ST&u)x3KMd^*I?tw v*K$Z>cM8RBB{T5KWNJ= zr)9oRt)2M`F%p;U9mEoDO-3&L&W(kKvU1AFx!LC_2i5I>0u=!~-M&QJ!2QHr%^e}9 zehO}_RQQ2uGh_K&L<)%|g^@y*=I&UY$|bn#4;skp&8k2#>RA%%gaQ8f{>uOfWtNFf znO#S=LJI#D!@Zi?x$q&m$+>T<+9eqAmqn4O5h znGSg?(qJdO;{H)1T(
)-lV1kPDP-8jl zLo-P9rdbp1vlFWXkJ$LgEOm{>+L>P#hB8GpchhQlWw#O~ZDdbwU#LZihyKk@1@>D* zq**#8v;@WD=&yh$*U~AJwkt8zX1)8ii=Gz2cAA(tLni$@b=U~raVP>1T5D^5A-Ek6 zV`wes6t~>m37~zP;`cPT4Zq^?J`)I$jGZq}4l4f|XHE+wj+Rg~cjG5wH$Hx+?m%u+ z*=t8S%4)!*g>P$yG(QWelef;MQ@~CoNsD%2Yrk{dG2y-ltM`-pS6Op(LyqqxOSV*a z>aXv){_sRhdRD51j=qjezV94Yn6(^fJIgCVlbqUg00JA^$6EC13fY^9J~vm(_a*ak zVg?Y?YJC cGXBFMpz;wQu)-L547*%b(I*rr TO@dp=sTeS8cwH=t{gwp`!xU7D3`ibuMdLf4VOk`A!}le; zO*koWJW>_y-9mdbBl+|s!==sc(^`s=@Vl6xT%Dzx2Di=)YL0JPE)Is^YT% yfR6L!^=&XqGY9)S2fH#3xY3*K z=vkVn$n;Wtz~_?1);Zm!yIis(%w-;E>_&xZ+K6QrFn#A~9G)xumohfns5<>#q+S+p=Y>bHVwv#u>Z1&c}NP2N=){@K?JLy_FLMrLhVVcrMNtgg?# z%Tn&LaoI4_bU}Y ySW)7 >XIW2-$i< cZ|dd*qiqA3W;6zZzZeax z*Jlviu-sVajn9#h89pXjuND-rHGhoq*t(UZJVo`pjEroN)h@cj7ObqSpwNlKWT*`~ z-Sl6Oau&(dp)wxH0@tiifbumYVj~++T!3}2EzEmUtxicer=%?WJ()WFhQDquMmfwF z4Fq6Dv5)i#P*Y#fmUAXx`8#x$z(rG9xzPY|HT`Bua@`h>V!tkp8DJxL;ML3p@X!KU zk|Mo%OwAz4dSCj^Aoho@S qx59Q}(2^)Yuu-a$X#Ji?V&h|dv zp&G>!qn~)^`{{V{#OO^?a&VNzm)jZs>1!L5Q!X8`&8F@G2DF5|Wt`4OzQJ Vr*U)Y(=X|BhSMh}Ny7la~ z;t|%~rtvUypoGDQG#Lj-F!(*1Em@MP5jKY=+YIXb?MtIT@km9zpez3|j2=`(9s0A0 z3JDn 3UseW#*g|A%JNTj@>1y`Fs(R2uf7-2u6u!-g$iB!vB)Y(5;DOj7!lY|=U4e6p+EPh zBNOeB!MXDh#V~Ez6rlON>T3yot&6_dnJ7<6IXmVS7)kp?QkxWMX@1W4bmMBNtQc93 z{dT0J40&aDU>3KOocK%=|A(<-(DlsNiCFs;AohizimT=?7bjoOY~`0X=hL(({pk2* z`5rewH2-GPVP}@GJ{`gq%Qarwd3AX;jSRRwmp0~JzeT2(2GFiu2U@ucIU^zoJ&H(c zNy|$T;>*^RRm7L^UkhsfZdA{pxlqEhrypfKKuJJkulC`IY-fRlGwf-|6_2V!7`Hw{ zT!@J*Vm@&%Ni0@hAT-=8sc>AniY3jGLlU>2L#NBNx0B9*FvfG~+#qoMia9!s`P?7R z`XXy)!B~D9t*MR;F*;XpBJ7fr;r K z(tDC4pQo`H5 QT)V6Xp n@~etX7g6UEM? z*}eO~{62ZdSPEY>_d|;w=@L+*W2$o&eJPzEhc%el`dYEVoByVJ@I+l4UG8#(Y?|pm zJQuROXP(P;B+mchxfo!Zd5yUQH!H1Odyi~sRXVa(9grlo=lH_jCLH#X0LqFIVI^_` zw+DWWmWqRoO=b6eP2c1sI>JYB8GzEluPTyq7iowH!>=t)%Gdm9SC}SjVe5R5K}&je zmQy+Xs-`A`tfEZBq);wWLh;sH_Jz{m1dHc2*&44M-zck90>cPfHJ%@nLHExrMGngW zjR3}Am2MCHw%N-PA1hjkL(V!sf8q da2%@>!?*Q@prvDLco{E-%AQ3k1RADv7tBXgI`L%l3zz$uf;-vN$AbhA_ zh7Q>Y`J{t66Qi0g^}}$)arYHiXSR_6K>1^vF}kOKE*vG!o8wK=%3$8-8Y1`O9YV@@ zj9{9(>W=zHF;SF(q |1nYsGIIU5>^p>xkSXa8|-Jo|*!(fm(A5Ds_C$3i+RteVXSD z@(-L8P4CItPSSDRX3Rsv)|h1(onK6Agr%0Vn^D8P+MY7{W`Pv3N`rSqvm~esWgK&D z#zRfZ{SlE(tAPlOw%>rhep7tLAJf0e@SqUB6RhdmuR5|{mzIjs>GFq3_s5-1GAd1D z_Kcnd^ok}Ro(Ko``yti60P-4+{zsNLs%<)!H>;Sg%RhN!x|JEHK6K|VM=fScJ-kW! zgU (2k+RmM-X?Gabaj^QQXlN9Yc&Um`EMAQ2b|i1^}Xb zRbtI RP$H0EfocK$D{6u^M_3-xrR^o3@`5kaM>tRF? zxLXO+5j5{f%4z_)qcP;yZkI9p&9#nUse_>S1qM75t*qwwoy$+1eN5?Rng&M=DNWI+ zJL4uxuxKvc*%copTT~Fi9|ZSJXZ6t4dTK$Th#Xd2^&qT-rKD*rf?`|3prml{CI(Zh z@9-UYxE=tx `4mhG7n#_^pTj h2Kruu9|O^8tPS?%N2=VSTe zYUYR3BWP153utiMRG`@wnL(hT04q{*D!RI5SaGysp3dEfaDF%WPR)^=qm{Xc1LC^t z73d2WCjaUfIS$Mj4mP~XxlLyM&OYF#Rjl5P_4UWbrPic7{1=Jv^T(HQ(K_9cUuO(n z x3r;w8R27x8(z%gZ zobO8z;|mT?QD|$2kW6K! ewMG)r0K?AoK=(xng|MOK5f zdtjdfMLuUDXI0L}m2V(f3?U2AxQ8~;B+R=3g5Yc*(YIVOIApP{Z@T rM z&XlpS6CdFJKT|AHiOg9!H}*Y)T$7Cd(k!eX2nGtmJm4hKq@$ENM`-b#I|mQVvb2hp z>%TuVsnlxl5Yy&5(`w4khXnzFJ(FhL6~SNFqVmMzismWT+~iE`@B>%VNwZ4=b!=UU zSY4Y`QoAcaWU0A*;LXGtTJcCp+e+fa$6Z}$vPSgq9bN!o ;7drcyx5T>$p=%MeV1N=1Uakk;%?cJV3=T z+y9!d%KN*I^fX^|{c<`I+ddFP`2^Icv~X`>xm$yJmSjp3HLJoZAQD1C_2%1m zgBV+kb-~{UvTAF-w?CZK{)Ix*FwlOKUWcnVkS$kgEC({MOgc6ZGz`Pm)oiVmXIH23 zwBk$L_+&);#%___&PH%oVU#s_y+>T6cP|q;M+_>GtHMs@gxYPI^c%X!K?_uiOka%R zMBn;zHD;O~ezW)QpH1Q7Ikpj^QdM!*Y#dM9ep83PkrTF|R(Pw{^L?+8;_NT>5FlGH z+fGMmX>SJ!c1si306l &Y9;!=+O z7%4$cS)>B|@}wJv*!gNzh1nxIy!ppBOPG PfJ>G+&q^kvkF#cIr6lCypbFv7M*Npbacfw@mmDSm?o~ymbFE7>e~( z;<8z$o6(t2!wWWS2^}ro@1s(#vYpVX5G_KO>K( {osgT7Bfk;I}kRg5*B_bu;R1FhUn&3YGJf{;bq=bql-ch*&O*JIwn0 z?yI<3p?=7DaS%@462xQEGaOq=&_pn_LZJ!|HK9vAg7BD;Ug)|=Ge+hcy%ps8!k0WFDEg)mb%Q%% z8TwvWbW6p4+P1o0s;`QzDq|X$#Sf3E@$QO(c1)SHslM98wC?}Hj3sj&p3*5P&Zd#N zbp8SY`ToJ)R$j(raJtJ)5;#NGsl6tSY?zYwzqm{i*4g?CZywGrjWnq%FcRhOs`Xta zOX}v1_@C(Ee}WkUEdrCfsTLuzzoSHMVYCs$AI}p@@svJ&M0Z#AV7{x$_Gk3IMgmMv zw#BO~O%+mMj*BFmB=m%Q%~9QWyO@INIlF6^Uhh#X$4-2oEcY{0b*_vsr;t`(mfT!! zuf!gT%b@7|3jyccp5*N>jQ}<~b@Yt$w++$TmwTg`f6!a=a>7n^sd~;JG&S6ePh$yN zbAw-}sE5S8-DcK$(O|c_z{nE!XqQ;`?K}cE@8NpgSr;W(O+@q(>-3d`Ti6lOI>?JI zT9A|WYZ)CU@OU4#Lf8e}-TOxfvOr>0=CpaIe+(DiyfSot+ND#=wKPWP?1)>`@5y4w z_V@{FH+VhFB@Q_qTN{ R3aF!x0|lFYcS;hAE4?P z?l-zAx;lKxufbzaedl!SM+hOV|9cIA%jy0TeN<#iz}ix)81`ZrZ{sCl=-fWy$^hIw zLBN+L&vHE@BB#Syl#d>WJ>N?>F*;=rNX{;u542cbligj5Cdqvqw_Kqf`fZ0VM6|~H z5oT?WBwLJPUo1siC>8cg*YlNa>|*#T2Ige! 8Fm{zxu^-K|lR&-?m|q-PGC;BwDij>P*Ve@K vzE2;#XC*3z~K2e zYxmmIIq5jtiEqSGWjgFHF{QCU-Ihr-EW+R(T_~Xl&pn#ohM?FV;!%$P(Kc$#fSp{? zb3!W1yj%YvK&^MV@ZAkV@zF-GQ=+uFnNp74J%+|_#dOZ$p+F!R#b)Oy?^~@2m;L^o zX@rg|&auHwSGtElnsN9ygu^#pT!K*6@AINn<4sFgpqnxaZjyrPxCF9M&T=O-w$%l! zm9|Qvf40b%1eKk=h6Hodw|>@;5QM}fn2!MdtRcovIu(Qc&7t3KYi>Uvq;0!||DgmE z2umZ^J)`yj@Q=K{;GsBR T zpMqpN8{du=2e?E8EOq{Ge6$e$Zwxd}AUX7D8CC{ooQS{4Nq9yFg2iI`4L;J{ir<^L z8%or&vg6J$8nUrAQ!J;w-*6BT!yQ5ELgjNydy~Kmr8DD`2CFx-_wjFxw)5qs0mEpk zkeks6 {P;|aM_p&TWGpM!M>`9?PbNP5!6?d!2HTtJMUiL@IZN8TnYm~z z)$*=RyDTrh 5y)q7kP-C+HNIEMP#O8>Qr(8Y!Q$0b5^ty5F+ z_Suf5 P^7 buCpDq4>v} zLUN;C`0wWi&^`NQC{W3!u!{YEJR@HHYh>}NKZ%JbmP(#+$nY5q84?g63P7L$8|Ayu z;0WdOc$@#v b;QuY6*RQKx+es{%*Ib}|M?mCw*xlqFO;(bk*zD}?=L?8 zS9w=nLGg#0qH+j?&}T?>NEwt`nJIAP(I`%R{-t!^f4|ayD3x8hU^y6(fc9TxCh @)sjvf=lihaJiiBm%++m``E6|L0fV0Sh6hNO6DQXJjwE@XrU6I+V^h%6!?s{ZNC@(AEST13 z)1;0%Jm9@!6N>lmBUb2lzR8xmU5^%g21LTFRehV;x4%C6X!W~V%I?Jb;Z%ZTvhCZe zQ%B9ai(SPQ4`-p3QjH2>jZqHZvExPSN4$piJdXN?M^boM^+ll!G0uu7oAUZ8dqSuu ze?p&!K#u*RwY6Vu-6a4rb+i2RNae)y7j=6IzyC?Q)(Xe_b`uMD{E(PWd^gF)#By|X zmB^DxQTQ&2a&v$GO)8FZXDr)aWI9I-Zn8)fj^cehGNkp{M!#yA`C#=2hG)kiHp^-F zU-|OAf9|il^C_V_5 xN-k&x|^9I=#bzXd}*3G{IN*>bth>3uDf-LdP0 z7h_bNNwc1@a2zoz9T+?*Vg0B3TIQv61l5`;h!h@Y3zZ@jm<%50j&l9ZXNmM;&8j!R zD&)$erAr8U_Rfg_%YPKHhE<@%cRBtC#6?XNv>}kd#|d)8nBI2KBI_q>_ldnKJ#N zwbblBo-a?7&Sfvm_f`mb z26-z(H1AhJ r#oiQLgTrR!`(UsM$f&6$ z7t)AB>i>>ElfTYi(Qb``E~eLme7>wL_{l2*-2(;c(3>1uYVePj8%g75tS>T>thG9x z!Cvc39}c&tuOS<)VLH;rsD(mUg^FBS7Yv#7;CvAMB;lz=Dd0NVX)~S;s wcrtdBTwGWfGVj>(k1Z#M?tR{nFuhYx0XcUM`)WB+ zbJuXTD?QTt4RJDhkBzGW)tHV9DHzFiD8{A_<~13baS5&`Gfj4<^r%+Q3}~f^j+`k8 zS?(CynE@f5sG=R9v(M{#B-$HAKu}r@y{7eDI*HL_wd>V#x}cBFL!jHm4hrbO7|9WF zs3d}^c%DZAK^g4IogefHQMq5}I5t@ykC!~{N0S@Q@6d4RTa2BXb5+wk5t1_JqRs~V zp2~^eS#009Z(8%+F}*$~L46YltaV6jn4w0=laniSq=jMjOV|-gOvPV%L6aT<%|a|D z9eR8sY;0<9q>)n2e)&b>IT>7Tg5!?(gRymMpJN?a&6E3_8WO|gV-1h)cz+VX2fvM< zi`Ud0M3(LXm_zy3S5R(GySn!Bem8v2K0Z`#r)z %^U$s!1b*0DJgR z7&)K?e50oSOo8IJ6R_RYfj7}~`9rq>Bsqs8+j1`k$Bg~sE>_2agmgtuPNf{z1N@Wd zZg&g8w=3boB$m;7ID5=|huMn3kv8G2pN?Uk#yzjz$nsvL``sd+b^sT*0E$g;vQgSR zBlqRnNwWuDqor~7C9dcVe)igD_8m?nr_ru2CynH=nk7C0q=o0lf$1PrE7K{VE<+1v z(5w_~?z}OaDS3i*Bw*-HDLs~Yxa!q)!`e! C?o1icv+YB($K`vzwf(>v zN+Lct1>`BP4EcedpX#rmL~N43ulokyD?I0H;9Nmp;9D-X;nlCO`6405^ma`}oC^;8 zw^2_}WLJVcjf1u)byg{{Gd>d8=Uc>6EAQx;k@5VbkxPG))8@;hkp43R`ZTm}O-G7v zv}`mhjf83}rpB^Gpk$JnU#4+bqb45To~-)2)pp6dxpCPnHPb*|8tkjM94`?FKAb?D zZ4J{wo7Ze#U0jGxe3u&bsoJIl>j ed^+e zx> {lLw_x&L!JRXP4jh^Ng1j?SP %xktCs$&!+A1d;&&PreWRmDIB!i EQ_p3AdKb Jg!k>d74Nj3rk0@S*#eBOU9)#*PrsvtK%-%~xi>_Ja~2u^cl3IYwZ zpQINM$F`c38i=P6Y%0-iP-+&LIlJeOQmG_t^SO+AU2}_G*t@>sbHKBNqUe6QyS!zX zY>aQO27N *gK6i(y^NDmoFaZ%3pv7T;r`%L! znr0lOEM&}C4KY?i;hPN1y5FO +mRA@7E)a7v9RQci;6;m*y{iFE3tN?6AVa_+H0-n%MH3=SKVs)ets45XSQ4U z$u%E>LI!Ey=Yd&gHAf0R$M5sVceK<(cTwqmcOlgbM`7ju<=H>;>GAlfT(A8V2{H;E zsaCa_; %z7`ti=a|DdCtZ~cYvLv{pFr2pT~t{ zJtkUa)GQzj1@@3l&$|ITz=OpmERYyr1*~<2qOrK0=w+*e0GN1l+DoKZs6>(78+)l! z#P6S5q4S;tD=t*cg`uJRJ9h%)>8*aGx(s}?Csx~ecL);^rHqF53-=U8PBo _<(?S#7SmJ)C} z*W*6ZW(?>(X&pEENWApQblWg#O*n=UF6mwNCZ#kC$n!FXnC8INpn#9I9?y9VFnInT zdPDQ4TA9&%Ua=YvP+#T$^LPm22lse_-pznlWe^mpMx*pSB F5_=dG+P+CM}>FvaftaD(2)4r!L^kk=5%T=j6FIpOCUWiiom%P=q z3S$e7(`>rDK6z9=kVc3pAlH7TrP;A0f`ycro?Agu3S30rwRl_xZ1R|lW-`EY8xMMv z|Kv{uB^ycoD98ux{@gi}yg)=VEKah0e7No4NwN~42&bI~XDyf3)8n7l&l^LtT9Cu& zdbYw)_)8Fw0f9A8wC3k?(FL}UJ*%C;fyQ#P4?|hrcQ7FT4q>TB4#({Ccq{7@2NLa3 zq|o;rD6`dY%5x f&+nMoaL}Rrkn#ulevFZt!*2{WEoX^} zEsA-01mIEPkI=$SFnXX0@5s3c5{rMw<^aTs*rzS-fzd1UxO_`<^H5v2iHw~|f9z}E z---KQcc -wP!>wYls+SaxKKG4eu;?7L;;YG*V!zhk_uEwR$J}H ziG^e8gQB6MirF+9#D@aiHi06QLO)Pn_TpkF*vLp$o&{t?Urr1VJeM;luCodvrhYqM zw($c?lB{UkrXq%KpOnC`6vulggsv74)vN?>Prj9Z9FeCZw>LAlDvUPtky2dV>=QHI zYtJ8dS#06bEZiM%rjk-Yg?ah!8&g6|leXU;A1j3B*xGp#* zw(517-NuWOz`eH+SFh{xCn|U`PHcQFiD*%ZRcfmMP}=I#b}~e=lUC9Y=#qk~_F5)9 zkAa|uf$L$OJwBJk*t{SOQmlm1^4UZt7U1(}#||PyM>lBP8Twr=ROsh{0uy7}$w e*IEbq86Ezv;c`7nKY6%%2Qt+3&Pn2K zaa=A?vv=Uk#)nwVRi+HuOAjW{B^mH tiuIL}&h4sb60`%lE*Hw*AyX=%i?m>r~ zOC)Q{vv}UxG_=3qxyc}*D+9Gv9_u$Kb1rp9KYSDI`1zoIbqM@PKmP3xq<5ewnj_JB z&}y*;Fc5SkBI~ctr!X6KM$Dp;SrwwxZa{0D$08_4{ ^r@$YZ_ z)Ao2L?Z#NskQwz{U!?nsQpAjvU&b1y@{UHxF56X8{c$u{>D~wuTqa$<35WIV86vL* zf (d81EfW3wA$O%^fsr(kkB8&zbQyI9kZxB+_ddFb+;du&Yt3&Bn(ImbQriG !6Ox?v5R5^H z3(!bOS$+o00e@!;#!-gI*O`XC%cyuV zs*)8X*;a_(Q@`Bdisgo$%Rh>jyyhI*PQpv`?Due#(ngK?yCOb|=6?FGAydv)s^#$& znskR_NdQCU`s?lB&>ZnWn?hJB%8gp>@Q+;Gsw#}!*RUniOJe6!u2vr~=e(rY6&rRA zr1eX4&55pjYKN!Nz0qGixz7yG#Awao_^c^^*I9@oKKMHaU=Q6t7iuG543bv(Tz2oU zk1S~C86w5t MPJZ(El0+b$4u__+J4sZP>`~{%z2cj8RZ&^q9VNk9W9M;+;x?Am8 zfMCFMw#s}oj#ewVfZo2jTI#2%JrDY16rrxyfdJWq_gymP)zj^$PZtBEP}iIP`K1vN zIB$EjYk$mvc+kZ!$V5+Dq)m)4civYjnXgsW;$IuBq@6O 2Ky-Wx^=bDaSKEXPNi6QcN z7}svqN|np+yglFUZV@C(%!!2&)GEKE_!N|-if>B8Too-~JN3()hPKJ}58O9IzR>a3 zG-9!=gz$lB%j48KPeh(bG9e;J!zWH_+bX#*g?)=zkF>mfW->*oE{uOF#xDicNY4um zA25b%d-NKecFTco17JAUgIZ32SiMwxi*;$j>(i&e#xuvYkb>rm1KVzvbNM!8$PFgE zco8L{Ki5R3&}1MMP5+KzdA(Q2qMN$hms3%h-3e4~W#=s!>lu#@J;2cHS3!rSLlW|; z4Ca3=?cge~B-i3BXG)(NKwAz=6TkpF=($mLeAA1f*uSMT5UsDMfYW&;pk~!}$|^H! z)1jK J+v|Z<~X#ipb^{))HA!E8LiIiRGY>8Q=7g1pKtG=Sf}N;_Q;R9 z&o3dm_}7UfM2*cv*_8T&QuppMMFqk?^Wy|%iB$_+t(HZF4Lk+?fjib8Sgms~6mj$y z{6qKeDD*FO#yed3f# lGjPASHKC%B_#naT4hdmW*RkCFgi>lFOJC9Mn> zzfP;zUw_7j-q5R~kRFXQe?|4ACDRNOiAuV}V+eKJqOJ{2eU#_6Tu1kRy%(#BTX-B+ zXkSHW(h5`@oY3cX#-OLPG-w1KEk(BJy`{v*NQiiC8ow{fYRDgN$ y79GbO?7yvjJeaF8?dB{){2K%RJ<*W6>N gCa* z*1}tFz>4n)48TmF$p5IQy{u|NLkovN`~8lwzf}$o@ab>1ra+yO54_z$7im4qCw?^+ z#!q1zWNREgpNCV~(dtkWfwlEwx*SMeFzcS(k9Em_1U*~QZDJy&EH0l1x{sm^&T>(K zRplAt;}jl#Ww?BTbdg5bfaz$aet#@Q2P>+PZhj;I@AN}P6dtFX*ZJNodV=Z?H4GQM zb#oZMa-?Zw!#n7F*)&QU3K?+zyy%ak!kVB`EmAoZI0z+%b|xhy?Rr8HeX!pdBQayc zb!?&L 5MH_?d)B&;G>xwmWkum5Q+~4^qOObI5e|_&$CT7rk@=@h?-(x)1q&- zE?99Fm99C44oHtaitp|Ya7Q|0EqBFTvv96FA1Qykn<(WzPu}Ff4E=$Hl}@AlD>LOE z;My!aS#9eJ0vTNA4#>TWCE=!dhA{^K15!WvFPC6q!IrdDkAKzM+dHgM*9HNP &8cz< z+PMh|O^FQkbh_T1bH5N(5JgsL!AT@a>W}cWJN^v{)E7pNTvPO5a&K0Hi9n(Fu2{E? z5K!4V!?BnGoLf=h#>dCq&bJV3mRlo1MQjKpkIfe9SwJF*ww{h-l9`z)yh6$W3A%Uz z!C>=d(SNA!D w^K%MO{z z-uTI$D&S=sR)U*{Fi&p* zda=!DX`7tms9;2`r;kV&9ZvU=u3_iLZFuxnSq>Ks2KTzh9f5}q2B- Asps}&ypm)d>FqMPGik+u$pFta|a0^0Hq+S+f&>aSY zQOnn8<&~0_h6k)<173)LjgJ!4;BvH}X(wztluT010rmRzr`mULkV2B*Wz*Dd 9HrocerZ+dI>zruWiNvj< zU`K=^aFEY$eE#sE)@ttLXpz@`G=PL&DcU|b^8*4+f>uq#%$UJt26jn>%nMe-PA%DT z=#oz^e8^io1K0Yb)>r*cA)cCOaRm>&q_{f%@@YPf`<}1nB r6B7@mKo%c4=KE24Z5P#eU@n1AyNRzSBqwZNTmZNtOQEXGG=P-_)a!j zzB*dx#}yGd`|7f|;X>S=zk#K_ZZmaVIAd|94?=#WFcuNtR {+6ZI-&pdK}? zz^GimDg|PcrgSh7o!f5aG`DZoBgs6t=z5kz!y`PX5fy-2VZi<48XY(vVzBirWqYbA zS*k$)I?R(2=-LWbfX=!wQ?9ml1$9v(c|d^?HvCiPEtL q#5~uR$dd z=Hky>Q=f28WT0O0e4%->2OgT7jb2g E)g$|@X-h)mIaguSZIpwo~+ zidu2k0#?4!sFgA;zu)f_N&p2TMuHV6?Q?#pmxY0QlIM=38CV1`2#&0>i21q3eP3QO z>3F$ID?GFw`f6HzGS7*9Ev4ys8;NWc^7=&oxm$g6?p{zI9H6Z-t>Y%L@E-ZSHZARF z25&Hkh|1`ae?L<6Iqbv&1zo>&uBuKN&RYDbLgoA}{x JK?rxdhqiRNo|6JcML*^OWjjrN|5mSY@f!pr}k4uYHut9o(oI!^G zp`f?bh>rya5J Vv^&32th-N=eQ$2ei%F&oY43_>uLM_eTpcBK9W5~ zLIop@FtWWw^gY^ua4%~%2bDK3p+D_gzD}#B?<)aAMPj9yRBoDiK|qq+JK6}pEvpC5 z+NI0KZGR`+U$Kt?rd~z1yx&G2J@gX+t4N%vzf9W^MjZvvBw(-OS-7p9QO%5^0wjfF z%$cBIjh|gM_)1&WpIL%+&bT|ey*&KrsN`3=3g_L{wTt~AH_$S*xkEuNw0f0vk1YCX zt+>F*adEm#HyU(yCBW@Wt^RE;xX;1dY%v0&L_n~suHn5(>(fOUGJoxMZ5yPabodU! z-mXCz00fEk)b{*8uKqfzsxI6Eg dn?%p&a&8Ax#>5}g5 z?ryjX&pF?B$MruPgT-2N%{AwIo?qqnYk@WECi@~bUQ}(#Y93 1W)pI=)`paR zSfkcG&ohYwCWPGt5TwWLHaas;mqF%=Xp%YI@dc(>QOPpSmcG&><_t#RJ>uxeT-JWX zo;ln3g!{eLz}nsta4W%`p+HDNeq(SbfsPRRuc|=A3J01PoHGlF8YNon0l~g=TPci^ zTf=ED>Us}nO$bTg8SffRFpKbwykl!}$y(Ltj%Oz060f-!0JBD1AOOBy&yes!Vt~O= zvGTu`K?s`Yt#EColcy=+5(BV|S#@!s*=wHDG3p<*W(>KTFI&!AY-g=ukFP03f$lqL zBZZA$u`o8z?O1I9Z<+{gu*YM_7Fl ux-E?941)03{9mPt rH!SkIQ!DeN2!y_j0|v|GB2t z#|?bl@bF{Y+V4q8N(H+7$#oHt77Hf`h}?kY#Nk| hpc=wYbTD?_c`HoJOQW3d&zI%(yuf|GR>YzI z36m<9zjft718Khf!ON6RI{ly(z(4t_O5`NoO%KFNCI<29e3>e~v+}gbojKhZGwopY z1gD?$74Ql8lFv`FglFG +FbcXT$e%G&-}t ztpxjQvT&=+Qhie;2n-f1IPFF2i7}d}yb0Z#vke*GTPcvdJh;#juEKg-beCngOx%}9 zg@%}TF0;X6QhB!jfFGPFEex2 zB4;ycHr2@USi=ei1s)PrHcZhJOr^Nd*X+Na;WOkSJ8EEG}{52&YYc zg!;M12!H;x$*50&A{g185BUBlTJ#kBYDu0eASg;=2oU1G$kw_`y@mF29k${bJL+=T zm7sOSh>@t$ wB(e)sRO(8|I9AG z-N8;3$zBUdmVco(4kcG&@v7|^?{@SNCfwN38*2l}iMd6-R0u0B%8|cX8^Q64#D9en z8mm{-Yg6Cv@i?=_5)#ttcPp|I1aIDbHrWy$d )1uYe? z;XjCI(>0QIXwS>F%cq$RsN-MT*~8^~kK>L0H04mK%zb8r<*MQt7XFZ0UW-iy+bk@E zzco=r(%VjDfu%^Qa>W@UuWx(w{60nNz7ePESNOS)Iowa?e^wL-M& s;e( qr+^*#?E`M7h~vl72!6TyBEy|(`(<3{XE z@_|QL{2;qoWou;6 J-H%PU4WP@`lDS9+*5JqQr+$UQBctJ3ok5 zZaSMwWgNGr)rl7b$lmR5ii2oD+#JlG+%>&HD)+_4so@o%?Ksy(^fokq8czxU0X3 z{YC%_F!+P sOVlD~7{daFyzf#*j zWx-(^%#lN`$R5SSbsO@kR^b4Uk06|<^DLscW8}n&AdZq0HUKm*^k>(ez0EX*#b#+? zh|=5;P@6wzwbxsczInhP@-bg98w^QK!gK6&eaDLbo4unhTS|vE%9&1R6Ejz=1`xte z$zs2`8?_oCVzdz2yd+OBbYgGVJk7k)nr1Y^ggnZ0G*BpPnAPJ^JX0GpGeG>RdN#u= z^nKKnzyeO0j|9 -KKg4v+o>VO>!2d_hrc zGdgp4%*Z6!wm6Orw^t1fIkIGK?c~_V?Q@C}PK%x01`B*!cGc4PK#=)TbNXbKBxUl( zci8EjO3VnF+~WDK8+vVfn@gMsy0Uk@fM|A3KuF!$`+PUk#AMKR^xUd+Xc_@-(=TBg zpbehAf91Aim8nz%M@B>^daRCh8*oORgGtZZ6ShkF%y@h(+qoFr;uEcmPE$_hN0~*n zb*GR4 l4I`MfV5*-bLWZ=n#sX)@eAjhBJi*A=_Qjr;P)Bbv``${=c{ zD0`Qx?stD*xDe_wB!4|?ZY@cYCaH)AH}zN{2m~H@Zw5OFxOYmaaDXNA)NcA$dLysk zi8=5kKG>>7dpuY%zm2=_18c%`DjrmN%?D}hxnuP1seIjZNkLD-7-;inD#5!0)~IO{ zX5B%Q_i%dXyaKnjl$D|K!NbMAA@n%f{~-dIz@l#z2!^}X^1MAOEKZDclS=6cT+UrL z&F`oIHoUM#6TaY;GuS8+wwe6M1;irzibD-9{I(y JVYCVFT8KbK zOEemeHBozn0V^FQ3+Rr?U<1KwH_s{X3zOLT9cN0F{X#WhOi1Iv z#IOAK)T#vE`xPSn;=JJqGczl$)fF|>0AuN;0+eijM;D*}yaM(4<;VdGtM4bbo)*H6 zId( e>g6_Fmd160!}UDxXng$nP sUy{1 z37iTa8PWrEc+}smn38#u9 OB*uLK)9DTpvZ@_gRWGRL95%tmYe)Lw8pf_-el4N?)D$O}|;m0fl9a`zW1KrM= zy`?t>PE#hGUs_U46e4t_qMJ4b@ZYO?g%8{ep5;`Oga{Ums0XQ9bZyNk&QdXKc<4WM zPFem8M``DI9&X4Bu#dIAg #tO&N(1E(5_M cg{HkV>(T01f8NvTb{&6y!MUM^f5q9TDS_yk8Ws>6`!>OM zZnPw#oaRQHKi{3EDG#1f|I6LJnY#bhwJfNn5{tlGtKV8NW%*|tYQ>Qy@pqtKD3>R+ zNz^>0N$Lm>lFlM3aO7i!-G+h3)(msdzrLz!NFb8 zIZNhL_w&!t4h4uj=k&@lU7S9K^2YU}IkWExJ)x0OgxFoSqi{u+#V$0W!`)F1Un!M# z)^K^H(t;DCy3qN!8B^8G_XWF-nJ{>{)F#`|qcT`+!(W>5=b5IF _{oKqh@}1 zdvO!Hk346}38YwPq7neiUpj2ULCf`U>=trUwfd1gbRhag63K~bYkktx){ZW1g`m`R zNJVdb0icM-7LLLxiM!Wr`tcC?>@7%YMS~6&5YFobX^~mCb7+Ye_7c%D9(b@}@@bVg zZR?1k7_Szz1fJ}JQEe7u8&)0Pc@_5I{WRjUgukVg 5;g^qY z?8uah3M+Kp7P%5j)@{$g 9N#lvO~Uu&xHg z)0K#AAlvR1;HJzU#bg%&+YGHSHUu?u=D!w^)N<#mY(uayyh!BQ*6e&-Pu8sTQh9GD zJx*jta}BocKq|bK+tu3 s<@xh&@HG*3y~hxhq-T;t!rty&zeQxZ5=)avO1fkjZCWkP7v z> bm5yOS_U8_llynn(^`uAgyx;gV=Ga?u_RW_&)*86;K ze(GX=GS*TdZyqp@|7la1Bzaj8H}XE2aiZvFvN(x-SVr>qN6xz7w`3J<3U>8pZ(}Y> z{OH5-Sb)8n^M=43f2pdn5K4OM*x-?5ayb-B*AJ7cOD?7N=OaZ+?dg|lC70)d=QE+n z%gsQXmoL3Zg qdQ{f5X6@CaP z7>5NAMlyDWAr3dOV;!vWZr2%$>^Zje}X3XkU J9zAw_Au_jlbWwK{!}8 zGbJ_gC0J5GfUnO74fN@(P3O7OiE+UJLrEh|_MAHQb4CS|V5vM$7sZx4Ev=|OwrIzz z`+F@~>}BtY%#EBZ?w+{zRz xX_w zBw#t@kWNyj;QCf^*9wY0&{KqV)j7%P*T8)f)g-labH#s>CnQ1!fzy7#od?-e7b=Fq zh6R)VTLRHT-|-~HN~x)*4QeIh#X)uoN4iTIRt3u1Dp+2$(M$mr19${4ZA!=C9j`vf zTf9E1K4bvERfrT4nZ!(UCx5*XaQO2Z;+R++k8#+gTJdw P>@@7@77y-!`e`8jqg($Dvb_udqiGwrSY;G^8TstdH&|Fv%dj~OjUGA z#59deovq=@ rJOD%kWD< z%3YUxSQxVf+(lcm?F9tB!=^NBmn9q$lDee9VfwZqtZ;hw#+H*`_-H?61pLt-QlX_f za8d3ss3w-ytLT#*<7FQhq5IA`LmFj9#B0 I@d7$2rH2Wv-44pt*Mu# zmCSYDt;eD(OYLz?%`b?%7410yPizjonYEu&@aPs&G||r(xKm0WF5RrWKN}C)(7rcD zhad+Z$A5Sf*nYn$38MYB*?ZRUL1J!L%khbxhbR6@Adw)tDO;bnS>XE`cNKnj$@(mI z--;J|Svo-&Wiy)-v~aWLRQAZf MCivA=i9N9-z>X}Ijlj-dXZE&qO^_rWWC3n$Y{}JUAgVTV|i*oP>28lBid4@ z%o(}FDNPCK1d(3TW027x>#F~{X_lL_7jr(!N%*neY^7So^`zQVEk71)Zzl8p7C{N| z44JTitN6|HM8ylV`DS74V*l*<+NUI0;H|f(v$V|ftiD8#$yyzfTh{bF`G<3yvP0bv zM}Lnjn;n*2eg~%Q7e|b8W3^CKR!8$KzRR8RDX6Ef;=&78kl`m7qgN87Z1ZC=uOWax z!H;PQ!)5dozEeJ>A3wwV`5sQkVVkBZN`Mt+o4_fid{Eu*4MqUt|H3>5Kz;xK%7onh zpUmu)c?bX@>sUZRZqyk>0eru1H^&+O i?iTy?}OZ#=tiVAf?p EHi{a7DegsDZy~ zOvRG@_0@dE#}+%{l^b)0bN+be1TNM<5%#>s2K0AlG_Mj7ctHFETnKRPU_RviKmN%r z1N%^>aSseFKzHEze?OImVurw!E*s4I_o1H={BA)|{T6{&snq`jLcD@q0SB*$XTU4o zynf&pDz~5aZw!!
t#g5b90#SiTyo%#40WvFF?+U;%(H$VGxXmLGXTB$t#-2!tp2)TL zCt;E0^<-;K4c|Y0ba8>OKZRaokN>+LV4H<80=P(i JtiEP6LZh6S2HqV8ZsgOtcz-X@!aLIs+>H=@GvI^l_R5IEi3XoZsjxv}5&x)dJ0K z>@fjOGG&=~>mCuEpMjNyn{aB}=C-+$!s-m8-jw1}7Hqw`wSnRqB@uRlw<=Yl!%V0t zgYC}}io54E-WiE^1=Lwx{{YC6f0GBmOJO=st-0Thv&Z&@Q@rfee^IL^I5k%PuQ>&N z%2d25L;k%ykZs$b^N9F@ObXK2wZZOdO6#ejZn=$O%#aC)IRRKOq4oj-7=WdE> zjQ$tlBjUDHlr4Wufg$}Z@3CJcQTckyfa7r3fRONSL%XAd)%wjs_8aX83B!M7wjQRX zx8Q2D*_%oL0WL^ODcs>}{Uxe1c`DJq$RAtaKrm^?`?i?%y@b64*avYwJjFXT%L6Lj z7thHW1}P8C^ib?P_N2%(6Jd@$ef_>VUS*(XE*R+O-vNR^sBbq3@bl#jdu2MRE+#7A z*&HD3Xl9F%QxW15_GL+dyjEHm9X&}%RHwYTcklxZaPlcY+KwtlCF6x`@RN8QeCjm| zH);xUm0{1W;;m`csuqw2+!^`P%Dbt;uRuU_d&?)FX(}Mw? ^SL#iU__oAX~j<>FwHPSy4*mc!*!gV&3wGqBz zLJY|Me=p8U?;q*mXHGUT5teLr+{Zpb82Apc;ICN6O{UE9E=CxkHUK}BGQ8ZM>nGi; zGuJd(ThxOXuj73*^tOA1&WkU)W7m~KQK% LA(y$Rv@8oI$*4G z&EWZvjtBXGab}IMpTGWx=AxIWHY*Q^Rc@vuy0>ojsV^#Y7Fzn{8zAK}A=c@;k)DP& z;F=m$`k`#S2eL6M)FAvgZhdzoU_@GwSNP~qA$Y!b>(%Z~-V3BW+8?OuHn`yQ)(!W5 z7ifrhlfeaak}qQ^M`8*e?_ZHaPV 9L!Vrgn1tOleQ7@Lu^?*LduJKH`=QV!Q09FJRC*Rb8BhJ_o%TRhsQKy^vC zeNfZu@zfJDfe)sHj+&${`r?ufCWOg>5pB%O?L(Ckov=|@E1!}UM-kSGh-nvb(4vHb zNcn3X9R(8Ln^B8@wA?r0YX&iIW%;ipFNCe=Q845nmsei)f>-Nv3wbUoA%@Yd9Dju_ zAyzuT8B?(_@aX6}lg>s{Df<&F6B*UzA=@nto+ss2b{t+4RoKDp!!)H$wTqjz`v$PK z&T^!Yn*;sbweO1204d~k9GczEC;?8C-#^L$fGDPH52dAHrAGk&{_;N@{a)!BhlBuy zPC&I}#<9Pb-J@&4&<|1|s=`t9obMjz3imuMHE(1_Z*eT7wt@`^rz3-D?{c5&J)cP& z= 8s$_T07avr0pMd@Ag}EK?81_{#?9I~USTK!1O|Kd(yeCO8+xyz0|48b z1wSST#s=0jEcMR&6jZ4oJ;#>9&6CTabC1(aKI&4ACF77UTxH0N#>DsYj$|Eo%+uN9 zDb5c8UrNU%I>VLp`eW WjiCnGDu% 5{ _!AU*0tSLS)2U$K1l4m($gJSa60ipm{S1=7Yq<`{~**(jC*kB6+z1 zPE8tjr4ql=kU V@4&2 zz<>3uHsCPd-{L5=zU(+*{U)=$0x?Ku`F_G~5J{Usjfx2(7op0q_hy8R9_LuCKiBzu zYw!a^IdLGM9n{Yk{(Oww`H>kIHjbB2Yi9Dxa)+HqD`;^vM-=N(h;d_p>=x4FIeQD+ zc$Q_fchS26+0jeF$zHY$JVM1bD%J>y{FmoXvqoy*Y)5mg_x{LJY1aCT_#|PBo*IVR zyFX4(R)cA;iaH8D@<1C()>4w)KnPE2OkaMI5Gr|0rhT25gyFeAga_y5mj?<+e3Qq4 zI Qh{sQq?;$aSnt}wFAlI*t7zxD%g$t4JQvK|w{~I#9jBqChu&L#r zB;sxFkooLln7NJ1OvITbjx?{>H8%q*K~TnJBskktb>y tBjmyoj+FFi&f}fb9t2LSPkb=G(6plX$!zn+J`#3sB?Vc6anUlJ#Ln_wr*M!3 zy2ofl0~sd%eadZ_q3-WI?{&)(CHm3#L?(R9fU4qbFdcMMD7`~pOByQ3k!AbR8I*yb z#h^w8yf hZ`{03<0rvj zUweeqHmO=G9aH87;&&G1y;%x3X9La+0Ve8$e?yrdeinmkJEeg=#i+C(&+*-A{7*_U zE`eh%nulbjdTM)Jj^T@;pWZuzL??0E=IxM)y(#`vXSQazxy1|mBx`2g23g0Og;Oxw z@5}?um1uKX$!L~!wQ?K(HG-NhzDbm)&FchN0goEN=+@GLg*ZW%mC95ocvf{p$WCLA zniY*UWKR!6sSorr$9!PdE=tTwq-vyJOff_T6l-m^p9GcOUAbxW6gu&E!0n$HX_#QL zk%Zj{&`r|8^3t}wW2LZa>>#P!OVR1nj~nPBF6@=1)3|k)C_WQELTC@+6C}nd+_PCL zQ{l@C1KP=p#&$;XdPOahSpm*MlL&g##B*J)3=iPtRM3pd#%B|zviaLjMPpuPyKjy~ z-2(8!HabBm_YP1npsvIMq{MMA-G#t}%Y}J Pa0+?ez^i4UxaY{99HB+Xrd2>e7QWPNvCqSJQae1nt1~JSuT**jAp{_> z|7A?H5-O%#P1kY9;oz*U bD7DcL013a9@D4L+>$4?9)uudFo(%sL+Cbz+_g_Jd7~EG@!T|IO;(yW zp0a@(sOCQ?Ctk#Oi^CICls=Zcko?fP*4!&kqwa+&=1%xDRZRHSZ_ !Rr03(P2Pt)Ywz`SvhbjUy?5wHBh z2v8a7cjT&PmkoD$@ k4HeN7MSrC+p7^Q}0}Hv@SQB;V#@V)Udc zF;~ry{^%STqS?8cqjxi~E-I2}x362xYfcY=$e%?FfC*MkyqLfYiOIc9qboCP=fxH& z&wP~L7F@&8MNCumdtt6U*E{1ymCXn}jrvoudh_KU7?1_y3qhef#i#D5%v_{*Dr}jk zd(Kj;ZM|N-)}Xz=j^V8w7$>7b353enR#%*5sdH}l!$X$V_;>2WRC(a>#|*W7)oMc& z4)y)eFE>Lr!B->K#vf~$#&=SPhW;pZ_ZvbLy=NIAzwK9eijxhYHCV56el GAwBNS;<&ph C||VkdShzsxp%#|CfH zV*73AwUE`VX!(B=m&Brm)Vv%mtxD8}8m|x_&U9+N>owl5Im16cYN8Vx;@uMr_X?dt z`#YE;#?WmcqOQsw4og`6SUA?mD;-JVk0Jc1!W^ANP(~}EeL0Wwd63YX<-e)j2QGol zqWkwjL$?HjNq@UmAfXcXE9QfX}7>+u%p{Do>ly)aww>*#-|5P4^pBO=-ACsowG9%{$h&D0r zACjN!?qTF 7(&ZLX~Pf zsw8xi(PKd9jY 30kh@pTBOq@Ue7E48AjI&cF9@g}L z@9ir&X}-$H`248VzJh)WM%#oKXX_ZGLN&IZ3DL^!*G7v7;PFS1p7lJlU;3b0ttSHd zI)q^;*_l6kw;OWU3zI|}uLy!d+YWnb@#=U{n`+4vKA=e1zp(5syYkVpF8Ld7B<)E! zn&07H9&JbKQ{irai67*?rSsX9OQ0He*Tn9wbQ%c2s%a3n>(vfJ6D38&M-%Vxd2`Vk zU9MzK+JUsmPAj1*{#vB?dEiqpetm1Q;$3lpadL7M;CY#~j=-#my SuVemu+;-q`0eZ kGt>okwuqL$7Y5ITAu7x2o!WYVf2SCWJ 8Mn!?#;p;w-;`5)( zek8j|PoKpiLKGPu4GRgJ%zi{1%^Sg;|AbC=ka7<^L#R-sx7%e-DIY}}sHp*+?p?v< z`Yj;DJ;!`~+EALgysrR`lu!02*5g&{a8BUr*BxGp<%k+JiCu0m7$|J?=zN$}