From 44ef429d5aec31fb819769b4a4510ea1521ff6a5 Mon Sep 17 00:00:00 2001 From: gitea Date: Thu, 23 May 2024 13:59:06 +0800 Subject: [PATCH] =?UTF-8?q?[modify]=20v0.0.5(2024/05/23)=201.=20=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E4=BA=86=E5=87=BD=E6=95=B0=E6=B3=A8=E9=87=8A=202.=20?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BA=86=E9=98=88=E5=80=BC=E5=92=8C=E6=AD=A5?= =?UTF-8?q?=E9=95=BF=203.=20=E5=88=A0=E9=99=A4=E4=BA=86just=5Fopen?= =?UTF-8?q?=E5=87=BD=E6=95=B0=EF=BC=8C=E4=BB=A5=E5=8F=8A=E5=AF=B9=E5=BA=94?= =?UTF-8?q?=E7=9A=84win32com=E5=BA=93=EF=BC=88Thank=20GOD!=E7=BB=88?= =?UTF-8?q?=E4=BA=8E=E5=8F=AF=E4=BB=A5=E4=B8=8D=E7=94=A8=E8=BF=99=E4=B8=AA?= =?UTF-8?q?=E5=BA=93=E4=BA=86=EF=BC=89=204.=20=E9=87=8D=E5=86=99=E4=BA=86?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E5=BC=80=E5=A7=8B=E7=82=B9=E4=BD=8D=E7=9A=84?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=EF=BC=8C=E7=9B=B4=E6=8E=A5=E4=BD=BF=E7=94=A8?= =?UTF-8?q?speed=E6=9D=A5=E5=88=A4=E6=96=AD=EF=BC=8C=E8=80=8C=E4=B8=8D?= =?UTF-8?q?=E7=94=A8=E8=A7=92=E5=BA=A6=EF=BC=8C=E6=89=80=E4=BB=A5find=5Fro?= =?UTF-8?q?w=5Fstart=5Fdp=E4=BB=A5=E5=8F=8Acopy=5Fdata=5Fto=5Fexcel=5Ffile?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E4=B9=9F=E8=A2=AB=E4=B8=80=E5=B9=B6=E5=88=A0?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rokae/brake/brake.py | 167 +++++++++++------------------- rokae/brake/configs.xlsx | Bin 10692 -> 10699 bytes rokae/brake/file_version_info.txt | 8 +- 3 files changed, 66 insertions(+), 109 deletions(-) diff --git a/rokae/brake/brake.py b/rokae/brake/brake.py index ee44fdc..9c8aa0d 100644 --- a/rokae/brake/brake.py +++ b/rokae/brake/brake.py @@ -2,25 +2,15 @@ import os import sys import openpyxl -from win32com.client import DispatchEx import time from threading import Thread -import pythoncom import pandas -def just_open(filename): - pythoncom.CoInitialize() - xlapp = DispatchEx("Excel.Application") - xlapp.Visible = False - xlbook = xlapp.Workbooks.Open(filename) - xlapp.DisplayAlerts = 0 - xlbook.SaveAs(filename) - xlbook.Close() - xlapp.Quit() - - def traversal_files(path): + # 功能:以列表的形式分别返回指定路径下的文件和文件夹,不包含子目录 + # 参数:路径 + # 返回值:路径下的文件夹列表 路径下的文件列表 if not os.path.exists(path): msg = f'数据文件夹{path}不存在,请确认后重试......' warn_pause_exit(msg, 1, 11) @@ -37,55 +27,57 @@ def traversal_files(path): def get_threshold_step(excel_file, AXIS): + # 功能:负载和速度100%,且是j2的时候,做特殊处理 + # 参数:新生成的excel,轴号 + # 返回值:速度差阈值,处理步长 conditions = sorted(excel_file.split('\\')[-2].split('_')) # 只有负载和速度是100%时,才会启用更敏感的step flg = 1 if conditions[0][-3:] == '100' and conditions[2][-3:] == '100' else 0 if flg == 1 and AXIS == 'j2': - threshold = 50 - step = 20 + threshold = 30 + step = 5 else: - threshold = 50 - step = 100 + threshold = 10 + step = 5 return threshold, step def find_row_start(excel_file, ws_data, conditions, AV, RR, AXIS): + # 功能:查找数据文件中有效数据的行号,也即最后一个速度下降的点位 + # 参数:如上 + # 返回值:速度下降点位,最后的数据点位 ratio = float(conditions[1].removeprefix('speed'))/100 speed_max = AV * ratio * RR / 6 - - row_max = ws_data.max_row - row_start = row_max - 1000 + row_max = row_start = ws_data.max_row threshold, step = get_threshold_step(excel_file, AXIS) - while row_start > 0: + while row_start > step+1: speed = ws_data[f"A{row_start}"].value if speed is None or int(speed) < 1: - row_start -= step + row_start -= 50 continue - - row_end = row_start - step - if row_end < 2: - msg = f"可能是{excel_file.replace('xlsx', 'data')}, 这个文件数据采集有问题,也有可能是程序步长设定问题......" \ - f"建议重新采集,或者先删除该文件夹,重新运行程序,先手动处理" - warn_pause_exit(msg, 1, 10) - _a = ws_data[f"A{row_start}"].value - _b = ws_data[f"A{row_end}"].value - if abs(_a-speed_max) < threshold and abs(_b-speed_max) < threshold and abs(_a-_b) < threshold: - row_start -= (step + 200) + _ = [] + for i in range(row_start, row_start-step+1, -1): + _.append(ws_data[f"A{i}"].value) + speed_avg = abs(sum(_))/len(_) + if abs(speed_avg-speed_max) < threshold: + row_start = row_start - 10 break else: row_start -= step else: os.remove(excel_file) - msg = f"可能是{excel_file.replace('xlsx', 'data')},这个文件数据采集有问题,比如采集的时机不对,请检查......" + msg = f"可能是{excel_file.replace('xlsx', 'data')},这个文件数据采集有问题,比如采集的时机不对,也有可能是程序步长设定问题,请检查......" warn_pause_exit(msg, 1, 9) return row_max, row_start def find_result_sheet_name(conditions, count): - # 该函数比较简单,功能是获取结果文件准确的sheet页名称 + # 功能:获取结果文件准确的sheet页名称 + # 参数:臂展和速度的列表 + # 返回值:结果文件对应的sheet name # 33%臂展_33%速度_正1 reach = conditions[0].removeprefix('reach') speed = conditions[1].removeprefix('speed') @@ -95,12 +87,17 @@ def find_result_sheet_name(conditions, count): def copy_data_to_result(ws_data, ws_result, row_max, row_start): + # 功能:将数据文件中有效数据拷贝至结果文件对应的 sheet + # 参数:如上 + # 返回值:- # 结果文件数据清零 - for row in ws_result.iter_rows(min_row=2, min_col=1, max_row=6000 - row_start + 2, max_col=2): + for row in ws_result.iter_rows(min_row=2, min_col=1, max_row=2000, max_col=2): for cell in row: cell.value = None # 将合适的数据复制到结果文件 + row_max = row_start + 399 if row_max-row_start > 400 else row_max + data = [] for row in ws_data.iter_rows(min_row=row_start, min_col=1, max_row=row_max, max_col=2): for cell in row: @@ -112,74 +109,16 @@ def copy_data_to_result(ws_data, ws_result, row_max, row_start): i = i + 1 -def copy_data_to_excel_file(wb_data, ws_result, row_max, row_start, excel_file, RC, RR): - try: - del wb_data['dp'] - wb_data.create_sheet('dp') - ws_dp = wb_data['dp'] - except Exception as Err: - wb_data.create_sheet('dp') - ws_dp = wb_data['dp'] - - data = [] - for row in ws_result.iter_rows(min_row=1, min_col=1, max_row=row_max-row_start+2, max_col=5): - for cell in row: - data.append(cell.value) - i = 0 - for row in ws_dp.iter_rows(min_row=1, min_col=1, max_row=row_max-row_start+2, max_col=5): - for cell in row: - cell.value = data[i] - i = i + 1 - - ws_dp.cell(row=5, column=7).value = RC - ws_dp.cell(row=6, column=7).value = RR - - wb_data.save(excel_file) - wb_data.close() - just_open(excel_file) # 为了能读取到公式计算的数值,必须要用 win32com 打开关闭一次 - wb_data = openpyxl.load_workbook(excel_file, data_only=True) - ws_dp = wb_data['dp'] - - return wb_data, ws_dp - - -def find_row_start_dp(data_file, ws_dp, row_max, row_start, conditions, AV): - ratio = float(conditions[1].removeprefix('speed'))/100 - av_max = AV * ratio - row_max_dp = row_max - row_start + 1 + 1 # title row - row_start_dp = row_max_dp - 5 - while row_start_dp > 6: - # 处理异常数据:当从数据文件中拷贝的有效数据超过5000时,会触发下面代码块 - angular = ws_dp.cell(row=row_start_dp, column=4).value - if angular is None or str(angular) == '0': - row_start_dp -= 50 - continue - _a = float(ws_dp.cell(row=row_start_dp, column=4).value) - _b = float(ws_dp.cell(row=row_start_dp - 1, column=4).value) - _c = float(ws_dp.cell(row=row_start_dp - 2, column=4).value) - _d = float(ws_dp.cell(row=row_start_dp - 3, column=4).value) - _e = float(ws_dp.cell(row=row_start_dp - 4, column=4).value) - avg = (_a + _b + _c + _d + _e) / 5 - if abs(avg - av_max) < 1: - row_start_dp = row_start_dp + 10 - 5 # +10 是因为结果文件 C2 的值是 10,-5是做了保守处理,相当于再往前移动 5 个点位 - break - else: - row_start_dp -= 5 # 保守一点,每次移动 5 个点位,如果想要加快程序运行,可适当调整更大一些,建议不超过 15 - else: - msg = "数据有误,未找到平衡的点,请确认!" - warn_pause_exit(msg, 1, 1) - - return row_start_dp - - def single_file_process(data_file, wb_result, count, AV, RR, RC, AXIS): + # 功能:完成单个数据文件的处理 + # 参数:如上 + # 返回值:- excel_file = data_file.replace('.data', '.xlsx') sheet_name = data_file.split('\\')[-1].removesuffix('.data') df = pandas.read_csv(data_file, sep='\t') df.to_excel(excel_file, sheet_name=sheet_name, index=False) conditions = sorted(data_file.split('\\')[-2].split('_')[1:]) - # print(f"conditions = {conditions}") result_sheet_name = find_result_sheet_name(conditions, count) ws_result = wb_result[result_sheet_name] @@ -188,16 +127,17 @@ def single_file_process(data_file, wb_result, count, AV, RR, RC, AXIS): row_max, row_start = find_row_start(excel_file, ws_data, conditions, AV, RR, AXIS) copy_data_to_result(ws_data, ws_result, row_max, row_start) - wb_data, ws_dp = copy_data_to_excel_file(wb_data, ws_result, row_max, row_start, excel_file, RC, RR) - row_start_dp = find_row_start_dp(data_file, ws_dp, row_max, row_start, conditions, AV) - - ws_result["G2"] = int(row_start_dp) + ws_result["C2"] = int(2) + ws_result["G2"] = int(10+4) wb_data.save(excel_file) wb_data.close() def now_doing_msg(docs, flag): + # 功能:输出正在处理的文件或目录 + # 参数:文件或目录,start 或 done 标识 + # 返回值:- now = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) file_type = 'file' if os.path.isfile(docs) else 'dir' if flag == 'start' and file_type == 'dir': @@ -211,6 +151,9 @@ def now_doing_msg(docs, flag): def data_process(result_file, raw_data_dirs, AV, RR, RC, AXIS): + # 功能:完成一个结果文件的数据处理 + # 参数:结果文件,数据目录,以及预读取的参数 + # 返回值:- prefix = result_file.split('\\')[-1].split('_')[0] wb_result = openpyxl.load_workbook(result_file) # 打开和关闭结果文件夹十分耗时间 for raw_data_dir in raw_data_dirs: @@ -220,12 +163,11 @@ def data_process(result_file, raw_data_dirs, AV, RR, RC, AXIS): # 数据文件串行处理模式--------------------------------- # count = 1 # for data_file in data_files: - # now_doing_msg(data_file, 'start') - # single_file_process(data_file, wb_result, count, AV, RR, RC, AXIS) - # count += 1 - # now_doing_msg(data_file, 'done') + # now_doing_msg(data_file, 'start') + # single_file_process(data_file, wb_result, count, AV, RR, RC, AXIS) + # count += 1 + # now_doing_msg(data_file, 'done') # --------------------------------------------------- - # 数据文件并行处理模式--------------------------------- threads = [Thread(target=single_file_process, args=(data_files[0], wb_result, 1, AV, RR, RC, AXIS)), Thread(target=single_file_process, args=(data_files[1], wb_result, 2, AV, RR, RC, AXIS)), @@ -242,6 +184,9 @@ def data_process(result_file, raw_data_dirs, AV, RR, RC, AXIS): def warn_pause_exit(msg, pause_num, exit_num): + # 功能:打印告警信息,并推出程序 + # 参数:告警信息,暂停的次数,退出的值 + # 返回值:- print(msg + '\n') for i in range(pause_num): _ = input("Press ENTER to continue......\n") @@ -249,6 +194,9 @@ def warn_pause_exit(msg, pause_num, exit_num): def check_files(raw_data_dirs, result_files): + # 功能:检查数据文件以及结果文件的合规性 + # 参数:数据文件夹,结果文件 + # 返回值:- if len(result_files) != 3: msg = "结果文件数目错误,结果文件有且只有三个,请确认!" for result_file in result_files: @@ -299,6 +247,9 @@ def check_files(raw_data_dirs, result_files): def delete_excel_files(raw_data_dirs): + # 功能:删除数据文件夹里的 .xlsx 文件 + # 参数:数据文件夹 + # 返回值:- for raw_data_dir in raw_data_dirs: _, raw_data_files = traversal_files(raw_data_dir) for raw_data_file in raw_data_files: @@ -307,6 +258,9 @@ def delete_excel_files(raw_data_dirs): def initialization(): + # 功能:初始化,记录开始时间,读取预定义参数 + # 参数:- + # 返回值:结果文件,数据文件夹,以及预定义参数 time_start = time.time() # 记录开始时间 try: # read init configurations from config file @@ -331,6 +285,9 @@ def initialization(): def execution(args): + # 功能:执行处理所有数据文件 + # 参数:initialization函数的返回值 + # 返回值:- raw_data_dirs, result_files, time_start, AV, RR, RC, AXIS = args prefix = [] for raw_data_dir in raw_data_dirs: diff --git a/rokae/brake/configs.xlsx b/rokae/brake/configs.xlsx index a23a93be3655c32e9584d2e6bfc2d8d51fa8715b..06657b6a5f47179f8acaa70c6a5dd3289c9006c5 100644 GIT binary patch delta 3372 zcmZ9PXEYpKx5sDH84^Z|mgvkV2_bqLCVGo5qD2=WdS?(sjWW?0qW2+0bVkXeMF~+O zqe~erx|nz+&%5qj_wEm8t#kH1`^#Sc-~XI`!x6*!EFdZ7bJKk(R6dX$snsV!AG1q( zi5LpaE%O#tUR)t>_u??q@1Pu-zwz{A%Kdgi@KjAT-N4A6Lf}s{%B$7X?I(Cb|5z$o36K}dYV6Cq^8uy%KiD$GtG(=p~S}P~Uf)RK9 zLdfG=g&;CfxnlyGeUt9UhQ}BAZa~Jsuq1{6zAUEzNSUK(kv{)v$vyVwBqHqHrb2k| z3k}sh$sbsOldb5iyj0Ee=U}lU-|?kL_P)3;veFTuI46S842S>@vs9ED&^N4nVd)7! zsxQcp?xw2#tPXki;#?J0S$dFhHkNTJ`GA5=(7-(+5TX(AicL@wot8Pj6xyilo66XD zub{ld8T8s-_u!(oANr)jT2EQL`4kr*&XnMsCk*2sON1)j3ckEJJ*R9uBx(kd`dUZH zFQYIdyvW^W#ZnML*hd2OAdXQvOuSOr-&*I^en8evSC^@ShvS=#B3<>HW9mKMwbwfoc-C=9bk|e$2?6u1CVFwLL2}oj zS;B7jwhSk3f!j_15YwhJs9(9M5Wj#Q6j+EULOvG%%pX-#5n1pP@;1r-o+P!g!@YtJ zGx{olIw>nwx+@SBtk^N^jp4wPXr8omqlnAn!CXI^;Bn~wMw(y!3^v(rpJvyrD%l>{X}ZYxPkb6r>V|p@zm3 z%F*=QW^VT$4gmz8)GAt#=irbKu>SH26*Bg>*yai?TVjp(Ns!q9s`9-P^LkBTxj|{s z#gMnJ_F%@fhHjM;^0gP-LQY*gN{*tE_iZ9pOEj$-ck!c|0c;P@t zi>Sr3r473{1%DE0Y-uecYs=0M2hvYIh*gB$QOxJlw!eW7Fi(AC+rBAUK8B1ICtQdY zO*$tNi48ZMi@s5`MR>}XFa9h!08YYGA3LX3GJDfIA-3n`kv-i>D>OU;3X`gCsSkO7 zM9fuaXWswuPk~^mz}yhmK0 zT9CAXP>lUb82u-#t^l{NC9hcRat53m)RRjKfrD7gql*tqB|&$5H>D%#m_ zO-O275|9kz>HD}J$>1phi#(v}vSkgjLFxxC`?9_gvBQSn=&ekb8qIRe8q=m9T@{kJ zkm_smKy1iIR}5eZwRX8A{`m_& zSgXMxnqy$x{;|f$&EE-aDH3PgQ{$;D+Z~p?4af6Tr4-#zpxVmYwO#u#ds21nrFOH$ zG&rsg3^vP;%bRVFy7)CdmB|1$SNV~bQRE0dv$=iNMj*V?fv!JlchV-csaTA{r$5;# zuJ~|VQ{)C?F+r|raN4UdxNqEf!aC&aZdEnxER!u%9RD-NEGAew20>3cK36Vo^SV{rDOZh; z|Ek&`Ab_L#zzC64-$~E32+i^dMrh{;w$)L#mtAi>Uq|-xKL}yGo6cs=TcuV7n|IeH zM>{kvt=}}BuBGM?r2TojOK*)COH6!*eK;#K8&QN2uCdRym3E9K2_mzK4w&juq{#{I zcr8w;V8wDO&cvIuTl0WlPz$830T&xc#xwr~!K3rmY|-ckoCCrptkkL$n@t73WhO3u zCuB?@DddFUhXopBnE`iML_IOTYS5!T3L_j&r1Gx>7&i3K)uGBWCCP-_ZHi2|?bIx<#Nv=!uJ}5%)==kW^dl<9T8pPkUE+5 zj*3a-M}2|aX?u5^U~`-nC6rWrP?5z_M)(obyfm+b`KRK?M%||)-Phz7Sh5TR#~-n? z$f^QFtb53A0?AjvHh12Gz9PLNG9#`_5mEKh^~{K9nsM#{?&M?tL*ywNEFtg^=bc=rcL_8!ne`5iCe!C zhHqgO3;WgBp)n9U*!Juvi?tS;A~Wk_j|tqir;oiEzc>m)8x5)IRvOMZiLOLEpHk{u zr@V~bg_R-HUm}X6S@}w63^az*t!y*!L-orNEL4q)m%tRZ#)fML5b&@QDDD6V0O+7< zZ%HBT+hlbl@MHbi?;4J#i+ITylor{lN;$AWnxwVeN0@Il--eLb%(C}9B(K;+UqqaZ z{@3g zrb4H`hZ={U-z?>;h>yzUH)f>uH%Zr(S~ftMR#duT5rzyzX<3_={xN)VY5ia$0W9>) zwO_-*N0MQ{8_PQ?MspB;J6t_cEh4Z<@UY*3t=!gZdUQ(C!~C_g56)$eX)T+#g~F(& zsaQ}@fq%ur)5pQmfHuglnecV}*MZogz^rQCNb9$1%Ovy?N3Ji*;BP9(E=ko}TP52)w8(=bxGAW_o_-zDR3#q4E67a|iu$-vj`p|nF39h4VK&wsmq%`xasLSRM@Anj zvT~819^F=R4K&Z)`NdMvVvJ{CkdwD&d;nL|XWZ^oP0 zn1!V#Guuq2PMA6C8*3%z4!_R)>h6s9*mI!<+Q-XJn(p!EVeXfF+zqUdr(n?RH)BTg z4T)i*nmzbN2}mSU9jvL}6q-xnCWnw?q0tC~`QGBNre9b-#>d9B{ibL5roe2Y8g*Lc zb3Yv$t2w(`dl$HwdtN#jQ`V@TMl_D$n?=WuZJXG!x)bW`YnzAP;>-s`pZiF$mha8} zf_DmYuatpTq7%T4Soy=i@_bIq%(Y|Wqx00dXN=$vurY{&%SQ_Ox^+vtJI=IEmjtvbXk;8`@PT>=#S z^U4u7*Y3Gbw+B+Hy)HxS^fzdGF?&qWWvGiHW%lbwb*6Zc{jTL(iIBdgAU3d_Cj-tub zt4w$2ARQ{6fksnD{lw=fx6$tw=d%I<%UDM@E{uh^);jKuSmH|2Q+Jzj?k}7Sa?0wt ztxU;&CbaWlvfs%vPVxez>Qn^SM|)H^#3Gmt8NNKi_P{CqJ~7W^6-A@|nDZ0Tuq~WP z1K8{w`qgq5*Y;E~gf*1kd3D`ne>T+D34>b$QR&Yn(4p%V;lA`%lPV+~o;`^i?F~`j89S zs7#SqeU%M(D@@~sFg%^Q_M>oWnCW_AHv%G+>X9Xxm|}k4=0=pbDOy zeDqc@-Rt<{Yg>g8hstF~R@WjDcCD)HXMR!lZet(G3O(A`_Po@_tB_{9_OkH$#fh)R zUc8CO{4g8um+l=w<kC1(zN4fjIL+8I+vOeEr$Rv0^L*bhZ!UejL8;c!Qg z&3LOv!+}cx2=7M$Ca5=Go5q<`FriQ#?s_DU0@^`)aQ25osct zeGv|}B6+2RmQXK^uvSc38bwc+HG0Oy6f3=f4q|Xz}=q5Nq8<7C&P8cP%K3l$O5 zNIe7HMV1|YWyCE1>rWElj>*?4l#K^?RztFWGX5bVykBLVmv?nL`=H}^n$L9u`ISli_u-t?=?<>!9g zeZD(;z}2?Fm&*`6?8;Gs(y?Q?3!SQ~2RoG9SE*^&tK2i(#7KDh3OK(~YBC5aQyZj2 zWpDY{}H)^XJ?nQ}eQt$DjS{jEb=% zFY7`)XMmT=j^qujR2lvW&6tH=m_dqHFyy__(Yhza2)4W|*3O@YSD1H=OvN}!7s6-b ztUCCEm7BZBAEH{R74@+BI4!i$nY^%=NAA>sk1am8WZyzz${ud|9KrtlTxxXT&zXsB ziv8}d10yakiS9lE%_FL>{rD_+lmVU=EaTRQeorbEq;Ys~z=<{tYPDibeAg`sY@z>r zmq5@x?fR5Iow&Fi>seVaGJya902?62a$X;0oNQgj2mtVM002S&0I`}0M4V+~Mf(Tl zYk`iNhkmhs`^hL-u4&ulOL{w==3Q{BA$bs<(IM|Ts+E`J((+N0TPM~8c8w#w{lc=d zi%aA2!Bv6Q%yM7TCQj{7 ziwauvR5T#ruRsZ2ED08uNtME%^6tE z@q`Un)1`;3yXC#iq!o%DpDcAz3w;i|pfoQ+bywBy!p+DE6iahx*kbv&H=x8W+l)BX ziqW#lNd#DFP_;>_{%Q%KXV?0exkG~xvA}#I2Rb#)V+}z~RJ%ExceB5bhQDCw4B@pt zw9ZHDBlu++O6@R$!fVySNdBE6R>C6tzJT890&^=P<9$OhR?NjXc zxEtANWtbL-I%C)s1%b%s(G7oP^Who0GI|M2LRl7ggZIE5kLX_NAUAEN+^mj7^IpoDy=?>A)w;%S}9{TA+#n)~W(weNM; zX3iVMiyIE#86pKtxvYvQ7h-j^EwBtCQb&R3DB*l?lk7!PrY;m3FCfPID+JMNT$?$Q z*bpxb7Ld1fVAc;dIj9R{K^5&5)rznst|2)LP)R9O z++#DYbzq6?bDn7QNaT5QW}$$o3#53=V0{G}C!I3V#69LhOn4JkcxMd{E(A0v^Si)v zag)zGop9-1^#;mi<;!dwss)$Ba~w;iKPa{3<4O|q-`ig1+C8S$*Gx^f+M}+1@h~r@ zn&p8f`%1AHsb0*YSlK9CmJ}dE$(1oF>d6MvCqZkbZFC*Em(2240bJED^M2y%;Clx| zk_|!`R8Ff{gVYDoSEQ1n<7vi)mT;X1FA9=w|A)#QS3$eho-426zqu^r;yLfy+C5w0 zsQD*TaMvS0So*R}{*}8)E{4K3C?8`b5n%Jf`x;-g;2AJ_x~}MU2vSr`_(6}N8WsV^ z{s>H|SbFI7!)D>0mD+v}dR8ZHorkcH;yVz{{sw15xLdIMLMB2}u{JTfeRUn3B3@a^ zxy8e70m;t#2?PKvh~;8h=$?Baq=2tkv1)-HJHsytPvWeKW8SizDS2)4>A>NHG(gHe z5{&e_b-B$jFQ(7UwRy{Xdbufcx3RNd`hmUSb5pY%khkMJn{bgbK_jPRvdEDnn*@X# zi7UuO-bD&kbitiB5`e6O<_kn^{eNMu@eLcd^(Wjq6{`a_mlX|qgZ3X*ubRz!!vQqA z9ABn`BDzf!GFi`#rNduAyUA4YzrFa05(d~sZmWnMnnS_OV<>ASyH zskM4N^`FoLS|6Vyv;6gOO4a=wo1j{)gNia3JEP9T!_e)m@3FVD{2`W$ZL5pTCa860 zZ=JQ(_6E$2d+Lw*?xJV?-Cli+jY<=ItH45ZIEOQnhA@(z42=~3s7~OAz8Z%mo!uAg zP>!lQ0RjpIU2=oq^wqJ$)!_3H)0Yl@bw6mPUpo8MWp~R^uGBJqW%1eC>MlJbsm6IC zFmUVUv-H;;?h~jKDV?wV1u#_X{9eDp#MEI^M26U%Wz5q%a3|lhNT6mFx98b_-Flbs zxkd$MDael|HPq6*xQ+Xgz%4O`LyMhaO&4|3XemlnKEDI(m`ln2~TwFtRp z>gt^nS?1PRKAiy2d}Y;fv;P8j1^F5_yZF9@?o}RH#Xd*0|fmn zw}VYMz=PlCmkJF0jB=7^TDSDp>DEy?ZTelM22*NklLH#ia5^%-A9d(u7lDeMEG4#6 z!+oM%#8cb4s(jWc!N`1?>!e(L