commit b789e0a0fae315e1bed2a4d29821123cce0c6f97 Author: Yovan Date: Mon Nov 2 10:51:23 2026 +0700 tugas10 diff --git a/Lab1_Eksperimen_Chunking.pdf b/Lab1_Eksperimen_Chunking.pdf new file mode 100644 index 0000000..283d2c9 --- /dev/null +++ b/Lab1_Eksperimen_Chunking.pdf @@ -0,0 +1,175 @@ +%PDF-1.4 +%東京 ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +7 0 obj +<< +/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +8 0 obj +<< +/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +10 0 obj +<< +/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +11 0 obj +<< +/PageMode /UseNone /Pages 13 0 R /Type /Catalog +>> +endobj +12 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20261102103738+07'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261102103738+07'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Lab 1 - Eksperimen Chunking) /Trapped /False +>> +endobj +13 0 obj +<< +/Count 6 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R ] /Type /Pages +>> +endobj +14 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2437 +>> +stream +Gau`V>Ar7U(4Q"]35D8=1QsMemcn1%1>>B5Am6D4HJT&=N\+FH,ge552&i2QIR-T#jhqs$8\6WqGWYZCB@7J6E.FClb7`ab)-l&sTJ"Gh5^hY0li[/DRs^X2/$WDO8D)4O^nF=jiT[8aoRV`L"ZMTh1WeZCH<1kXFY@I0Vl/o(po@RV2_-=3F)S9"`gN_1>_rdgrqg$Z+k,nodOFY.VdZ;-(Rr?Fe+__QL/:SqQ$E56=dUd,r1haU6'O>u`nL_bd8ba0([E'g-gPspCCjrY<@#s(L%34i<tLhKY)H;OJ78gPt^b]6;T5:p)JEULbM"GK`Au(-7#Lu]O:5b,(l+q&`1CU=A,@c>@L(1l&*9[>*rJZi&Hgn`*>#'>:()N#]$Md!W)0?gN5QDbKjmni!U"[]6<7I/kt6qh439`e&X3gnq?;j+Jj=Bs\3=il>+.D0;F8Q=J)s3o6iSX67=J3M/Pi[Ea6c$R&XN"BBS"g.l;`>-Qr/K82c9Oo"nC@,H'9tEa7;-<,J-EhRO[%UW?;Pn<@`X6.j.[1'"RQ8>cb[HLj&IhSO.2FM[N%&#`-lNn(g07UNibK6pTUa`@2`=tV=,f"P-#>u&@\7CNB-AS'THCh)Qb2o)-XM1l8B[a>+ck`W-MN#FkMn7mL/)Fj%Fp8adLAQ%g13>VtSd.1WY!_2Sh+HCIp#K%`';)lb*`6E(6g'^47o?p/Z=6./`F)upH?IL6UR9!VRfdd4S5?^[$=l?6T+E(cG#b5Pl[fugnCam&G1tW=CX4h9h1gOr_U-S`Tq1kaLUqaekZ0glc?l.R279HR2hP^r7LmhC.GOkRCMeaBjc]JK>TV\YLLIBX*l4`A5m/u"?&D2UTGC4%I/UKjS%>6QHf9>Y\h?@OlLh"<$am'YTA$$X9]?Bp9^(X9)-%,=<4[q"h#h*7)(o"l[)G0ZYEZugQ5\'ns3Z;XkCH(g`'U2KhAeC;QbN9,ES=du!PVCmEalV][mV$?^f3,T[_W-"@h07oP/B18K`@:6[%bGX%tQVck6mj2ea10NOYI\HX"k/cLf@u^U6@o\.UXlFn^U/gBKmaRkh"0D#=/5)/g)8mL"a<,h8$5(J>AHQucCq8jIXUl0P5'Ls$=&2C>eD1+5)m4AF)b">1)_&6NcX%d,AAOg9g=drFjAb=ogumu`DC3'SO\kScK9T&4ca0AD%`p&__.[6_(RTNsa=,s@Td`fb5FgG&P#OTsNnqd+-d:^d&i@%gc&3X9R..D-.U&GV&A(X5+'Ke0'gX'!McXr84n[$/bSp2mpq@ZuM[?-/Q<"5o"T6n\[k*e>3TYZ2@'QlA$8l8e>-i(L!c9>PgJ5RT([N<4I)EY^W'3lZ?iac/dFRX[&r\-oO.kTF1\"!K"Qt=Kea#^bh)o)=YL&NRlY@G83>T9_h;g`&XDpW@c[-i(I*LaYk$C_Q6>8R5\fY'h^+Y#b3W!=&RuNBYa$DU)7Zre8W<;a+m(>?@W7LKK_.UY23Q]3#GU*skqYpO3X_5BNm'o/WV\g$`6fRb@)P4+q<8%[KD$q_^psdlB79m=!;A_u[.pW4hu3hi&%$L!*YH-NbL%@NZHr)?F*bp2$M9Ad)Qe`XdVIZfckF))*%([fK?d-:~>endstream +endobj +15 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2104 +>> +stream +Gb"/'95iTT'#*[5'G)f<#;ZdXo?Je*f(L_*A$sJGc'RFo7$bo3df#7M@"8*KRZA?>P3$NH6\8QTK]u"?^@/+;.7bTc'gE3Ii8l#/Gca5BYeLCM9U;o4$a98:+3oV+XXrh%p$d=69%@g]i8\`[b(9Ou"kdLWA@p)%(+N0cV-pLcr#aRa7K_qdGgR1:-Z%N_D8^*iJ6"mfH`2?D=@,m'm[uEMp;/DPa[Q/!oB"^Mk4,Tn'A0ihRn===C?arTQTV#p;+U/RM%HFZG\kmZ:4p:R0-V^1WFB?Zus`+THI1-!SnO@5`QR?KUa%9"Q%,T4;V*eL#sH:V.^3PL-&'!=9UNXU#Tar];#Qn=(l4(a^`A1'?YQg/8KpAokh&s3Y=N`eBfccE0Ojh%!?>JeIF]Vn]QGMoTK_Yjp(9.LQ8GPkKkL'i[("EC@l1i%7@rPfpYV'GM>ptDs<9&'PO/Z4#HR(#osce&s@esbu,r,?K6V-[]SNJT@ePEqj%rdq9I-QaL@(:tI>,C?\!@2J1M63DuF_/d)l&I)KI2f'ms(H7s?[qF6h7uWs/i,W5H7g<9hXk?CS,Ck(Bli;`.3(o"'B0_g<$dD4oN\Q3Hsk+`krS'A>!s!-UiP`TXDVXl?3dr.[sH[MEGX)sKj^)e@Z);g'2k7boi'8ROa@hj`_6k"CI&IUH:\:L(u(']V9pC1R9NXG.Mgu6^lZ'e2&&#d*J-OVrrA?eMkD9"LnW`V;/\@T(EP@i!D0+fF4a;i8JE%)O]_+N'?NuAXG;%Qj+l"%`+mZ9`2BaRdQLR-]OMMI`f@G$mB!a8)ASa9_$-Lm_(\85Ud*"/].pONfRo@0HM+QVb6!?.$-:ItVS!t9MGb0dJ`N!t4;oGF5gMl"C.BIN$NXL)onRFiIjj"41h3`s9uV]V)K(ZJrmpToMtM^@Wkp4>gA3AOmSYms[O\dHdREOjgud!U5Y-CZnU`Q*,,lCjYt;Y'R)V!)#Bs%a`VK>KdtQfV]RXNf(,J3)52F>!8O.faue^N!IaUs%XH""I6kp'DbLN!']2IbBZAq5XY=VBkIFc%G0RfON-Kp2&FPgU@&JY<:q[n&!>b91cXloT$Z'c-FH`OSL-KobD-,t,D-,V2N;GpJ=,ZFG9)'J1$o/:b2\1(,4=X+u?'Dg3a'INE;b[Ad459&G6SL^R%oiQGqBCJlUVDrG^PnrJ[,ne]kanWrP*9>,8[W=]PRedp?KU:$CJ"$D7o6E:'DHk?L41qHGfPQr^5E;Sh*kjq8g>mqbOPAlYWN%F9S;j*e^],?USH;F*3\+u\)Sb@m)A4oBNQ4,(c5X5p[m)LdO8p!V`EmX9%Pc":+8FE]4f#9;3&'pXV2.dXLUX?rdHc]Cq)e!00tu+=:98Yo.+OV3B@t)?r3#Er^Z3L_Di02+$B$JM`O\F%B0iUp06.F?3@[Oi%]bKq?;E+fc!4lKseMgqNmf5pMbZK$W:k_<@_)i"%7=;S)YRWT#%1+l&&W78$FYF%ALUj1BQY8d_jQ6ahdOih<4=#^&k@A(;#_43j=ViEE_rV3M@liT8Ir+kVA,~>endstream +endobj +16 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2372 +>> +stream +Gb"/'bAQ+T'&Dk($FmTrHnf)b*6l[-8F[A%!_&R=DS.D.94Y$T9tIqc@/te):e_GAOX80\Z:d)#JaBO#?&s',)8d/c&;T/F"utMU:N']u'A2)*HlAQmjs.Oq*.:$9hQ5eW3a-XOU\Q`Q04K2$fB0B4[2VK=>Xcb\\Ums(`N(tQNJ?J;=JA7eZPTjVL!BY9gi#]TO7o%Y8[MSIlPPrX.U'"lCY7pIs'Am?WN8SX.D&!+8$[C9^u#jg1\("aRi*cPE0aK3q03dn4"M>k"s$'c#-QXSj#r4Yd9Fi!_u^X<%9bT]X,K['Tuk-7c,esZ6.6;Y)heJYH)qlGB;cs5XAOu!iF%mm6PK-rYM"3Lm86dBi!jhRV&[B13[G._fqq4mioYtg$GeK_Y,m-`WH2%Ca`E-(9V+0I#QIo$#>LX<.u/)%pR%\3i-YOkaeD:LLiLj3fob3UF)*O>2l'R2%j'AV3cII!4m>H*B5"iq/F:S,EI%2(M4O>dp?7+IC=t98r'\d]U3Tsb$,j+HIfo3j']&4MZ46>O3X(3QPp^nYl#E?efsnC,?e6oWdSD'FT!g.64aI>_Ha,1(*Ao+@hW(kYGnNnlR_^TKpP/DnDkZ4VuO8*LGP>H.YiW2*r>S-8!Ys#`-+<$S@\iK;skXjhRW,>Qu1B+*1N[9*Y#>^aLERtaL&IXOp^tTpZqsH.(i&k&q2"2WuT5H1/4WDH>daqBiQTAHU#Z3,IH2LJk^=@['\J^iDE2VIPbb17SD,L%gfdh1ad,0[q0Is]*>)P"Oac4rd[*AA.;j,L\SJf>,Wm#T'ZaM82ea4TGmehlHt@kN8m0.\T0S6E!uX2OFWfcY2;HS[<]OMi)HG#_V@NW:<+*DR4Q5t+Q[/qNr=B9j)C+Hd&?pkc$7`#E&dgL^CiTf(]XQ\l)'R-l/Zm.]fQ#^R[A*)r[B4Q6p#1L'"Sr\&$cJHQURJn^[[!&bENH]=bC2D7/K6=)>9aD$EQCR2)OO(=PUGAj#)65hd687BH>^SZS^2Z5H[J*lpC$thg<[cRT?QKIPLJdomhG[cl%AKC%\Hi",b@K3+'&423dOEe,qi0q/@Aa5g^bCF#LaE8kX;Lgd)!+&C,4O/pBK'gRAP*uRDKL@I=[2AGunhOM9o^@$eb+T9<*A`I`@1!NLWl=ffKrm2G3Zh2l4pJA$#F,(eEj.##a[O?2/Ug&J/Ie6^n0Y,pZgdPSGg!Zu"jK/QDWNS-4`Y<1T=LGu7gIC5j+[PGQH-LJ2q:>5LgSr_]QR2Eeg-`S^SE+j>;N/\sak<3hYNi&"?,R%ja6&FAk+c`@8A>BJ[kmi&[FVQCfMRHWQ5H=hQ.s>rOldT=_.+/Hk%;RKuY(CA3QMVV7n)PIJe?`7P.4'mpH]LT;/o0jJ6P)^NqG@;j0)mf^PVV#iU-+kHs8C=6O87/lSG">Z7B_9'/r5ic^Yl;0s63Y_B<,QlE]RLEhphWJm8")hIPQ.?qnMa.+4$)lrcXVZT2bi?Cl-"&J'3_5iH+7Y$eF9@f4t;eC?pa/$"\8^8J8o?J`J^:(Z5OdLSI'7htAG^~>endstream +endobj +17 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 780 +>> +stream +GarW8bAQ&g'L_]pMV/,OMGtAiMO!N8Zj;OA;SYqD7M[5r&h/Nq(A[iZU+N9/2A3g+HY2]-PX(6K@XK4Sn846*%frrRrWQc%Xg.n[n,(f(ls/5_6BQ-2le*U$`E(pWgX`VQE3Xh$QSj+FV]o2#C[.+[LkQHc]e+eE;hD2msA!^@#u-U/R'k[]W0UN92eL$%OF7Y>'c_q4Dr/*0ND'#8cRJ0r')fpE@:2M`knSt0rXPB8]i?f0'N6Gt6<=M#R[VKcECBsW9@et)V0'((Kpns)%NQ9629C:14Nnb52kD<"Bg"?D7T7?%@Mbc?;pF8Z_m?^F)G__j8(r&,CbR7^UP0rkg&@dKWBo7cdUU.[dYWp*_G$GDqR/T(Fh>jk)jrq(,VTuD;Y$_H;pDNPc8GmUfA5Z3+"M?*#hkal1Sumj%rqE+^Us&6t5^D*Xf"BM$+ohl26P@M;L,5rW-bjHO9~>endstream +endobj +18 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1784 +>> +stream +Gat%#CN%rc'`FV1EQ4]&]K3(Re9Mn[L3/8,TNRblcFu\tE+Mm).u@.QBgEEq(,=V=p3S?.E4n7-Iah!8[G\ncD:e0Sj'J7efRHsQD:&'#0f_>)-PpFY3,rg0gi]mC@/WYGAL&uN@IXeb?aRG[O1l]Y7]-psoO;k"ibWpQ],.@]=Ct(\O0m9eoC$?p);`UcE8PG]PDR:lh,%,X7<4,.O#U#Eh-ddrAKp3X/tR4f%u*82d?2bQ3qSnG`T(37<)l&l,t5"&3B_TQ(=!63Cr>=Z6^Yh0]:fcgNcCVS"]lo.&fi7mQ9.OTV5X>L_/I<_'2/]V[Lq$]P6qBrXMCA0MItWV'QFrpp;o_0jMNWiI'O4Y&;=Dl#fXT2,>cp+@L&#>.A3?W?"=gZ#e4Hc5_RHD&1PY4_kL=M*=S]^:V,Y6QN[N,ANW0Y22e#Jr:7A;9QB\$T<4!]ZW46So+qgh2MDZ#=77J#jU)Fc#4T2s;aVa>.Mlb!"KtQ2J;'_o!gKuP)H:>/F5*^:We_1eEjJqc&B8jK/MM..WESp$s-nP&'DZrEX;B>u?8H8?;@[1RsWQ5grTWU[dU()Iu/FI7&>4$(L5Mo^OIgN8AJ%Y[EqU;t#k85sUfso:#pDu0e$AuJ1V:aJOLWcmQnG_T?H>pHi2TFWp+an,J)CnMNMH$6^AJ\4fr"4IOTiXZa9A7pmN2ZK!p[R[pjX5;_hlEK,?KLcCf3`DJDmbU8]?V7Rm8!=qEi9]-/M@(7KP,CQiRdD].sQ;'fd=[/+s,cAV"@(@^_E&P4N[u^+\.ViIDGk_ks0d>1'h#W!t.5`.`h(V+4.18-I3,#hn7bi#3i@;\DGX2Y/=07QrMQ6BT\CpE#@!IY&qt6YYe;WtBaXGoorZg!++[tPq78e6+!C.^6A!lTrBA[U&(@9S^h[Re[+I@S1l1S-I6XmO;%h(4;W^"p_u3*KCIW4i,5Yr*stfpPcBnmUfTO8+-PJ&_$KnM+oPXqFpUh%AqPd,!@33,Yq^HUT*1FfjpG*m-P8XedUA8rGQpRtW26WS),8a,XS+;^s9Frgs]Zb2so0Ct9\(+dK]#g^Jf)TtqqV..,cVc*f"NJGRendstream +endobj +19 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2510 +>> +stream +Gau0D>BAQ-&q9SYfO6-@.\R_WMB,n=4ueNbS_!&q8`]IeC,eiu&n)Cq$NbGkfo%b,M^(k&o58!b;M@-kG\L$-9OJGfjf-r@eV!2^8\I7*#nHh,3KHg#kQRC7B&?J=kclo^ii8;6Eej@,+p^/O&_6I!WaZ9L=^Nb5tnSZj[/0I7hN;K0gI=_YmZO"noFkld#h5(8f/tMUI&RUZSE8.JU.(3&$h)pSh"-Pjdn?%GGHj4"U;JiqqphV?N!&J;H`'!I!X'!.Z?s1%#_dZ-*NGO^c:hUs':,f]t+U?VcR6.%PXS@#sMln#'Q-pSn/$H8g=BV+#+.5*nFYNN>J]@&I^.9K`e"N+^*VSn,$-,_=G6(ed)[)?nVl:X[(9ag#gF%cQ'6gPpCf(Bq#nk('TnA@*ac6dXe7&`m)-'gd#8N&`/)/\AMC;?sluMhm[CYO4JE\B$G\P'g1L_)4rG(ikSSMG"WN<`WBA<>d;)1_8$)fARLQ>OMQJHO6![)4Yq0K@2@K8hLapPP;>Cp%HlK,hX9E;ro4E93T=95_;J[VH9'V^qu2SVhDk6i95%=fk>f_4meLi:'ULkbQW=YghtM>*1]^O;!iujMG$IrIhu_A8!5@o+4g?,H19]0P!FBdOnBh81*RGC$YnsM%HLBEQbZLqDBeAocbgp5Z=gQAcN_9hW^W+;gKC8G)\nO0u6HEO"B[^V\SiWU)m=t#*L7`s*,o;9W:dV=+5t(i0.Lm1m>r(P__3$.bq?WDaOAkQgUL^dZWnD@*R8b6TQdL1J?kHbKeSIq:7#)uNnb`.%Wdn+QkdG/;hW1o`d[f>\o[mU.28W3W_%`]D0\&jJjtW_UQs;A&)+:F.Cj?s&R_m/"*Z':AP%RUsbUCrI%1)=u:SoluTVKs#!Ff?Nhr/6n"5[u6$oY;@!M%$<_0"_SY8D=/0LdIYk#aGXl@?[J>]DO)2BZJ/7PX%2':K\YCl-\g(ScYH7C]'G7]\u]%q^M,F($iba*e?t4$i[qNTu*EN<8&FjbdH^b]HR8Qn!@o%,=#sm[,Jle6"Jh=GK0]AL'[>,D?b-[tP^kJuQo2O0uQT.2-+k(aJ_CLV6_pq:27E(0`62$XT6.+)kXl-(G4/WOeTBcWC5pMg:ag;UV"i2*MKt#[qgPcQYq@8BrLCIe[K_#jN%o1^pk\F>@9)%C5riJpH52Q3sOA;!Ca%.%@U3S'V\0rl[Rg#Qs?K>kZ]Z9T=FO.J:_^hcJn?Fs2Jo+a8io/XI;fdq:(c["&#kS*WLe6kbIl#P$:8V6+8bg,/VN%WoUR0+8ma(&>2^p^+>d@-5JJISeW)U]fjfV5$48J=@H)QTC2ah]C#WU[@'&i#6CQPI88-%Qslb"9i&/NFj"L@-8pZ@kg7)@]>M/E.+-:q:^+est/u7a+pN)7cbM(_149[;rmG`ABZf^'eATVCWXDQVA[&8B:@X5cq>^6j$A%)r]Mfk:mmn]e*r_Q\0<[T4!CA=TZY/e#'bHs[H.Sjc4Yk)T!=B+fT;"Rk&TQf7P@OjXC0s9L,!C*mX8=S1lja`!&Z"E"F`%qm#ON!*57([Uh1[&i;6o9Tg16Jd->3i4l"XrrD!VGrrendstream +endobj +xref +0 20 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000436 00000 n +0000000641 00000 n +0000000846 00000 n +0000001051 00000 n +0000001256 00000 n +0000001461 00000 n +0000001667 00000 n +0000001737 00000 n +0000002032 00000 n +0000002123 00000 n +0000004652 00000 n +0000006848 00000 n +0000009312 00000 n +0000010183 00000 n +0000012059 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 12 0 R +/Root 11 0 R +/Size 20 +>> +startxref +14661 +%%EOF diff --git a/Lab2_Indeks_dan_Pencarian.pdf b/Lab2_Indeks_dan_Pencarian.pdf new file mode 100644 index 0000000..c2004f3 --- /dev/null +++ b/Lab2_Indeks_dan_Pencarian.pdf @@ -0,0 +1,156 @@ +%PDF-1.4 +%東京 ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +7 0 obj +<< +/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +8 0 obj +<< +/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +10 0 obj +<< +/PageMode /UseNone /Pages 12 0 R /Type /Catalog +>> +endobj +11 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20261102104028+07'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261102104028+07'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Lab 2 - Bangun Indeks & Cari) /Trapped /False +>> +endobj +12 0 obj +<< +/Count 5 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R ] /Type /Pages +>> +endobj +13 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2175 +>> +stream +Gb"/(D/\Gm')nJ0nGpNLg`k^7OsNa%(5H^Q]7%d&Bk$^5JRE:K%O6g..4Rc:rqHRE=`tG4o'1b;mU.kpT/7s>dM\0pJA'Qe^0_piO.S,,?MtmJ;hcY*:T)g`[;cL@FTW_rO_V';u1%t%/VY&-.FY0<<@gSc7J1?uA'GjqiK0#4%dYd9T"*_VZl>B8/&N@'A@IA8Q:cgsn/4pmiZ"WkB<`IaDD;!C"2RPnbBdrE0>0A&%,*Ll4]`iY@eps^58#'=lWC6d1035[+@/D:q5(W0l)\9$=O*aX$d+uTpWPIXp1Y($.r<9D%oQB=K`EED;%cq)?].>3#jE$L=;$?n6A-q$g1WU8&fVTA#9%Ha()U@Vet_^&d?AnRW`mgE!2@=WEGoLDV*f$_7XNra0.*60;X[1Od7[&'(^LGq3S*j+YkV5-1BkdP$R'kVa+Jpli98af=uNaHt;T9b4`F7&*]s[(M_tgua3qP%o-/;GI!)01IR=a9S;q$qqtsMNf94X(c^8nY9.m%rK-2NUtRp4rEW`Wd1g9n'<@$%sue'ou@_(`LiWY:on_iCZW'G,&bs!4mVHH[9``eh+FDT,D`).HYW;P(V&%"'Ef@@&G4d2>P/'LC.3Rgg?;ZA]#WZX?hYnl39cXhmK$b;r,0<>.kU0]?'$cb]cp2J[V3[l>mfL2mgutP@fl!j,3P=W:KnK?s'a_@qUM9!8,-h\6Q^R"($WK[f'IUh>-EkW4!),P?gDJ*DQ?n.j#NgZq[eafbs'%A/-qNfL=?mi>"K'd."mmhD;2237VFQ!7,a`WZU((*]X)Y9LdeYQ,"O#ZeVYhiOc)_-=NI"/LnZ&FgAW6?6c^1RP^&icBgIVb:(*2Z&?8StJ*:-UP[<;Z(_ItA>.6kFXV"c@&9mEsCpjI12naZ?l_>E$NjpGs9Q^*S0YW:&Z=kZZ_Q:>D^dHbme1\[WK,t=S;lp.^BA@(EcM#%4()m.b&ZQL0<>f_sfm%8QiR=#YV,&jo!7])CZ_Jj7FNrn;jUY:h8e`=#JLho(0KO7,Rk2].k7FikS$^jXTFXt_,Zk%[Sd"5e?@C2i3WBa0-N@g81j`HAR(nl#]+@?J>WMYuNkn]CMbK1+7(_e`&25WAB6@V$GTYUU28#l5aR!=8V/3()pHo;-+5mWKmlo4tLj3ZBl=QcIO;pmq>eiQ)nV&^buonWnQf/hsjddQH0eV.b?Sfmt71hjX"nn?pA1_)X\0C&\@+.389V?[P!HDm4#3g/J_Of]=$GCp/80a!,RAFj0k2mM1_01YFi;cILnQ)\BALY]TMZrD?en^H1T;^IN3/9ld$mSmi?mJ.LVqb#uN6ARS'u&$XtUSWKrSA\%);\Q+[$:"Pr7im467Z])S&`s1d%],YJ&_FJ"&6FPiX$.8AGUi531/mJ@"Eo(T7Gb&D+GLh<)c?8!F38>;m0FK5mX:(anE!OG+<8%"Nn-DZIegHapL!VOoC']g""t_4h+6?`:`"MAK\jBdeY8A;KIJF>JVThPN&5*8B`(aXA&$(I%Fq\Q!(&:)-f1uR\gV](j+m?'0,.QBh4Z%c?kHql9IR7oru@'4fGeWk]>)IB%)7Xq9p[dsrSk;iK`6Hqk]nlLRIT\GhXH;VrIk0ehV:fb^endstream +endobj +14 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2206 +>> +stream +Gb"/)6$(eh'#*[5lq@q54#Tco,"8AacD5>ae"ed6U>@qp=UC/mPCsbQFN4?pZQM]ZYpHDjr:8Y+Mb%<.>qGJbpKJ4L!XO>Rhg#OtEXEVd3:IKV$fGMQL?Vq;ANKZEC*b;817#<7/.QojE;9!=Q8G8^b9Kc,Xkb#jfQ)9g:OV\.*U94(rKEhmDSPd)(F.;)clkMZQ&KZ+HH;AljJndYl\_6A!aZO35C/8f9@O4n[]J;5[&%k_1BOOELtGX0liE;Eo!R/M3_`buj5Y:6j\5*ncEimLj7m$joj/F))KiMc5OC9DN]E#Lj1Y9!XpUHgVX>9l+Nk8AbjT6-"+,Bjg43a32RKr-HKa)?2,N:[V6p/&82ON7j[Xk;%I<_^&e"Ie(u5-9h7LM>Xe)>82+P8X?]lMorYL$%S:nARPJA&`W%2SbQ'J(^IYXYed&HCoC_l1Bb-d<7b[c9+BL&4f+OD:eTO*5P"g5H,1MI2dab%@9?D<.d5cYYM8#mJ:HOuS6rS+l(28]G'*j%D0EL[N40]nFR5sC+@UYS!3Kk'Cp'Fkj_A"a$/FU"\J=Qg]W8I1S<DW$d';>6ZrC5"cd9p.#]ptC3#H*SC:47VI+P@sppOb-`koJq=r)+'>5;cm7]_3/WW_JcnlHn(9@&10.oobD-\[4,,7aXjWGceqg9<3"j-1#5(:*R]M97uEUi;]]3810I$)L1iB0pbk-U]-(,D->OY,-V4gkliFRuY2&-?L'j@P4+C&gUY_K4?rpAJ-g\h/asC>W9A("&&2#.b,H]'XSO,C*C0D;FLs)L-N)2Cs'JRs_tY/`ro&6KUgo2A]43l$dn=rcbFCd)pqMs_5O'VHB>X\V#_/63Db0YU-V=**hL?TmK`AeWpqnr@s6"m!oGr++DnNnm9:u/SU!rbSGR6,Y@(rX`:!%Y)&\]4,>FZO]4p5CMkN_$V@jn%,JqjK-_d_P!13ep/#@>,TO,T#J=C4^$Ij/S0c!.`f6>"uBt*d_BQeJWmVJZ-MZu4t0j=)/A,s>/,S[l8`Dq%9Ae5A9.J2['9>WpY@?GD.3;+r:T0P"'.7Xrl"f+P`q\iY.VA5_!54"h8]oiIW\oF6Y^j=Z/O+pQ]".eo,Bt++0Z=A1dEIb3ioo6,iSHR)/)ek%&+sB$BR5M?[#"X:-??Rl0!!c55_ed^om!h!n>-W30J/##*,mir#a:s+P\/G6$Ns;lakn>kdFIg;oZhtS,hS`=**,3M`@VO+Ba''--Hpe2'ROZOmHUf'7[endstream +endobj +15 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1692 +>> +stream +Gatm:gQ!4A&:NH>-j:d,a?/pNf=15L,S33d;@&#TQ(f1@H)A#'1[3(Prd.U-A$6o)!YR0CfpV9.3iD85mLj+cK/3Jp/AUj/4FL@!=i#7lj_\uFgK3n)p?MV,pgl$!2T9]a?S#gNAm*!kIsiO\L:AO/X*%9V7>G9mmARi:Co>?>N@fn_[R#tWJU@qYTK0We1#M3sIc)?Q1cg24@-mJ0418gq>X1R/,K"m54o'p%JES-31Dk.l`ZA(`$lUA-M$3kZS_T6dF;=9bSRU:3TVe/?p'@(fQomF^jf\6Sn2sPp3M8gV#/,D`8Kg-=5X<;c\B"rV!e*V&!]pg\OoEX'*m`]'<+n$YOH48(M'DbS`e1)iC&GM_ueeEs&5L&_:(*FVju5Z+L7kUK:*-6&u[1E>'E57ku/;32&A^O3>jALX4*a>+#Ns>%HZUk%4LJO_8n4>Et)7,[I!+J+nUe#o1i^Z5DZ"\'Z4rOs#4ChGidk!@L%W.1Wga=W-$"F.o(@]1b+l*!`/!&Jrn\rE+$VQKho#OCq>qV4RDEl8ZZ"=+'?RTEh=aX6fj+pZ;&ppRW47\aXCgZ&so#",1RqUMu]1tS6APuGb/NJ+EY"um7Vc[^*]'1Oca;>QN.o=7lqs=OW2\FmlT8o@!>C.RG;_Q81o+Z)XAPFO%.:#aYfOC1XKgf@Q[p8U)[TE*Xm0K.cVQ&JP4(D=.kFgA'KgnD/:K/oAXt-:0LG,PejD`h4niH%B#/]CB4W,_h9:b^R]RBIfRA@-]4jB2d-+=sOqZ[5iTfA&4\IHS`^fq^@[&#q:ALsJ^P>k4I:U>.rc^;8;k<">R!FuRAmZQuE)-\W0aBGucM&b-YDbP$GLs_,f]gFOj97D$oJcSi]J(=^b-a/I[:%[Y$S=el[2kW6tRh&fk/4L,t5L_(`GNl&u5@hpT~>endstream +endobj +16 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2086 +>> +stream +GatU3qet61&cUU9`Pb1R)K7]#.L\'VJN`)M]"^r[ZUc$7?]t96j])LTFTq55ZZpjp)V:h:4^3T)3.R44Z"SM(;#Mp!\p*N$9@PC/gk+?($M[tH(LQg92'e`hh(G!%Gh?g>KTU<8bJ/\M*OOoE%G!5;DXo-Q@AYb^cmR'd:MgJF%62Y-S8qacH07PHm29TfN_[(r"YtZ0)[i!=3I,ENoSpC"Iac_42(KiAkqPUlF..RNDL@JWF#D.8c>+QmO9C"8k[2G8hsGQ^e-(2PF4Zg1+E[V_YYh/BS#EB;X"D4%=Rm;.pbG1.C:Do9VdaU2(+*59'GHjNSEoH;5T(5-)GB9YX4O"K8EF#:[;(qObp9;[Lh7(7(IuJ@ip;bY&NM9j)8f@oR1_MA;:"mp`jOhdS+eZk$Z.&FI7[l@-0BJ'Rcu$:Zqsdcfh>/F;UH\N>"n>9fVGH^UHmtppa+UI09m.'`brgaYnA?\/':ML.(/j)$uF$7$:(u?c".15t9FeH'*mk^I]_iJ(mEV?C`,.h(K)_<[bZh4d4I&>@u-1;lM'%r/RZq23oSZk_uTJ*:s2!-)B\$e6E5Ae[B`W@336_*Z(,dQl7+ZsJM>f$8cPrCF1:N*iV+5N6;>Fh(9:>-BL/M]c:N(`mcfi\Um=SE74*/b956aqAY^NIdYnpdPUos8N&TZdT8T4;P0;+sg01rAmEWPU/#&;\%bgD:Wl>;M.s;aXO%hrZ8CK#in$*VRtg5JZf;h`8bgI29lkH4VG\Z_Ta[;0SX44Y=0B7h,!icnLPrA_ONMT6aa3lU,q,A;8B^1\-Xq`O(T)`i0K&fVP9O7;>rX%(Yr(\6YjBUU'V@Tab(qb#5/`G%9-K[A(^EO/9+i3LH\6\.8E3G_=Jodo5$7=*&qcYukb?D!X2['Q#^Rgea3S[47%XB91?6RN[86r6LZjjd4jB\tSXBm:-*oO53AbgS`+W13l494E"`=8h3Uc>+^kg,;LaDeN)l&6ISo4,]]ih`BM,>R'.L7j(d`&L&FVH-,(P6b/2_;Dd=\cj%Q70fkPDAPm@o'YlX#jO:UL&TYia$Sm3H;/"ki'Jh>7[j`.hK>I/(.=e5.3qaEI4>/)GPR7?1LgfELY@/WcGg>M:hd)/7Wm:=*b,^ol#HnnE*0S_PDmG?>/!N2m#s'n'57UT=G$*dH_a#Y6q/>\sYu?7J_mk2.uX+AeNRs`A=AKMVG?3Tr>4JaoE#IQ,g$Cfs!94@iKdm[n5U'ZgQP+e8oK=)I5j-O7c->@'@MRg230o&"E#7LO-pqr'7Ti0-U5)>bq8>S%X.'MM#,ngVAFO\>leqp->;V*A2]30"_e,`u'S(I\cpc_Pt9]j5f*nP/+p~>endstream +endobj +17 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2537 +>> +stream +Gat=,CNH8n&cM-)d5>,e6G!L2[._YrEi&Kkb0\O#b7rmp$nh!RE'9RWi4a?j_JBQdAf$LEc''kF\8e[@qp*a=]t&=Fn6<)06G9N0hRg&Qh7O]EaE?:ClbY1VlS"W5M@-N_0RJ?'CKg.u(QTC"Uhe;e^@?)GGW0Y\DQ_#]YY6^!\^r%JFGaWY_L'dW6%aptp4dO71F=?5G9_@qR*CS*bSE%->YA(;.q/M)1Am1Hi%c&tge=BbEYl)R`",hVce=p#(E9qGg>,0i`%CBIrW`@P(;F#K)/9#](Wf);(7SCmDjBu2)ohcG_$;sB8;7]r!V"A9n7hsLkT/=J:>4LWI0G8>?i[ru6Df$nTRm`_"B;E=3rfYNqSr"V'NN;Xg+;\[P\KW(qU6Q%NB>cr,3#5`R>CWGAn0;,BBo;3n^I2IVbE)]VAq;J]m]3,C5atb_M3ftkNqk&m-d4iPXqhlOHZuuBO(q]nC*glP\8]@g1a.=A3XClK9'"B`%idH&mb3e?/*4QgM8H]m[:i]7Nh.`]dYuDBm0p*oNn::PY!#CC$Ad1_RL'J,5PbOhM9kZU8"'N;#+gZ-D7/U/?("$["TC=9FEXdTH!Nm\UN4KKf.+HDX/M;QXBblTEDPLoi+bV[9_,@F]s!9(?gJrlk`Y;n3(mdQfnM%MC_[k60$b;n+-_n%MHmg$e.?M4WN)*Pb8bp_.RXY,6CN@9E*gB0nJ6V=hW6?P#V&G-1>U9)0("ec037E.q:om@G@^2.(n(TM+@S?S=29ai@.+A@j+%dD[]T%ld@)?#FM(B\BDZ!H),nY\jkSJ5mV%#F#[-W]A<%66`B;VJi;W=RB=02D[a2IK'i'eN\g^r-5CE?;".NcXq7iq+`.];-o'_99&oEksLCl>FT4.dA%,`A1S3pGLcg1emJGKGJ)An?r=D$+*1kDMs7*9G?`,"6iAJ-_+n'>uflkP1(l3iLo/XFHM-r6G^[=g*MZpbEq)krKeLcfaW-dSlqde!7V+:u3nU1\APOpcp:TS(<2Omg]Q0,A/`VG3GN@9)t%a<#a;ueHA:mo,-9!2[._/Y]dVrbp<-(I#3O'B"]3H:M?-bt-\.#8&ai_5j1MAd17IBC!QB=e(Ajb\pn)1J+#WqfoS,hI"l/)j7s()(5r;[p+l(=!f*.^lRtI66NgY/9JdGc!d]^b)o:Mg:pW-<-7nK*A`5-i.Z88]AX0rA,u^Rrb.d*EQNZ[/2Ru`qrR*L5haJk=M5n\`.D<1[8*BcS=EBd%_g3^(o>Wm2VW.N_">T4M\roa8'skgUV-i0,ou,_i\q\`[qirZe+CRMs3_mhBk4ntp\'0X-dm!m'%OC@?2tN;I2gP>JWAbY6GpH&Uiq1E4O.@;e\O?a.caWOi38oBZXnc3@DMT.YXc66?:!**dkg7;gV>+O+)bLUe3c:Zg@6igb1>6-F5B][UV1i**0m:9.5KYc@=GW-S%TNica.]'FMdAC:O0NC_G?^9F_)7'kNoaHpE"[>%HhPJpu2LhDf3XDSE\MkHM34>n"4pHdl+^AR55McV=#q7O41YN'4N>1ZD5XtH1d(Q5odTgNZ7ACD-;IXe7odMVdr9;j=]n\/:hX;Dgi;5D<[EL&&Dcp(0K/)D77gf`$[Vje+WPS2;ZOr-0pg;`o?]CM.A-BWM0GVendstream +endobj +xref +0 18 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000436 00000 n +0000000641 00000 n +0000000846 00000 n +0000001051 00000 n +0000001256 00000 n +0000001461 00000 n +0000001531 00000 n +0000001827 00000 n +0000001911 00000 n +0000004178 00000 n +0000006476 00000 n +0000008260 00000 n +0000010438 00000 n +trailer +<< +/ID +[<55fd2463e8f658fbb5e6ab0241aacf37><55fd2463e8f658fbb5e6ab0241aacf37>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 11 0 R +/Root 10 0 R +/Size 18 +>> +startxref +13067 +%%EOF diff --git a/Lab3_Hit_at_k.pdf b/Lab3_Hit_at_k.pdf new file mode 100644 index 0000000..e8a7dc1 --- /dev/null +++ b/Lab3_Hit_at_k.pdf @@ -0,0 +1,195 @@ +%PDF-1.4 +%東京 ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 9 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 480 /Length 37218 /SMask 6 0 R + /Subtype /Image /Type /XObject /Width 960 +>> +stream +Gb"-6H!Fg%iN[(OlC1:+U[[ai"%1.iR79ePbbq8Zq@hI\t*^APCms3,i=<(/_FY9i_q0;nTS2iBq3P6th0luK9Xp]h(#_F0)Jfk>N+:*QA#_F0)Jfk>N+:*QA#_F0)Jfk>N+:*QA#_F0)Jfk>N+:*QA#_F0)Jfk>N+:*QA#_F0)Jfk>N+:*QA#_F0)Jfk>N+:*QA#_F0)Jfk>N+:*QA#_F0)Jfk>N+:*QA#_F0)Jfk>N+:*QA#_F0)Jfk>N+:*QA#_F0)Jfk>N+:*QA#_F0)Jfk>N+:*QA#_F0)Jfk>N+:*QA#_F0)Jfk>N+:*QA#_F1\#B!E&>$>*4DcQi:r;gJZg9ti+Ks>OO3o*E,mJQS%5@%DP/hSb%,U>H5>IPYfBk[S:c9(K^q+Z)"f^[sZ0.&"mL4k3o1RMq3$8QV\7*!i`ZEcrY(f@,%'lS$ZY$JX\3HE(&IP,!A>ch2;hS"87RF=D#r3Mb.0o\ns2EC_F/^p3s8DEPf<7"!c`/lB']5O0p?^K4s0?:`*h?nm:/=XjJ0djSk`u@J5BlSQh5M#'ZYctPYtea&c[YrS\CqTl495FVRPa9]Z(n11p$:5EH1Ron$DU(Mo(DZ=ET0Bc2d#p_q>E<01UC8mC9:^L^?bME#_F1`;1H<-q=u,X%o3HA[gEPn'AskgW>X,h;-mKGOCunT7\lL&U3K"@3SMfUkh5#P?rjK=,LHG'A-P3HF3>_hT89b0%m'i.DPTe>NBJM\e$!dG=$;,ekj:;'nqKM;$O$6GX:S'ZT!0\d+2HFMG[V_QrF`-!EE@2\Sr>DFr+$4X>e$Q8`]9^F,$\BK,AUXJGTa,!Eh)dBRV`04Rg,gZY.U-g1)L+St%TL:7F.Ym_/@$%hF_9eZ'oB-Nj\[ftW5JPR*lJbUm`<;KLIpTBh"TWrGjd57A<`Tt9N#499?2X30%fp):qXs]U@Edui[;08^@sr`Pr;HR-',1-.hIU%YCO18Oq>'ioDVa%1p5\4skC<9cL[jp)!.&+_L(-Y6*e!V?8]SlCP*-Fq)H"eu`/g\[h\3f3cL8K-1GO@);it^OQ9>L^4"EfWh2k&>jGha++0mlup!NT.-#jf<7,Idc@!Hm^[B"8l>Q_g9k_dnaQT5B[@m"@lOi[SC#X0/mZ%fW)00\QS(FIoM>c0GNdDinV.]):=M,fJHm7[d\TU_SiuDiWs8Wi.TLNCg=<6*]=YZW5kF-_HhZr6Y?j@f:JZ+8WFLF$n_W]H5>R\&AbbcR:HnU8_M&AY*^/f2h6'qhb%@toRb"3%Mr)-o5Q(#gl:9u=ERc<)o())$\T=r\]rG,8$38K$d=;2p\$=6,V+Ui^:7XEV5Q#J@EcU3[N<-i4_rIf$^+)R$%L`cI:n^3nDQ_IZfs>;nJ@iF?LCP>&;GsMrY7:R,h07c@e#1_'5Q:F&!46Lkb3=Y-p[/38#9J-*=L*)8f#j7.X)7:PUIXY$Xkhghh;$dLrqKu5*E@bRGOA_LHM*r(TO,%1cMR3CO$,CIo]Vu9LX)DMF2s:]KuRiSU.+?V7RdR^]_'WFC7=S(pYHrSHK]R3RaVin7VJ7AXBDmd]mJA7?QT:Gr;8<*17WeSpYL78>[1N+8kRjHD*:!D$GM=;oB+;nUa,s0o^n*#MsK32GAM)s\$pSUmbNfdcRW)"o^_RQ>.(C#YM>O%B,WU?!!#;N6(L)]maS5"gAao$G,YD1S=Js3leCM:C:1d(q`:mgIJ\1p?XEfCs8MnBp[8">G-X;l**Oeu`/SUnkrLa*W/[(\Ncb:7V^lDnYh0$U5q'&-RR.qZdWO38ir]p@?B[)b\bc?f/5);nLj,&eZs6dNnL`;"^%>gU@:#X096@khVgpJ%g4"jo@)ar-0#":A*o&%im^,QCL*bpAXsX#6=G0U12lcD,/-8]`%QY!"]q_B.UT>okIlB+99(Qp=j=RIX_:7M%eFKE81c>Q-!c]36pGg)O0j0j0dVG/!0q!W_4laZHIT7=S$or@lb^]489l8AkW*)K9\?bY&bH:C:jW$?+an'A".`Pf\S/cYY0T5?KUYHRDU5Q7UIA7oPU6IiDT:Lgfd[;0QHk4J,[PUTOiI]2T/Pq,pfK7i"8gtmP\I:Ji8gUC7e>hR`\p[$S>Nr<+oW8[CHE5DP?i_S7H"Q"ERG+rWqSrU!'A7]P?4)N\.j+^3m/5`l.Bn(U0n](Zf6I9T2`LL&cTcV9U?:gqZYI[uqsHPD4(#[I2#oc3K-1GO:e&7f-s_=o2JlX:MA;*u0K3h#i9p%$l06lp6\eF-3r/RG1i([dZ(Ejjgc.J]^>Rh=3,q:`$%)3](L;MM`a_oMpYMT2LGCYAp[@!(CY#Qo'e04!JcuO>^K^@fBec*N$FY9/eR8.>h3t5:qYCtWe3W!+I0%.R*$DEVB6@n(^juTYQE,W-m'G#!/j+4;9SEfgCc:%QA&fSDUJi4KeZ,H[R^%l6a-U<9=1n+H.ff(]/$Cr+B=lu+n/N,p*rfL7VgKiuFKn'FpYL7j_Lahq'^0)!'YnL[2OcDr3_h&!*,7B,[g^AnF(^gCg4BeEL<`@n&@phH\9As7NRF3Bfg%PEVd,X7>(G5ofdn]Qs**9rh3jNoN>*]6PG-N)mGs%ncc]XZ<\:pXb.!-!7MgGl7'EOb@d!s]/SWo:>N<*(MnI'n9s_`6\]lHdl*d02J7d/NCS:YGCDQF]9sjQ+FHC7lPr?>@fF))Gn"nDU!enqej<#mgnM&J=<=foJ>n89I$Z5JP=(-Pq*!D=OPb2*.tjN6IkVgMu37c'r@kS2kEdsb6_83t,DhB/!%M'Q>]=t`7Z@D`3)"G3L(3G#N.Mm"(3f,t&HLC`=]=Nk:WK$ab.ZX(riGFsH>U.dZQC/KG"ac%[:d'omJ=#9rQBFdq:)BB!9!XEMc4CK%fFtcB2oOi;![=V34X2\8Gac@K+g?F<0p#:\q]_@Uc53_^(YBnd5.#_4g<.@Y78TCRk]D,5@2?>ENqa?G].0@T@LFGnA>cp?S+2qpT(L3)EDi6HFapn+sS>YT0H\1It%%;7udMi2Ob-M]G5\=teXr0[>6Y('4Rkg:.qE7(L4t=!cX`oMVH0d#dW0m::srOK`r!9!+>T6=C(IA9iF=r$mTGPjb-pCE_GT@FouhRn-NKF,nm2g9=a"+Y.:WYt7e:hOp"i.2-aX]p#3bSbOGo`"gd$iRJPrqq%er`Kd8U&=TH^q`4qH"6NA\aeN/`CD"uAFu)`r;:m6l6R]$'YN2P?@,EFR;h\jflf$(XIr:od^;",@YdN-)rB#HY?igtMbEH?2B\oW!`(LuWpkkGVI/X]B6KrK8=3k\%e:gE82cThGWTuV)MJ)B\`r6K$$S5K<6,pak:-0eda:<,#e,NfI6>*.d:"[N\jpZE\eL(o"SN]OQ*Z,[na?XLEgc/%43kp8mN:/sOSc[Yor!6^]^0=7%[qWb-H";(kL*d$r2]_U]ZNXOAn?+"Ft;AVRmi.HVW?6).Q:n:P.kX'4$oiAn1:31Jt6UO3]GCkbIG1ZW_fs>=>I!bqPq!mBj8Pjc/;lX5j'^`M;SFl#_NsN-t[(pM,P?LI]2DjQ#7Z*-ZjTG.)_si(lT9jN@?)c^[!/'c#hP=rqg4_q]f]%cCTAh1W`QBAS6DrfLJss;%AUe24*OH5g,j604NHX&6UO5#2]l>:F@k"Ra)?:sl;dMK/E?N^r;:s8`[/Zri8VH+8UoU&A]s8/J*sgrrqu@`p$PSq;[O5]/$HTpS>"-rE+7j]r_G`R`9DYb=0Gq12:sm<`L/MpCc5Lo8sP@eR^9<*P*;*[@k;>D<_l7%4TG"K5Q.ClbP(>XeWQGm*;(#Tj8&!Tn%JIe3X;sOO,op*T@:he>e5%ASpTm&t^TA85W0>IGUf3a"ZBn(=)VsI"Wn"`1?4aZnQ='p)$]%=)j;b%`/"U-oXZJ1L3*dK"n4P]J/VZ$Aadkkq)cCO5J?DBuLd*PVL>.%uM2U%o@?fuYX@PoBj:Q;BKlZhQ/FWqNtSZgnRGQ7X9+$"A[Q1NUu5(-TTkcTs,;'[D4W)HSU4l)ZkOl:E]]%crA]Y$@Lf[BEVduAKVJ,T%l>OG+5?Fk6Xe+jO)=^iFukTP*=O[gI4HCaJf:HhXj-O]oW;"$l-qt*R[REpIXM_;&+u1!j%aR`h?mi6N#=3bTfPZ^MA=[\(+i],YP"sPY!*Aj:S0hUZ*ESH(o!b!cC?mnr;HNgq:TSm`f1ob-RU8f-/Ee&I/jfi&7@>'j7?2!<2f<-,EF)r-)l+1[;=OEZ&GYPXk)aeX&l]DeZ)ZPlM)Pi.6`7NLECta>!?`Q/G231q"XX@]mHf@GE7]A1d4.Ci7bSSlI.$Z@.<*j"i&$bD;3PF_:5D\UJn'cIJWSMhnHc2_rmYA?IJi>qG5e`";XUUHFiXq=/VWG/ugl/)'./Wf/G<7Y"tZR#mgq'^H@!qs+:-sn\#.HUK6a<^"[/*q>'h4-75a:L;*Z`lc-?tWRok3CDXb%C/0Np+0K<]oB+:r;c?\T.oiUFR^qGVpYT%:WRe1UX#[K=D!DiJa.8ilS#_S";(&qK9hfKTW`AS]T5s5H.J8VbcN=\?OIDWlXBDl-It!9$)fP]Oom?E!R?E`-/p_eg)*bOt^AC\([X%AY#?+Q"e[pIagHl;Ak>n]gala0AJfrG-cg%@o-;Br-dh+ZbV]J!I<303L31T[pY9iKiSgB"Smc&9P:'.L3ltTKV?up&t'dn81C_a*@B[=0IXENqW2?p@InAhS"9PK@neW=jsZfbDI#bqYU01a2`],mC.f;r4K\TGMdhX9q+%iIXZIP4Zkhh3B>dF]nLoapW8UHeUN:md4&4$=c4%IMM[0e.B67?7bb@g?[hf^a!7-[m*Ft@1L7V9p%.eHnaVHrN_WAG0AYf8Y?J>Kd%E[%;,R>lrncMgpYC&N5Q:26IJ`^uhIPBLD=-)eY?\VY`JPC?@U_P"o[?,Ko$O4&2,,TG;EI/U"JHfSqXj$6OHAbXX7+cMX/L13o(!bbDsP\gj[Egu$8cMIW+sk_:<-Y)"3O+8]6J/!P\[_M>Y2\<)f6EqU3S,Z#mfe2osR3D52O#_GljfWo)3;boOk'c!PTCCCRY92/.Zm'G$fLJZbpUqI&D8-/r\&-P91"@3S%5nF/b&-P91"@3S%5nF/b&-P91"@3S%5nF/b&-P91"@3S%5nF/b&-P91"@3S%5nF/b&-P91"@3S%_%itMX6/a6>@.pE5nF/b+H,\?\p!Iu>qK4;lg&>qgTc7jJfk>N+:(N+AbQHSXIRU('Ho7"@8]@1cdFT]e'MA+:*Qa*GKV-,(!N*"@3S%JND6CbXD05+:*QA#Qf.Ed@#jXD*\^95nF1a8>@sSg"V*$#_F0)e<*45-F1)a5nF/b&?$h5c:hbe$\BK,Jfml[)P*;;hA-5[&-P:lN\idQOhE=PJfk>N5TOQ\Ag2S+&-P91"G&V^kfcHg2KQ$,]/FN[[#Q.g8VG57Ybq7,`(Bb*hdj,3C$^mki+H=Z?A8a9ecThF4JC)ZiG4"#>p30JB?=$Y@#%R`JOm1*&(Dq]6:=qJq/nUP9AX4A=?,A1cdFT^!3)6m-IZK:?f@(IJ``mrf10e@KcaZo#W9Ein:YNrVc[n;cAYjlIW5sY[KoZs8'j$-Yo0ZGOOBDILq*=$"TJ-iGKgfL9I6/fuTW2-5bC=T?GjlI@QSp96$Df>+2ltftZJ,fIk!*HO;M/hoiJ,KNN1M=uaj)#S8CE_.P#^6ZE-J+ALB-spn"kS5oeZN2i;l?P0YTbhsJoJ2\BlY5.FCl_D>B'ObPg*eZ5('m#Opr?g!5RciJ$/,6N#=DOi67TFN[qJSh7@%4L1RgDJm_#XRKVSe+k,L5dcenCp9pWUs7$9ODB[IS#^6ZE-J+ALB-spn"bdq+-4"f'R3k50>KAY3%[0AkA&jTG?[OR_0ejq!"E*2pD(f5.3,6sAd*PTb:X@D7d*\UY?b1?YFIU*WD[UdNG>tbP]q!s>4ClMEj#t]&rUsHhmr=pR@n%>S]-(2`R3k50>KAY3%[0B6]mFtu^USO+F6Cj!Y$DtHC=T>LS=CQ6'a4U3h07b1$PuF?),0b4:qGO]-Vp>h]6E_')`Q9cO$EWrnJn%]cCOS$pJ4@P1"NO4[r0*<%<(+9W@gr61.B[!5Q:Gq`5a#,prG8u_VIPUa`Wt-\sH`LC$O3IX&c>`Fm>4IDKicYp[-]O!.+;$@@RXOjN*G`d(b)1;JZQ$]e'r9il-ggcC?nBpi)M!RCB\Rq6I0hrVQPe3m:7j^uac[rI;,J&YB:k'5E*L[f8ju$b"n\INcs.DG\l*p)MW74'aQKIc>L7#TMLoeOcr3P*6#[#"TT[Z,(cJt"qX`^cMsJll1MP:5JtLrVZY*(oo=IMP)C;$5m6l,@p4N_cJqJ]h]Y'[lDn^]!qI3M)kbF1W2fPp]B9`kCU@X<@3=skmRX^EkIGOA^(6j*KX6q!iHUtgJ"r;[BH)0t2XKi2_VH/75gHhV]-G98b]gp1M7U8+L>h;(#.hH]95YJ'Xu3[A=ZOsSF&OkFHgi#`QZg$gl8TC.=4'=eeFq:J]:T2/8'g2ZBX$)#U.'nnD1VMUj1=X!se962PKNR"hobIp&fr-1.Z[jtO_(e\JeSdb\?NZE1Eo9Wg5!IuIhQ8gMhp%=<$M(.`8kK]V+!.4J.eP-$lp8KI)))=qQ'Zq%+-si?8\SQARV-NHUq"XW1p$+Oiq<"/b!+=?W?=a+](qJJBY6aNMa_QK7,fS3(r_fWeqYIJ-f?:f(7rQuVV@fn)Eb<#!I`R3m(aAVgg00?p=u2EK#Z0)'Ng<)ibgm2_Pkrql-anG;IS40gc.C"&r+p?^2O!Ia[@.$PT):$`S:&fE*(RS12-FI&b.md42cC3Agka`5Fr6\bYLrquQq=&)=YEHcW!4*U+d6\iDUrqhEZ)gZaR!5S-ba,P4:T0E5Hf3a$uaX=u\%?$7,hR79j.??iF6!1lg?t#ZVO#`b`HOJVbi]#5k39-b%7?S/ELnjNuL%'kmbqEB$Dgt%F,-fFQnsdVe7`^d*0SP!.[(Nk3+.B55S:napa[bFp_f;$ktA>V-Go>b>Ktep5_H;k]h^(m-O)m[;36bd6_,$HLtl*VtV&eCm`IZqt]EIe\uUmc5Gg%F?ID%L(,)D'0r1<.LKaA9G9LAZ:"nSQf_0u)kCOhYBr>[.EUE%IJim1bEa`EEb-t?e\(hkVa[F&+jg"VesIJ0Wg#e't#V,S-3=$R9G$-bmWSXIRU[P[3S:IRRZ/a+X[U*-rIH;I1H0%/t-7/"'c6(`QT$GpXG:FR"^_%k=L8j[T]7V='R[k/'clr-amD@:b_W@/%TJ0%Z&RYs#6Ekf*$-Dl^(Ld1c9A.3cVWJI0E.e3d,W)U\G.LF?gqQlS\J$uZXZVO$0TVn$3paN$`DpX,/7JrgAcs3A2Ru-cM.uQXh<+HD`8u[W=;C_5A4fD>u%k7n17Ja?Y8:S(Rpp3ab`h95oMYJWs,&bh\8k&ZNP0dI05F[N%B.Jm)qc%)7H:k/mT2I00#!Jna`"+=(bshK8R7Mh9ZM:b%7E2!uP`#5bV'9j)K1\T1TT[Z,Ud(?*oOf\45PnDj%!kOFLn@Qpnch7U<5]:s$-bp8Z^F@6[^,\7:IW9rVj*)6.A\5,d"=qZr1K5">bjTRM=bF2Ma.)u%k7n17Ja?Y8:S(Rpp3ab`h95oMYJWs,&bh\8k&ZNP0dI05F[N%B.Jm)qc%)7H:k/mT2I00#!Jna`"+=(bshK8R7Mh9ZM:b%7E2!uP`#5bV'9j)K1\T1TT[Z,Ud(?*oOf\45PnDj%!kOFLn@Qpnch7U<5]:s$-bp8Z^F@6[^,\7:IW9rVj*)6.A\5,d"=qZr1K5">bjTRM=bF2Ma.)u%k7n17Ja?Y8:S(Rpp3ab`h95oMYJWs,&bh\8k&ZNP0dI05F[N%B.Jm)qc%)7H:k/mT2I00#!Jna`"+=(bshK8R7Mh9ZM:b%7E2!uP`#5bV'9j)K1\T1TT[Z,Ud(?*oOf\45PnDj%!kOFLn@Qpnch7U<5]:s$-bp8Z^F@6[^,\7:IW9rVj*)6.A\5,d"=qZr1K5">bjTRM=bF2Ma.)u%k7n17Ja?Y8:S(Rpp3ab`h95oMYJWs,&bh\8k&ZNP0dI05F[N%B.Jm)qc%)7H:k/mT2I00#!Jna`"+=(bshK8R7Mh9ZM:b%7E2!uP`#5bV'9j)K1\T1TT[Z,Ud(?*oOf\45PnDj%!kOFLn@Qpnch7U<5]:s$-bp8Z^F@6[^,\7:IW9rVj*)6.A\5,d"=qZr1K5">bjTRM=bF2Ma.)u%k7n17Ja?Y8:S(Rpp3ab`h95oMYJWs,&bh\8k&ZNP0dJ/o]OHBbc+O=7Rfl$mIoJri.1t8SiC`,PoHl5^V4dPc;goGFa&$?#REsl8GMPXcUtYUd\!Wmrp3RG1$C'_,(<.`a;acE;@u-l,W$;arAcAYX(BJ?!"^+iS=H,)]QnPu#Rh,(FQcQk2k3*^4of8b()%@FP*-Fk';K7[!'L#2F/@Ltl-EBGJ+fk_)"a%j&O3*kj:-E3WJDWq&t02lr]GJsA,qFGNkT](RT_IbhE1dOgU:rR[lSm0XPIdgnF65HgM`(*hgP6ih/:h$mC(i_+OZma/H5!7OYQ&KV':@\WQ6%NTMc@dW$F29%fJC8-caQZ%fge@h!)>]6%]@AY[b_6U2IJeokFKjfWh4#no!9p!PjR-.n[B!d\!Wmrp3RG1$C'_,(<.`a;acE;@u-l,W$;IrH[GCN;sM%&`L*E`f(c8OHu25e.($5?XHKVroT\g9"H=sT<0[Dlr0$$kUE>PIS-T\)t]e$&O33>1`$Z#=WPDMPa(794?YhJ!"oqYMn7m/bEr2+r9a%!b2U`2'cDXq;C^\(P`!+>%k8=aK1\IOiR"4;N#;]tA&aII>+Uk=!<>i_1b%RqT76X93ncSP2Duj-,rRY^e$N/X=rfeS\.i-krRo?QcTW']K:Ysjihr#u]Km&X(%HoK6?D,4T8*gm!W\E3:NfojnDHS4Ll@9DroP.YDQI$L.f;L\F[nUjG#F-6H,I"._LGHT2&ld;KJSiqG'?ba'_r7RtlA'e.H7JrBJOZ2JiV':@b8SB^rqK&Xn5<*?'+97chF(V*nm[,hF;G">cqQlI+T7&1E#!Je^`(qfsH%R/;.cL\uK]^.F5F;6EFUpY-&-+7b[)ctW_M&?u@-Gt2-/B3ucW1g2p8Q+MoTW5c5G_??%=1jML`]S/RL?1B:YE-k=,B+*jmgA5#Csi:J)"a`^OH+]i5HL>/ErHkJ$uZXZVO$0TVn$3paN$`DpX,/7JrgAcs0\$IWnIZbk:;RMu*A>`JYPaWDgk-W/T%dq[V5u\T4P6(Amgm/*UY:J$uZXZVO$0TVn$3paN$`DpX,/7JrgAcs2t1kH(bd(+i\i5]C[]EcSJ^3\W8Wd%NhH3b`;B!78K1Iq)'!IXY0mOudGce##j'P*27lD5o56RPh,[Rj%a`b2U`2'cDXq;C^\(P`!+>%k8=aK1\IO@FCcj?-ga=S2te-6pu^+Ve8h&d!u-!NZULR\3e%2X=EjHDnX\u9cJS3Is6,2W`?*CmbE*D\ojk(#3jH7(4Z%Wa;FQ6;@u-i<9+L7:oT6m<"bW-TDiX1Ud&'dr7RtlA'e.H7JrBJOZ2JiV':@b8SBa)T?Te#Z^&M%I4?uV4f),r:FR$(K-!uAiWka%js'cW9VamW=o?sN,%Jtu8k&ZB;NWp%6)>cS:e>=Qd\"cPqQlI+T7&1E#!Je^`(qfsH%R/;.cL\uK]XKWfhA2+>?UmWV]oWt;bBLVPZ!W&kWu$hIS`UL0%-B:7/F6T`LhQ.1'Tn<<5_Q]'mYEQ<%?l4Q%\9DrRjl?5?h:gfr$$(cr3P*r01*A2s?&,pCJ2XD+-J2:S'U9r(!X=o?sN,%Jtu8k&ZB;NWp%6)>cS:eAa65Gt?S4^ShDRRhq:ZM:b%7E2!uP`#5bV'9j)K1\T1TTYCJIqF<)B.Jm)qc%)7H:k/mT2I00#!Jna`"+=(bshK8R7Mh9ZM:b%7E2!uP`#5bV'9j)K1\T1TT[Z,Ud(?*oOf\45PnDj%!kOFLn@Qpnch7U<5]:s$-gGprV>[)UT_E.lKHWj#7VS.nGb7Wclel+d\!Wmrp3RG1$C'_,(<.`a;acE;@u-l,W$9J,JiWR\nW:=nBlshsB"#UWmCT^OH.IYJ2+p(T2!$fWer$rUsHd-ISNg/H5!7OYQ&KV':@\WQ6%NTMc@dW$Km'pZFGOg=Yfd,=ddX\isN;!68`A?[:`JGB`f+RlBXiOoB0.nRnJON'U5g(%G3p7Ga[BQ&>?&-)%kOYWr$^qo?[pWNQO:*,)\3o(2I4CSRVu3d0kgdmX&=Q,TAD.P_2kV,YB/.2@,\*`OWK#F0s'p\nEk6AT[Q=*Z^&M%I4?uV4f),r:FR$(K-!uAiWka%3qrVYX&c>4*BL.&?[1N5S'C)?^daeOD>a>0FIGaM[^,\7:IW9rVj*)6.A\5,d"=qZr1K3LkMDiCGOJkOH?JoR%mTubBj1E-mB9m3Y3YqA'e;+8Mi!;%k8=aK1\J$H[gM9br7LSgTY+=]%F`FHVMsO4?TWCWM3-r[r1"_SL2dWXXsJ<;irfu.A\5;8u]lNoUo(o5H#jW\bG:Rm-K/tp0G1I6!_-DjiYrD6`X4fk3;,Us8D\AnA,>j=en8d=o?sN,%Jtu8k&ZB;NWp%6)>cS:e@U,n'C)4l-EBGJ+fk_)"a%j&O3*kj:-E3WJDWq&t4aNB/>\LCh<-LT8JRn:FR%K;bBL8TEga>pd$4"\QHC,Dr&!IcC?niDD>eLS?6#)lGShaCh<-LT8JRn:FR%K;bBL8TEga>pd$4"\QH1&n)(mpH1U1`h1F?4eCVs72a;9>J5-mEcW1g2p8Q+MoTW5c5G_??%=1jML`]S/lJ2:.ace*)Jq>gR$OI,bqWXmo/6K,qiPIQp=*#:kqnP%Ch<-LT8JRn:FR%K;bBL8TEga>pd$4"cFlGtLPOK`>IVt[o(2J=eu]l4*j3s[qQlI+T7&1E#!Je^`(qfsH%R/;.cL\uKKAl,Cq&0ro]]<([=.oDZ.nY,9r(!X=o?sN,%Jtu8k&ZB;NWp%6)>cS:e@U,n'?Q9JZW.c0'_0AX]l#7#3n%-rbQ09B:k'?5u/pEnP`%KhOk+=MYKI`U!C>9GO*i)B!\>3V\d!\c'gLJS#q&3mQ+(<5?h:gfr$$(cr3P*r01*A2sISKmGB`fNnEAK-hqGJRs1$%ZFmC)ur,GM.:Nq$WD;2M`1m.cRAasE*MMshs.2DAO8k%U0#F12A5p`5M]g(horn`"jH$alWVgUHu"RYUDrSDnil"#qUCh<-LT8JRn:FR%K;bBL8TEga>pd$4"\QH1&lahQCq!i(IVR:@"^@!T,[r7n)Q?MWI0qC)JT<0[Dlr0$$kUE>PIS-T\)t]e$&O33>1`+@/kAlFOB.Jm)qc%)7H:k/mT2I00#!Jna`"+=(bshK8R7Mh9ZM:b%7E2!uP`#5bV'9j)K1\T1TTW.7hOb;u3:QBds82?&-)&Vt59E%71a!pVprr0_]mC]"rV"uaR\l&a3Duc3oOf\45PnDj%!kOFLn@Qpnch7U<5]:s$$G\7=abO=>$BX9&L6.Yl`Ih44Ce26n%JJZUQ9PIS-T\)t]e$&O33>1`&@S3AWI4lI;eqOcah!4ZGEGi4smk:7Q:n[X\;PkKoni_o'AT4[#iFWRA(`bCBN_T;C0El-EBGJ+fk_)"a%j&O3*kj:-E3WJDWq&t02lrg]7=eZraZ!!j&E:/4Q%hRkiK"9o&O3csSo[p?UhXgbdKVJ-Q)pgERI]b;"kJqARu+sKXa):G.Z/H5!7OYQ&KV':@\WQ6%NTMc@dW$F29-N/5\jN.B5*?BRqg,69KhRn+6i:tFPKsGpk?8!t8^qd`;>IG@VoCDI\Dr4lYc&hp@AasE*MMshs.2DAO8k%U0#F12A5p`58E9PV.3B;P;IK6Tnk>'(Z%2q&'J/p^p'Q`#(SN6eeRPemKb*GSJ[^,\7:IW9rVj*)6.A\5,d"=qZr1K5">WDB=N>jeX5f"ZtB$?VC!,6.]7G[IcZ"#\]X]DQ-GABjI5?h:gfr$$(cr3P*r01*A2sYncDc\r_t8p'7fkKaB`Unf96jlu$o!0ES8=O[eH04,1hrVH31c-9P!@`>9CURll(ae_Xs?G(NfF:V=HZ^&M%I4?uV4f),r:FR$(K-!uAiWka%/GlcNBf;Gts8Mu:f36SSCpju4q3WmH!+=$<0i#nJ1,8j&8WtJih/Lf<8H:G\fJ*!T6\Bo>J;kL,QJu=FJ$uZXZVO$0TVn$3paN$`DpX,/7JrgAOC`ojpD-i6?8]GLSeBlTOHHh%0QeXB=Gk]NMRQ!:\cbcP?J.V*EEAJ+fk_)"a%j&O3*kj:-E3WJDWq&t02`r`naK2q/:bI=:_Yo,X14p=a9^&/uQmdOgUA2lnS0lr0$$kUE>PIS-T\)t]e$&O33>1`$f'3B&m@l-cD(55F840>1>Bp[4=CrIYIn/-(3Pqe,nk_ZXXsJ<;irfu.A\5;8u]lNoUo(o5H#jW\bG:Oe#1r:[dUs-:0l)ebDV@>F[nUjG#SU5jpg[0Fdp,+H,"U3GU;ocE#ZY.U7J:Ng>B3&&6M\[no_&1;9am/3-^q`5.I@@q0$)gtVm;euW#NG\GNtXXsJ<;irfu.A\5;8u]lNoUo(o5H#jW\bG9KOH=YQg8V9*s8@.9dtS>QC0[6sRd05)>Dg&;bflS9a2c5IG^/T1\Pq,Q_1N!ICpl"2pqjO!3]ahd-bcejd\!Wmrp3RG1$C'_,(<.`a;acE;@u-l,h0C!k;@&+BU\)Gk#S,5ldd\TTZRRhq:ZM:b%7E2!uP`#5bV'9j)K1\T1TT[Z,XP'5(+u;%Ho&\%RDr/uX,5p3;im0e')2"MY.cR@jMS)4c.M_K+8j](qIni[2*&1oU?657.Ch<-LT8JRn:FR%K;bBL8TEga>pd$4"cFlHsmIld/'YQUQrRo?QcTW']K:Ysjihr#u]Km&X(%HoK6CiIQY$8AQQS)S9nqcpN.uQXh<+HD`8u[W=;C_5A4fD>u%k7m.meqG-o][)@<]kNiP[rbQ09B:k'?5u/pEnP`%KhOk+=MYKI`U!C>9hmd[$F6CjCh07ahT7(nC`XTU$+oU*:im0e')2"MY.cR@jMS)4c.M_K+8j](qJ$)q\Alr0&m1sSWDSa0*2?,>;C=UaH,5p3;im0e')2"MY.cR@jMS)4c.M_K+8j](qJ(@jiVht%Y.uQXh<+HD`8u[W=;C_5A4fD>u%k7m.mes[9,Q6<PIS-T\)t]e$&O33>1`,E&Ag73,b2U`2'cDXq;C^\(P`!+>%k8=aK1\IOZ#"pH]mKJe:HVLX?G/t?W)p1Zd%NgNn(t0F7f'HV`LhQ.1'Tn<<5_Q]'mYEQ<%?l4Q%\:/c2P@`_#b,X&"Ci+oX7$r&!$l%(?f:krbQ09B:k'?5u/pEnP`%KhOk+=MYKI`U!C>9Y@#%Ndki-gTuT-&OW@1Nm0/R,Xt]P?c`XmYWFSN:oABd3N/b2U`2'cDXq;C^\(P`!+>%k8=aK1\J$H[cM2#`+P&0C%9dq"TAi!1bIbT<0[Dlr0$$kUE>PIS-T\)t]e$&O33>e@GDs6l-EBGJ+fk_)"a%j&O3*kj:-E3WJDWq''e9Lo=/f#1ei>`Z,\J&]&^C!b*=IZ"q7`hCu`Qsf4cH-fui=#4r"rfSl.&tVj*(O5n>F[nUjG#SU5k#E8a-c>.&0)>GXrWP*;(Wg`e$j`Quk+e$OpeLl3'&l84Y&Ch<-LT8JRn:FR%K;bBL8TEga>pd$4"cFlI.])L;gIs(#(`TcI0R\k5ldcI[IrmJn4rRo?QcTW']K:Ysjihr#u]Km&X(%HoK6?BDDIo[OQI!GJXjY,G!q=%k8=aK1\IOZ"AL2cd,t.E+*9fUs4JE[;.EtYTPSP:FD(mB.Jm)qc%)7H:k/mT2I00#!Jna`"+=(Fmf2g4$,MI+i.csBpoeK&CC1f?&-)&Vt5>Rb,:"-HS*dHVd0>DU6rmb(W2/(@PB!Y4$2GL*+oOf\45PnDj%!kOFLn@Qpnch7U<5]:s$$G\7\Q5h9Pq,p"J,Xi3X3R7t=1NA5RPemG1m.cRAasE*MMshs.2DAO8k%U0#F12A5p`5M]g$jhH1gIQX/Kf\p?_&*je^EoqQlI+T7&1E#!Je^`(qfsH%R/;.cL\uKKAl,*"`e1X]r9/Y3jfB>HM):FIGaM[^,\7:IW9rVj*)6.A\5,d"=qZr1K3LkMDi/9q1;*j)a;&Ob-XT3HE'Ya(NPZ.6:2!5?h:gfr$$(cr3P*r01*A2scS:e@U,n'>/DdHMePfui=#4r"rfSl.&tVj*(O5n>F[nUjG#SU5i=a%1]gZM:b%7E2!uP`#5bV'9j)K1\T1TT[Z,Ud(?*oOf\45PnDj%!kOFLn@Qpnch7U<5]:s$-bp8Z^F@6[^,\7:IW9rVj*)6.A\5,d"=qZr1K5">V+X%GOODe`"PqrX&lLQ/aCle2g"91h7ImsP`br]AasE*MMshs.2DAO8k%U0#F12A5p`58=L14F=KoTY_o$)m3PAG>_o$!#Jd"2L8AS&8cfb+::K7fAOPFFLL%KM'2JVO^UErAbe[;2[SRe5e6.fSd5OYQ&KV':@\WQ6%NTMc@dW$Km'pLeD_mpaRL=#q[(SdYVHdF$RK>uODYc'nq92f..9Tna]#Mg(PPPN1ih#66i0mW;^jTKrY[fWer,D=?HUl-knJ=&):WfX,@Vo]Pcprp3RG1$C'_,(<.`a;acE;@u-l,h0C!k=(:Eg$SLHHhZC!45@>c,61,"6oZcj0k1CfUnspDp)[+0;S9ZT%N[_\rOEo+5PnDj%!kOFLn@Qpnch7U<5]:s$$G\7=kE>?6\jN>cTSJEo#ee$#BOp+mdT)qc^t^J8P)KKbL=qh!?a*OZ1sfl=gJ;4B$?X9lal`OE\.4V;irfu.A\5;8u]lNoUo(o5H#jW\bBak/Mp2.'nMjD>D_ceba:*b0P8NGVr\d6)$&$Z8X!V$HimFm>nD0E&F$Ma.)pd$4"cFlI*kNRlIDrYQ7WIN$*i0"oB4FQh<0"&`5GoqI_D]+mpF[nUjG#SU5jh=0J3VO3ce&LC7F2q!_]6+.mlPlBV!*Za-l?ZtKI?_83t0aiWi6^qd`QLCP3fUnsmg5l^i^Mn'qocfJ:,fr$$(cr3P*r01*A2s`XH$O\#ibgP.1T+!=)A*S?l00'"'Js45JE+\240B_)RCW+!:IW9rVj*)6.A\5,d"=qZr1K3LkMDhHe>Ucq=0J@$mFegYp$1(MRRhq:ZM:b%7E2!uP`#5bV'9j)K1\T1TTW.7hOd;t)ANepE:Mr,Jlc6fE1q=6b"k1nPYMnVcW1g2p8Q+MoTW5c5G_??%=1jML`]S/lJ2:.eZ2c@2Jq0H;54R>2E3>H\8c#B-;BpeD0km?PIS-T\)t]e$&O33>e@G>:\@B&9Ql'tS\uJc25'W"$^d*#dka7a)3!\0HDm.VUqQlI+T7&1E#!Je^`(qfsH%R/;.cL\uKKAl,XH`pXc)/rUqQlI+T7&1E#!Je^`(qfsH%R/;.cL\uKKAl,I&)GZFHHKcS:e>=Qd\"cPqQlI+T7&1E#!Je^`(qfsH%R/;.cL\uK]XKWfhA2+>?UmWV]oWt;bBLVPZ!W&kWu$hIS`UL0%-B:7/F6T`LhQ.1'Tn<<5_Q]'mYEQ<%?l4Q%\9DrRjl?5?h:gfr$$(cr3P*r01*A2scS:e>=Qd\"cPqQlI+T7&1E#!Je^`(qfsH%R/;.cL\uK]XKWfhA2+>?UmWV]oWt;bBLVPZ!W&kWu$hIS`UL0%-B:7/F6T`LhQ.1'Tn<<5_Q]'mYEQ<%?l4Q%\9DrRjl?5?h:gfr$$(cr3P*r01*A2scS:e>=Qd\"cPqQlI+T7&1E#!Je^`(qfsH%R/;.cL\uK]XKWfhA2+>?UmWV]oWt;bBLVPZ!W&kWu$hIS`UL0%-B:7/F6T`LhQ.1'Tn<<5_Q]'mYEQ<%?l4Q%\9DrRjl?5?h:gfr$$(cr3P*r01*A2scS:e>=Qd\"cPqQlI+T7&1E#!Je^`(qfsH%R/;.cL\uK]XKWfhA2+>?UmWV]oWt;bBLVPZ!W&kWu$hIS`UL0%-B:7/F6T`LhQ.1'Tn<<5_Q]'mYEQ<%?l4Q%\9DrRjl?5?h:gfr$$(cr3P*r01*A2scS:e>=Qd\"cPqQlI+T7&1E#!Je^`(qfsH%R/;.cL\uK]XKWfhA2+>?UmWV]oWt;bBLVPZ!W&kWu$hIS`UL0%-B:7/F6T`LhQ.1'Tn<<5_Q]'mYEQ<%?l4Q%\9DrRjl?5?h:gfr$$(cr3P*r01*A2scS:e>=Qd\"cPqQlI+T7&1E#!Je^`(qfsH%R/;.cL\uK]XKWfhA2+>?UmWV]oWt;bBLVPZ!W&kWu$hIS`UL/q!M:AnL2TWDb#d,OKZ$E,b<^JHJ]<3)TTJ/mZ'\_$;(SmbKre4P[il_q3MLH1U/+(+ct8j$r:A62IpQ`LhQ.1'Tn<<5_Q]'mYEQ<%?l4Q%\;:Ud&L'A[Q@-&cdN9K9ML_Rl>6q!.f]PLCP=Sk#RZn:Bgendi(/_S.+5(n%VN#):G.Z/H5!7OYQ&KV':@\WQ6%NTMc@dW$F29-N.()iTTLhCY,_g-1g9D"+[gsgD4k2fs>MTq7^Ch<-LT8JRn:FR%K;bBL8TEga>pd$4"\T$cY3B9*KIXV:$F6?<=jUE+b!.Z(OG*M4I55an;!!j)G0?'5JJ$uZXZVO$0TVn$3paN$`DpX,/7JrgAcs/R&kD^E>!^K.AK[]K01M-VP>$>*dmd9c+Ig5sL:X;pTM=bF2Ma.)9a%!b2U`2'cDXq;C^\(P`!+>%k8=aK1\IOiR"4;N#;]tA&aII>+Uk=!<>i_1b$hc`Q-"$^rVW_9PIS-T\)t]f[U#?[6,G-j2]]iG8gO$EOcd-7\T8*gm!W\E3:NfojnDHS4Ll@:7rN;G=>m^T2rr.EGp"+%^Y$V1@h(JZZZJtm1V'9Er;bBLVPZ!W&kWpf./SD4jq"a%_^=rGRqJ@SSa#>)r\+adHGOOBULCRm,AJLY?l-HO,kO61chUBqPe$RFbqok>uV*EEAs">f9)2"MY.cR@jMS)4cC?]OpF58on[qsj.oB0D+I`_B8"TR,f6bL*D_uBW3$a%bqqX[uQIcK$eJ#utd1&rk/[&DJY,0+V(=o?%1<+HD`8u[W=;C_5A4fG2uiTH[hDVP!1Du2;MSBEm8_$'@)r$("S]1l$qcJcZrs7kW869cuR6[uiApCA%r*O%T:#Vo='L12[3t"U\_Y:ZRA9c5:@\%D!BE/#l9LA]a$Ok2cZaX5\s"L;7RTWE_!'1@mr1pq5?h:g^SFh>%!kOFLn@Qpnch7UV(hJnoT.UBq^(:SN+3["5?l;d+>5BD*Q=/-QcJpn;g.Q^?Um=Uf:k5H:k/mT2I00#!Jo];#7M@/H_e5lai[UR5oETe;/nd:J]<0TV);f"rojVgi8%"rOIFXr_PGJfEUTdI.rX8d&%S4Sm^!`MV(QeW4Ab#e`;NW]t.A\5;8u]lNoUqL:l-C]dl'"%55&%QEe[pk-Cg$R/CpQSG]Y(6JD[4+"ht$,OHo?=$s_Rl=]hIXUH).fJ$E8:S(RpjYl7a;FQ6;@u-i<9+L7;"&:O2Ij$Pp#f2);r:e9e*u.r)iPMTIJ&_hQbDqfFdKi;guM=bm<6g2j'j"'e[p$DGB`e$j2NZT[OaI#g>1S5oB+;oLoBrYM]huOb2WFIV'9Er;bBLVPZ!W&kWpeSd\!)S3j^L-pl;ZDc^uccB2L1$C*+C"Xm21"hlD\8H?gt?I.c#k.T\k5oFh5;r:8%r@THN16psFbFEArU.fJ$E8:S(RpjYl7a;FQ6;@u-i<9+L7:s3j.m[qudXEq,2P-eknq#B6_.3IZXYBo?Rud/Vf;<^!sB;HLon[Z,a?D=iI@jn>J$?CX,mn@_,=R\jBej1>]6D&PM.Z2'LBjACOe9mbom;q+6;qK)W4C,LmKo1I^m00VJgn,I7X^@qM]nEQ4/)RsV/j;I!4LBQGn=3:3??bUQn8`T^651lObr9)*a5B`uL"g@'Jmk;C>qtbQPm\P9qb\naZ/BlIBrLg,Io:!s#D$^\7(Y/ug],"8EZLB=J6lTHN/8,H.:b,beCtP*21&;P9ulC#iRO'Z7R[R$a2&@!QHF>Z]EimFT[TDpg8Be]h%m<:3nXRRBOK[sq69PMbtOel2OArj$e:-X#[AAkmAjCLV4cbMTm+q<./Ii][R1EdoBUX?/i4&,[pRe5cEViRtNpQhB'Hf<4E&fCUEdB.pN-p`@jj@E;uK7:^D:G[q;Ersbke[!`A-L$N@1Gt_Zm8nB>N/ADs+3/*o+b3bpH&4chj?WX5-DXD2"YFP_u<1V8gME(8js'`.2=.L@m6h#\3DX9rt5naaSF.NjF58/Wgp2"et7rbo"Y-F[Bl',O)>Jr4qIF1s2URT\sknLpY"i@LoC1L*87*'A@MU4k7:TEXF?Auqt%#Ks7`QlN9Bd+DqiX;OljbL4\YT4hD<$\\VI0Us7spM7.eG7?RTu\S"$Imrc;dfC$X?P_o'AD(fBp<:7siC*;2m#;'IaW;^tDl:T5b))tQ@4_#=)G_&Z:2%KUn5[pGVsQ9=cE!@g_;D`ur]GC>+qZ->5-\kMlq*t,2/V=kT_Gk(5PL-#goo>IYK[p!C=2B44[7`L_;<_]q8cWe2a(R+Q>E*eC\n#rH[B%j*aRaT&n=,%U4iK0MA!HS+4LMMT8i%)\#k.XIG<=VD5/uns0\8uV4p@qbQkOnqm5rnX4f+CX8r3e'Bmb)L]*?7PXpc/P_p+I;^tE#`dCT7#l6?lYlaQ1:7siC*;2m#;'IaW;^tDl:T5b))tQA'+R%/L`X:(b3J%1RF4_R+7>\sM:T5aN5Gb)BDTi@9JtP@")_41'jokN:cXD;K'dXK&5,G!,rKN0P\M51*$\JB%Dl`s;R1nf14GWlt<0Tq4q36aLp/_cTI!]O]7Q9N;8]q')#6R9'"!>:b;tL1V^0roOgpl5P#lg7oE2.1$2Ca9%Zq=.$5J?6+%j!chtcid%<=LrlfH9+%\nX`0a?"-t,?:U-rnC#M"F!5P#gE5_%o!qD6H+Ij>aT&n=,%U4iK0MA!HS+&n[*1D*>PLh0q>d@[*:d%>Rnq]i'gOVm[='d`4E<0YI`q4Ln&o]LPAQ"0i]4#J`RZC>&3(F%"*nk/#a*;Smgd?37T*$*h'\;VEUn(7$;X%05']R8\TL&V)N3hstLf3a$0E^\\sM:T5aN5Gb)BDTi?nDYnL'%#c2)q,Hc/,&6B&("o^1Q2S7,mh[$>Ri)'OQ_l+SIqaL=':4^i"Pp_9dkk7MaiQr8R$bBueZ/WIq\.BRp$5=:/Li7@nF/+K$qlp>SNhVS"Mue!r\PMYWn;.&rA?8;Gk;[8k"TlQk-2#6%:f9NP]VWMt6/A5F7tcrOH=*nQc(Mc"jBHcf:UVENlo:rS?YZc(+IMM.l"nQ-*@&4aXV7UIGnD9MfV-<)c!Na1o=Umh2E!H2o4Aicjm.?.j("ZWDqh&A6Tr"DV#5N$LT>=-Sbp]a=d)@bnFErkZ+5,?l]OUsVi?OKhmTqVUI/O(VoO-Rm,,7f[(3Kaqo`>rD8pfBY@]hd[TE4-j8\oLT!.-krOJ=de!L0Leb91MkbFlHPdu,;mC.r9nDV9^8R&ZA*>.\q]j&½lsQl,KE"pG0u2HN7qjokN:cXD;K'dXK&5,G!,rKN0P\M51*$dp0po;p'/D/=#h/XifsX@[nj+=Nm]E18.&0.rVPl.UtWHT/,cq"lC6HuA&_=fKljq04cG,=dbND6AWmbR*oST#:Ou.S:u+HqHg6q^E7+Eaq;3(8Tp^k@nGcCi+#jVn[!C;,-n.JDU%sM/UmV>c?!.30h*IVphM<+/dtk?!s_&VuE[NZSXOrL^n>&^k2Qegpq$2Wqt#:ER,7pBkaQoB^FWkIm?8LHIAs%]1,31aiR#IEUVbt[l7[#7f@k5Ua4FOC?X!65_%oVm#Rs:6t4B<<0XVZ;GeVsk(>./Ii][R1EdoBUX?/i4&,[pRe5cEViRtNpQhB'Hf?;*GM;4I/WoX-3fu]A4Q7mL9CtkENB=#'sbDYqWUqG@6hp9TnNrED3LsG9[2S?@]r<>+kWSd?fqp/(JoOjc5r^\M2,fY4(71&i?'d[m15,\q#5:=Cu]d?37RHb'Hk0pE[3WICYX?OO4>r,@utB)VPCQ&F-&R$TY$Ldhgm\Gi$&>M$QZZ[R1=)acKP./g^!$Tlt^q9P(]N]@'L+[LV?3UDg&U-rJ7Vc8nbT2JN12s,bHJG\rf`"B.)cK]c9T'C7FT-ko&*m!$mf#/l9;t/nD]1kKsC#=)AFJO4e^D:/+pZIPI"I\+`-.2=tNVTKi#fp.&"X+u/4Y.SA>hW@3lJo0I6/rH[)oQ\aijc@7GjpJ_&lj_PC4rbO]`s6Z_bSXgaIFF2+$H,-?T5M"W=h=OjbcW7eL7+M%pcjjn;]D]3'J$Ii^[pXFl/CR58K?RpUX?V0[2?oOgpl5P#lg7oE2.1$2Ca9%Zq=.$5J?6#FF9q80rg:6#ebHJc2P[lCB)T:4AF?fZESer'*:3()LNWIee()ueEmO`dp[tCU:??@K4lF\I&,Z"_lKDX5`3o,k+ao:CkGUnO]Y$?Y/PWG#CUMmSi8D"Yf:'t@TgOV3o]bE"kh)\&P7]$/'GhQe3MP``S3VSd3UDg&U-rJ7Vc8nbT2JN12s,`r\,G!+WE!f$2cbJ.bR*oST#:Ou.S:u+HqHg6q^E7+Eaq;3('OT(hcKpU1FLPAGn0]rW@3lGo-kD#m&*gIaGp"g=qL==IU`QFc^[7MjuMdhUnU7dbbnF)Yb+fj+ueY:C-g4=q2H,^5B?o)RHLe=P)9m/0qV?@_L>*GM;4I/WWb\FG:!r=][Z%?2hDCPOpU8#Nk8*J:l$2p:TYWn;.&rA?8;Gk;[8k"TlQk-3.=nUI>Dt4AP'`NdbbZCn(b*=JINZ:'&lr`?kTg.-VoOjc5r^\M2,fY4(71&i?'d[m15,acYb'Q3/n(t\Fi",_bp$;T5-2!AjZhoi2Vll;NQ515O*]\9%kF[7,4MBt)oa1SbkHI16M@tb#T2JP'IS*%cgmAVPASF*ho;p2`[XYpX]7TMGm6=\`3H?5Q\Tb.8kg?/:AS!XkkNga'c^?R:0JH>$ds6:e5PNrSB@!0OabIS/p=nZMP?YJ3WRY`f[VRs8b*7f#03\&hd%>Rnq]i'gOVm[='d`4E<0YI`q4Lo]8(4ojg[OljD;Wo?NdR7SgcJPUqXj#ffFmIWGb*=L"4c:+0-DDPEH;qZV4p@qbQh.4YTHgf;SQc+cSoe!FEM^PUAXrhq0;%S%mENPH$O[Zg[M,5I^6AheS;M2`Pn]991qo@4[&3!m!eclQeFV:oOjc5IdN%+M(tW#;8RcS7>]fe&#q$d5H$oEI`]s/GO3sJ6jA:[m/GMa<$Fq+8\*T1n%JH:&Of,WRGC)Be0lN`b0$aO1I6NQ3UGA3.$5A<.2=/qVc8o%NVT6)')$r-7-^*IrAd#(SVU`a@a+8tZtU0c%3@Y,DAu?=151OIP*4&W?aabg&P\fW[C4BarL)iQ?i-Qc@a(`LPd$cX;'Is]K.9LG&#p2??S_9:WDf]q=a0W/hbQY6TqQ\bE:Df%7'q(#;R?6brqL!bV!-Mr'D,q/_g_!2&rA?8;Gk;[8k"TlQk-2F@6^\WUjgG4T+_kOM.kJA=_@CM5CT,Y!WrPn[UJX]]ntt/GMb9i5:7h6LjJg`?'E$-2-X9hSPJoErG(EM&ueGDn?bW!'T"Q:;Gk;[8k"TlQk-2F@6^\WUjhRrGiFQ-N"$RfrRoKAcIpm]T)TTobngHIB*$?j?o7u7+Y5$*3T&5gq_LJ"DZI82jhU^q!&Og\9`T;A$OU"pkOX>A/aT,1l3^Q*5TR^n!`o*,IN?A#6dq*YaPRW&/Gs6s8MGBLkr*5YMS[D]/)6!gU([YS=Ej[qn&JHl7(3VG>Femg#DJh#&[iS"]/:)RK2Ua()/HEj2R)Jn8KTBp@aN+NE,uEl02>Q=D3"S8kM_;@XS1QH%Wr+SGk6bkg9%jgFJWW-n-TRnN+HhY_*"U.p,LGFU/?^fXcP1qOBQe$jM>bX7c&S8k;K$.On=\TZ[7M1`B"Z8g[9$jQG^(eS>'\r$G42I.?48qL.c3zzzzzzzzzzzej=R[q4k$m/@-jO/,,j8UfdLs?+kQ,c'gL'-Vj$VV2JBTXe/r-clcE=JHH-(9UHA4*q&F_Xjq/l>[4j`m@'0"EU21-C]A."L:O,kZ_SND$rs'*5;rqkd;jNu7M%hB1tHi$jG>4q:;pTK)FX6KN=bnLj+R-g?hT7MK>Y,9Fe9HqpNoheR`_hJVYMMe^K[jRmk4aPnrH.13n$6\Sb;Ym`9@#4hiWMuk_^/+fCi&dl1QS/7+_[kqVT[3*+a2_iak091#3cX/tkg?/,XB;`GhnIUi<:jf7!hZNmn.,+S`Pog`gUAQ)bfiaR?RpZ&$VK[0Q+pN@!2NMn*E6V#n">[_5MitoCQ1e>C!rf@-VpalFQ:B#m-JOQ5JMOd`"st,JGosDi.!VPr9\EP,GN#,#M8%Q!)2mmT[9;3i'.0lXNGV":7XGBr:JF697_YF9q+'>lDq,Vp=h6-^>->EDd-H#N>jh5m4mhN+:A*LPKAOg)i/1kd%RjsPlO]8..h-P\Bc0pI!iTC?m)3-k\/F*@&AE;91VV#l#[.?*2riC>ePG@1c2RBZY$uPrd0n@5^ru8n@tD+^]!k1=BHCPX]`!-p9,)ol34$VkD4qMjQ,C?`5J'"B7BjUkN1geFn(=i5kY_%*'8>fcHbj;2r67EZYeI*cCI&_F?1KcTB,(o;lILWUVh+gUU>U;S*BSEq5!D._0!8!B50hI!6o9)R=<-saB7)>1.^-V7TgOTFa,\^7Ws8X-dCiYjK:4lGDWA`rY?ec;gt]Vl]1/RCC:-o9;18?8F`MM#hYpDr:e%37,UDHU`u_;P)59?J.!T&m]6E^E5Ej3Q-Wg)r:hAIpGIMf%e[92E&L0c4DKbC!;5_lkpYC&9bo_H5dGA#>s(X#6.m&%IjPn.:?!LV9!s;8qK*$"Jrro5I.k>u-O[k3U/M4)b?9/'Wq@@,]Rl<93%'un2LklZW'@dUiY"E?\6-Yo[;kq`>q)$Hj5ns7#-BjE.2\OMg!<[*4&cq9Y5RV>UVG52jeSFsn!C0Ubg-IT2e7dsdQ-,`nm+G>5KZqb+qb4\U8k;Lgqp-BGr`>:T77Dp:Za3,r_,mQ8[ffl27un];U7e0P>/!B)&=&N(:nF8M$PtEpriCMHiq:o1=$L*kqXp^+<(05E]6Kdm/Be;=M>he69:s8OUP:`O1)Y!;/aQ"dPRH910pH+glfb!$L;3.PWZ?LiRNVbdQoo\T;"\SFYhn=0?3nR`NQ@o`Gg>&kkBc%mKc]RS.@g5D(BZ?VMl$KnVEC2D-U7AS#G\J"Zl86(eCHPa"R(UIUBJrMp&$)0\n+.4J8'/8N7a)4mFkVLKL*5(#=%9q+'.m+J`.c_#^Ybr>%Vm"f>sOsQNaR6$nAB@!/n4-$(#K.-M"9c4fd5QCYfqEOc8O$AASLD/(RM%f4=V$"fET[M<8Wl/p?d6SDf\D"cTg%2oq0L[lak5!D0Qbs?sd=m'])TOFuM]j>VmCE!m!&ebrMo[?TkKn\46g(Mf.O$7mOaq^uUAH4R>"_D=Z(D[c%)]S\8P>AW%eV]0!D/CeQdl.7`@/%3a1)c/*g"E'mTahPBb(%QU7B@Y"L,G]rfIfm`nr&Wr'49rCaW"qMdQbVRR57Rf:RgmEnLK7i^??2uj9eHA^(Yb1uHF*mWIR5f1qE,`%^X/f!ne*H\Q4S-ce++7*c;8N+.!=/n1$(qc:5lgrczzzzzzzzzzzzzzzzzzzzzzzzzzzzzz(p38aRNM'A~>endstream +endobj +6 0 obj +<< +/BitsPerComponent 8 /ColorSpace /DeviceGray /Decode [ 0 1 ] /Filter [ /ASCII85Decode /FlateDecode ] /Height 480 /Length 149 + /Subtype /Image /Type /XObject /Width 960 +>> +stream +Gb"0;0`_7S!5bE.WTRn$TE"rlzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz!!!"l)g#n)&-~>endstream +endobj +7 0 obj +<< +/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject << +/FormXob.c7f64c81112aecbfd584bc06b36e526e 5 0 R +>> +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +8 0 obj +<< +/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +10 0 obj +<< +/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +11 0 obj +<< +/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +12 0 obj +<< +/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +13 0 obj +<< +/PageMode /UseNone /Pages 15 0 R /Type /Catalog +>> +endobj +14 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20261102104126+07'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261102104126+07'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Lab 3 - Ukur Hit@k) /Trapped /False +>> +endobj +15 0 obj +<< +/Count 6 /Kids [ 4 0 R 7 0 R 8 0 R 10 0 R 11 0 R 12 0 R ] /Type /Pages +>> +endobj +16 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2382 +>> +stream +Gau11:QqP3'8Q&U'Rb8-S5b6.peNNaVCJ'`2`%FL`jt3Z$sHj;#ZcON9e%5KeH05m1)Q@`F0WQ@IqngG;QBs+"q8$?]W2'^fYO&$*4QNk:V]=NK\\N`\D)DfN0]\:R2:q!HP,:@bi/B,hUn2=HH@D0U&9*].SAVLamm[g)/!o'S"+P9M"(sNnK6&U;Z7O0gkAn3cKdsfjarpTkc(@=(fE.-5#Cf+o<2)2^qQe_O+CY`dMB$-mF:1n:cJ/I/:]GkHe/:2Lei>s'p7qLfG^.V#4O>"/k$4DLoe_]q5D\8guXcP,Lr1q^GpgZtF=7i1-&WBP5]VfH.>SL,#gYn_lC&[nM>E#b@j8&:]#D&j[#Zu&+i!@\be;LC4U[N!NrmE@S9FuM9j=L3p-tsI)1[ZuAm9cc(I+e<@oHO#FI`;l65%)R8_+tBf\ig"Z4C8E+]CPK+?\uFB"1JnIKL]!glCY3*V,19ZW8P.o5CL#\_nFlAnSmW9MSW)UPHMZT_GU&*&@+k;#a;of%ZBrTTV+Y(!a8m^DVj)O]<8AfL^U;.@eYQi-NGoH_7pc,a%/+S6g/duLYqRRt0SQ(;*!3-MdZq]/N4XrOu*75YRW(L%T[%)R)$k1]=9(3^0$q;e/UbVX1im^T17aFs+d]V[[s(cO6/.jGFT$d\M&KO3A]"WhuW&rMW1@&c%!hI\l.'Zt]k314)\,X4V4U9'+.p\C72+5a4(O%Rr(-]eP.2fFZ)39NrXqTN$K[AH.O7HaA'TrJY1uD,=7q>?IB_@2IbaeWAhk]G&Z-Z'"?6eU2HYKBuPTT'=/JN=G:pNCoGE%_WJ<@7@:s89HSVCYG=+",DSH6UV`"@*sK#CsjdS@`EZNCU08T+VB6g9:VrW/^*VKV^Iuqe_Y#qC#TuG1Hl]drm\6J6>7W\dq."!a%8,\u,Pq6@X=LPUl,BDR,]b"bIocfRrDddN5W*\ZNu?r5:$o4O270*@Wp&hbXdIW_\#krV(35t8EtJ6PG'N3cprm(t=2^1[0*g*d+O8,rp9H[Q')B^ALk\%@M0`8A87aj[L_dS2`$+$8dQEJc8?R#SeXRWV^C]X+_=h(dcg]g8Uf!WC>Y:e0-,T?;7-K[R0]m&$uXpmSk$(1[J)2mJR2T@-P#R>024(ah+YN,:FBW`c:gNK12pD=T1ns#1,;$L0g6J"16\JsaUlb1UQ`e24Rm;KSW%qKa%pa?9J&Da`O!A!VmloH/=s[Lr<^Ua!^EM66s@LU*)@JM%j9VG1q*X.9$\h%a2=,6j5T4&T1k`\04*RLoA*W;4aGq;pU9G`jLp<0rF.u^k:\T\h_,IRE""c$=Q7&%*4tTr%W=N^h@Sk<(V5K&XHo1c4CatT7/o]'3@jZ1md^=QI:dd2I#n*KiZJ$.3kcs2)O\V0-'f:Gc7L_'$\DB]Y9Z>qI3nP2>A&4GqVmpcEH[tfNO722h(g/BXR`Wc^4Q'`QbJ(De4IF1HL(,9`t%>+1o/mT-Q13CGb3oJV[H:e/EdKunn<;W8ffRj\Wud%UGq]-nRs8ie'LFi`pn:cp?A-REB50OB?%`JD5?Qh4W[D]`AuZ`r+Uc\\:MmhQ_md[~>endstream +endobj +17 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1792 +>> +stream +Gau0F9lo&I&A@C2lqtm3#SnOJnBePeUhM0tFHT[ql+n0,VI`mSX\e(ho'\YjpgmYDQo0#k#sRkGnc?[dHpHW*!WCnU#4Q4*YMLFqJAht*`$/<2Jj1$WYHo-%%S2ticSNNpd0Pg5(Me62OV^=%L5@06+H`?`KLH+:pNC1*RtGqdM`Y%RFECl%?Y@VPLlt?)%&bF\8JEJTb%>!n;B)LC9THe[`.SbGqQj8lmYgGHq^#Ic2`b+[6khOb/T7eWM3]ZpQVs_RkS2n1cb;MtV?PG*8._#GHdNhS+5IA2c'5ho=7687]&O_pitRorhitl)aO7^NWY4aJHujhU^"ki]a4#%!g21)\e9X=T?`[m#E/5.(5cTX^=M`>IngRJHH2/Ri5EOYJ-e`!Fj+>eDDG`5LB^5KTmf+Z%IgFpF@_s;TC^"QM@bs?>5(dZ)t8b(RAaNoOdJ8PA)o_=)gBCkeib1?[aao$-TGqq645!nrNd;+ghf"E[%9jnQHBXX,W9mE7nGW@Rb$IA35FoF'^\/Qt)J,_N=&U`nFYH2$EHufPmG`;@srnqU@h=4IrAUm:n*[50BhQN1bigWH^eXOVn*fkVh^G?FQ2t8E-'gO?Nq9oA;A66Y9NmX-2O,IRs_oW-*2!bHjfhY-uEST]?SrhV-.A&\?k)ZdcNbSE+GAoF?c`0\O8%`oY26SdaP!$oAEkU;k_tK6E(fOh%/@9`#0B;T_??NG`<;mnb=!oh&O&lH/=PYP7q^C(_c5D#g#bP*\B3/6B+iX]a?*^B@Xt].P7q^cME5iATJN!][*\I#6V:^9=bBI_PbGQR,4F0!8521An`7>$Pb8Ljkgbd=4F/d3Et8S*!ToN9c3_Y?M]n4,[*7%46R#=Oq[DWlK5ZI1o7ZBA85;b_I]Hb8SdhsC965,(,[q@I7I;)l$H$EQ>dle'Gb-W=gOl)!%f$d=YqE,A$rF*Pm8[/1:`D,ET"jf<,$f,NmMf@D#^\=3;Jk-*P;:I!i'YURDJ?Ih;u$$&rlIs%>EB7WVM@mSYBE^5GF8=>5T>c[~>endstream +endobj +18 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 692 +>> +stream +Gar&<:N+u1*51N6]OO"!"Hpca;g"P;Y3Mf(>04Db]*Em?RJP_HpQ];m/"lO'_2[qo($DQssNCkY.)@fAb)[:HPV6M6B(99adoIEY/YZ!p/4.Kt^3@%o2E*8?un]YUl/7OaHtVVF;RM^X@]1&l(p7i$\t%RZ7M:_&IGkS"H%bmmN1HkX]rEeA]E`qaXY+SC;ee@#Qoja&p=kXcDAXM!M^NVV`j&MWsK"a]83fe\c0R;@kOC2.pa8@VErq,c4/iV%/B;UPjZ>*8nX@X&aTW7EJGL5\%rM3nb[,i2RCH7aX*dqfc_cqkS&Ya3n*!Z3lC;+6anf>~>endstream +endobj +19 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2455 +>> +stream +Gat%$D/\1M&cV3*1#S`*?-j!/=agsU[LOqS[C28U[en$#KV>oMf#q@Iq\#Mrn:**GUaT,t0=;3edZQ]+6RE-q(rd,$dX)s?0O4!%.kq"uClrHFg]KZ7m\<%,D_D;i+POVr%"!Hf;PPhb)AW;l$0r%G)g_u_J246rZUpUR#oW@"T=Z?ROq/ad9;ia6&i'E3B<#7QHg=B_qL;eNPg.io<'d$0c\$#E#O2cp)a>KIl%2J'Xu1m\0XTDIN1[:JVn0"C5o1-4`X)]T`G]q(T/#g*q:+H<03g9OHm[SVE.BL."puf_("[Q#admt^c6iN<1"h>%RH<_'8Alo<)-9,BiM<=3@,eZ3b92sClUlmA;8:M'rYMfF:83Y0%!G''.qZ^EY7s.Y>ODC@u`tXREK3mY+!2cP8a&O;]e52rVGL=r7B8]GLmp#"d%^Fk>esd:F^R'c;hl;$"-U_8eH.`#XFYYeoBXu(MG6F#0VCDEUs3CDd6Gcp@EbuI(d,d!],YLGg[&Y?/C"Cp]NBDr(.S+&4]T^*('DrAK\in.2boYAMqT#1G3T\)m+L3\Fn+9cPKY:K6oBJ"LF]n!YE[3lR,C9YsRgde1&1;XgcCd^`ue5^^LQL\T'*SG>P!QV+/?`3^eDV4aBuqDE(\LR$C?G9FI`%&?+OQ`BXCj"lP5MD[Q6q'mo)_HO.Ef+_^9Z_g;]=AGSILoXf11@Ub$u/`C`^n@cj_^L*;>/djWC<3LJnLN,'3+Gf.eEe/Y<,g)5[\^*8C4Vi*X=UA68#erp'579-5$n0Qk%9-*JGP[\rlkVp/m0"GGQOb\JoGJl1aSEe+k1M*)edV%<(;_P&Z&NDm.6'c1L`-cnUJ0HQ%rt/Q4/WeaC.paXr&!.H%DF=FW^'#L("g8Dc7hAa<`=3h.hr#Ol?MMXK&$\"pHLoj_I:gAEja_%L0$;UsV@.Eleo!L5TB3QDNU#D47s];QU"k7>\sa&VLab.nZ'6%;0D_Q=hU2Z@Y67X]L%-C4J`CPk,VkF]bn6/be=`KlR&[A;mnAlWjo&[r5d4L+T5/-(s1U)s_(2*"D"VK`ZNEW,Gf-$k3HZ.8G:0PD4K*p$i9C5::NqK]3(0-&f,;$qi=QHqMShZs?MoTS"_P6"*,jb!4"JT5hA>)63VUeSh*Q=mBkL[%(*UenM'b_,]X_lB@?b.a:o9Gb<4D'Q.pP'TAd6kYIB-%AIc,.0RXGfq^i6`bJ!nGf1?NXq3YSW[:$?/_Q0?!9i:'9QGb9HFgHV2CVq_)QoH>6G#u]P%gf!`c/nqh^6rh-7_THj4&[EGdT?;U2CAiI$NcH`';C'f':kr`+GcJT[*Bg]B4:A@R]gaZ?/qe#Zc/?H,!j+)prmC73GF%YY=f-dScF8Do:'k%+)FWG2"QM9Nfb+nBc)@lB.#WahtY=%6'-iZepC]Rg;8)LVr3F4R\T4.(X$#l&ifd"_r_=Q4bc.D0o'!:].RP^65IAU1#gRNAjWC_g_VZeofFHm1@65F$t)(,9dbpk+/c=t08"MWcE*'/6/+rT@.P#Hk2e\9NprJN`qP-:c';$$V3`HB2OhNoHcBab_oBC*Cc:UO$+D]ppj;`8Zf.l5nXJ2f(I7eUklRngHgn"1@Y1Rqk+T-CO6kGuBrl3jZoa[%rXLW\]jDS6SOY>O?=$jlB'I^H#oiEo9gP+@AVpF=JFRf/!8kl7(-1HA8OI4]ZH'-p:M6Mi9]Ob%ki!\$DgiKMlM4$C)`NKJ]jfGtPA09'np8q!?amgWAf_s5*J)lAgorAd_n<%X7Ai#2AdE8]M8*?`rT'Z;&:GGj^VgY0lc0!'R'g`757t`JrrD@_bNT~>endstream +endobj +20 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2544 +>> +stream +Gat=,D0+Gi')q<+Z%JN>WK_!_a`;3?P\GG8-G=Y%?fPBUO;cGC"=]]`a8fUWT<&!@ZN*S_/U*c7#E^3]h.Lr(I=X^%_\b`;]rD']\UbRc63nrXPW15#68/A8l-5+!!JEMdE7`14cDo#WrJ4",N,E\ODM$i[T?kOiaXQcDhSpu<RG,`a6Y+f;M+U[EGLNc@\]s@?B"H$4mh6I#:EUJje0WSaS\=:PhET_SI2X+bM>dOha4Ed2P:nY\BE!7O``#>^)aAiIr;5f@RiOYGgr.c9@2OeGXU3Xgt-N@^_9D_K=_?VH/gU,#Oi4iChni9YOt3"UEqH6'q!0J$QXR_?1L;i)k9^=]tT@,S3jaX@CF4c7L$%'-#MT9KVOK!_ZCaJa"0&2Q&q^aI7(5U./.Um$oa@C;2b&HOg!>`KHRt->2+<8G>1Kq2/b,/ON(SI@q$!UV@,AN]gF96.]ZP3@l3lop1\bJ`d!\ZEq=5O>El`;D+V>hL6PiIrVA;2]DM.JQN";":PSe$V]j;c*f&/kECG9tMl4?ON:bIh@PjW;Z<[.++]P%nPKo[<%*j@9^E^"K$')6#.I+[VDlAZIB&T-dH(Xb7io^64=8t"XIbXG@8KPoiJ:gC`q\J>J[HA>$;u6oMCDpp;T:L5re\s&dOUXWbuRJKcQ]9n\mtbP1.A3o"q8(39s):^(rn9pAO]g\%c:`^4te^NlU['o?;sm4E`ZQGdQjuE1]+7+-\msFh+ST$Wbj9\,muI[J=`YbX'+!.6er[GqKn!cat^3Q6%`e*A^rH;5J?`@h7>GQm>%a8Onli3/+Psc=.Y72p7/Yk*%Xgg.VhcH')Bps/$Z(17b;S8m;2_'(,3;O:R3M5^m3XdHUiWo@5>NE5G1b-7Z=%]0SNGue#k37bl;QBc<(mPq,-'s,=*@ol3re/H1(Y!dun?;YX%9'r0/gtK[fqa=[_77(LA*?:$ZT4Y$dNils9LD'V2J58:AES^A2J^at)ThA@HL_BTY8(2&$OKDsMYl:tHI]s^rFoa7%W[(h@8tFpW66<^Wmntl6H$%n0K;\?1ND`\"]Ye/OgsP>%[:@rW(V'm39CaK?+)DX22o-!,1`@)u~>endstream +endobj +21 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1060 +>> +stream +Gasanmr-oN&H0m]nRUl0e%/a!!eEnA/uQ.8)Uur9M>G&(=Tba6Z+Y>3G"to!s8B;_&:T;>82+^ipnN:CK-\o4],6^npt_0V[F4+&mC3/hHX2`/#ah=DfUNmrD)Mj7CbY.73;T0e)iY%HbgP&a[miiT8Z=9&r0@c!CiSX2?O,E'co1^p[>/g(+(j/EFoc!p8t*mj\A%FeA;Q6QjTKf=5BG(K__2nWqIc,]Q'Vhuul)GeoT#5)\%KhD7mnDiGYoqGOW'<-[)2e;h9@!/).["GrqZB]bGmr%Rm^%rdpYHp4+48e`tAc5PWH8Yo#]siL8(6hS9#e,X3GjhSAfbJD,;]i!gQ)RBWUfIYVN"YXGNdHou1lSG-`HK@G2,_#6`?#=cdl@kK.iG&l6U21gs+JWD]UIE(0YJ`U(:;?nK^,1eKE1.[t^gh+C9GO/*R_AQhrf(&^X$d[j1)cn6-;F(K@,0pRJeWRcTr1fRA(U70OD;lRkm1o6hkh\IkY5m_S#'5GS?>Q0#rC1"%jNe_<2&Am8udjXL*JnNL1<=uEmhQTe$."%q9_$N4@dW+5a3s<:7#OoV6h^07K/]c?,OLe&d6;ku(XLmY2IjZ5iX!6V&5KYEL094E1hLAS5crmUpou?RB#N<[hIF17Ni6[7k>"7gHj2e>C*rSXSg(s[pZAlG\B=5"b?gHf8pK$EZURQ4>JS9Ct2>(IEq9i*aQH%W;Rh_uH;&TbEunPsf3>iXBSZ'=l`(5!W%"!?Ls&FN@@35+@Hd&RU0ZP#endstream +endobj +xref +0 22 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000536 00000 n +0000037959 00000 n +0000038315 00000 n +0000038583 00000 n +0000038788 00000 n +0000038893 00000 n +0000039099 00000 n +0000039305 00000 n +0000039511 00000 n +0000039581 00000 n +0000039867 00000 n +0000039960 00000 n +0000042434 00000 n +0000044318 00000 n +0000045101 00000 n +0000047648 00000 n +0000050284 00000 n +trailer +<< +/ID +[<37d39ad66933a680d84b75d7bbad124d><37d39ad66933a680d84b75d7bbad124d>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 14 0 R +/Root 13 0 R +/Size 22 +>> +startxref +51436 +%%EOF diff --git a/README.md b/README.md new file mode 100644 index 0000000..40e8cd0 --- /dev/null +++ b/README.md @@ -0,0 +1,145 @@ +# Penjelasan Output Praktikum RAG (Lab 1 窶� Lab 3) + +Dokumen ini menjelaskan hasil eksekusi `lab1_chunking.py`, `lab2_indeks_cari.py`, dan `lab3_hit_at_k.py`. Alurnya berurutan: **memotong dokumen (Lab 1) 竊� membangun indeks dan mencari (Lab 2) 竊� mengukur kualitas pencarian (Lab 3)**. + +--- + +## Lab 1 窶� Eksperimen Chunking + +### Tujuan +Memahami bagaimana dokumen PDF dipotong menjadi potongan kecil (*chunk*), dan bagaimana **ukuran chunk** serta **overlap** memengaruhi jumlah dan isi chunk. + +### Langkah 1 窶� Membaca 500 karakter pertama +Output menampilkan awal teks `pengantar_rag.pdf` (judul "Pengantar Retrieval-Augmented Generation (RAG)" dan definisi RAG). Baris ringkasan kualitas teks: + +``` +{'File': 'pengantar_rag.pdf', 'Total karakter': 4061, + 'Karakter rusak (U+FFFD)': 0, 'Persentase huruf': '83%'} +``` + +| Metrik | Nilai | Arti | +|---|---|---| +| Total karakter | 4061 | Panjang seluruh teks hasil ekstraksi PDF | +| Karakter rusak (U+FFFD) | 0 | Tidak ada karakter korup, jadi ekstraksi PDF berhasil bersih | +| Persentase huruf | 83% | Sebagian besar isi adalah huruf; sisanya spasi, angka, tanda baca. Ini wajar untuk teks normal | + +**Kesimpulan:** PDF dapat dibaca dengan baik sehingga aman dipakai sebagai bahan chunking. + +### Langkah 2 窶� Jumlah chunk untuk setiap kombinasi ukuran dan overlap + +| Ukuran | Overlap 10% | Overlap 15% | Overlap 20% | +|---|---|---|---| +| 500 | 9 chunk | 10 chunk | 10 chunk | +| 1000 | 5 chunk | 5 chunk | 5 chunk | +| 1500 | 3 chunk | 4 chunk | 4 chunk | + +Pola yang terlihat: + +1. **Ukuran chunk makin besar 竊� jumlah chunk makin sedikit.** Pada ukuran 500 ada 9窶�10 chunk, pada 1500 hanya 3窶�4 chunk. +2. **Overlap makin besar 竊� jumlah chunk cenderung naik atau tetap.** Overlap membuat jarak geser antar chunk lebih pendek sehingga dibutuhkan lebih banyak chunk untuk menutup teks yang sama. Pada ukuran 1000 jumlahnya tetap 5 karena teks hanya 4061 karakter, jadi perubahan overlap kecil belum menambah chunk. +3. Ukuran chunk lebih berpengaruh daripada overlap terhadap jumlah chunk. + +### Langkah 3 窶� Memeriksa 3 chunk acak (ukuran 500) +Untuk tiap chunk ditampilkan penanda seperti: + +``` +chunk #1 [0:500] awal_tengah=False akhir_tengah=True potong_tabel=False +``` + +| Penanda | Arti | +|---|---| +| `[0:500]` | Chunk mengambil karakter ke-0 sampai ke-500 dari teks | +| `awal_tengah` | `True` jika chunk **dimulai di tengah kata/kalimat** | +| `akhir_tengah` | `True` jika chunk **berakhir di tengah kata/kalimat** | +| `potong_tabel` | `True` jika chunk **memotong tabel** | + +Pada chunk #1, `akhir_tengah=True` terlihat dari teks yang berhenti di "...memungkinkan j" (kata terpotong). Ini menunjukkan **kelemahan chunking berbasis jumlah karakter**: batas chunk tidak mempedulikan batas kata atau kalimat, sehingga makna bisa terpotong. Overlap berguna untuk mengurangi dampak ini karena potongan yang terpotong muncul lagi di awal chunk berikutnya. + +--- + +## Lab 2 窶� Membangun Indeks dan Mencari + +### Tujuan +Membangun indeks dari seluruh dokumen, lalu menguji apakah pertanyaan menemukan chunk yang benar. + +### Langkah 1 窶� Membangun indeks +``` +etika_keamanan_ai.pdf: 4 chunk +machine_learning_dasar.pdf: 4 chunk +pengantar_rag.pdf: 5 chunk +Total 13 chunk | ukuran=1000 overlap=15% | backend=lsa +``` + +- Ada 3 dokumen, total **13 chunk**, dengan konfigurasi **ukuran 1000, overlap 15%**. +- `backend=lsa` berarti pencarian memakai **LSA (Latent Semantic Analysis)**, yaitu representasi vektor berbasis statistik kata. Ini bukan model embedding neural seperti MiniLM. + +### Langkah 2 窶� 5 pertanyaan uji +Setiap pertanyaan menampilkan 3 hasil teratas dengan format `skor`, `dokumen#nomor_chunk`, dan `relevan`. + +- **skor** adalah kemiripan (cosine similarity) antara pertanyaan dan chunk. Makin dekat ke 1, makin mirip. +- **relevan=True** berarti chunk tersebut memuat **kata kunci jawaban** yang sudah ditentukan. + +| Pertanyaan | Kata kunci | Hasil peringkat 1 | Penilaian | +|---|---|---|---| +| Siapa yang memperkenalkan RAG dan tahun berapa? | `Lewis`, `2020` | `pengantar_rag.pdf#0`, skor 0.741, **relevan** | Benar. Chunk berisi info Lewis dan 2020 | +| Berapa dimensi embedding yang dihasilkan model MiniLM? | `384` | `pengantar_rag.pdf#1`, skor 0.969, **relevan** | Benar dan skornya sangat tinggi | +| Apa itu overfitting dan apa cirinya? | `overfitting`, `data latih` | `machine_learning_dasar.pdf#0`, skor 0.939, **relevan** | Benar. Peringkat 2 juga relevan | +| Undang-undang apa yang mengatur pelindungan data pribadi di Indonesia? | `27 Tahun 2022` | (terpotong di screenshot) | Cek hasilnya di terminal | + +Hal yang bisa dibaca dari hasil: + +- Jawaban yang benar umumnya berada di **peringkat 1** dengan selisih skor yang jelas dari hasil lainnya. Contoh: pertanyaan MiniLM, 0.969 dibanding 0.351. +- Pada pertanyaan pertama, peringkat 2 (0.736, dokumen etika AI) skornya hampir sama dengan peringkat 1 (0.741) walaupun **tidak relevan**. Ini tanda LSA kadang bingung pada pertanyaan yang kosakatanya umum. +- Hasil yang `relevan=False` di peringkat bawah itu normal. Yang penting jawaban benar ada di posisi atas. + +--- + +## Lab 3 窶� Mengukur Kualitas dengan Hit@k + +### Tujuan +Mengukur secara kuantitatif apakah chunk jawaban benar muncul di **k hasil teratas**, lalu membandingkan dua ukuran chunk. + +### Definisi +**Hit@k** = proporsi pertanyaan yang chunk jawabannya muncul dalam k peringkat teratas. + +``` +Hit@k = (jumlah pertanyaan yang jawabannya ada di top-k) / (total pertanyaan) +``` + +Contoh: Hit@5 = 0.933 berarti 93,3% pertanyaan berhasil. Karena nilainya kelipatan 1/15, kemungkinan ada **15 pertanyaan uji**, dan **14 di antaranya berhasil**, sedangkan 1 gagal. + +### Hasil perbandingan + +| Koleksi | Total chunk | Hit@1 | Hit@3 | Hit@5 | +|---|---|---|---|---| +| Ukuran 500, overlap 15% | 25 | 0.933 | 0.933 | 0.933 | +| Ukuran 1000, overlap 15% | 13 | **1.0** | **1.0** | **1.0** | + +### Diagnosis kegagalan (k=5) +``` +ukuran 500, no. 7 (Bagaimana menyatukan ketepatan dan kelengkapan menjadi satu angka?): +chunk jawaban ada tetapi baru muncul di peringkat 18 (> 5) +``` + +- Pertanyaan no. 7 gagal pada ukuran 500. Chunk jawabannya **ada** di indeks, tetapi **berada di peringkat 18**, jauh di luar top-5. +- Pada ukuran 1000, tidak ada pertanyaan yang gagal. +- Perhatikan bahwa Hit@1, Hit@3, dan Hit@5 pada ukuran 500 sama-sama 0.933. Artinya pertanyaan yang gagal tidak sekadar "hampir masuk", tetapi benar-benar jauh dari top-5. Memperbesar k tidak menolong. + +### Dugaan penyebab (sesuai kesimpulan program) +1. **Ketidakcocokan kosakata.** Pertanyaan memakai istilah berbeda dari dokumen. Contoh: pertanyaan memakai "ketepatan dan kelengkapan", sedangkan dokumen memakai istilah Inggris seperti *precision* dan *recall* atau *F1*. LSA berbasis kata sehingga sulit menjembatani parafrase ini. +2. **Chunk kecil memuat sedikit konteks.** Chunk 500 karakter hanya memuat sedikit kata konteks, sehingga skor kemiripannya kalah dari chunk lain yang topiknya lebih umum. + +### Kesimpulan dan catatan penting +- **Ukuran 1000 karakter lebih baik** daripada 500 karakter pada eksperimen ini (Hit@k 1.00 dibanding 0.93). +- Skor sempurna **jangan dianggap bukti sistem sudah sangat baik**. Korpusnya hanya 3 dokumen kecil (13窶�25 chunk) dengan topik yang jelas berbeda, sehingga pencarian relatif mudah. Pada PDF yang lebih besar dan topiknya lebih mirip antar dokumen, Hit@k diperkirakan turun dan perbedaan antar ukuran chunk akan lebih terlihat. +- Laporan akhir tersimpan ke `Lab3_Hit_at_k.pdf`. + +--- + +## Ringkasan Keseluruhan + +| Lab | Pertanyaan yang dijawab | Temuan utama | +|---|---|---| +| Lab 1 | Bagaimana ukuran dan overlap memengaruhi chunk? | Ukuran besar menghasilkan chunk lebih sedikit. Chunking karakter bisa memotong kata atau kalimat | +| Lab 2 | Apakah pertanyaan menemukan chunk yang tepat? | Umumnya ya, jawaban benar di peringkat teratas dengan skor tinggi (backend LSA) | +| Lab 3 | Ukuran chunk mana yang lebih baik? | Ukuran 1000 lebih baik (Hit@k = 1.0) daripada 500 (0.933), tetapi korpus kecil sehingga hasil perlu ditafsirkan hati-hati | diff --git a/__pycache__/laporan_utils.cpython-314.pyc b/__pycache__/laporan_utils.cpython-314.pyc new file mode 100644 index 0000000..249f1ca Binary files /dev/null and b/__pycache__/laporan_utils.cpython-314.pyc differ diff --git a/__pycache__/rag_utils.cpython-314.pyc b/__pycache__/rag_utils.cpython-314.pyc new file mode 100644 index 0000000..c8e5703 Binary files /dev/null and b/__pycache__/rag_utils.cpython-314.pyc differ diff --git a/buat_dokumen_contoh.py b/buat_dokumen_contoh.py new file mode 100644 index 0000000..fd9e605 --- /dev/null +++ b/buat_dokumen_contoh.py @@ -0,0 +1,144 @@ +"""Membuat 3 PDF contoh (bahasa Indonesia) di folder dokumen/. + +Jika Anda sudah punya PDF sendiri, cukup letakkan di folder dokumen/ dan +lewati skrip ini. Skrip ini hanya agar seluruh lab bisa dijalankan langsung. +""" +import os +from xml.sax.saxutils import escape +from reportlab.lib.pagesizes import A4 +from reportlab.lib import colors +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle + +BASE = os.path.dirname(os.path.abspath(__file__)) +FOLDER = os.path.join(BASE, "dokumen") + +DOKUMEN = { +"pengantar_rag.pdf": { + "judul": "Pengantar Retrieval-Augmented Generation (RAG)", + "bagian": [ + ("1. Pengertian RAG", [ + "Retrieval-Augmented Generation (RAG) adalah arsitektur yang menggabungkan pencarian dokumen dengan model bahasa besar. Model tidak hanya mengandalkan pengetahuan hasil pelatihan, tetapi juga menerima potongan dokumen yang relevan sebagai konteks tambahan saat menjawab pertanyaan.", + "Pendekatan ini diperkenalkan oleh Lewis dan kawan-kawan pada tahun 2020 di Facebook AI Research. Tujuannya adalah mengurangi halusinasi dan memungkinkan jawaban bersumber dari dokumen terbaru tanpa harus melatih ulang model dari awal."]), + ("2. Alur Kerja RAG", [ + "Sistem RAG terdiri dari dua tahap, yaitu tahap pengindeksan yang berjalan offline dan tahap penjawaban yang berjalan online. Pada tahap pengindeksan, dokumen dibaca, dipotong menjadi chunk, diubah menjadi vektor embedding, lalu disimpan dalam vector store.", + "Pada tahap penjawaban, pertanyaan pengguna diubah menjadi vektor dengan model embedding yang sama. Sistem lalu mengambil k chunk yang paling dekat dengan vektor pertanyaan dan menyisipkannya ke dalam prompt untuk model bahasa."]), + ("3. Embedding dan Kemiripan", [ + "Embedding adalah representasi teks berupa vektor bilangan real dengan dimensi tetap. Model MiniLM menghasilkan embedding 384 dimensi, sedangkan model berbasis BERT-base menghasilkan 768 dimensi. Teks dengan makna mirip akan menghasilkan vektor yang berdekatan.", + "Kemiripan antar vektor biasanya diukur dengan cosine similarity, yang bernilai antara -1 dan 1. Semakin mendekati 1, semakin searah kedua vektor tersebut. Pencarian semantik tetap dapat menemukan dokumen walaupun kata dalam pertanyaan berbeda dari teks, misalnya pertanyaan tentang kendaraan roda empat yang cocok dengan dokumen tentang mobil."]), + ("4. Chunking", [ + "Chunk yang terlalu kecil kehilangan konteks, sedangkan chunk yang terlalu besar mencampur beberapa topik sehingga skor kemiripannya menjadi encer. Praktik yang umum adalah ukuran 500 sampai 1000 karakter dengan overlap 10 sampai 20 persen.", + "Overlap mencegah informasi penting terpotong tepat di batas antar chunk. Pemotongan berbasis kalimat atau paragraf lebih rapi daripada pemotongan karakter murni, tetapi ukuran chunk menjadi tidak seragam. Tabel perlu diperlakukan khusus karena baris tabel kehilangan makna bila terpisah dari baris judul kolomnya."]), + ("5. Vector Store", [ + "Vector store menyimpan embedding dan mendukung pencarian tetangga terdekat. FAISS adalah pustaka buatan Meta untuk pencarian tetangga terdekat yang sangat cepat. ChromaDB menyimpan embedding bersama metadata seperti nama file dan nomor halaman, sedangkan Qdrant menyediakan layanan server tersendiri.", + "Untuk koleksi besar, banyak vector store memakai indeks HNSW yang mencari tetangga terdekat secara aproksimasi demi kecepatan, dengan sedikit pengorbanan akurasi."]), + ("6. Evaluasi Retrieval", [ + "Hit@k adalah proporsi pertanyaan uji yang jawaban benarnya muncul di antara k chunk teratas. Sebagai contoh, bila 12 dari 15 pertanyaan terjawab pada lima chunk teratas, maka Hit@5 bernilai 0,8. Metrik lain yang sering dipakai adalah MRR dan Recall@k.", + "Disarankan menyiapkan minimal 15 sampai 30 pertanyaan uji lengkap dengan kata kunci jawaban. Bandingkan beberapa pengaturan chunk pada pertanyaan yang sama agar perbedaannya adil."]), + ("7. Halusinasi dan Sitasi", [ + "Halusinasi adalah keluaran model yang terdengar meyakinkan tetapi tidak didukung fakta. RAG mengurangi halusinasi, tetapi tidak menghilangkannya sepenuhnya, terutama bila chunk yang diambil tidak relevan.", + "Sistem yang baik menampilkan sumber berupa nama file dan nomor halaman sehingga pengguna dapat memeriksa jawaban. Bila chunk teratas tidak relevan, model sebaiknya menjawab bahwa informasi tidak ditemukan daripada mengarang."]), + ], + "tabel": {"setelah_bagian": "4. Chunking", + "caption": "Tabel 1. Perbandingan ukuran chunk", + "data": [["Ukuran", "Kelebihan", "Kekurangan", "Cocok untuk"], + ["500 karakter", "Presisi tinggi, hemat token", "Konteks sempit, kalimat sering terpotong", "FAQ dan pertanyaan faktual singkat"], + ["1000 karakter", "Seimbang antara konteks dan presisi", "Kadang memuat dua topik", "Dokumen laporan umum"], + ["1500 karakter", "Konteks luas", "Skor encer dan boros token", "Dokumen naratif panjang"]]}, +}, +"machine_learning_dasar.pdf": { + "judul": "Dasar-Dasar Machine Learning", + "bagian": [ + ("1. Supervised Learning", [ + "Pada supervised learning, model belajar dari data yang sudah berlabel. Tugas klasifikasi memprediksi kategori, misalnya menandai surel sebagai spam atau bukan spam. Tugas regresi memprediksi nilai kontinu, misalnya harga rumah berdasarkan luas dan lokasi."]), + ("2. Pembagian Data", [ + "Data biasanya dibagi menjadi data latih 70 persen, data validasi 15 persen, dan data uji 15 persen. Data validasi dipakai untuk menyetel hiperparameter, sedangkan data uji hanya dipakai satu kali di akhir untuk menilai performa akhir. Data uji tidak boleh dipakai saat tuning karena akan menyebabkan kebocoran informasi."]), + ("3. Overfitting dan Underfitting", [ + "Overfitting terjadi ketika model sangat baik pada data latih tetapi buruk pada data baru. Cirinya adalah selisih besar antara akurasi latih, misalnya 99 persen, dan akurasi validasi, misalnya 72 persen. Model seolah menghafal data latih, termasuk derau di dalamnya.", + "Underfitting terjadi ketika model terlalu sederhana sehingga akurasi latih maupun validasi sama-sama rendah. Cara mengatasi overfitting antara lain regularisasi, early stopping, dropout pada jaringan saraf, dan menambah jumlah data."]), + ("4. Cross-Validation", [ + "Cross-validation menilai model secara lebih stabil daripada satu kali pembagian data. Pada k-fold cross-validation, data dibagi menjadi k lipatan dan setiap lipatan bergantian menjadi data validasi. Nilai k yang lazim adalah 5 atau 10.", + "Untuk kelas yang tidak seimbang, gunakan stratified k-fold agar proporsi kelas pada tiap lipatan tetap terjaga."]), + ("5. Metrik Klasifikasi", [ + "Akurasi menghitung proporsi prediksi yang benar, tetapi menyesatkan bila kelas tidak seimbang. Precision mengukur seberapa banyak prediksi positif yang benar, sedangkan recall mengukur seberapa banyak data positif yang berhasil ditemukan.", + "F1-score adalah rata-rata harmonik dari precision dan recall sehingga nilainya rendah bila salah satunya rendah. Untuk deteksi penyakit, recall diutamakan karena melewatkan pasien jauh lebih berbahaya daripada alarm palsu. Untuk filter spam, precision diutamakan agar surel penting tidak terbuang."]), + ("6. Gradient Descent", [ + "Gradient descent adalah algoritma optimasi iteratif yang memperbarui parameter ke arah berlawanan dengan gradien fungsi kerugian. Learning rate yang terlalu besar membuat pelatihan divergen, sedangkan yang terlalu kecil membuat pelatihan sangat lambat.", + "Pada mini-batch gradient descent, gradien dihitung dari sebagian kecil data, biasanya berukuran 32 atau 64 sampel per langkah."]), + ("7. Decision Tree dan Random Forest", [ + "Decision tree membagi data berdasarkan fitur dengan kriteria Gini impurity atau entropi. Pohon yang terlalu dalam cenderung overfitting. Random forest menggabungkan banyak pohon yang dilatih pada sampel acak berbeda (bagging) lalu mengambil suara terbanyak, sehingga variansi prediksi menurun."]), + ("8. Regularisasi", [ + "Regularisasi L1, atau Lasso, mendorong sebagian bobot menjadi tepat nol sehingga berfungsi sebagai seleksi fitur. Regularisasi L2, atau Ridge, mengecilkan bobot tanpa menjadikannya nol. Kekuatan regularisasi diatur oleh hiperparameter lambda."]), + ], + "tabel": {"setelah_bagian": "5. Metrik Klasifikasi", + "caption": "Tabel 1. Ringkasan metrik klasifikasi", + "data": [["Metrik", "Rumus", "Dipakai ketika"], + ["Akurasi", "(TP+TN)/(TP+TN+FP+FN)", "Kelas seimbang"], + ["Precision", "TP/(TP+FP)", "Biaya false positive tinggi"], + ["Recall", "TP/(TP+FN)", "Biaya false negative tinggi"], + ["F1-score", "2*P*R/(P+R)", "Kelas tidak seimbang"]]}, +}, +"etika_keamanan_ai.pdf": { + "judul": "Etika dan Keamanan Kecerdasan Buatan", + "bagian": [ + ("1. Bias pada Data", [ + "Model kecerdasan buatan mewarisi bias yang ada pada data latihnya. Contoh yang sering dikutip adalah sistem seleksi pelamar kerja milik Amazon yang dihentikan pada tahun 2018 karena cenderung merendahkan pelamar perempuan, akibat dilatih dengan data riwayat perekrutan yang didominasi laki-laki.", + "Mitigasi bias mencakup audit dataset, pengukuran metrik kewajaran antar kelompok, serta pengujian model pada subkelompok yang berbeda sebelum digunakan."]), + ("2. Privasi dan Pelindungan Data Pribadi", [ + "Di Indonesia, pelindungan data pribadi diatur dalam Undang-Undang Nomor 27 Tahun 2022 tentang Pelindungan Data Pribadi. Prinsip pentingnya adalah minimisasi data, persetujuan yang eksplisit dari pemilik data, serta hak subjek data untuk mengakses dan meminta penghapusan datanya.", + "Anonimisasi menghapus identitas secara permanen, sedangkan pseudonimisasi hanya mengganti identitas dengan kode yang masih dapat ditelusuri kembali. Keduanya mengurangi risiko, tetapi hanya anonimisasi yang benar-benar melepaskan data dari pemiliknya."]), + ("3. Transparansi dan Keterjelasan", [ + "Model kotak hitam sulit dijelaskan kepada pengguna yang terdampak keputusannya. Teknik seperti LIME dan SHAP memperkirakan seberapa besar kontribusi tiap fitur terhadap satu prediksi. Keterjelasan penting pada bidang berisiko tinggi seperti kredit, kesehatan, dan hukum."]), + ("4. Serangan Adversarial", [ + "Serangan adversarial mengubah input sedikit saja, sering kali dengan derau yang tidak terlihat oleh mata, sehingga model salah mengklasifikasi. Contoh terkenal adalah stiker kecil pada rambu stop yang membuat model penglihatan komputer membacanya sebagai rambu batas kecepatan. Pertahanan yang umum adalah adversarial training, yaitu melatih model dengan contoh serangan."]), + ("5. Prompt Injection", [ + "Prompt injection adalah serangan berupa instruksi berbahaya yang disisipkan dalam masukan pengguna atau dalam dokumen, dengan tujuan membajak perilaku model. Risikonya besar pada sistem RAG karena dokumen yang diambil diperlakukan sebagai konteks tepercaya.", + "Mitigasinya antara lain memisahkan instruksi sistem dari data, menyaring keluaran model, dan menerapkan hak akses minimum pada alat yang dapat dipanggil model."]), + ("6. Tata Kelola dan Akuntabilitas", [ + "Tata kelola yang baik mencakup audit berkala, keterlibatan manusia dalam pengambilan keputusan penting (human in the loop), dan pencatatan log keputusan. Dokumentasi model dalam bentuk model card diperkenalkan oleh Mitchell dan kawan-kawan pada tahun 2019 untuk menjelaskan tujuan, data, dan batasan sebuah model.", + "Akuntabilitas tetap berada pada manusia dan organisasi yang menerapkan sistem, bukan pada model itu sendiri."]), + ], + "tabel": {"setelah_bagian": "5. Prompt Injection", + "caption": "Tabel 1. Ringkasan risiko dan mitigasi", + "data": [["Risiko", "Contoh", "Mitigasi"], + ["Bias", "Seleksi otomatis menolak kelompok tertentu", "Audit dataset"], + ["Kebocoran data", "Model mengungkap data pribadi", "Anonimisasi"], + ["Serangan adversarial", "Stiker pada rambu stop", "Adversarial training"], + ["Prompt injection", "Instruksi tersembunyi di dokumen", "Pisahkan instruksi dari data"]]}, +}, +} + + +def buat_pdf(nama_file, isi): + st = getSampleStyleSheet() + judul = ParagraphStyle("j", parent=st["Title"], fontSize=18, spaceAfter=14) + h = ParagraphStyle("h", parent=st["Heading2"], fontSize=13, spaceBefore=10, spaceAfter=4) + p = ParagraphStyle("p", parent=st["BodyText"], fontSize=10.5, leading=15, spaceAfter=6) + cap = ParagraphStyle("c", parent=p, fontName="Helvetica-Bold", spaceBefore=6, spaceAfter=3) + sel = ParagraphStyle("s", parent=p, fontSize=9, leading=11, spaceAfter=0) + doc = SimpleDocTemplate(os.path.join(FOLDER, nama_file), pagesize=A4, + leftMargin=60, rightMargin=60, topMargin=60, bottomMargin=60, + title=isi["judul"]) + cerita = [Paragraph(escape(isi["judul"]), judul)] + t = isi["tabel"] + for nama, paragraf in isi["bagian"]: + cerita.append(Paragraph(escape(nama), h)) + for kal in paragraf: + cerita.append(Paragraph(escape(kal), p)) + if nama == t["setelah_bagian"]: + cerita.append(Paragraph(escape(t["caption"]), cap)) + data = [[Paragraph(escape(c), sel) for c in baris] for baris in t["data"]] + tb = Table(data, hAlign="LEFT") + tb.setStyle(TableStyle([("GRID", (0, 0), (-1, -1), 0.5, colors.grey), + ("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey), + ("VALIGN", (0, 0), (-1, -1), "TOP")])) + cerita.append(tb) + cerita.append(Paragraph("Sumber: ringkasan penulis.", p)) + doc.build(cerita) + + +if __name__ == "__main__": + os.makedirs(FOLDER, exist_ok=True) + for nama, isi in DOKUMEN.items(): + buat_pdf(nama, isi) + print("dibuat:", os.path.join("dokumen", nama)) diff --git a/dokumen/etika_keamanan_ai.pdf b/dokumen/etika_keamanan_ai.pdf new file mode 100644 index 0000000..2779b4b --- /dev/null +++ b/dokumen/etika_keamanan_ai.pdf @@ -0,0 +1,93 @@ +%PDF-1.4 +%東京 ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20261102015146+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261102015146+00'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Etika dan Keamanan Kecerdasan Buatan) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 2 /Kids [ 4 0 R 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2190 +>> +stream +GatU3gN)%,&:O:Si6fg%X@V.hSL.([1`[rE56)=)&HW9E,`mu^ofW\5V!Q0uXc18c8-!usHhN&Y:%4"qs'T9PW/C5eoJ\$05P];RlB^1d8:bQ"eWh)-1Rp2o?O;6o5@&e?,dJ'.s]#^SoXN7>$Ii$e1BbRESoAFcibUAoGbC(:=nYa'ff4+Rp5&&/&ZX#o%[_$FY7I-<17*mS'`>^DHXWntKVk,MYLi9fC6hNnPN'!s*A3[?Ti4fmS1+RV;P(Ed6%4N37:j$D@U.#,2j!CMB;s7)Cig0;niM?:)XUS5,nS.T:.He_ZB1Nmn/Po?l!H7aR\ZZj2tD]gJf)(E?i[BXOVZuD!aQ$C*9Gd\HA`G>T8H9?5?P?Xsc7mQS.+9K;=-[!tO>1DVP+R#$XrC"YGP4aDCt=p/Z7-46ZNgI4k8XLu$YQ)u33bGUmg6;sU/GR6Z[7T#s@\a\QKd439\O[+(h4SFGnTE&=Q,!<@]j>;IIH_>XJso`!QoZ+nJVXBDi>Rm>UOktj!fFD4j;HqF;SNf%m8%A*WfmIr9uijK?<4!-$E;s??f2n2"Ku(@0HbjT!MpFJQKJP&-^!+X.coPo1+JZk]OF>I?M.L7G(/LH81i*PC\Jo;E`)?r[4FBLgF14N`L4h+NS"H$2\hU9OE$nG5rhEr4::d2XjcrfQ4:bjoUXE\L)D1cY$c>1X)toZ#UN_I,4\UUnXpcb;an[)c;iF,-!_2<#c5B,7XtqaIc(JY66#a)d?0S/Gq,7=W'UgMsofET6JTT6RHTc_jufNPp@qcUp?#F#5t8o:kF@pA#L)WfJ#11j>SNuYIYRMkSio-aPDbNE5k?Vmj"9p7[AZq,>"&j.2kL;CfPUtCMAN6ZUead:fCR:0kl"9U^t,/-o$=rb*Q7Ggo0dE*HecmfH;q-lq'jG,dithChU=2^'E!sq2B;k=c\'N^:J]i\GO-07-YJ>&bt\:LG6WSi`cLN&WeZ2O@hi,TlTe!$AHb8g1#'B8(p;B)K-!:(dEH71=TuYd*Bl=NM!D$R"JJCC\]+U*^+J7b%R>`*-Lb\.bEjbcr>Tg(sO:Sno/(KR-tY!&MO6Sd#'>KFYjVB%jJ6&_&sTf`-r-1L1B4S0u]>g$EEQgP.T*i^b^39Duj&n]`)T0/8+RA4oL+()CO*LjGR`:/E[Ca*A:PIf_q:07f+;g$^2ZN_/,AFq3eTu?981mF3uf:=Vp):,ap%Nj;cR]_EKJrMQ?Yi5+.rdqXZ--\.l\].c%VTOdOH!dqO,#nm/cnQDR'2P3sr(,.G08k\aX=)g"?PN2D$8$=<=^7u^H`@TW]$[?D&d*dpinC0'aB?(C)C)>7aNY_-F(ZP878!6R0*]at3PEcBDQFhpVr5F*:`W+`#0XL~>endstream +endobj +10 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 536 +>> +stream +GasIc997OU&AIm?I)dih+`=ocjTYc=^]BSX?7(Y'>X)\,>DRdTfI=H!9"U?7/'<+Nf8Mu>8!Nm]JIa?hHkm8tZp,`-eK4*ZHtl`t;lbkI[SA'Wp(.d)M*$aD40LVhYBBIOS.82D,Mc\1b0EfiCo<2B!rCXY,lSK=E2>'T_WqhNsWNg,9`mR0*];!M$`LS;i:L*&lF)qAqlK-ck9b&RsESBsNe"`m+*T46g8rWuqQ20,(o>E=X@A_0(h/%sYT/gCI2Uh/G;;pQ2'.`&b'^X=BF.qsq?deZg9BeicO'[*t/0,VQE0HD/^8Z>qV(mZsdr091b7.61+ju_t[^A(=A^Hs_=W.YD58,cB'=k2&d\8`,Ok1i!0%%1bHZ;QEh=\GqcVNlSf]TM,+o^lo=09^KmoC0"-t@.b=;2@(E~>endstream +endobj +xref +0 11 +0000000000 65535 f +0000000061 00000 n +0000000102 00000 n +0000000209 00000 n +0000000321 00000 n +0000000524 00000 n +0000000728 00000 n +0000000796 00000 n +0000001099 00000 n +0000001164 00000 n +0000003445 00000 n +trailer +<< +/ID +[<7636af93ea22ee58f59714add6924af0><7636af93ea22ee58f59714add6924af0>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 11 +>> +startxref +4072 +%%EOF diff --git a/dokumen/machine_learning_dasar.pdf b/dokumen/machine_learning_dasar.pdf new file mode 100644 index 0000000..1eb0d39 --- /dev/null +++ b/dokumen/machine_learning_dasar.pdf @@ -0,0 +1,93 @@ +%PDF-1.4 +%東京 ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20261102015146+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261102015146+00'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Dasar-Dasar Machine Learning) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 2 /Kids [ 4 0 R 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2005 +>> +stream +GatU3bBDVu&Dd46Y]O]>&LgV=A^f-lVg26]Pch8MP-#]EdfbGn);LXj1\B?;5Mof91WJ:Rf\9W&q#O]:QlgVt>CC%3X=jLcn=)A7;`9O\hL*&)@g<-H/'uBApA1=@Cs8P%@/cZq"T^&U/A,XNT0p8Kg#[QeEBUU/p5.[FAM2V*_0%C%j:,bbad5YGN$1;-"n?ampTZe(OMlX\%_6joFG/mU7fT#+%I7XQiN[`oITPMHD:8(5b.V&$=0/'1[qMkn]4b%8OlrkqqdEh:1"PAiY`-7<7m`WGqF(n0XKk%iCo`bGo]h68ZsmG(t22F?@,2)%YHR4)L-'I17V%TdX)u"R$hVG4e9]QB*1ckfimBcFRE7ofN*%;1G&2.64C7n3`Ne0I!,<:?5o\t,)r-BSo1CmjpG[$#ph)nj[\CSo,9'/&/\-PA(;(SFg9Rj_Y1Aqf*60=K$I2VT'V[4?PI#dEDu>gcHrk:OWlsodr!cjaU!=srA^VP\[<+1Cq?RFd6AD"=r-og6^-afeVc6VfU;#,W2UPPDkRk\"K/9q=t88_/PnU*Q!G?5^bFMle'Y$hEJNr[Su%17pS^2c$Hd.b#n]E8nZmLp(5KELdKQGEr\e&.$NKH2VX*N)s!Q3;?+i_1rrRqMW(p*K3r\a,lC0*l'LP@O>]A1u#mm+MnIh%,3rmbK?u.)X'F,GlS=j"VH)YMbYUV3X6R96a>/kS?QQ/fBanYKRGrQrl:lhP4q!u>KK?tc?6$.FI30KH>22kol:853D4W`EIA5QX-.eQM/gU>WU-OEl8.=V1@(U,J)5AR2*8+h)nqcaMe&A3l8>t5V:,5Z%KV3K`ebQe=LuhG3'9F:pOI96:9<_Xm0#W)UrXp'K6AZd:OsTT03Cqh>*=[-l!$2uFdtlO0'"lR/[]%?U<]bgatfKh\HNF,P]`m(P4Wl;13(4;a%1,JoBt#1_.jZr4\T)h(\F_rYUo-Xh1:;dtjNfqtoJiH+j(PrQX*ZmeV*PI>dc4Pg6N,_=DEcYX_NYb[.E`A*MH%SY:J>F9(,L?NSIi#2Vr2-]I;TBUJfs0VXrq82[_]r;%>9<-fIY1K8Q@]dSBU0JEVY(Du@C"OcA]G1uriJlI'(mbhi*FU+G&f68l?V)+$GGF1Buk:c`r>^4ZK?VJ<5HiNR6KS]S?Rm>Q6!N["4C'EYVoC:,dEW=nRNPfM.o:Z/bendstream +endobj +10 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 909 +>> +stream +Gat=hc#28i&;9M$MEN;?X'5KuO%#NXl9h$*R*3XJ.7OrL;IYNrI>=%=a),;gJH/ms`VjsmB:70N:E"_l2:7bL52E*#:+=pp3`"n&D?S7\P>#(7RqIY^#fU0SdVa`Y8%cQQlK!Mru2@8gX[g9gQIel*lmP?k?<66^2C8CBbDNekYSgaIIGX(\f12#'obWTrPt)q/_B5Ud-U/b#*e=K`.BKlLb!&-mV4*'FLpU8PO>7,:@ID#NM9V-NH@0BYGksN[DXtgP42ct'#&s<(&qkJeV#adJ#Wb%c5VJ1V0$$X2@44G3s1HK&D8kNE4YV)bEcEVk+4ZU?QkD\bs2m;=ZRkRQs(pu[)ngB.-4K2rrsTf;dlBZ3p5pI5F:u\7@DVo2eB^YXAiu156A2D%OqmmNsAsD/p%>88?gRA-g)Vu^&l+kO"&\sL+`s@S1hRnp1udGJPSDV7;Z^7Kjl(f$^qdm$VXXj2!c]C:UdpcekkM)7$`QKfEb^1X`^p4lef\@Z@D%U$TjH^5Rnfh?;[VnNdsqY\7#V:h8+]>$WbhPGA%9H](&mITdUn,iG>8&]!mAV3padZ@7"]qgB@/h\qU"]k[<9.;"uXYNeE?hSGV0,H0AF'9aDaKqT?Y%P*dNiAD]E(i`d+Z_pe7&Ku+N0O4P)0#a&P"R%j6j\WC&<4q'`N".#Ne+Z?GD0TD8_QP_fSp;m36,8Sb]3utS\$rZm.pm.!9s$>eJbP1(clg$E_2X._iieH4=N^JrG3f=IWA!Ga'`i_F4g7VT$kr6%38dbjt;(%@endstream +endobj +xref +0 11 +0000000000 65535 f +0000000061 00000 n +0000000102 00000 n +0000000209 00000 n +0000000321 00000 n +0000000524 00000 n +0000000728 00000 n +0000000796 00000 n +0000001091 00000 n +0000001156 00000 n +0000003252 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 11 +>> +startxref +4252 +%%EOF diff --git a/dokumen/pengantar_rag.pdf b/dokumen/pengantar_rag.pdf new file mode 100644 index 0000000..49f2d7d --- /dev/null +++ b/dokumen/pengantar_rag.pdf @@ -0,0 +1,93 @@ +%PDF-1.4 +%東京 ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20261102015146+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261102015146+00'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Pengantar Retrieval-Augmented Generation \(RAG\)) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 2 /Kids [ 4 0 R 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2164 +>> +stream +GatU4bAs(C']&X:ma=\K$m6_89.,q!At7?r[Utk#,MK(*1e^7aQ+rr4Rtm(IaH=Z+ACQ$7kM2C:OH6$L]l"4PG8pD=@mE=NRl^rmhL[YXl,2ZM^U'TF\QmQ4RX=AnjVeaknD/XecQF/8l6'1oBQHt=[rcV_#0qY7c_5u"E7D^;E,b5S?*f1HJOf'm1lW-:@(&#jPaQ(s*QR-elRW':TDsYJaOtlBe0GZhM]E+&X%$]\2=sQ*PTZ'b,.9PWi+XM1U4,^isVXd#s-$/heOVp$d;(9)cWKWW;'nWHk.@o#G$0SRU3MP&'b?1Q,;-"n%XJSH$lk^.bBEfCe],*\#$o^ktc3`l4P2S'SOtg<+9*SKoA0t]@PZF"sR*DFEOXP:]UX/M/W[Ybo#/BeaH_h)A&;-c-oKs^PoMKF9>a?75rA?ua,ASaPG3@8mh?-RY]-EUr[fug]TUab?Bjg>p'XD+mQ"D*X(jM+VYLUh2O7r0GZ;*8jK)T2=rl>aQ>*p*:#.L:mk+2)Qf;Q(/jAD,QuQlOnfX:Xae$@$Oso@3)n0OtVt:'K?Q,*#4@tc4B9XW6E3!#.`?b^A$JaA+XVrj5P0q86F_,S)AV\?C<22)@e8"MI*!bkE#\q/j@9\XVGgs"`U31m`LfI1f3LEt_.KSm7EeW7`md,lp8k*(uMLGT:.Vh[2P9#+;.>t2LWuINKoZCl%(T3S>gnPC.n1NIuAV%Lrp9$SjDgqM^3n[a(>*"f26"@b?7`TM^(hmj72`_KN\8\)Z.InP<6if]"+.^.c+p5PdcoaOBEI>@LRHEg(00[8j5hp7%Fa.ujWmP;5ICRs8i(l@FD6Iib>k^k;:"ebpVaQ9HF"ibrHH0K#8:C#Oc$ki4H$K.Rq*%VIm?TPYMR;C(6\/ckW/,:nF6>hh"*C<,?aIRMNDENS6t#n3q%=+-TAN7/Y$AIh.RaB5gO@4R#>AcnRpkI\RNuLLD;;;CXpsg5DH,38$5:tK/B0*\7KXZ`6eKQP/8R^]6i3$V/jXF1L,lMNi2%2LLV:G\k\iEa&M"&<,ZoUH+YD+"RMLTo\]$i$1N8_(X]SFE;5]iS:L+5B*kg`k1MTf`)$hJe23mudhpXiNCO)?'Ek@OI^';2lBjbiC'jK=bD]YF]n0.B$_$Cs'KhMr'ha_.[TH*O7r4sFT32Gs\[\\j;EOlF.p$mSj;2"skAf22K_JQ.<8lK]H_k_$H$XG]R)p&DAfOJH7h3G)ER7jC?\(\Pp%<6$J6Nn_YAMKF.8<(-S>fKmW%'R]V#B!JAp0%ZN$B#652X(_&QJ.*E6G';M?kVK2h+tt&S,gqg"T0l-=/OZrel_iX!\5gr0oFS!^-##d[U4'9G7^E2GPcNG$b)D5[u#fDR?n**A"'I[$bdI(qa*B?Wf>#H!s?9+-O.4'j"Y+l]jfHkAS^%)LT+^U".1b>VAI9:'E-g;Ea/>L$=9e$M3_dg>^=B^RIHjm!QQmp0^QS(I90dN\U+`R1Ul*J]%a9GucVnCYTZCeopi+mu0Ti>m[:IVLbek1O`D:,..PIG7PJ@`_FLn+a6WgOtf=St(^#P7$\bl3'%[o*qq&%lfEl=TL`4X00Wr"fol!l#pns,X(^4HQW],o+GoEB6l\p!7NV_R(l)j79:$_U.38GCSaDIW2hc?[_Emo*IqZZZ!.g^8p^>WM)'@)5r00UEuL?&)=eO%I"_aAYGc2H#Bs!Z1g0)~>endstream +endobj +10 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1431 +>> +stream +GatU2gN):C&:N^lqU%kH77&+>:V2Y]P&*$V#*M)-BFNp`7`n9-bC?/5,9_G%Y&]E*OlghucT0fK6m4aVs1ca6/iWFokD5i=D/Lbj##j7R6(^'1In*AF1.c.CSHk(lEe,6u#lTG2O,eQS^N]6h-OtO7-0c_jNng82oERd)^dU'f6Se#\eB7_TRp4VE.*#ZSF#l@N7):'N/Dl*LXg#2g'kIhUB)s@L4@nK-K&WKqU\H]s[4*(E]2O_f[^7Vkd0!\(Ki.OpCcmW.kq&7;-a0Sf1qBo&6!g[3W+:7uG5hp#n+aL8"g]md-4rE@?@BX(h2eXmb6;cL)6GVI:#]P9T-\s!J*ElKVrM2MN"rOuO.odXU29.&qWUQk[2R,.s3a]`E2D_iVYc]tqYD)7io3OM`l0<7VoZNG2@AO`m*8d3Rs_9l0BBLr)m5iLWGId_.QAq*0U(VX:!=)einXaMKu]YdoXEQ)E7,[)e0%:EGrr:mfuuLE8mho/HI+[R6ONXd.C/Un`d$4)#&d%Za%g-G<]0f_B0<\')6O)FG_0Ol;U$t^0d=MZV;**6*V!/Q8io3"MV7Q;-V!8&qodH"M@V?@1natQ:;M6V`U4h)?\%:\oQLB)L/RP1X@ug-"m[B)49lQ1*C1qg)X:`=cii.3.XOWX2ct\?jWAm.=SZuG"WT]"#*l29g#==2AR51`4o[#=Llm-#c`;CB@koM5m0*3RaV[`!nD-'raq,5h@G>+@ipXREQ`X6V)Z4CPDd-2bN5H_`Kn1e.r2OarrV16u1q.6i;2-_?43.+JLVPHu1UEPV]3*Oj-+8%UuI*":sD,kKhW8WoQEOgg4D+CA!oas[FHpBJ%E\`9mJdE$V;k4B(JfFZ:2P16H'eZ\sp!ng_ef*i=6+$.KX'j$5-bR_*7\S+\R_20kel(IrKbZ(XKq>'QB$s:DF;1:?$`1GH/8jr5NWWI5r:7@GmY1#@Q&gj\Sj$k14i;;C$2Em;fV8A\i`j:(&uaaa.7+5P.5m0;RUE`><^i7&@B,"S8\!h"d)Y,nOiuT'p3jfOlLL4C%NJkno^#~>endstream +endobj +xref +0 11 +0000000000 65535 f +0000000061 00000 n +0000000102 00000 n +0000000209 00000 n +0000000321 00000 n +0000000524 00000 n +0000000728 00000 n +0000000796 00000 n +0000001111 00000 n +0000001176 00000 n +0000003431 00000 n +trailer +<< +/ID +[<94e0708a6e608b44ad056752ec8119b5><94e0708a6e608b44ad056752ec8119b5>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 11 +>> +startxref +4954 +%%EOF diff --git a/lab1_chunking.py b/lab1_chunking.py new file mode 100644 index 0000000..c4d57ab --- /dev/null +++ b/lab1_chunking.py @@ -0,0 +1,155 @@ +"""Lab 1 - Eksperimen chunking (15 menit). + +1. Baca satu PDF dengan baca_pdf() dan cetak 500 karakter pertama. +2. Potong dengan potong() memakai tiga ukuran (500, 1000, 1500) dan overlap 10-20%. +3. Lihat 3 chunk acak tiap ukuran: terpotong di tengah kalimat atau tabel? +""" +import os +import random +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from laporan_utils import Laporan +from rag_utils import FOLDER_DOKUMEN, baca_pdf, norm, potong, potong_dengan_posisi + +BASE = os.path.dirname(os.path.abspath(__file__)) +PDF = os.path.join(FOLDER_DOKUMEN, "pengantar_rag.pdf") # ganti dengan PDF Anda +UKURAN = [500, 1000, 1500] +OVERLAP = [0.10, 0.15, 0.20] +OVERLAP_UTAMA = 0.15 +SEED = 42 + + +def wilayah_tabel(teks): + """Cari rentang karakter tabel ("Tabel N." sampai baris "Sumber:").""" + hasil, pos = [], 0 + while True: + a = teks.find("Tabel ", pos) + if a < 0: + return hasil + b = teks.find("Sumber:", a) + b = len(teks) if b < 0 else teks.find("\n", b) + hasil.append((a, b)) + pos = b + + +def awal_di_tengah_kalimat(teks, mulai): + """Chunk dimulai di tengah kalimat bila karakter bermakna sebelumnya bukan akhir kalimat/baris.""" + sebelum = teks[:mulai].rstrip(" ") + return bool(sebelum) and sebelum[-1] not in ".!?:\n" + + +def akhir_di_tengah_kalimat(teks, akhir): + ujung = teks[:akhir].rstrip() + return bool(ujung) and ujung[-1] not in ".!?" + + +def memotong_tabel(wil, mulai, akhir): + """True bila chunk beririsan dengan tabel tetapi tidak memuat seluruh tabel.""" + for a, b in wil: + beririsan = mulai < b and akhir > a + utuh = mulai <= a and akhir >= b + if beririsan and not utuh: + return True + return False + + +def main(): + # ---- Langkah 1 + teks = baca_pdf(PDF) + n_huruf = sum(c.isalpha() for c in teks) + kualitas = { + "File": os.path.basename(PDF), + "Total karakter": len(teks), + "Karakter rusak (U+FFFD)": teks.count("\ufffd"), + "Persentase huruf": f"{n_huruf / len(teks):.0%}", + } + print("=== Langkah 1: 500 karakter pertama ===") + print(teks[:500]) + print(kualitas) + + # ---- Langkah 2 + print("\n=== Langkah 2: jumlah chunk ===") + tabel_jumlah = [["Ukuran (karakter)"] + [f"overlap {int(o * 100)}%" for o in OVERLAP]] + for u in UKURAN: + baris = [u] + for o in OVERLAP: + n = len(potong(teks, u, o)) + baris.append(n) + print(f"ukuran={u:5d} overlap={o:.0%} -> {n} chunk") + tabel_jumlah.append(baris) + + # ---- Langkah 3 + wil = wilayah_tabel(teks) + rnd = random.Random(SEED) + cuplikan, statistik = {}, [["Ukuran", "Jumlah chunk", "Awal di tengah kalimat", + "Akhir di tengah kalimat", "Memotong tabel"]] + for u in UKURAN: + pot = potong_dengan_posisi(teks, u, OVERLAP_UTAMA) + n = len(pot) + a = sum(awal_di_tengah_kalimat(teks, m) for m, _, _ in pot) + z = sum(akhir_di_tengah_kalimat(teks, e) for _, e, _ in pot) + t = sum(memotong_tabel(wil, m, e) for m, e, _ in pot) + statistik.append([u, n, f"{a}/{n} ({a / n:.0%})", f"{z}/{n} ({z / n:.0%})", f"{t}/{n}"]) + pilih = sorted(rnd.sample(range(n), min(3, n))) + cuplikan[u] = [(i + 1, pot[i][0], pot[i][1], pot[i][2], + awal_di_tengah_kalimat(teks, pot[i][0]), + akhir_di_tengah_kalimat(teks, pot[i][1]), + memotong_tabel(wil, pot[i][0], pot[i][1])) for i in pilih] + print(f"\n=== Langkah 3: ukuran {u} (3 chunk acak) ===") + for no, m, e, isi, aw, ak, tb in cuplikan[u]: + print(f"--- chunk #{no} [{m}:{e}] awal_tengah={aw} akhir_tengah={ak} potong_tabel={tb}") + print(isi) + + # ---- Kesimpulan otomatis dari angka + rincian = "; ".join( + f"ukuran {r[0]}: awal {r[2]}, akhir {r[3]}, memotong tabel {r[4]}" for r in statistik[1:]) + kesimpulan = ( + "Pemotongan per karakter tidak mengenal batas kalimat maupun tabel. Hasil seluruh chunk (overlap " + f"{int(OVERLAP_UTAMA * 100)}%) - {rincian}. Jadi pada setiap ukuran, mayoritas chunk berakhir di tengah " + "kalimat dan sebagian besar juga dimulai di tengah kalimat (hanya chunk pertama yang selalu bersih). " + "Persentasenya tidak turun banyak saat ukuran diperbesar; yang berubah adalah jumlah chunk dan " + "banyaknya topik per chunk. Tabel 1 terpotong oleh sedikitnya satu chunk pada tiap ukuran, dan teks " + "tabel hasil ekstraksi sudah bercampur antar kolom, sehingga baris tabel yang terpisah dari baris " + "judulnya sulit dipahami. Perbaikan yang layak dicoba: pemotongan berbasis kalimat/paragraf dan " + "memperlakukan tabel sebagai satu chunk utuh." + ) + print("\nKesimpulan:", kesimpulan) + + # ---- Laporan PDF + L = Laporan(os.path.join(BASE, "Lab1_Eksperimen_Chunking.pdf"), + "Lab 1 - Eksperimen Chunking", "Dikerjakan dengan Python (pdfplumber + fungsi potong)") + L.h1("Langkah 1. Membaca PDF dan memeriksa teks") + L.p(f"File yang dibaca: {os.path.basename(PDF)}. Berikut 500 karakter pertama hasil baca_pdf():") + L.kode(teks[:500]) + L.spasi() + L.tabel([["Pemeriksaan", "Hasil"]] + [[k, v] for k, v in kualitas.items()], [6 * 28.35, 8 * 28.35]) + L.p("Teks terbaca bersih: tidak ada karakter rusak, kata utuh, dan urutan paragraf benar. " + "Satu catatan: teks Tabel 1 hasil ekstraksi tercampur antar kolom karena sel yang lebih dari satu baris " + "digabung per baris tampilan, bukan per sel.") + L.h1("Langkah 2. Jumlah chunk tiap pengaturan") + L.p("Fungsi potong(teks, ukuran, overlap) memakai jendela geser per karakter; langkah = ukuran x (1 - overlap).") + L.tabel(tabel_jumlah) + L.cap("Tabel 1. Jumlah chunk untuk 3 ukuran x 3 overlap. Makin besar ukuran, makin sedikit chunk; " + "makin besar overlap, makin banyak chunk.") + L.h1(f"Langkah 3. Tiga chunk acak per ukuran (overlap {int(OVERLAP_UTAMA * 100)}%, seed {SEED})") + L.tabel(statistik) + L.cap("Tabel 2. Statistik seluruh chunk pada tiap ukuran (bukan hanya yang dicuplik).") + for u in UKURAN: + L.h2(f"Ukuran {u} karakter") + for no, m, e, isi, aw, ak, tb in cuplikan[u]: + L.p(f"Chunk #{no} (karakter {m}-{e}) | awal di tengah kalimat: {'ya' if aw else 'tidak'} | " + f"akhir di tengah kalimat: {'ya' if ak else 'tidak'} | memotong tabel: {'ya' if tb else 'tidak'}") + L.kode(isi) + L.spasi(4) + L.h1("Kesimpulan") + L.p(kesimpulan) + L.halaman_baru() + L.h1("Lampiran: kode lab1_chunking.py") + L.kode(open(__file__, encoding="utf-8").read()) + L.simpan() + print("\nLaporan: Lab1_Eksperimen_Chunking.pdf") + + +if __name__ == "__main__": + main() diff --git a/lab2_indeks_cari.py b/lab2_indeks_cari.py new file mode 100644 index 0000000..4529d40 --- /dev/null +++ b/lab2_indeks_cari.py @@ -0,0 +1,143 @@ +"""Lab 2 - Bangun indeks & cari (25 menit). + +1. Letakkan 2-3 PDF di folder dokumen/, jalankan indeks, pastikan tiap file melaporkan jumlah chunk. +2. Ajukan 5 pertanyaan dengan query(); periksa apakah chunk teratas benar-benar relevan. +3. Ajukan 2 pertanyaan berparafrase; apakah pencarian semantik tetap menemukannya? +""" +import io +import os +import sys +from contextlib import redirect_stdout + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from laporan_utils import Laporan +from rag_utils import bangun_indeks, cocok, query + +BASE = os.path.dirname(os.path.abspath(__file__)) +UKURAN, OVERLAP = 1000, 0.15 + +# kw = kata kunci yang HARUS ada di chunk teratas agar dianggap relevan +PERTANYAAN = [ + {"q": "Siapa yang memperkenalkan RAG dan pada tahun berapa?", "kw": ["Lewis", "2020"]}, + {"q": "Berapa dimensi embedding yang dihasilkan model MiniLM?", "kw": ["384"]}, + {"q": "Apa itu overfitting dan apa cirinya?", "kw": ["overfitting", "data latih"]}, + {"q": "Undang-undang apa yang mengatur pelindungan data pribadi di Indonesia?", "kw": ["27 Tahun 2022"]}, + {"q": "Bagaimana cara memitigasi prompt injection?", "kw": ["memisahkan instruksi"]}, +] +PARAFRASE = [ + {"q": "Bagaimana mencegah model bahasa mengarang jawaban yang keliru?", "kw": ["halusinasi"], + "catatan": "Teks memakai kata 'halusinasi'; pertanyaan memakai 'mengarang jawaban yang keliru'."}, + {"q": "Kapan sebuah model dianggap terlalu menghafal data latihan?", "kw": ["overfitting"], + "catatan": "Teks memakai 'overfitting' dan 'data latih'; pertanyaan memakai 'menghafal' dan 'data latihan'."}, +] + + +def ringkas(t, n=150): + t = " ".join(t.split()) + return t if len(t) <= n else t[:n] + "..." + + +def jalankan(indeks, daftar, k=3): + hasil = [] + for s in daftar: + atas = query(indeks, s["q"], k) + rel = [cocok(a["teks"], s["kw"]) for a in atas] + hasil.append((s, atas, rel)) + return hasil + + +def main(): + # ---- Langkah 1 + print("=== Langkah 1: membangun indeks ===") + buf = io.StringIO() + with redirect_stdout(buf): + idx = bangun_indeks(ukuran=UKURAN, overlap=OVERLAP, backend="auto") + log_indeks = buf.getvalue() + print(log_indeks) + idx_leksikal = bangun_indeks(ukuran=UKURAN, overlap=OVERLAP, backend="tfidf", verbose=False) + per_file = {} + for m in idx.meta: + per_file[m["file"]] = per_file.get(m["file"], 0) + 1 + + # ---- Langkah 2 + print("=== Langkah 2: 5 pertanyaan ===") + h2 = jalankan(idx, PERTANYAAN) + for s, atas, rel in h2: + print(f"\nQ: {s['q']} (kata kunci: {s['kw']})") + for r, (a, ok) in enumerate(zip(atas, rel), 1): + print(f" #{r} skor={a['skor']:.3f} {a['file']}#{a['no']} relevan={ok} | {ringkas(a['teks'])}") + n_top1 = sum(rel[0] for _, _, rel in h2) + n_top3 = sum(any(rel) for _, _, rel in h2) + + # ---- Langkah 3 + print("\n=== Langkah 3: parafrase ===") + sem = jalankan(idx, PARAFRASE) + lek = jalankan(idx_leksikal, PARAFRASE) + for (s, a1, r1), (_, a2, r2) in zip(sem, lek): + print(f"\nQ: {s['q']}") + print(f" {idx.backend}: top1 relevan={r1[0]} top3 relevan={any(r1)} | {a1[0]['file']}#{a1[0]['no']}") + print(f" tfidf : top1 relevan={r2[0]} top3 relevan={any(r2)} | {a2[0]['file']}#{a2[0]['no']}") + + # ---- Kesimpulan dari angka + sem_top3 = sum(any(r) for _, _, r in sem) + lek_top3 = sum(any(r) for _, _, r in lek) + sem_top1 = sum(r[0] for _, _, r in sem) + lek_top1 = sum(r[0] for _, _, r in lek) + kes = (f"Pada 5 pertanyaan langsung, chunk peringkat 1 relevan pada {n_top1}/5 pertanyaan dan salah satu dari " + f"3 chunk teratas relevan pada {n_top3}/5 pertanyaan. Pada 2 pertanyaan parafrase, backend " + f"'{idx.backend}' menemukan chunk relevan di peringkat 1 pada {sem_top1}/2 dan di 3 teratas pada " + f"{sem_top3}/2, sedangkan pembanding TF-IDF (kecocokan bentuk kata) {lek_top1}/2 dan {lek_top3}/2. ") + gagal_top1 = [s["q"] for s, atas, rel in h2 if not rel[0]] + if gagal_top1: + kes += ("Pertanyaan yang chunk peringkat 1-nya belum relevan: " + "; ".join(f'"{q}"' for q in gagal_top1) + + ". Penyebab yang paling mungkin: kalimat jawaban terbelah di batas chunk sehingga kata kuncinya " + "terpisah, sementara chunk di sebelahnya memuat topik yang mirip. ") + if sem_top1 == lek_top1 and sem_top3 == lek_top3: + kes += ("Pada dua parafrase ini, semantik dan TF-IDF memberi hasil yang sama, jadi tes ini belum bisa " + "membedakan keduanya: pertanyaan masih berbagi akar kata dengan teks (model, jawaban, data latih) " + "dan TF-IDF n-gram karakter menangkap variasi bentuk kata. ") + if idx.backend == "lsa": + kes += ("Catatan: backend LSA hanya semantik ringan (hubungan antar kata dipelajari dari korpus yang kecil), " + "sehingga parafrase yang benar-benar tidak berbagi kata akan sulit. Untuk hasil lebih kuat, pasang " + "sentence-transformers; kode ini otomatis memakainya.") + print("\nKesimpulan:", kes) + + # ---- Laporan PDF + L = Laporan(os.path.join(BASE, "Lab2_Indeks_dan_Pencarian.pdf"), "Lab 2 - Bangun Indeks & Cari", + f"ukuran chunk {UKURAN}, overlap {int(OVERLAP * 100)}%, backend: {idx.backend}") + L.h1("Langkah 1. Membangun indeks") + L.p("Tiga PDF diletakkan di folder dokumen/. Setiap file melaporkan jumlah chunk-nya:") + L.tabel([["File", "Jumlah chunk"]] + [[f, n] for f, n in per_file.items()] + [["Total", len(idx.chunks)]], + [9 * 28.35, 4 * 28.35]) + L.kode(log_indeks.rstrip()) + L.h1("Langkah 2. Lima pertanyaan dan pemeriksaan chunk teratas") + L.p("Sebuah chunk dianggap relevan bila memuat semua kata kunci jawaban. Tabel menunjukkan 3 chunk teratas.") + for s, atas, rel in h2: + L.h2(s["q"]) + L.tabel([["Peringkat", "Skor", "Sumber", "Relevan?", "Cuplikan"]] + + [[r, f"{a['skor']:.3f}", f"{a['file']} #{a['no']}", "ya" if ok else "tidak", ringkas(a["teks"], 140)] + for r, (a, ok) in enumerate(zip(atas, rel), 1)], + [1.6 * 28.35, 1.3 * 28.35, 3.8 * 28.35, 1.6 * 28.35, 8.7 * 28.35]) + L.p(f"Ringkasan: chunk peringkat 1 relevan pada {n_top1}/5 pertanyaan; ada chunk relevan di 3 teratas pada {n_top3}/5.") + L.h1("Langkah 3. Pertanyaan berparafrase") + L.p("Dua pertanyaan memakai kata yang berbeda dari teks dokumen. Dibandingkan: pencarian semantik " + f"({idx.backend}) vs TF-IDF.") + baris = [["Pertanyaan", "Catatan", f"{idx.backend}: top1 / top3", "TF-IDF: top1 / top3"]] + for (s, a1, r1), (_, a2, r2) in zip(sem, lek): + baris.append([s["q"], s["catatan"], f"{'ya' if r1[0] else 'tidak'} / {'ya' if any(r1) else 'tidak'}", + f"{'ya' if r2[0] else 'tidak'} / {'ya' if any(r2) else 'tidak'}"]) + L.tabel(baris, [5 * 28.35, 6 * 28.35, 3 * 28.35, 3 * 28.35]) + for (s, a1, r1), _ in zip(sem, lek): + L.p(f"Chunk teratas untuk \"{s['q']}\": {a1[0]['file']} #{a1[0]['no']} (skor {a1[0]['skor']:.3f})") + L.kode(ringkas(a1[0]["teks"], 400)) + L.h1("Kesimpulan") + L.p(kes) + L.halaman_baru() + L.h1("Lampiran: kode lab2_indeks_cari.py") + L.kode(open(__file__, encoding="utf-8").read()) + L.simpan() + print("\nLaporan: Lab2_Indeks_dan_Pencarian.pdf") + + +if __name__ == "__main__": + main() diff --git a/lab3_hit_at_k.py b/lab3_hit_at_k.py new file mode 100644 index 0000000..270cb6b --- /dev/null +++ b/lab3_hit_at_k.py @@ -0,0 +1,171 @@ +"""Lab 3 - Ukur Hit@k (25 menit). + +1. 15 pertanyaan uji beserta kata kunci jawaban dari dokumen. +2. hit_at_k untuk k = 1, 3, 5 pada dua koleksi dengan ukuran chunk berbeda. +3. Kesimpulan 3-4 kalimat: ukuran terbaik, pertanyaan yang gagal, dugaan penyebab. +""" +import os +import sys + +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from laporan_utils import Laporan +from rag_utils import FOLDER_DOKUMEN, baca_pdf, bangun_indeks, cocok, hit_at_k, norm, peringkat_hit + +BASE = os.path.dirname(os.path.abspath(__file__)) +OVERLAP = 0.15 +UKURAN = (500, 1000) # dua koleksi yang dibandingkan +KS = (1, 3, 5) + +SOAL = [ # q = pertanyaan, kw = kata kunci yang harus ada di chunk jawaban, jenis = langsung / parafrase + {"q": "Siapa yang memperkenalkan RAG?", "kw": ["Lewis"], "jenis": "langsung"}, + {"q": "Berapa dimensi embedding yang dihasilkan model MiniLM?", "kw": ["384"], "jenis": "langsung"}, + {"q": "Seberapa besar tumpang tindih antar potongan teks yang disarankan?", "kw": ["10 sampai 20 persen"], "jenis": "parafrase"}, + {"q": "Pustaka apa buatan Meta untuk pencarian tetangga terdekat?", "kw": ["FAISS"], "jenis": "langsung"}, + {"q": "Jika 12 dari 15 soal terjawab pada lima hasil teratas, berapa skornya?", "kw": ["0,8"], "jenis": "parafrase"}, + {"q": "Berapa rentang nilai cosine similarity?", "kw": ["-1 dan 1"], "jenis": "langsung"}, + {"q": "Bagaimana menyatukan ketepatan dan kelengkapan menjadi satu angka?", "kw": ["rata-rata harmonik"], "jenis": "parafrase"}, + {"q": "Mana yang diutamakan untuk skrining pasien: tidak melewatkan kasus atau menghindari alarm palsu?", "kw": ["deteksi penyakit"], "jenis": "parafrase"}, + {"q": "Berapa banyak lipatan yang biasa dipakai pada validasi silang?", "kw": ["5 atau 10"], "jenis": "parafrase"}, + {"q": "Teknik apa yang membuat sebagian bobot menjadi tepat nol?", "kw": ["Lasso"], "jenis": "parafrase"}, + {"q": "Undang-undang nomor berapa yang mengatur pelindungan data pribadi?", "kw": ["27 Tahun 2022"], "jenis": "langsung"}, + {"q": "Bagaimana melindungi sistem dari instruksi jahat yang disusupkan ke dokumen?", "kw": ["memisahkan instruksi"], "jenis": "parafrase"}, + {"q": "Kasus perekrutan yang merendahkan pelamar perempuan dialami perusahaan apa?", "kw": ["Amazon"], "jenis": "parafrase"}, + {"q": "Siapa yang memperkenalkan model card?", "kw": ["Mitchell"], "jenis": "langsung"}, + {"q": "Apa kekurangan chunk berukuran 1500 karakter?", "kw": ["boros"], "jenis": "langsung"}, # jawaban ada di tabel +] + + +def diagnosis(idx, soal, k): + """Tebak penyebab gagal secara otomatis dari data (bukan menebak sembarang).""" + r = peringkat_hit(idx, soal) + if r is None: + return "kata kunci terbelah di batas chunk (tidak ada satu chunk pun yang memuatnya utuh)" + return f"chunk jawaban ada tetapi baru muncul di peringkat {r} (> {k})" + + +def main(): + # sanity check: setiap kata kunci memang ada di dokumen asli + korpus = norm(" ".join(baca_pdf(os.path.join(FOLDER_DOKUMEN, f)) + for f in sorted(os.listdir(FOLDER_DOKUMEN)) if f.endswith(".pdf"))) + for s in SOAL: + assert all(norm(k) in korpus for k in s["kw"]), f"kata kunci tidak ada di dokumen: {s}" + + koleksi, hasil = {}, {} + for u in UKURAN: + print(f"\n=== Koleksi ukuran {u} ===") + koleksi[u] = bangun_indeks(ukuran=u, overlap=OVERLAP, backend="auto") + hasil[u] = {k: hit_at_k(koleksi[u], SOAL, k) for k in KS} + print({f"Hit@{k}": round(v, 3) for k, v in hasil[u].items()}) + backend = koleksi[UKURAN[0]].backend + + ranks = {u: [peringkat_hit(koleksi[u], s) for s in SOAL] for u in UKURAN} + + # ---- kesimpulan otomatis + kunci = lambda u: (hasil[u][5], hasil[u][3], hasil[u][1]) + a, b = UKURAN + terbaik = a if kunci(a) > kunci(b) else b if kunci(b) > kunci(a) else None + fmt = lambda u: "/".join(f"{hasil[u][k]:.2f}" for k in KS) + if terbaik: + lain = b if terbaik == a else a + s1 = (f"Ukuran {terbaik} karakter lebih baik: Hit@1/3/5 = {fmt(terbaik)} dibanding {fmt(lain)} pada " + f"ukuran {lain}.") + else: + s1 = f"Kedua ukuran seimbang: Hit@1/3/5 = {fmt(a)} (ukuran {a}) dan {fmt(b)} (ukuran {b})." + gagal = {u: [i for i, r in enumerate(ranks[u], 1) if r is None or r > 5] for u in UKURAN} + s2 = "Pada k=5, pertanyaan yang gagal: " + "; ".join( + f"ukuran {u} -> " + (", ".join(f"no. {i}" for i in gagal[u]) if gagal[u] else "tidak ada") for u in UKURAN) + "." + penyebab = {} + for u in UKURAN: + for i in gagal[u]: + penyebab[(u, i)] = diagnosis(koleksi[u], SOAL[i - 1], 5) + print("\nDiagnosis gagal (k=5):") + for (u, i), c in penyebab.items(): + print(f" ukuran {u}, no. {i} ({SOAL[i-1]['q']}): {c}") + + # hit@k per jenis soal (langsung vs parafrase) + per_jenis = {} + for jenis in ("langsung", "parafrase"): + sub = [s for s in SOAL if s["jenis"] == jenis] + per_jenis[jenis] = (len(sub), {u: hit_at_k(koleksi[u], sub, 5) for u in UKURAN}) + + # kalimat 3: dugaan penyebab berdasarkan jenis diagnosis + if penyebab: + terbelah = [x for x in penyebab.values() if x.startswith("kata kunci terbelah")] + peringkat = [x for x in penyebab.values() if x.startswith("chunk jawaban ada")] + dugaan = [] + if peringkat: + dugaan.append("kosakata pertanyaan berbeda dari kosakata dokumen (parafrase, mis. istilah Indonesia " + "vs istilah Inggris di teks), dan chunk kecil memuat lebih sedikit kata konteks sehingga " + "skor kemiripannya kalah dari chunk lain yang topiknya lebih umum") + if terbelah: + dugaan.append("kata kunci jawaban terbelah di batas chunk dan overlap belum cukup menyelamatkannya") + s3 = "Dugaan penyebab: " + "; serta ".join(dugaan) + "." + else: + s3 = "Tidak ada kegagalan pada k=5, sehingga tidak ada penyebab yang perlu dianalisis." + s4 = ("Catatan: skor mendekati sempurna karena korpus hanya 3 dokumen kecil (13-25 chunk) dengan topik yang " + "jelas berbeda; pada PDF yang lebih besar dan lebih mirip antar dokumen, Hit@k diperkirakan turun dan " + "perbedaan antar ukuran chunk akan lebih terlihat.") + kesimpulan = " ".join([s1, s2, s3, s4]) + print("\nKesimpulan:", kesimpulan) + + # grafik + fig, ax = plt.subplots(figsize=(6.4, 3.2), dpi=150) + lebar = 0.35 + for j, u in enumerate(UKURAN): + xs = [i + (j - 0.5) * lebar for i in range(len(KS))] + bars = ax.bar(xs, [hasil[u][k] for k in KS], lebar, label=f"ukuran {u}") + ax.bar_label(bars, fmt="%.2f", fontsize=8) + ax.set_xticks(range(len(KS)), [f"Hit@{k}" for k in KS]) + ax.set_ylim(0, 1.12) + ax.set_ylabel("proporsi soal") + ax.set_title(f"Hit@k pada 15 soal (overlap {int(OVERLAP * 100)}%, backend {backend})", fontsize=10) + ax.legend(loc="lower right", fontsize=8) + fig.tight_layout() + gambar = os.path.join(BASE, "_grafik_hit_at_k.png") + fig.savefig(gambar) + plt.close(fig) + + # laporan PDF + W = 28.35 + L = Laporan(os.path.join(BASE, "Lab3_Hit_at_k.pdf"), "Lab 3 - Ukur Hit@k", + f"backend: {backend} | overlap {int(OVERLAP * 100)}% | koleksi ukuran {UKURAN[0]} dan {UKURAN[1]}") + L.h1("Langkah 1. Lima belas pertanyaan uji") + L.p("Soal dibuat dari tiga dokumen di folder dokumen/. Sebuah chunk dianggap benar bila memuat semua kata kunci " + "jawaban. Sebanyak 7 soal bernada langsung dan 8 soal diparafrase (kata-katanya sengaja berbeda dari teks).") + L.tabel([["No", "Jenis", "Pertanyaan", "Kata kunci"]] + + [[i, s["jenis"], s["q"], ", ".join(s["kw"])] for i, s in enumerate(SOAL, 1)], + [1 * W, 2 * W, 10 * W, 4 * W]) + L.h1("Langkah 2. Hasil Hit@k") + L.tabel([["Koleksi"] + [f"Hit@{k}" for k in KS] + ["Jumlah chunk"]] + + [[f"ukuran {u}"] + [f"{hasil[u][k]:.3f}" for k in KS] + [len(koleksi[u].chunks)] for u in UKURAN], + [4 * W, 2.5 * W, 2.5 * W, 2.5 * W, 3 * W]) + L.gambar(gambar, 13) + L.h2("Hit@5 menurut jenis soal") + L.tabel([["Jenis soal", "Jumlah"] + [f"ukuran {u}" for u in UKURAN]] + + [[j, n] + [f"{v[u]:.3f}" for u in UKURAN] for j, (n, v) in per_jenis.items()], + [4 * W, 2.5 * W, 3 * W, 3 * W]) + L.h2("Peringkat chunk pertama yang memuat jawaban, per soal") + L.p("Angka 1 berarti chunk teratas sudah benar. Angka > 5 berarti gagal pada Hit@5.") + L.tabel([["No", "Jenis", f"Peringkat (ukuran {UKURAN[0]})", f"Peringkat (ukuran {UKURAN[1]})"]] + + [[i, s["jenis"], ranks[UKURAN[0]][i - 1], ranks[UKURAN[1]][i - 1]] for i, s in enumerate(SOAL, 1)], + [1.2 * W, 3 * W, 5 * W, 5 * W]) + if penyebab: + L.h2("Diagnosis soal yang gagal (k=5)") + L.tabel([["Ukuran", "No", "Pertanyaan", "Diagnosis"]] + + [[u, i, SOAL[i - 1]["q"], c] for (u, i), c in penyebab.items()], [1.5 * W, 1 * W, 7 * W, 8 * W]) + L.h1("Langkah 3. Kesimpulan") + L.p(kesimpulan) + L.halaman_baru() + L.h1("Lampiran: kode lab3_hit_at_k.py") + L.kode(open(__file__, encoding="utf-8").read()) + L.simpan() + os.remove(gambar) + print("\nLaporan: Lab3_Hit_at_k.pdf") + + +if __name__ == "__main__": + main() diff --git a/laporan_utils.py b/laporan_utils.py new file mode 100644 index 0000000..42b40ad --- /dev/null +++ b/laporan_utils.py @@ -0,0 +1,76 @@ +"""Pembuat laporan PDF sederhana (reportlab) untuk ketiga lab.""" +import textwrap +from xml.sax.saxutils import escape + +from reportlab.lib import colors +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet +from reportlab.lib.units import cm +from reportlab.platypus import (Image, PageBreak, Paragraph, Preformatted, + SimpleDocTemplate, Spacer, Table, TableStyle) + + +class Laporan: + def __init__(self, path, judul, subjudul=""): + st = getSampleStyleSheet() + self.s = { + "judul": ParagraphStyle("j", parent=st["Title"], fontSize=19, spaceAfter=4), + "sub": ParagraphStyle("sj", parent=st["BodyText"], alignment=1, textColor=colors.grey, spaceAfter=14), + "h1": ParagraphStyle("h1", parent=st["Heading1"], fontSize=14, spaceBefore=12, spaceAfter=6), + "h2": ParagraphStyle("h2", parent=st["Heading2"], fontSize=11.5, spaceBefore=9, spaceAfter=4), + "p": ParagraphStyle("p", parent=st["BodyText"], fontSize=10, leading=14, spaceAfter=5), + "sel": ParagraphStyle("sel", parent=st["BodyText"], fontSize=8.5, leading=10.5), + "selb": ParagraphStyle("selb", parent=st["BodyText"], fontSize=8.5, leading=10.5, fontName="Helvetica-Bold"), + "kode": ParagraphStyle("k", fontName="Courier", fontSize=7.2, leading=8.6, + backColor=colors.whitesmoke, borderPadding=4), + "cap": ParagraphStyle("cap", parent=st["BodyText"], fontSize=8.5, textColor=colors.grey, spaceAfter=8), + } + self.doc = SimpleDocTemplate(path, pagesize=A4, leftMargin=2 * cm, rightMargin=2 * cm, + topMargin=2 * cm, bottomMargin=2 * cm, title=judul) + self.c = [Paragraph(escape(judul), self.s["judul"])] + if subjudul: + self.c.append(Paragraph(escape(subjudul), self.s["sub"])) + + def h1(self, t): self.c.append(Paragraph(escape(t), self.s["h1"])) + def h2(self, t): self.c.append(Paragraph(escape(t), self.s["h2"])) + def p(self, t): self.c.append(Paragraph(escape(t), self.s["p"])) + def spasi(self, h=6): self.c.append(Spacer(1, h)) + def halaman_baru(self): self.c.append(PageBreak()) + + def cap(self, t): self.c.append(Paragraph(escape(t), self.s["cap"])) + + def kode(self, teks, lebar=105): + baris = [] + for b in teks.expandtabs(4).split("\n"): + baris += textwrap.wrap(b, lebar, subsequent_indent=" ", drop_whitespace=False) or [""] + self.c.append(Preformatted("\n".join(baris), self.s["kode"])) + + def teks_chunk(self, teks, maks=700): + t = teks if len(teks) <= maks else teks[:maks] + " ..." + self.c.append(Preformatted(t, self.s["kode"], maxLineLength=100, newLineChars="竊オ")) + self.c.append(Spacer(1, 4)) + + def tabel(self, data, lebar=None, header=True): + d = [[Paragraph(escape(str(x)), self.s["selb"] if (header and i == 0) else self.s["sel"]) + for x in baris] for i, baris in enumerate(data)] + tb = Table(d, colWidths=lebar, repeatRows=1 if header else 0, hAlign="LEFT") + gaya = [("GRID", (0, 0), (-1, -1), 0.4, colors.grey), ("VALIGN", (0, 0), (-1, -1), "TOP")] + if header: + gaya.append(("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#e6e6e6"))) + tb.setStyle(TableStyle(gaya)) + self.c.append(tb) + self.c.append(Spacer(1, 6)) + + def gambar(self, path, lebar_cm=15): + from PIL import Image as PI + w, h = PI.open(path).size + self.c.append(Image(path, width=lebar_cm * cm, height=lebar_cm * cm * h / w)) + + def simpan(self): + def kaki(canvas, doc): + canvas.saveState() + canvas.setFont("Helvetica", 8) + canvas.setFillColor(colors.grey) + canvas.drawCentredString(A4[0] / 2, 1.1 * cm, f"Halaman {doc.page}") + canvas.restoreState() + self.doc.build(self.c, onFirstPage=kaki, onLaterPages=kaki) diff --git a/rag_utils.py b/rag_utils.py new file mode 100644 index 0000000..4f3e460 --- /dev/null +++ b/rag_utils.py @@ -0,0 +1,153 @@ +"""Fungsi inti RAG sederhana: baca_pdf, potong, bangun_indeks, query, hit_at_k. + +Backend embedding: + - "neural": sentence-transformers (kalau terpasang & modelnya bisa diunduh) + - "lsa" : TF-IDF n-gram karakter + TruncatedSVD (semantik ringan, offline) + - "tfidf" : TF-IDF n-gram karakter saja (pembanding berbasis kata/leksikal) + - "auto" : coba "neural", kalau gagal pakai "lsa" +""" +import glob +import os +import re + +import numpy as np +import pdfplumber +from sklearn.decomposition import TruncatedSVD +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.preprocessing import normalize + +FOLDER_DOKUMEN = os.path.join(os.path.dirname(os.path.abspath(__file__)), "dokumen") +MODEL_NEURAL = "paraphrase-multilingual-MiniLM-L12-v2" + + +# ---------------------------------------------------------------- baca & potong +def baca_pdf(path): + """Baca seluruh teks sebuah PDF (semua halaman digabung).""" + halaman = [] + with pdfplumber.open(path) as pdf: + for p in pdf.pages: + halaman.append(p.extract_text() or "") + return "\n".join(halaman) + + +def potong_dengan_posisi(teks, ukuran=1000, overlap=0.15): + """Sliding window per karakter. Mengembalikan list (mulai, akhir, teks).""" + if not 0 <= overlap < 1: + raise ValueError("overlap harus 0 <= overlap < 1 (mis. 0.15 = 15%)") + langkah = max(1, int(ukuran * (1 - overlap))) + hasil, mulai = [], 0 + while mulai < len(teks): + akhir = min(mulai + ukuran, len(teks)) + potongan = teks[mulai:akhir].strip() + if potongan: + hasil.append((mulai, akhir, potongan)) + if akhir == len(teks): + break + mulai += langkah + return hasil + + +def potong(teks, ukuran=1000, overlap=0.15): + """Potong teks menjadi chunk berukuran `ukuran` karakter dengan overlap pecahan.""" + return [t for _, _, t in potong_dengan_posisi(teks, ukuran, overlap)] + + +def norm(s): + """Normalisasi untuk pencocokan kata kunci: huruf kecil, spasi/baris baru jadi satu spasi.""" + return re.sub(r"\s+", " ", s).lower().strip() + + +# ---------------------------------------------------------------------- indeks +class Indeks: + def __init__(self, chunks, meta, backend="auto"): + self.chunks, self.meta = chunks, meta + self.backend = self._pilih_backend(backend) + self._fit() + + def _pilih_backend(self, backend): + if backend in ("auto", "neural"): + try: + from sentence_transformers import SentenceTransformer + self._st = SentenceTransformer(MODEL_NEURAL) + return "neural" + except Exception: + if backend == "neural": + raise + return "lsa" + return backend + + def _fit(self): + if self.backend == "neural": + self.Z = np.asarray(self._st.encode(self.chunks, normalize_embeddings=True)) + return + self.vec = TfidfVectorizer(analyzer="char_wb", ngram_range=(3, 5), sublinear_tf=True) + X = self.vec.fit_transform(self.chunks) + if self.backend == "tfidf": + self.Z = normalize(X) + else: # lsa + nc = max(2, min(64, X.shape[0] - 1, X.shape[1] - 1)) + self.svd = TruncatedSVD(n_components=nc, random_state=0) + self.Z = normalize(self.svd.fit_transform(X)) + + def skor(self, pertanyaan): + if self.backend == "neural": + q = np.asarray(self._st.encode([pertanyaan], normalize_embeddings=True)) + return np.asarray(self.Z @ q.T).ravel() + q = self.vec.transform([pertanyaan]) + if self.backend == "tfidf": + q = normalize(q) + return np.asarray((self.Z @ q.T).todense()).ravel() + q = normalize(self.svd.transform(q)) + return (self.Z @ q.T).ravel() + + +def bangun_indeks(folder=FOLDER_DOKUMEN, ukuran=1000, overlap=0.15, backend="auto", verbose=True): + """Baca semua PDF di `folder`, potong, lalu bangun indeks. Mencetak jumlah chunk per file.""" + chunks, meta = [], [] + for path in sorted(glob.glob(os.path.join(folder, "*.pdf"))): + nama = os.path.basename(path) + bagian = potong(baca_pdf(path), ukuran, overlap) + if verbose: + print(f" {nama}: {len(bagian)} chunk") + for i, c in enumerate(bagian): + chunks.append(c) + meta.append({"file": nama, "no": i}) + if not chunks: + raise FileNotFoundError(f"Tidak ada PDF di {folder}") + idx = Indeks(chunks, meta, backend) + if verbose: + print(f" Total {len(chunks)} chunk | ukuran={ukuran} overlap={overlap:.0%} | backend={idx.backend}") + return idx + + +def query(indeks, pertanyaan, k=3): + """Kembalikan k chunk teratas: list dict(skor, file, no, teks).""" + s = indeks.skor(pertanyaan) + urut = np.argsort(-s)[:k] + return [{"skor": float(s[i]), "file": indeks.meta[i]["file"], "no": indeks.meta[i]["no"], + "teks": indeks.chunks[i]} for i in urut] + + +# ------------------------------------------------------------------- evaluasi +def cocok(teks, kata_kunci): + """True bila SEMUA kata kunci muncul dalam teks (tanpa peduli huruf besar/kecil).""" + t = norm(teks) + return all(norm(k) in t for k in kata_kunci) + + +def peringkat_hit(indeks, soal): + """Peringkat (mulai dari 1) chunk pertama yang memuat kata kunci; None bila tidak ada.""" + s = indeks.skor(soal["q"]) + for rank, i in enumerate(np.argsort(-s), start=1): + if cocok(indeks.chunks[i], soal["kw"]): + return rank + return None + + +def hit_at_k(indeks, daftar_soal, k): + """Proporsi soal yang chunk berisi kata kunci jawabannya masuk k teratas.""" + hit = 0 + for soal in daftar_soal: + atas = query(indeks, soal["q"], k) + hit += any(cocok(a["teks"], soal["kw"]) for a in atas) + return hit / len(daftar_soal)