From 2e2df69336add822d1aef351c5d99bacd7bfcf6b Mon Sep 17 00:00:00 2001 From: Seyifunmi Owoeye Date: Sat, 28 Mar 2026 20:44:30 -0400 Subject: [PATCH 1/6] Initial commit: refactored Inferelator codebase --- .DS_Store | Bin 0 -> 10244 bytes Manifest.toml | 1477 +++++++++++++++++ Project.toml | 27 + archive/Packages.txt | 23 + archive/packages01.txt | 148 ++ examples/Workflow.jl | 197 +++ examples/runWorkflow.jl | 117 ++ experimental/.DS_Store | Bin 0 -> 8196 bytes experimental/MTL/MultitaskGRN_equations.pptx | Bin 0 -> 188546 bytes experimental/MTL/MultitaskInferelator.jl | 28 + experimental/MTL/admm 2.jl | 380 +++++ experimental/MTL/admm.jl | 194 +++ experimental/MTL/buildMultitask.jl | 141 ++ experimental/MTL/main 2.jl | 250 +++ experimental/MTL/main.jl | 228 +++ experimental/MTL/multitaskData.jl | 96 ++ experimental/MTL/prepareMultitask.jl | 272 +++ experimental/MTL/taskSimilarity.jl | 138 ++ .../MTL/~$MultitaskGRN_equations.pptx | Bin 0 -> 165 bytes src/.DS_Store | Bin 0 -> 14340 bytes src/00_Data/geneExpression.jl | 250 +++ src/00_Data/priorTFA.jl | 259 +++ src/01_Prior/.DS_Store | Bin 0 -> 6148 bytes src/01_Prior/mergeDegenerateTFs.jl | 434 +++++ src/02_GRN/.DS_Store | Bin 0 -> 6148 bytes src/02_GRN/GRN.jl | 116 ++ src/02_GRN/buildGRN.jl | 410 +++++ src/02_GRN/combineGRN.jl | 294 ++++ src/02_GRN/combineGRN2.jl | 89 + src/02_GRN/prepareGRN.jl | 451 +++++ src/02_GRN/utilsGRN.jl | 54 + src/03_Metrics/.DS_Store | Bin 0 -> 6148 bytes src/03_Metrics/Metric.jl | 73 + src/03_Metrics/calcPRinfTRNs.jl | 658 ++++++++ src/03_Metrics/constants.jl | 9 + src/03_Metrics/plotting/plotBatchMetrics.jl | 928 +++++++++++ src/03_Metrics/plotting/plotSingleUtils.jl | 94 ++ src/03_Metrics/utils.jl | 96 ++ src/Inferelator.jl | 94 ++ src/Utils/dataUtils.jl | 338 ++++ src/Utils/installPackages.jl | 35 + src/Utils/networkIO.jl | 34 + src/Utils/partialCorrelation.jl | 96 ++ 43 files changed, 8528 insertions(+) create mode 100755 .DS_Store create mode 100755 Manifest.toml create mode 100755 Project.toml create mode 100755 archive/Packages.txt create mode 100755 archive/packages01.txt create mode 100755 examples/Workflow.jl create mode 100755 examples/runWorkflow.jl create mode 100644 experimental/.DS_Store create mode 100755 experimental/MTL/MultitaskGRN_equations.pptx create mode 100755 experimental/MTL/MultitaskInferelator.jl create mode 100755 experimental/MTL/admm 2.jl create mode 100755 experimental/MTL/admm.jl create mode 100755 experimental/MTL/buildMultitask.jl create mode 100755 experimental/MTL/main 2.jl create mode 100755 experimental/MTL/main.jl create mode 100755 experimental/MTL/multitaskData.jl create mode 100755 experimental/MTL/prepareMultitask.jl create mode 100755 experimental/MTL/taskSimilarity.jl create mode 100755 experimental/MTL/~$MultitaskGRN_equations.pptx create mode 100755 src/.DS_Store create mode 100755 src/00_Data/geneExpression.jl create mode 100755 src/00_Data/priorTFA.jl create mode 100755 src/01_Prior/.DS_Store create mode 100755 src/01_Prior/mergeDegenerateTFs.jl create mode 100755 src/02_GRN/.DS_Store create mode 100755 src/02_GRN/GRN.jl create mode 100755 src/02_GRN/buildGRN.jl create mode 100755 src/02_GRN/combineGRN.jl create mode 100755 src/02_GRN/combineGRN2.jl create mode 100755 src/02_GRN/prepareGRN.jl create mode 100755 src/02_GRN/utilsGRN.jl create mode 100755 src/03_Metrics/.DS_Store create mode 100755 src/03_Metrics/Metric.jl create mode 100755 src/03_Metrics/calcPRinfTRNs.jl create mode 100755 src/03_Metrics/constants.jl create mode 100755 src/03_Metrics/plotting/plotBatchMetrics.jl create mode 100755 src/03_Metrics/plotting/plotSingleUtils.jl create mode 100755 src/03_Metrics/utils.jl create mode 100755 src/Inferelator.jl create mode 100755 src/Utils/dataUtils.jl create mode 100755 src/Utils/installPackages.jl create mode 100755 src/Utils/networkIO.jl create mode 100755 src/Utils/partialCorrelation.jl diff --git a/.DS_Store b/.DS_Store new file mode 100755 index 0000000000000000000000000000000000000000..49f7ee53e44778925011b721ec4b5baa7ba9a653 GIT binary patch literal 10244 zcmeHMTWl0n7(V~Bv@>*+=?z$}14V9DXek$q0^9APKnpEx=@n>acSkxhJG1W0Zi}_h z7mZ+4;)B;m@P0`M0#S(()PN7ZG!1HaFvd$l0>=1Y;)BNjoHJW@TbuBLQ8Fi)^Pl<8 z|DW@pZ@zzK&sjnUgi^6;LR3PCh!>TLjMYCB+E1@ZMesB;Q3CoCVvsb^%>&h~QG1fL zukaD@5%3Z45%3Z45%@14fZuFd#3C;J+DE`gz(-&l0s4MW@uD*5$q_Eus{<>x1wfgP zYF4m~`vB&ndNSzA5iXf4j>&fq;JJdg#Q^S({VFv_8T8}`m)xBIcPHS@4Bid}*zD-1 zGIIh!F8$g^z(-&*0(9?QNHmfpQPQ8ie~%}!j3-#c?Ql3^+7Yx6um2Ig-gf>{-Q;oN zcI+ms68-eFV8U!$$sooQCkbK_J?2Esh;5rR^9xB%7e8vqIM)?Sw#W|k-IJ&KM^Ag9 z=8Tga>eN=bp*G`WL?$|hn!q}V;Kfl|#Kq9f+{mcu)VA3%F8J>GYaX0aj-4It&DiJ0 z&0@(T4t?a20Vm--$a9f|X*h0G)fEu(@~0IPilSH~?rZJ02U=atO*tVg*+cKz@KHoE zZ7tGn=mWhSTTDbWT&i{!+cwLhbT!>uQF?BsJg1QQmw8U zu2hF=*Hl-k8#b&N9+t%Niq-eE^d2I9)2qyI5Q^H?O54yCs)Abs3 z%p9xEZpTi#^=#Zu1E-?qy-Eq4t|>vsaoY8ygHn>-n=yO-;-z<2t=qUIysfc$OeroY zEt6-;-3Ykl8V?zIvP*NKx)n351A7LHnA_ixHXI{j>TQ-5*CpkwG*((ttOVu+=7~y$ z*qMqLkE9Iu&{%14i6RHoMUtqT*&D-U?X+Wbz!>WZ1k}4kv4`sGlRDO1EeBStLAEaa@kW z)qzmGC@Pot)43Zp$8AdJ)_7E*2Su^l>DQ8a(*UwE(VS!LB)Cg;Zb;5dKi?B`DEwl^0 zd<8-#759#>h&2TI!N}yDllI;#!i6caOWnR>M?>SzUH^yd`ep{pr}CMzW-p>iR=c@= zoQs^j8Ny~K0^@#8#xU+Pa!e#`V9wlm^W}x=9!w5>aH>>Q3TYaMW7K-FyhK%|Vfygf zW%3GDDZtd>x0UKDRS_^_`E9jYBP)5Bj}r4;udb365c$nU`F>f+$IO(*=FOQKkmb4% z=IL35Fw?O5fU3|myM=_mLcS$Gk{jfABzzH4y&RUnJx~LipdK1vCsKSjv>?Iv!G0vU zh9ozUIzZ44#6gk?t?ROGx%tknFF)>+mML1*hSCI0NV41GosE!RK%Z zF2ffv3RmF=xCXyYpmoy(TIc;u>+BEp;p0t5Ti#|rCOpz-f0ifr=6r;w_0dT@zjSNT zGHvSsYBH%sd;Q`g;3ME8;3ME8aBC2tUp6c%J^$Z2_5c6O$gg|^d<1S!1Ry`$8V=#w zruz9w&)QYI`tYKK+Kq6@T(Dxt@hI7GJl@dbc(yG@owpBWenHez{}C>+h2>xWGk~58 R`1^l<|9_LGYq#V6|1UH=MKu5b literal 0 HcmV?d00001 diff --git a/Manifest.toml b/Manifest.toml new file mode 100755 index 0000000..6fd1fda --- /dev/null +++ b/Manifest.toml @@ -0,0 +1,1477 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.7.3" +manifest_format = "2.0" + +[[deps.AbstractFFTs]] +deps = ["ChainRulesCore", "LinearAlgebra", "Test"] +git-tree-sha1 = "d92ad398961a3ed262d8bf04a1a2b8340f915fef" +uuid = "621f4979-c628-5d54-868e-fcf4e3e8185c" +version = "1.5.0" + +[[deps.Adapt]] +deps = ["LinearAlgebra", "Requires"] +git-tree-sha1 = "7e35fca2bdfba44d797c53dfe63a51fabf39bfc0" +uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" +version = "4.4.0" + +[[deps.AliasTables]] +deps = ["PtrArrays", "Random"] +git-tree-sha1 = "9876e1e164b144ca45e9e3198d0b689cadfed9ff" +uuid = "66dad0bd-aa9a-41b7-9441-69ab47430ed8" +version = "1.1.3" + +[[deps.ArgParse]] +deps = ["Logging", "TextWrap"] +git-tree-sha1 = "22cf435ac22956a7b45b0168abbc871176e7eecc" +uuid = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" +version = "1.2.0" + +[[deps.ArgTools]] +uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" + +[[deps.Arpack]] +deps = ["Arpack_jll", "Libdl", "LinearAlgebra", "Logging"] +git-tree-sha1 = "9b9b347613394885fd1c8c7729bfc60528faa436" +uuid = "7d9fca2a-8960-54d3-9f78-7d1dccf2cb97" +version = "0.5.4" + +[[deps.Arpack_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "OpenBLAS_jll", "Pkg"] +git-tree-sha1 = "5ba6c757e8feccf03a1554dfaf3e26b3cfc7fd5e" +uuid = "68821587-b530-5797-8361-c406ea357684" +version = "3.5.1+1" + +[[deps.Arrow]] +deps = ["ArrowTypes", "BitIntegers", "CodecLz4", "CodecZstd", "ConcurrentUtilities", "DataAPI", "Dates", "EnumX", "Mmap", "PooledArrays", "SentinelArrays", "Tables", "TimeZones", "TranscodingStreams", "UUIDs"] +git-tree-sha1 = "a3ae633a80f26751041e8c17c091c9e910d5537d" +uuid = "69666777-d1a9-59fb-9406-91d4454c9d45" +version = "2.7.4" + +[[deps.ArrowTypes]] +deps = ["Sockets", "UUIDs"] +git-tree-sha1 = "404265cd8128a2515a81d5eae16de90fdef05101" +uuid = "31f734f8-188a-4ce0-8406-c8a06bd891cd" +version = "2.3.0" + +[[deps.Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" + +[[deps.AxisAlgorithms]] +deps = ["LinearAlgebra", "Random", "SparseArrays", "WoodburyMatrices"] +git-tree-sha1 = "01b8ccb13d68535d73d2b0c23e39bd23155fb712" +uuid = "13072b0f-2c55-5437-9ae7-d433b7a33950" +version = "1.1.0" + +[[deps.Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[deps.BitFlags]] +git-tree-sha1 = "0691e34b3bb8be9307330f88d1a3c3f25466c24d" +uuid = "d1d4a3ce-64b1-5f1a-9ba4-7e7e69966f35" +version = "0.1.9" + +[[deps.BitIntegers]] +deps = ["Random"] +git-tree-sha1 = "091d591a060e43df1dd35faab3ca284925c48e46" +uuid = "c3b6d118-76ef-56ca-8cc7-ebb389d030a1" +version = "0.3.7" + +[[deps.Bzip2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "1b96ea4a01afe0ea4090c5c8039690672dd13f2e" +uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0" +version = "1.0.9+0" + +[[deps.CSV]] +deps = ["CodecZlib", "Dates", "FilePathsBase", "InlineStrings", "Mmap", "Parsers", "PooledArrays", "PrecompileTools", "SentinelArrays", "Tables", "Unicode", "WeakRefStrings", "WorkerUtilities"] +git-tree-sha1 = "8d8e0b0f350b8e1c91420b5e64e5de774c2f0f4d" +uuid = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +version = "0.10.16" + +[[deps.Cairo_jll]] +deps = ["Artifacts", "Bzip2_jll", "CompilerSupportLibraries_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "JLLWrappers", "LZO_jll", "Libdl", "Pixman_jll", "Xorg_libXext_jll", "Xorg_libXrender_jll", "Zlib_jll", "libpng_jll"] +git-tree-sha1 = "a21c5464519504e41e0cbc91f0188e8ca23d7440" +uuid = "83423d85-b0ee-5818-9007-b63ccbeb887a" +version = "1.18.5+1" + +[[deps.CategoricalArrays]] +deps = ["Compat", "DataAPI", "Future", "Missings", "Printf", "Requires", "Statistics", "Unicode"] +git-tree-sha1 = "73acb4ed51b1855e1b5ce5c610334363a98d13f1" +uuid = "324d7699-5711-5eae-9e2f-1d82baa6b597" +version = "1.0.2" + +[[deps.ChainRulesCore]] +deps = ["Compat", "LinearAlgebra", "SparseArrays"] +git-tree-sha1 = "06ee8d1aa558d2833aa799f6f0b31b30cada405f" +uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +version = "1.25.2" + +[[deps.ChangesOfVariables]] +deps = ["InverseFunctions", "LinearAlgebra", "Test"] +git-tree-sha1 = "3aa4bf1532aa2e14e0374c4fd72bed9a9d0d0f6c" +uuid = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0" +version = "0.1.10" + +[[deps.Clustering]] +deps = ["Distances", "LinearAlgebra", "NearestNeighbors", "Printf", "Random", "SparseArrays", "Statistics", "StatsBase"] +git-tree-sha1 = "3e22db924e2945282e70c33b75d4dde8bfa44c94" +uuid = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5" +version = "0.15.8" + +[[deps.CodecLz4]] +deps = ["Lz4_jll", "TranscodingStreams"] +git-tree-sha1 = "d58afcd2833601636b48ee8cbeb2edcb086522c2" +uuid = "5ba52731-8f18-5e0d-9241-30f10d1ec561" +version = "0.4.6" + +[[deps.CodecZlib]] +deps = ["TranscodingStreams", "Zlib_jll"] +git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9" +uuid = "944b1d66-785c-5afd-91f1-9de20f533193" +version = "0.7.8" + +[[deps.CodecZstd]] +deps = ["TranscodingStreams", "Zstd_jll"] +git-tree-sha1 = "da54a6cd93c54950c15adf1d336cfd7d71f51a56" +uuid = "6b39b394-51ab-5f42-8807-6242bab2b4c2" +version = "0.8.7" + +[[deps.ColorSchemes]] +deps = ["ColorTypes", "ColorVectorSpace", "Colors", "FixedPointNumbers", "PrecompileTools", "Random"] +git-tree-sha1 = "b0fd3f56fa442f81e0a47815c92245acfaaa4e34" +uuid = "35d6a980-a343-548e-a6ea-1d62b119f2f4" +version = "3.31.0" + +[[deps.ColorTypes]] +deps = ["FixedPointNumbers", "Random"] +git-tree-sha1 = "67e11ee83a43eb71ddc950302c53bf33f0690dfe" +uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" +version = "0.12.1" + +[[deps.ColorVectorSpace]] +deps = ["ColorTypes", "FixedPointNumbers", "LinearAlgebra", "Requires", "Statistics", "TensorCore"] +git-tree-sha1 = "8b3b6f87ce8f65a2b4f857528fd8d70086cd72b1" +uuid = "c3611d14-8923-5661-9e6a-0046d554d3a4" +version = "0.11.0" + +[[deps.Colors]] +deps = ["ColorTypes", "FixedPointNumbers", "Reexport"] +git-tree-sha1 = "37ea44092930b1811e666c3bc38065d7d87fcc74" +uuid = "5ae59095-9a9b-59fe-a467-6f913c188581" +version = "0.13.1" + +[[deps.Combinatorics]] +git-tree-sha1 = "c761b00e7755700f9cdf5b02039939d1359330e1" +uuid = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" +version = "1.1.0" + +[[deps.Compat]] +deps = ["Dates", "LinearAlgebra", "TOML", "UUIDs"] +git-tree-sha1 = "9d8a54ce4b17aa5bdce0ea5c34bc5e7c340d16ad" +uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" +version = "4.18.1" + +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" + +[[deps.ConcurrentUtilities]] +deps = ["Serialization", "Sockets"] +git-tree-sha1 = "21d088c496ea22914fe80906eb5bce65755e5ec8" +uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb" +version = "2.5.1" + +[[deps.Conda]] +deps = ["Downloads", "JSON", "VersionParsing"] +git-tree-sha1 = "8f06b0cfa4c514c7b9546756dbae91fcfbc92dc9" +uuid = "8f4d0f93-b110-5947-807f-2305c1781a2d" +version = "1.10.3" + +[[deps.ConstructionBase]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "76219f1ed5771adbb096743bff43fb5fdd4c1157" +uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" +version = "1.5.8" + +[[deps.Contour]] +git-tree-sha1 = "439e35b0b36e2e5881738abc8857bd92ad6ff9a8" +uuid = "d38c429a-6771-53c6-b99e-75d170b6e991" +version = "0.6.3" + +[[deps.Crayons]] +git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15" +uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" +version = "4.1.1" + +[[deps.DataAPI]] +git-tree-sha1 = "abe83f3a2f1b857aac70ef8b269080af17764bbe" +uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" +version = "1.16.0" + +[[deps.DataFrames]] +deps = ["Compat", "DataAPI", "DataStructures", "Future", "InlineStrings", "InvertedIndices", "IteratorInterfaceExtensions", "LinearAlgebra", "Markdown", "Missings", "PooledArrays", "PrecompileTools", "PrettyTables", "Printf", "Random", "Reexport", "SentinelArrays", "SortingAlgorithms", "Statistics", "TableTraits", "Tables", "Unicode"] +git-tree-sha1 = "a37ac0840a1196cd00317b57e39d6586bf0fd6f6" +uuid = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +version = "1.7.1" + +[[deps.DataStructures]] +deps = ["Compat", "InteractiveUtils", "OrderedCollections"] +git-tree-sha1 = "4e1fe97fdaed23e9dc21d4d664bea76b65fc50a0" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.18.22" + +[[deps.DataValueInterfaces]] +git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6" +uuid = "e2d170a0-9d28-54be-80f0-106bbe20a464" +version = "1.0.0" + +[[deps.Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[deps.Dbus_jll]] +deps = ["Artifacts", "Expat_jll", "JLLWrappers", "Libdl"] +git-tree-sha1 = "473e9afc9cf30814eb67ffa5f2db7df82c3ad9fd" +uuid = "ee1fde0b-3d02-5ea6-8484-8dfef6360eab" +version = "1.16.2+0" + +[[deps.DelimitedFiles]] +deps = ["Mmap"] +uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab" + +[[deps.DensityInterface]] +deps = ["InverseFunctions", "Test"] +git-tree-sha1 = "80c3e8639e3353e5d2912fb3a1916b8455e2494b" +uuid = "b429d917-457f-4dbc-8f4c-0cc954292b1d" +version = "0.4.0" + +[[deps.Distances]] +deps = ["LinearAlgebra", "SparseArrays", "Statistics", "StatsAPI"] +git-tree-sha1 = "c7e3a542b999843086e2f29dac96a618c105be1d" +uuid = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7" +version = "0.10.12" + +[[deps.Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" + +[[deps.Distributions]] +deps = ["AliasTables", "ChainRulesCore", "DensityInterface", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns", "Test"] +git-tree-sha1 = "0b4190661e8a4e51a842070e7dd4fae440ddb7f4" +uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" +version = "0.25.118" + +[[deps.DocStringExtensions]] +git-tree-sha1 = "7442a5dfe1ebb773c29cc2962a8980f47221d76c" +uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +version = "0.9.5" + +[[deps.Downloads]] +deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] +uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" + +[[deps.EnumX]] +git-tree-sha1 = "c49898e8438c828577f04b92fc9368c388ac783c" +uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56" +version = "1.0.7" + +[[deps.EpollShim_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "8a4be429317c42cfae6a7fc03c31bad1970c310d" +uuid = "2702e6a9-849d-5ed8-8c21-79e8b8f9ee43" +version = "0.0.20230411+1" + +[[deps.ExceptionUnwrapping]] +deps = ["Test"] +git-tree-sha1 = "d36f682e590a83d63d1c7dbd287573764682d12a" +uuid = "460bff9d-24e4-43bc-9d9f-a8973cb893f4" +version = "0.1.11" + +[[deps.Expat_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "27af30de8b5445644e8ffe3bcb0d72049c089cf1" +uuid = "2e619515-83b5-522b-bb60-26c02a35a201" +version = "2.7.3+0" + +[[deps.ExprTools]] +git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec" +uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04" +version = "0.1.10" + +[[deps.FFMPEG]] +deps = ["FFMPEG_jll"] +git-tree-sha1 = "95ecf07c2eea562b5adbd0696af6db62c0f52560" +uuid = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" +version = "0.4.5" + +[[deps.FFMPEG_jll]] +deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "JLLWrappers", "LAME_jll", "Libdl", "Ogg_jll", "OpenSSL_jll", "Opus_jll", "PCRE2_jll", "Zlib_jll", "libaom_jll", "libass_jll", "libfdk_aac_jll", "libvorbis_jll", "x264_jll", "x265_jll"] +git-tree-sha1 = "8cc47f299902e13f90405ddb5bf87e5d474c0d38" +uuid = "b22a6f82-2f65-5046-a5b2-351ab43fb4e5" +version = "6.1.2+0" + +[[deps.FFTW]] +deps = ["AbstractFFTs", "FFTW_jll", "LinearAlgebra", "MKL_jll", "Preferences", "Reexport"] +git-tree-sha1 = "797762812ed063b9b94f6cc7742bc8883bb5e69e" +uuid = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" +version = "1.9.0" + +[[deps.FFTW_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "6d6219a004b8cf1e0b4dbe27a2860b8e04eba0be" +uuid = "f5851436-0d7a-5f13-b9de-f02708fd171a" +version = "3.3.11+0" + +[[deps.FileIO]] +deps = ["Pkg", "Requires", "UUIDs"] +git-tree-sha1 = "6522cfb3b8fe97bec632252263057996cbd3de20" +uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +version = "1.18.0" + +[[deps.FilePathsBase]] +deps = ["Compat", "Dates", "Mmap", "Test"] +git-tree-sha1 = "3bab2c5aa25e7840a4b065805c0cdfc01f3068d2" +uuid = "48062228-2e41-5def-b9a4-89aafe57970f" +version = "0.9.24" + +[[deps.FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" + +[[deps.FillArrays]] +deps = ["LinearAlgebra", "PDMats", "SparseArrays", "Statistics"] +git-tree-sha1 = "173e4d8f14230a7523ae11b9a3fa9edb3e0efd78" +uuid = "1a297f60-69ca-5386-bcde-b61e274b549b" +version = "1.14.0" + +[[deps.FixedPointNumbers]] +deps = ["Statistics"] +git-tree-sha1 = "05882d6995ae5c12bb5f36dd2ed3f61c98cbb172" +uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" +version = "0.8.5" + +[[deps.Fontconfig_jll]] +deps = ["Artifacts", "Bzip2_jll", "Expat_jll", "FreeType2_jll", "JLLWrappers", "Libdl", "Libuuid_jll", "Zlib_jll"] +git-tree-sha1 = "f85dac9a96a01087df6e3a749840015a0ca3817d" +uuid = "a3f928ae-7b40-5064-980b-68af3947d34b" +version = "2.17.1+0" + +[[deps.Format]] +git-tree-sha1 = "9c68794ef81b08086aeb32eeaf33531668d5f5fc" +uuid = "1fa38f19-a742-5d3f-a2b9-30dd87b9d5f8" +version = "1.3.7" + +[[deps.FreeType2_jll]] +deps = ["Artifacts", "Bzip2_jll", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "2c5512e11c791d1baed2049c5652441b28fc6a31" +uuid = "d7e528f0-a631-5988-bf34-fe36492bcfd7" +version = "2.13.4+0" + +[[deps.FriBidi_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "7a214fdac5ed5f59a22c2d9a885a16da1c74bbc7" +uuid = "559328eb-81f9-559d-9380-de523a88c83c" +version = "1.0.17+0" + +[[deps.Future]] +deps = ["Random"] +uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" + +[[deps.GLFW_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Libglvnd_jll", "Xorg_libXcursor_jll", "Xorg_libXi_jll", "Xorg_libXinerama_jll", "Xorg_libXrandr_jll", "libdecor_jll", "xkbcommon_jll"] +git-tree-sha1 = "b7bfd56fa66616138dfe5237da4dc13bbd83c67f" +uuid = "0656b61e-2033-5cc2-a64a-77c0f6c09b89" +version = "3.4.1+0" + +[[deps.GLMNet]] +deps = ["DataFrames", "Distributed", "Distributions", "Printf", "Random", "SparseArrays", "StatsBase", "glmnet_jll"] +git-tree-sha1 = "b873c384d3490304c18224b1d5554cdebaafb60b" +uuid = "8d5ece8b-de18-5317-b113-243142960cc6" +version = "0.7.4" + +[[deps.GR]] +deps = ["Artifacts", "Base64", "DelimitedFiles", "Downloads", "GR_jll", "HTTP", "JSON", "Libdl", "LinearAlgebra", "Preferences", "Printf", "Qt6Wayland_jll", "Random", "Serialization", "Sockets", "TOML", "Tar", "Test", "p7zip_jll"] +git-tree-sha1 = "1828eb7275491981fa5f1752a5e126e8f26f8741" +pinned = true +uuid = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" +version = "0.73.17" + +[[deps.GR_jll]] +deps = ["Artifacts", "Bzip2_jll", "Cairo_jll", "FFMPEG_jll", "Fontconfig_jll", "FreeType2_jll", "GLFW_jll", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Libtiff_jll", "Pixman_jll", "Qt6Base_jll", "Zlib_jll", "libpng_jll"] +git-tree-sha1 = "27299071cc29e409488ada41ec7643e0ab19091f" +uuid = "d2c73de3-f751-5644-a686-071e5b155ba9" +version = "0.73.17+0" + +[[deps.Gettext_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Libiconv_jll", "Pkg", "XML2_jll"] +git-tree-sha1 = "9b02998aba7bf074d14de89f9d37ca24a1a0b046" +uuid = "78b55507-aeef-58d4-861c-77aaff3498b1" +version = "0.21.0+0" + +[[deps.Ghostscript_jll]] +deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Zlib_jll"] +git-tree-sha1 = "38044a04637976140074d0b0621c1edf0eb531fd" +uuid = "61579ee1-b43e-5ca0-a5da-69d92c66a64b" +version = "9.55.1+0" + +[[deps.Glib_jll]] +deps = ["Artifacts", "Gettext_jll", "JLLWrappers", "Libdl", "Libffi_jll", "Libiconv_jll", "Libmount_jll", "PCRE2_jll", "Zlib_jll"] +git-tree-sha1 = "fee60557e4f19d0fe5cd169211fdda80e494f4e8" +uuid = "7746bdde-850d-59dc-9ae8-88ece973131d" +version = "2.84.0+0" + +[[deps.Graphite2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "8a6dbda1fd736d60cc477d99f2e7a042acfa46e8" +uuid = "3b182d85-2403-5c21-9c21-1e1f0cc25472" +version = "1.3.15+0" + +[[deps.Grisu]] +git-tree-sha1 = "53bb909d1151e57e2484c3d1b53e19552b887fb2" +uuid = "42e2da0e-8278-4e71-bc24-59509adca0fe" +version = "1.0.2" + +[[deps.HTTP]] +deps = ["Base64", "CodecZlib", "ConcurrentUtilities", "Dates", "ExceptionUnwrapping", "Logging", "LoggingExtras", "MbedTLS", "NetworkOptions", "OpenSSL", "PrecompileTools", "Random", "SimpleBufferStream", "Sockets", "URIs", "UUIDs"] +git-tree-sha1 = "5e6fe50ae7f23d171f44e311c2960294aaa0beb5" +uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" +version = "1.10.19" + +[[deps.HarfBuzz_jll]] +deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "Graphite2_jll", "JLLWrappers", "Libdl", "Libffi_jll"] +git-tree-sha1 = "f923f9a774fcf3f5cb761bfa43aeadd689714813" +uuid = "2e76f6c2-a576-52d4-95c1-20adfe4de566" +version = "8.5.1+0" + +[[deps.HypergeometricFunctions]] +deps = ["LinearAlgebra", "OpenLibm_jll", "SpecialFunctions"] +git-tree-sha1 = "68c173f4f449de5b438ee67ed0c9c748dc31a2ec" +uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a" +version = "0.3.28" + +[[deps.InlineStrings]] +deps = ["Parsers"] +git-tree-sha1 = "8f3d257792a522b4601c24a577954b0a8cd7334d" +uuid = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" +version = "1.4.5" + +[[deps.IntelOpenMP_jll]] +deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl"] +git-tree-sha1 = "ec1debd61c300961f98064cfb21287613ad7f303" +uuid = "1d5cc7b8-4909-519e-a0f8-d0f5ad9712d0" +version = "2025.2.0+0" + +[[deps.InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[deps.Interpolations]] +deps = ["Adapt", "AxisAlgorithms", "ChainRulesCore", "LinearAlgebra", "OffsetArrays", "Random", "Ratios", "Requires", "SharedArrays", "SparseArrays", "StaticArrays", "WoodburyMatrices"] +git-tree-sha1 = "88a101217d7cb38a7b481ccd50d21876e1d1b0e0" +uuid = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" +version = "0.15.1" + +[[deps.InverseFunctions]] +deps = ["Dates", "Test"] +git-tree-sha1 = "a779299d77cd080bf77b97535acecd73e1c5e5cb" +uuid = "3587e190-3f89-42d0-90ee-14403ec27112" +version = "0.1.17" + +[[deps.InvertedIndices]] +git-tree-sha1 = "6da3c4316095de0f5ee2ebd875df8721e7e0bdbe" +uuid = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" +version = "1.3.1" + +[[deps.IrrationalConstants]] +git-tree-sha1 = "e2222959fbc6c19554dc15174c81bf7bf3aa691c" +uuid = "92d709cd-6900-40b7-9082-c6be49f344b6" +version = "0.2.4" + +[[deps.IteratorInterfaceExtensions]] +git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856" +uuid = "82899510-4779-5014-852e-03e436cf321d" +version = "1.0.0" + +[[deps.JLD2]] +deps = ["FileIO", "MacroTools", "Mmap", "OrderedCollections", "PrecompileTools", "Requires", "TranscodingStreams"] +git-tree-sha1 = "1059c071429b4753c0c869b75c859c44ba09a526" +uuid = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +version = "0.5.12" + +[[deps.JLFzf]] +deps = ["REPL", "Random", "fzf_jll"] +git-tree-sha1 = "82f7acdc599b65e0f8ccd270ffa1467c21cb647b" +uuid = "1019f520-868f-41f5-a6de-eb00f4b6a39c" +version = "0.1.11" + +[[deps.JLLWrappers]] +deps = ["Artifacts", "Preferences"] +git-tree-sha1 = "0533e564aae234aff59ab625543145446d8b6ec2" +uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +version = "1.7.1" + +[[deps.JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.4" + +[[deps.JpegTurbo_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "b6893345fd6658c8e475d40155789f4860ac3b21" +uuid = "aacddb02-875f-59d6-b918-886e6ef4fbf8" +version = "3.1.4+0" + +[[deps.KernelDensity]] +deps = ["Distributions", "DocStringExtensions", "FFTW", "Interpolations", "StatsBase"] +git-tree-sha1 = "ba51324b894edaf1df3ab16e2cc6bc3280a2f1a7" +uuid = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b" +version = "0.6.10" + +[[deps.LAME_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "059aabebaa7c82ccb853dd4a0ee9d17796f7e1bc" +uuid = "c1c5ebd0-6772-5130-a774-d5fcae4a789d" +version = "3.100.3+0" + +[[deps.LERC_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "aaafe88dccbd957a8d82f7d05be9b69172e0cee3" +uuid = "88015f11-f218-50d7-93a8-a6af411a945d" +version = "4.0.1+0" + +[[deps.LLVMOpenMP_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "eb62a3deb62fc6d8822c0c4bef73e4412419c5d8" +uuid = "1d63c593-3942-5779-bab2-d838dc0a180e" +version = "18.1.8+0" + +[[deps.LZO_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "1c602b1127f4751facb671441ca72715cc95938a" +uuid = "dd4b983a-f0e5-5f8d-a1b7-129d4a5fb1ac" +version = "2.10.3+0" + +[[deps.LaTeXStrings]] +git-tree-sha1 = "dda21b8cbd6a6c40d9d02a73230f9d70fed6918c" +uuid = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" +version = "1.4.0" + +[[deps.Latexify]] +deps = ["Format", "Ghostscript_jll", "InteractiveUtils", "LaTeXStrings", "MacroTools", "Markdown", "OrderedCollections", "Requires"] +git-tree-sha1 = "44f93c47f9cd6c7e431f2f2091fcba8f01cd7e8f" +uuid = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" +version = "0.16.10" + +[[deps.LazyArtifacts]] +deps = ["Artifacts", "Pkg"] +uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3" + +[[deps.LibCURL]] +deps = ["LibCURL_jll", "MozillaCACerts_jll"] +uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" + +[[deps.LibCURL_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] +uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" + +[[deps.LibGit2]] +deps = ["Base64", "NetworkOptions", "Printf", "SHA"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[deps.LibSSH2_jll]] +deps = ["Artifacts", "Libdl", "MbedTLS_jll"] +uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" + +[[deps.Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[deps.Libffi_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "c8da7e6a91781c41a863611c7e966098d783c57a" +uuid = "e9f186c6-92d2-5b65-8a66-fee21dc1b490" +version = "3.4.7+0" + +[[deps.Libglvnd_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll", "Xorg_libXext_jll"] +git-tree-sha1 = "d36c21b9e7c172a44a10484125024495e2625ac0" +uuid = "7e76a0d4-f3c7-5321-8279-8d96eeed0f29" +version = "1.7.1+1" + +[[deps.Libiconv_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809" +uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" +version = "1.18.0+0" + +[[deps.Libmount_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "97bbca976196f2a1eb9607131cb108c69ec3f8a6" +uuid = "4b2f31a3-9ecc-558c-b454-b3730dcb73e9" +version = "2.41.3+0" + +[[deps.Libtiff_jll]] +deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "LERC_jll", "Libdl", "XZ_jll", "Zlib_jll", "Zstd_jll"] +git-tree-sha1 = "f04133fe05eff1667d2054c53d59f9122383fe05" +uuid = "89763e89-9b03-5906-acba-b20f662cd828" +version = "4.7.2+0" + +[[deps.Libuuid_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "d0205286d9eceadc518742860bf23f703779a3d6" +uuid = "38a345b3-de98-5d2b-a5d3-14cd9215e700" +version = "2.41.3+0" + +[[deps.LinearAlgebra]] +deps = ["Libdl", "libblastrampoline_jll"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[deps.LogExpFunctions]] +deps = ["ChainRulesCore", "ChangesOfVariables", "DocStringExtensions", "InverseFunctions", "IrrationalConstants", "LinearAlgebra"] +git-tree-sha1 = "a2d09619db4e765091ee5c6ffe8872849de0feea" +uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" +version = "0.3.28" + +[[deps.Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[deps.LoggingExtras]] +deps = ["Dates", "Logging"] +git-tree-sha1 = "f00544d95982ea270145636c181ceda21c4e2575" +uuid = "e6f89c97-d47a-5376-807f-9c37f3926c36" +version = "1.2.0" + +[[deps.Lz4_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "191686b1ac1ea9c89fc52e996ad15d1d241d1e33" +uuid = "5ced341a-0733-55b8-9ab6-a4889d929147" +version = "1.10.1+0" + +[[deps.MKL_jll]] +deps = ["Artifacts", "IntelOpenMP_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "oneTBB_jll"] +git-tree-sha1 = "282cadc186e7b2ae0eeadbd7a4dffed4196ae2aa" +uuid = "856f044c-d86e-5d09-b602-aeab76dc8ba7" +version = "2025.2.0+0" + +[[deps.MacroTools]] +git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.16" + +[[deps.Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[deps.MbedTLS]] +deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "NetworkOptions", "Random", "Sockets"] +git-tree-sha1 = "8785729fa736197687541f7053f6d8ab7fc44f92" +uuid = "739be429-bea8-5141-9913-cc70e7f3736d" +version = "1.1.10" + +[[deps.MbedTLS_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" + +[[deps.Measures]] +git-tree-sha1 = "b513cedd20d9c914783d8ad83d08120702bf2c77" +uuid = "442fdcdd-2543-5da2-b0f3-8c86c306513e" +version = "0.3.3" + +[[deps.Missings]] +deps = ["DataAPI"] +git-tree-sha1 = "ec4f7fbeab05d7747bdf98eb74d130a2a2ed298d" +uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" +version = "1.2.0" + +[[deps.Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[deps.Mocking]] +deps = ["Compat", "ExprTools"] +git-tree-sha1 = "2c140d60d7cb82badf06d8783800d0bcd1a7daa2" +uuid = "78c3b35d-d492-501b-9361-3d52fe80e533" +version = "0.8.1" + +[[deps.MozillaCACerts_jll]] +uuid = "14a3606d-f60d-562e-9121-12d972cd8159" + +[[deps.MultivariateStats]] +deps = ["Arpack", "Distributions", "LinearAlgebra", "SparseArrays", "Statistics", "StatsAPI", "StatsBase"] +git-tree-sha1 = "7c3ff68a904d0f7404e5d2f7f5bc667934d8d616" +uuid = "6f286f6a-111f-5878-ab1e-185364afe411" +version = "0.10.4" + +[[deps.NaNMath]] +deps = ["OpenLibm_jll"] +git-tree-sha1 = "030ea22804ef91648f29b7ad3fc15fa49d0e6e71" +uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +version = "1.0.3" + +[[deps.NamedArrays]] +deps = ["Combinatorics", "DelimitedFiles", "InvertedIndices", "LinearAlgebra", "OrderedCollections", "Random", "Requires", "SparseArrays", "Statistics"] +git-tree-sha1 = "33d258318d9e049d26c02ca31b4843b2c851c0b0" +uuid = "86f7a689-2022-50b4-a561-43c23ac3c673" +version = "0.10.5" + +[[deps.NearestNeighbors]] +deps = ["Distances", "StaticArrays"] +git-tree-sha1 = "e45bb6034fdef63d0c49b82ba9b889215bf8b344" +uuid = "b8a86587-4115-5ab1-83bc-aa920d37bbce" +version = "0.4.24" + +[[deps.NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" + +[[deps.Observables]] +git-tree-sha1 = "7438a59546cf62428fc9d1bc94729146d37a7225" +uuid = "510215fc-4207-5dde-b226-833fc4488ee2" +version = "0.5.5" + +[[deps.OffsetArrays]] +deps = ["Adapt"] +git-tree-sha1 = "117432e406b5c023f665fa73dc26e79ec3630151" +uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +version = "1.17.0" + +[[deps.Ogg_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "b6aa4566bb7ae78498a5e68943863fa8b5231b59" +uuid = "e7412a2a-1a6e-54c0-be00-318e2571c051" +version = "1.3.6+0" + +[[deps.OpenBLAS_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" + +[[deps.OpenLibm_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "05823500-19ac-5b8b-9628-191a04bc5112" + +[[deps.OpenSSL]] +deps = ["BitFlags", "Dates", "MozillaCACerts_jll", "NetworkOptions", "OpenSSL_jll", "Sockets"] +git-tree-sha1 = "1d1aaa7d449b58415f97d2839c318b70ffb525a0" +uuid = "4d8831e6-92b7-49fb-bdf8-b643e874388c" +version = "1.6.1" + +[[deps.OpenSSL_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "c9cbeda6aceffc52d8a0017e71db27c7a7c0beaf" +uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" +version = "3.5.5+0" + +[[deps.OpenSpecFun_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] +git-tree-sha1 = "1346c9208249809840c91b26703912dff463d335" +uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" +version = "0.5.6+0" + +[[deps.Opus_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "e2bb57a313a74b8104064b7efd01406c0a50d2ff" +uuid = "91d4177d-7536-5919-b921-800302f37372" +version = "1.6.1+0" + +[[deps.OrderedCollections]] +git-tree-sha1 = "05868e21324cede2207c6f0f466b4bfef6d5e7ee" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.8.1" + +[[deps.PCRE2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15" + +[[deps.PDMats]] +deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"] +git-tree-sha1 = "949347156c25054de2db3b166c52ac4728cbad65" +uuid = "90014a1f-27ba-587c-ab20-58faa44d9150" +version = "0.11.31" + +[[deps.Pango_jll]] +deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "FriBidi_jll", "Glib_jll", "HarfBuzz_jll", "JLLWrappers", "Libdl"] +git-tree-sha1 = "0662b083e11420952f2e62e17eddae7fc07d5997" +uuid = "36c8627f-9965-5494-a995-c6b170f724f3" +version = "1.57.0+0" + +[[deps.Parsers]] +deps = ["Dates", "PrecompileTools", "UUIDs"] +git-tree-sha1 = "7d2f8f21da5db6a806faf7b9b292296da42b2810" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.8.3" + +[[deps.Pixman_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LLVMOpenMP_jll", "Libdl"] +git-tree-sha1 = "db76b1ecd5e9715f3d043cec13b2ec93ce015d53" +uuid = "30392449-352a-5448-841d-b1acce4e97dc" +version = "0.44.2+0" + +[[deps.Pkg]] +deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + +[[deps.PlotThemes]] +deps = ["PlotUtils", "Statistics"] +git-tree-sha1 = "41031ef3a1be6f5bbbf3e8073f210556daeae5ca" +uuid = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a" +version = "3.3.0" + +[[deps.PlotUtils]] +deps = ["ColorSchemes", "Colors", "Dates", "PrecompileTools", "Printf", "Random", "Reexport", "StableRNGs", "Statistics"] +git-tree-sha1 = "26ca162858917496748aad52bb5d3be4d26a228a" +uuid = "995b91a9-d308-5afd-9ec6-746e21dbc043" +version = "1.4.4" + +[[deps.Plots]] +deps = ["Base64", "Contour", "Dates", "Downloads", "FFMPEG", "FixedPointNumbers", "GR", "JLFzf", "JSON", "LaTeXStrings", "Latexify", "LinearAlgebra", "Measures", "NaNMath", "Pkg", "PlotThemes", "PlotUtils", "PrecompileTools", "Printf", "REPL", "Random", "RecipesBase", "RecipesPipeline", "Reexport", "RelocatableFolders", "Requires", "Scratch", "Showoff", "SparseArrays", "Statistics", "StatsBase", "TOML", "UUIDs", "UnicodeFun", "UnitfulLatexify", "Unzip"] +git-tree-sha1 = "bfe839e9668f0c58367fb62d8757315c0eac8777" +uuid = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +version = "1.40.20" + +[[deps.PooledArrays]] +deps = ["DataAPI", "Future"] +git-tree-sha1 = "36d8b4b899628fb92c2749eb488d884a926614d3" +uuid = "2dfb63ee-cc39-5dd5-95bd-886bf059d720" +version = "1.4.3" + +[[deps.PrecompileTools]] +deps = ["Preferences"] +git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f" +uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +version = "1.2.1" + +[[deps.Preferences]] +deps = ["TOML"] +git-tree-sha1 = "8b770b60760d4451834fe79dd483e318eee709c4" +uuid = "21216c6a-2e73-6563-6e65-726566657250" +version = "1.5.2" + +[[deps.PrettyTables]] +deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "Reexport", "StringManipulation", "Tables"] +git-tree-sha1 = "66b20dd35966a748321d3b2537c4584cf40387c7" +uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" +version = "2.3.2" + +[[deps.Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[deps.ProgressBars]] +deps = ["Printf"] +git-tree-sha1 = "b437cdb0385ed38312d91d9c00c20f3798b30256" +uuid = "49802e3a-d2f1-5c88-81d8-b72133a6f568" +version = "1.5.1" + +[[deps.PtrArrays]] +git-tree-sha1 = "4fbbafbc6251b883f4d2705356f3641f3652a7fe" +uuid = "43287f4e-b6f4-7ad1-bb20-aadabca52c3d" +version = "1.4.0" + +[[deps.PyCall]] +deps = ["Conda", "Dates", "Libdl", "LinearAlgebra", "MacroTools", "Serialization", "VersionParsing"] +git-tree-sha1 = "9816a3826b0ebf49ab4926e2b18842ad8b5c8f04" +uuid = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" +version = "1.96.4" + +[[deps.PyPlot]] +deps = ["Colors", "LaTeXStrings", "PyCall", "Sockets", "Test", "VersionParsing"] +git-tree-sha1 = "d2c2b8627bbada1ba00af2951946fb8ce6012c05" +uuid = "d330b81b-6aea-500a-939a-2ce795aea3ee" +version = "2.11.6" + +[[deps.Qt6Base_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Fontconfig_jll", "Glib_jll", "JLLWrappers", "Libdl", "Libglvnd_jll", "OpenSSL_jll", "Vulkan_Loader_jll", "Xorg_libSM_jll", "Xorg_libXext_jll", "Xorg_libXrender_jll", "Xorg_libxcb_jll", "Xorg_xcb_util_cursor_jll", "Xorg_xcb_util_image_jll", "Xorg_xcb_util_keysyms_jll", "Xorg_xcb_util_renderutil_jll", "Xorg_xcb_util_wm_jll", "Zlib_jll", "libinput_jll", "xkbcommon_jll"] +git-tree-sha1 = "34f7e5d2861083ec7596af8b8c092531facf2192" +uuid = "c0090381-4147-56d7-9ebc-da0b1113ec56" +version = "6.8.2+2" + +[[deps.Qt6Declarative_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Qt6Base_jll", "Qt6ShaderTools_jll"] +git-tree-sha1 = "da7adf145cce0d44e892626e647f9dcbe9cb3e10" +uuid = "629bc702-f1f5-5709-abd5-49b8460ea067" +version = "6.8.2+1" + +[[deps.Qt6ShaderTools_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Qt6Base_jll"] +git-tree-sha1 = "9eca9fc3fe515d619ce004c83c31ffd3f85c7ccf" +uuid = "ce943373-25bb-56aa-8eca-768745ed7b5a" +version = "6.8.2+1" + +[[deps.Qt6Wayland_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Qt6Base_jll", "Qt6Declarative_jll"] +git-tree-sha1 = "8f528b0851b5b7025032818eb5abbeb8a736f853" +uuid = "e99dba38-086e-5de3-a5b1-6e4c66e897c3" +version = "6.8.2+2" + +[[deps.QuadGK]] +deps = ["DataStructures", "LinearAlgebra"] +git-tree-sha1 = "9da16da70037ba9d701192e27befedefb91ec284" +uuid = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" +version = "2.11.2" + +[[deps.REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[deps.Random]] +deps = ["SHA", "Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[deps.Ratios]] +deps = ["Requires"] +git-tree-sha1 = "1342a47bf3260ee108163042310d26f2be5ec90b" +uuid = "c84ed2f1-dad5-54f0-aa8e-dbefe2724439" +version = "0.4.5" + +[[deps.RecipesBase]] +deps = ["PrecompileTools"] +git-tree-sha1 = "5c3d09cc4f31f5fc6af001c250bf1278733100ff" +uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +version = "1.3.4" + +[[deps.RecipesPipeline]] +deps = ["Dates", "NaNMath", "PlotUtils", "PrecompileTools", "RecipesBase"] +git-tree-sha1 = "45cf9fd0ca5839d06ef333c8201714e888486342" +uuid = "01d81517-befc-4cb6-b9ec-a95719d0359c" +version = "0.6.12" + +[[deps.Reexport]] +git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b" +uuid = "189a3867-3050-52da-a836-e630ba90ab69" +version = "1.2.2" + +[[deps.RelocatableFolders]] +deps = ["SHA", "Scratch"] +git-tree-sha1 = "ffdaf70d81cf6ff22c2b6e733c900c3321cab864" +uuid = "05181044-ff0b-4ac5-8273-598c1e38db00" +version = "1.0.1" + +[[deps.Requires]] +deps = ["UUIDs"] +git-tree-sha1 = "62389eeff14780bfe55195b7204c0d8738436d64" +uuid = "ae029012-a4dd-5104-9daa-d747884805df" +version = "1.3.1" + +[[deps.Rmath]] +deps = ["Random", "Rmath_jll"] +git-tree-sha1 = "52b99504e2c174d9a8592a89647f5187063d1eb1" +uuid = "79098fc4-a85e-5d69-aa6a-4863f24498fa" +version = "0.8.2" + +[[deps.Rmath_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "58cdd8fb2201a6267e1db87ff148dd6c1dbd8ad8" +uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f" +version = "0.5.1+0" + +[[deps.SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[deps.Scratch]] +deps = ["Dates"] +git-tree-sha1 = "3bac05bc7e74a75fd9cba4295cde4045d9fe2386" +uuid = "6c6a2e73-6563-6170-7368-637461726353" +version = "1.2.1" + +[[deps.SentinelArrays]] +deps = ["Dates", "Random"] +git-tree-sha1 = "ebe7e59b37c400f694f52b58c93d26201387da70" +uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c" +version = "1.4.9" + +[[deps.Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[deps.SharedArrays]] +deps = ["Distributed", "Mmap", "Random", "Serialization"] +uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383" + +[[deps.Showoff]] +deps = ["Dates", "Grisu"] +git-tree-sha1 = "91eddf657aca81df9ae6ceb20b959ae5653ad1de" +uuid = "992d4aef-0814-514b-bc4d-f2e9a6c4116f" +version = "1.0.3" + +[[deps.SimpleBufferStream]] +git-tree-sha1 = "f305871d2f381d21527c770d4788c06c097c9bc1" +uuid = "777ac1f9-54b0-4bf8-805c-2214025038e7" +version = "1.2.0" + +[[deps.Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[deps.SortingAlgorithms]] +deps = ["DataStructures"] +git-tree-sha1 = "64d974c2e6fdf07f8155b5b2ca2ffa9069b608d9" +uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c" +version = "1.2.2" + +[[deps.SparseArrays]] +deps = ["LinearAlgebra", "Random"] +uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + +[[deps.SpecialFunctions]] +deps = ["ChainRulesCore", "IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] +git-tree-sha1 = "41852b8679f78c8d8961eeadc8f62cef861a52e3" +uuid = "276daf66-3868-5448-9aa4-cd146d93841b" +version = "2.5.1" + +[[deps.StableRNGs]] +deps = ["Random"] +git-tree-sha1 = "4f96c596b8c8258cc7d3b19797854d368f243ddc" +uuid = "860ef19b-820b-49d6-a774-d7a799459cd3" +version = "1.0.4" + +[[deps.StaticArrays]] +deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore", "Statistics"] +git-tree-sha1 = "0f529006004a8be48f1be25f3451186579392d47" +uuid = "90137ffa-7385-5640-81b9-e52037218182" +version = "1.9.17" + +[[deps.StaticArraysCore]] +git-tree-sha1 = "6ab403037779dae8c514bad259f32a447262455a" +uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" +version = "1.4.4" + +[[deps.Statistics]] +deps = ["LinearAlgebra", "SparseArrays"] +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[[deps.StatsAPI]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "178ed29fd5b2a2cfc3bd31c13375ae925623ff36" +uuid = "82ae8749-77ed-4fe6-ae5f-f523153014b0" +version = "1.8.0" + +[[deps.StatsBase]] +deps = ["AliasTables", "DataAPI", "DataStructures", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"] +git-tree-sha1 = "29321314c920c26684834965ec2ce0dacc9cf8e5" +uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" +version = "0.34.4" + +[[deps.StatsFuns]] +deps = ["ChainRulesCore", "HypergeometricFunctions", "InverseFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"] +git-tree-sha1 = "35b09e80be285516e52c9054792c884b9216ae3c" +uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c" +version = "1.4.0" + +[[deps.StatsPlots]] +deps = ["AbstractFFTs", "Clustering", "DataStructures", "Distributions", "Interpolations", "KernelDensity", "LinearAlgebra", "MultivariateStats", "NaNMath", "Observables", "Plots", "RecipesBase", "RecipesPipeline", "Reexport", "StatsBase", "TableOperations", "Tables", "Widgets"] +git-tree-sha1 = "88cf3587711d9ad0a55722d339a013c4c56c5bbc" +uuid = "f3b207a7-027a-5e70-b257-86293d7955fd" +version = "0.15.8" + +[[deps.StringManipulation]] +deps = ["PrecompileTools"] +git-tree-sha1 = "a04cabe79c5f01f4d723cc6704070ada0b9d46d5" +uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e" +version = "0.3.4" + +[[deps.SuiteSparse]] +deps = ["Libdl", "LinearAlgebra", "Serialization", "SparseArrays"] +uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9" + +[[deps.TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" + +[[deps.TZJData]] +deps = ["Artifacts"] +git-tree-sha1 = "72df96b3a595b7aab1e101eb07d2a435963a97e2" +uuid = "dc5dba14-91b3-4cab-a142-028a31da12f7" +version = "1.5.0+2025b" + +[[deps.TableOperations]] +deps = ["SentinelArrays", "Tables", "Test"] +git-tree-sha1 = "e383c87cf2a1dc41fa30c093b2a19877c83e1bc1" +uuid = "ab02a1b2-a7df-11e8-156e-fb1833f50b87" +version = "1.2.0" + +[[deps.TableTraits]] +deps = ["IteratorInterfaceExtensions"] +git-tree-sha1 = "c06b2f539df1c6efa794486abfb6ed2022561a39" +uuid = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c" +version = "1.0.1" + +[[deps.Tables]] +deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "OrderedCollections", "TableTraits"] +git-tree-sha1 = "f2c1efbc8f3a609aadf318094f8fc5204bdaf344" +uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +version = "1.12.1" + +[[deps.Tar]] +deps = ["ArgTools", "SHA"] +uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" + +[[deps.TensorCore]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "1feb45f88d133a655e001435632f019a9a1bcdb6" +uuid = "62fd8b95-f654-4bbd-a8a5-9c27f68ccd50" +version = "0.1.1" + +[[deps.Test]] +deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[[deps.TextWrap]] +git-tree-sha1 = "43044b737fa70bc12f6105061d3da38f881a3e3c" +uuid = "b718987f-49a8-5099-9789-dcd902bef87d" +version = "1.0.2" + +[[deps.TickTock]] +deps = ["Dates"] +git-tree-sha1 = "385ff4318d1159050cb129f908804ff95b830de0" +uuid = "9ff05d80-102d-5586-aa04-3a8bd1a90d20" +version = "1.3.0" + +[[deps.TimeZones]] +deps = ["Artifacts", "Dates", "Downloads", "InlineStrings", "Mocking", "Printf", "RecipesBase", "Scratch", "TZJData", "Unicode", "p7zip_jll"] +git-tree-sha1 = "d422301b2a1e294e3e4214061e44f338cafe18a2" +uuid = "f269a46b-ccf7-5d73-abea-4c690281aa53" +version = "1.22.2" + +[[deps.TranscodingStreams]] +git-tree-sha1 = "0c45878dcfdcfa8480052b6ab162cdd138781742" +uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" +version = "0.11.3" + +[[deps.URIs]] +git-tree-sha1 = "bef26fb046d031353ef97a82e3fdb6afe7f21b1a" +uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" +version = "1.6.1" + +[[deps.UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[deps.Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[[deps.UnicodeFun]] +deps = ["REPL"] +git-tree-sha1 = "53915e50200959667e78a92a418594b428dffddf" +uuid = "1cfade01-22cf-5700-b092-accc4b62d6e1" +version = "0.4.1" + +[[deps.Unitful]] +deps = ["ConstructionBase", "Dates", "InverseFunctions", "LinearAlgebra", "Random"] +git-tree-sha1 = "6258d453843c466d84c17a58732dda5deeb8d3af" +uuid = "1986cc42-f94f-5a68-af5c-568840ba703d" +version = "1.24.0" + +[[deps.UnitfulLatexify]] +deps = ["LaTeXStrings", "Latexify", "Unitful"] +git-tree-sha1 = "af305cc62419f9bd61b6644d19170a4d258c7967" +uuid = "45397f5d-5981-4c77-b2b3-fc36d6e9b728" +version = "1.7.0" + +[[deps.Unzip]] +git-tree-sha1 = "ca0969166a028236229f63514992fc073799bb78" +uuid = "41fe7b60-77ed-43a1-b4f0-825fd5a5650d" +version = "0.2.0" + +[[deps.VersionParsing]] +git-tree-sha1 = "58d6e80b4ee071f5efd07fda82cb9fbe17200868" +uuid = "81def892-9a0e-5fdd-b105-ffc91e053289" +version = "1.3.0" + +[[deps.Vulkan_Loader_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Wayland_jll", "Xorg_libX11_jll", "Xorg_libXrandr_jll", "xkbcommon_jll"] +git-tree-sha1 = "2f0486047a07670caad3a81a075d2e518acc5c59" +uuid = "a44049a8-05dd-5a78-86c9-5fde0876e88c" +version = "1.3.243+0" + +[[deps.Wayland_jll]] +deps = ["Artifacts", "EpollShim_jll", "Expat_jll", "JLLWrappers", "Libdl", "Libffi_jll"] +git-tree-sha1 = "96478df35bbc2f3e1e791bc7a3d0eeee559e60e9" +uuid = "a2964d1f-97da-50d4-b82a-358c7fce9d89" +version = "1.24.0+0" + +[[deps.WeakRefStrings]] +deps = ["DataAPI", "InlineStrings", "Parsers"] +git-tree-sha1 = "b1be2855ed9ed8eac54e5caff2afcdb442d52c23" +uuid = "ea10d353-3f73-51f8-a26c-33c1cb351aa5" +version = "1.4.2" + +[[deps.Widgets]] +deps = ["Colors", "Dates", "Observables", "OrderedCollections"] +git-tree-sha1 = "e9aeb174f95385de31e70bd15fa066a505ea82b9" +uuid = "cc8bc4a8-27d6-5769-a93b-9d913e69aa62" +version = "0.6.7" + +[[deps.WoodburyMatrices]] +deps = ["LinearAlgebra", "SparseArrays"] +git-tree-sha1 = "c1a7aa6219628fcd757dede0ca95e245c5cd9511" +uuid = "efce3f68-66dc-5838-9240-27a6d6f5f9b6" +version = "1.0.0" + +[[deps.WorkerUtilities]] +git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7" +uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60" +version = "1.6.1" + +[[deps.XML2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Zlib_jll"] +git-tree-sha1 = "80d3930c6347cfce7ccf96bd3bafdf079d9c0390" +uuid = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a" +version = "2.13.9+0" + +[[deps.XZ_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "9cce64c0fdd1960b597ba7ecda2950b5ed957438" +uuid = "ffd25f8a-64ca-5728-b0f7-c24cf3aae800" +version = "5.8.2+0" + +[[deps.Xorg_libICE_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "a3ea76ee3f4facd7a64684f9af25310825ee3668" +uuid = "f67eecfb-183a-506d-b269-f58e52b52d7c" +version = "1.1.2+0" + +[[deps.Xorg_libSM_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libICE_jll"] +git-tree-sha1 = "9c7ad99c629a44f81e7799eb05ec2746abb5d588" +uuid = "c834827a-8449-5923-a945-d239c165b7dd" +version = "1.2.6+0" + +[[deps.Xorg_libX11_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libxcb_jll", "Xorg_xtrans_jll"] +git-tree-sha1 = "808090ede1d41644447dd5cbafced4731c56bd2f" +uuid = "4f6342f7-b3d2-589e-9d20-edeb45f2b2bc" +version = "1.8.13+0" + +[[deps.Xorg_libXau_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "aa1261ebbac3ccc8d16558ae6799524c450ed16b" +uuid = "0c0b7dd1-d40b-584c-a123-a41640f87eec" +version = "1.0.13+0" + +[[deps.Xorg_libXcursor_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libXfixes_jll", "Xorg_libXrender_jll"] +git-tree-sha1 = "6c74ca84bbabc18c4547014765d194ff0b4dc9da" +uuid = "935fb764-8cf2-53bf-bb30-45bb1f8bf724" +version = "1.2.4+0" + +[[deps.Xorg_libXdmcp_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "52858d64353db33a56e13c341d7bf44cd0d7b309" +uuid = "a3789734-cfe1-5b06-b2d0-1dd0d9d62d05" +version = "1.1.6+0" + +[[deps.Xorg_libXext_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] +git-tree-sha1 = "1a4a26870bf1e5d26cd585e38038d399d7e65706" +uuid = "1082639a-0dae-5f34-9b06-72781eeb8cb3" +version = "1.3.8+0" + +[[deps.Xorg_libXfixes_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] +git-tree-sha1 = "75e00946e43621e09d431d9b95818ee751e6b2ef" +uuid = "d091e8ba-531a-589c-9de9-94069b037ed8" +version = "6.0.2+0" + +[[deps.Xorg_libXi_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libXext_jll", "Xorg_libXfixes_jll"] +git-tree-sha1 = "a376af5c7ae60d29825164db40787f15c80c7c54" +uuid = "a51aa0fd-4e3c-5386-b890-e753decda492" +version = "1.8.3+0" + +[[deps.Xorg_libXinerama_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libXext_jll"] +git-tree-sha1 = "0ba01bc7396896a4ace8aab67db31403c71628f4" +uuid = "d1454406-59df-5ea1-beac-c340f2130bc3" +version = "1.1.7+0" + +[[deps.Xorg_libXrandr_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libXext_jll", "Xorg_libXrender_jll"] +git-tree-sha1 = "6c174ef70c96c76f4c3f4d3cfbe09d018bcd1b53" +uuid = "ec84b674-ba8e-5d96-8ba1-2a689ba10484" +version = "1.5.6+0" + +[[deps.Xorg_libXrender_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] +git-tree-sha1 = "7ed9347888fac59a618302ee38216dd0379c480d" +uuid = "ea2f1a96-1ddc-540d-b46f-429655e07cfa" +version = "0.9.12+0" + +[[deps.Xorg_libxcb_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libXau_jll", "Xorg_libXdmcp_jll"] +git-tree-sha1 = "bfcaf7ec088eaba362093393fe11aa141fa15422" +uuid = "c7cfdc94-dc32-55de-ac96-5a1b8d977c5b" +version = "1.17.1+0" + +[[deps.Xorg_libxkbfile_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] +git-tree-sha1 = "ed756a03e95fff88d8f738ebc2849431bdd4fd1a" +uuid = "cc61e674-0454-545c-8b26-ed2c68acab7a" +version = "1.2.0+0" + +[[deps.Xorg_xcb_util_cursor_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_xcb_util_image_jll", "Xorg_xcb_util_jll", "Xorg_xcb_util_renderutil_jll"] +git-tree-sha1 = "9750dc53819eba4e9a20be42349a6d3b86c7cdf8" +uuid = "e920d4aa-a673-5f3a-b3d7-f755a4d47c43" +version = "0.1.6+0" + +[[deps.Xorg_xcb_util_image_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_xcb_util_jll"] +git-tree-sha1 = "f4fc02e384b74418679983a97385644b67e1263b" +uuid = "12413925-8142-5f55-bb0e-6d7ca50bb09b" +version = "0.4.1+0" + +[[deps.Xorg_xcb_util_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libxcb_jll"] +git-tree-sha1 = "68da27247e7d8d8dafd1fcf0c3654ad6506f5f97" +uuid = "2def613f-5ad1-5310-b15b-b15d46f528f5" +version = "0.4.1+0" + +[[deps.Xorg_xcb_util_keysyms_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_xcb_util_jll"] +git-tree-sha1 = "44ec54b0e2acd408b0fb361e1e9244c60c9c3dd4" +uuid = "975044d2-76e6-5fbe-bf08-97ce7c6574c7" +version = "0.4.1+0" + +[[deps.Xorg_xcb_util_renderutil_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_xcb_util_jll"] +git-tree-sha1 = "5b0263b6d080716a02544c55fdff2c8d7f9a16a0" +uuid = "0d47668e-0667-5a69-a72c-f761630bfb7e" +version = "0.3.10+0" + +[[deps.Xorg_xcb_util_wm_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_xcb_util_jll"] +git-tree-sha1 = "f233c83cad1fa0e70b7771e0e21b061a116f2763" +uuid = "c22f9ab0-d5fe-5066-847c-f4bb1cd4e361" +version = "0.4.2+0" + +[[deps.Xorg_xkbcomp_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libxkbfile_jll"] +git-tree-sha1 = "801a858fc9fb90c11ffddee1801bb06a738bda9b" +uuid = "35661453-b289-5fab-8a00-3d9160c6a3a4" +version = "1.4.7+0" + +[[deps.Xorg_xkeyboard_config_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_xkbcomp_jll"] +git-tree-sha1 = "00af7ebdc563c9217ecc67776d1bbf037dbcebf4" +uuid = "33bec58e-1273-512f-9401-5d533626f822" +version = "2.44.0+0" + +[[deps.Xorg_xtrans_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "a63799ff68005991f9d9491b6e95bd3478d783cb" +uuid = "c5fb5394-a638-5e4d-96e5-b29de1b5cf10" +version = "1.6.0+0" + +[[deps.Zlib_jll]] +deps = ["Libdl"] +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" + +[[deps.Zstd_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "446b23e73536f84e8037f5dce465e92275f6a308" +uuid = "3161d3a3-bdf6-5164-811a-617609db77b4" +version = "1.5.7+1" + +[[deps.eudev_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "c3b0e6196d50eab0c5ed34021aaa0bb463489510" +uuid = "35ca27e7-8b34-5b7f-bca9-bdc33f59eb06" +version = "3.2.14+0" + +[[deps.fzf_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "b6a34e0e0960190ac2a4363a1bd003504772d631" +uuid = "214eeab7-80f7-51ab-84ad-2988db7cef09" +version = "0.61.1+0" + +[[deps.glmnet_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "31adae3b983b579a1fbd7cfd43a4bc0d224c2f5a" +uuid = "78c6b45d-5eaf-5d68-bcfb-a5a2cb06c27f" +version = "2.0.13+0" + +[[deps.libaom_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "371cc681c00a3ccc3fbc5c0fb91f58ba9bec1ecf" +uuid = "a4ae2306-e953-59d6-aa16-d00cac43593b" +version = "3.13.1+0" + +[[deps.libass_jll]] +deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "HarfBuzz_jll", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "e17c115d55c5fbb7e52ebedb427a0dca79d4484e" +uuid = "0ac62f75-1d6f-5e53-bd7c-93b484bb37c0" +version = "0.15.2+0" + +[[deps.libblastrampoline_jll]] +deps = ["Artifacts", "Libdl", "OpenBLAS_jll"] +uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" + +[[deps.libdecor_jll]] +deps = ["Artifacts", "Dbus_jll", "JLLWrappers", "Libdl", "Libglvnd_jll", "Pango_jll", "Wayland_jll", "xkbcommon_jll"] +git-tree-sha1 = "9bf7903af251d2050b467f76bdbe57ce541f7f4f" +uuid = "1183f4f0-6f2a-5f1a-908b-139f9cdfea6f" +version = "0.2.2+0" + +[[deps.libevdev_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "56d643b57b188d30cccc25e331d416d3d358e557" +uuid = "2db6ffa8-e38f-5e21-84af-90c45d0032cc" +version = "1.13.4+0" + +[[deps.libfdk_aac_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "646634dd19587a56ee2f1199563ec056c5f228df" +uuid = "f638f0a6-7fb0-5443-88ba-1cc74229b280" +version = "2.0.4+0" + +[[deps.libinput_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "eudev_jll", "libevdev_jll", "mtdev_jll"] +git-tree-sha1 = "91d05d7f4a9f67205bd6cf395e488009fe85b499" +uuid = "36db933b-70db-51c0-b978-0f229ee0e533" +version = "1.28.1+0" + +[[deps.libpng_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "e015f211ebb898c8180887012b938f3851e719ac" +uuid = "b53b4c65-9356-5827-b1ea-8c7a1a84506f" +version = "1.6.55+0" + +[[deps.libvorbis_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Ogg_jll"] +git-tree-sha1 = "11e1772e7f3cc987e9d3de991dd4f6b2602663a5" +uuid = "f27f6e37-5d2b-51aa-960f-b287f2bc3b7a" +version = "1.3.8+0" + +[[deps.mtdev_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "b4d631fd51f2e9cdd93724ae25b2efc198b059b1" +uuid = "009596ad-96f7-51b1-9f1b-5ce2d5e8a71e" +version = "1.1.7+0" + +[[deps.nghttp2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" + +[[deps.oneTBB_jll]] +deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl"] +git-tree-sha1 = "1350188a69a6e46f799d3945beef36435ed7262f" +uuid = "1317d2d5-d96f-522e-a858-c73665f53c3e" +version = "2022.0.0+1" + +[[deps.p7zip_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" + +[[deps.x264_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "14cc7083fc6dff3cc44f2bc435ee96d06ed79aa7" +uuid = "1270edf5-f2f9-52d2-97e9-ab00b5d0237a" +version = "10164.0.1+0" + +[[deps.x265_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "dcc541bb19ed5b0ede95581fb2e41ecf179527d2" +uuid = "dfaa095f-4041-5dcd-9319-2fabd8486b76" +version = "3.6.0+0" + +[[deps.xkbcommon_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libxcb_jll", "Xorg_xkeyboard_config_jll"] +git-tree-sha1 = "a1fc6507a40bf504527d0d4067d718f8e179b2b8" +uuid = "d8fb68d0-12a3-5cfd-a85a-d49703b185fd" +version = "1.13.0+0" diff --git a/Project.toml b/Project.toml new file mode 100755 index 0000000..5b3c1fa --- /dev/null +++ b/Project.toml @@ -0,0 +1,27 @@ +[deps] +ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" +Arrow = "69666777-d1a9-59fb-9406-91d4454c9d45" +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +GLMNet = "8d5ece8b-de18-5317-b113-243142960cc6" +GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" +InlineStrings = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" +Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +Measures = "442fdcdd-2543-5da2-b0f3-8c86c306513e" +NamedArrays = "86f7a689-2022-50b4-a561-43c23ac3c673" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568" +PyPlot = "d330b81b-6aea-500a-939a-2ce795aea3ee" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" +StatsPlots = "f3b207a7-027a-5e70-b257-86293d7955fd" +TickTock = "9ff05d80-102d-5586-aa04-3a8bd1a90d20" + +[compat] +GR = "0.73.17" diff --git a/archive/Packages.txt b/archive/Packages.txt new file mode 100755 index 0000000..0a84b65 --- /dev/null +++ b/archive/Packages.txt @@ -0,0 +1,23 @@ +JLD2 +CSV +Arrow +DataFrames +OrderedCollections +Interpolations +InlineStrings +StatsPlots +PyPlot +Colors +Measures +GLMNet +Random +StatsBase +Distributions +CategoricalArrays +SparseArrays +TickTock +ProgressBars +NamedArrays +ArgParse +FileIO +GR \ No newline at end of file diff --git a/archive/packages01.txt b/archive/packages01.txt new file mode 100755 index 0000000..c3e7336 --- /dev/null +++ b/archive/packages01.txt @@ -0,0 +1,148 @@ +Adapt [79e6a3ab-5dfb-504d-930d-738a2a938a0e] +AliasTables [66dad0bd-aa9a-41b7-9441-69ab47430ed8] +ArgParse [c7e460c6-2fb9-53a9-8c5b-16f535851c63] +ArgTools [0dad84c5-d112-42e6-8d28-ef12dabb789f] +Arrow [69666777-d1a9-59fb-9406-91d4454c9d45] +ArrowTypes [31f734f8-188a-4ce0-8406-c8a06bd891cd] +Artifacts [56f22d72-fd6d-98f1-02f0-08ddc0907c33] +AxisAlgorithms [13072b0f-2c55-5437-9ae7-d433b7a33950] +Base [top-level] +Base64 [2a0f44e3-6c83-55bd-87e4-b1978d98bd5f] +BitIntegers [c3b6d118-76ef-56ca-8cc7-ebb389d030a1] +CRC32c [8bf52ea8-c179-5cab-976a-9e18b702a9bc] +CSV [336ed68f-0bac-5ca0-87d4-7b16caf5d00b] +CategoricalArrays [324d7699-5711-5eae-9e2f-1d82baa6b597] +ChainRulesCore [d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4] +ChangesOfVariables [9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0] +CodeTracking [da1fd8a2-8d9e-5ec2-8556-3022fb5608a2] +CodecLz4 [5ba52731-8f18-5e0d-9241-30f10d1ec561] +CodecZlib [944b1d66-785c-5afd-91f1-9de20f533193] +CodecZstd [6b39b394-51ab-5f42-8807-6242bab2b4c2] +ColorTypes [3da002f7-5984-5a60-b8a6-cbb66c0b333f] +Colors [5ae59095-9a9b-59fe-a467-6f913c188581] +Combinatorics [861a8166-3701-5b0c-9a16-15d98fcdc6aa] +Compat [34da2185-b29b-5c13-b0c7-acf172513d20] +CompilerSupportLibraries_jll [e66e0078-7015-5450-92f7-15fbd957f2ae] +ConcurrentUtilities [f0e56b4a-5159-44fe-b623-3e5288b988bb] +Conda [8f4d0f93-b110-5947-807f-2305c1781a2d] +Core [top-level] +Crayons [a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f] +DataAPI [9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a] +DataFrames [a93c6f00-e57d-5684-b7b6-d8193f3e46c0] +DataStructures [864edb3b-99cc-5e75-8d2d-829cb0a9cfe8] +DataValueInterfaces [e2d170a0-9d28-54be-80f0-106bbe20a464] +Dates [ade2ca70-3891-5945-98fb-dc099432e06a] +DelimitedFiles [8bb1440f-4735-579b-a4ab-409b98df4dab] +DensityInterface [b429d917-457f-4dbc-8f4c-0cc954292b1d] +Distributed [8ba89e20-285c-5b6f-9357-94700520ee1b] +Distributions [31c24e10-a181-5473-b8eb-7969acd0382f] +DocStringExtensions [ffbed154-4ef7-542d-bbb7-c09d3a79fcae] +Downloads [f43a241f-c20a-4ad4-852c-f6b1247861c6] +EnumX [4e289a0a-7415-4d19-859d-a7e5c4648b56] +ExprTools [e2ba6199-217a-4e67-a87a-7c52f15ade04] +FileIO [5789e2e9-d7fb-5bc7-8068-2c6fae9b9549] +FilePathsBase [48062228-2e41-5def-b9a4-89aafe57970f] +FileWatching [7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee] +FillArrays [1a297f60-69ca-5386-bcde-b61e274b549b] +FixedPointNumbers [53c48c17-4a7d-5ca2-90c5-79b7896eea93] +Future [9fa8497b-333b-5362-9e8d-4d0656e87820] +GLMNet [8d5ece8b-de18-5317-b113-243142960cc6] +HypergeometricFunctions [34004b35-14d8-5ef3-9330-4cdb6864b03a] +InlineStrings [842dd82b-1e85-43dc-bf29-5d0ee9dffc48] +InteractiveUtils [b77e0a4c-d291-57a0-90e8-8db25a27a240] +Interpolations [a98d9a8b-a2ab-59e6-89dd-64a1c18fca59] +InverseFunctions [3587e190-3f89-42d0-90ee-14403ec27112] +InvertedIndices [41ab1584-1d38-5bbf-9106-f11c6c58b48f] +IrrationalConstants [92d709cd-6900-40b7-9082-c6be49f344b6] +IteratorInterfaceExtensions [82899510-4779-5014-852e-03e436cf321d] +JLD2 [033835bb-8acc-5ee8-8aae-3f567f8a3819] +JLLWrappers [692b3bcd-3c85-4b1f-b108-f13ce0eb3210] +JSON [682c06a0-de6a-54ab-a142-c8b1cf79cde6] +JuliaInterpreter [aa1ae85d-cabe-5617-a682-6adf51b2e16a] +LaTeXStrings [b964fa9f-0449-5b57-a5c2-d3ea65f4040f] +LazyArtifacts [4af54fe1-eca0-43a8-85a7-787d91b784e3] +LibCURL [b27032c2-a3e7-50c8-80cd-2d36dbcbfd21] +LibCURL_jll [deac9b47-8bc7-5906-a0fe-35ac56dc84c0] +LibGit2 [76f85450-5226-5b5a-8eaa-529ad045b433] +Libdl [8f399da3-3557-5675-b5ff-fb832c97cbdb] +LinearAlgebra [37e2e46d-f89d-539d-b4ee-838fcccc9c8e] +LogExpFunctions [2ab3a3ac-af41-5b50-aa03-7779005ae688] +Logging [56ddb016-857b-54e1-b83d-db4d58db5568] +LoweredCodeUtils [6f1432cf-f94c-5a45-995e-cdbf5db27b0b] +Lz4_jll [5ced341a-0733-55b8-9ab6-a4889d929147] +MacroTools [1914dd2f-81c6-5fcd-8719-6d5c9610ff09] +Main [top-level] +Markdown [d6f4376e-aef5-505a-96c1-9c027394607a] +Measures [442fdcdd-2543-5da2-b0f3-8c86c306513e] +Missings [e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28] +Mmap [a63ad114-7e13-5084-954f-fe012c677804] +Mocking [78c3b35d-d492-501b-9361-3d52fe80e533] +MozillaCACerts_jll [14a3606d-f60d-562e-9121-12d972cd8159] +NamedArrays [86f7a689-2022-50b4-a561-43c23ac3c673] +NetworkOptions [ca575930-c2e3-43a9-ace4-1e988b2c1908] +OffsetArrays [6fe1bfb0-de20-5000-8ca7-80f57d26f881] +OpenLibm_jll [05823500-19ac-5b8b-9628-191a04bc5112] +OpenSSL_jll [458c3c95-2e84-50aa-8efc-19380b2a3a95] +OpenSpecFun_jll [efe28fd5-8261-553b-a9e1-b2916fc3738e] +OrderedCollections [bac558e1-5e72-5ebc-8fee-abe8a469f55d] +PDMats [90014a1f-27ba-587c-ab20-58faa44d9150] +Parsers [69de0a69-1ddd-5017-9359-2bf0b02dc9f0] +Pkg [44cfe95a-1eb2-52ea-b672-e2afdf69b78f] +PooledArrays [2dfb63ee-cc39-5dd5-95bd-886bf059d720] +PrecompileTools [aea7be01-6a6a-4083-8856-8a6e6704d82a] +Preferences [21216c6a-2e73-6563-6e65-726566657250] +PrettyTables [08abe8d2-0d0c-5749-adfa-8a2ac140af0d] +Printf [de0858da-6303-5e67-8744-51eddeeeb8d7] +Profile [9abbd945-dff8-562f-b5e8-e1ebf5ef1b79] +ProgressBars [49802e3a-d2f1-5c88-81d8-b72133a6f568] +PtrArrays [43287f4e-b6f4-7ad1-bb20-aadabca52c3d] +PyCall [438e738f-606a-5dbb-bf0a-cddfbfd45ab0] +PyPlot [d330b81b-6aea-500a-939a-2ce795aea3ee] +QuadGK [1fd47b50-473d-5c70-9696-f719f8f3bcdc] +REPL [3fa0cd96-eef1-5676-8a61-b3b8758bbffb] +Random [9a3f8284-a2c9-5f02-9a11-845980a1fd5c] +Ratios [c84ed2f1-dad5-54f0-aa8e-dbefe2724439] +RecipesBase [3cdcf5f2-1ef4-517c-9805-6587b60abb01] +Reexport [189a3867-3050-52da-a836-e630ba90ab69] +Requires [ae029012-a4dd-5104-9daa-d747884805df] +Revise [295af30f-e4ad-537b-8983-00126c2a3abe] +Rmath [79098fc4-a85e-5d69-aa6a-4863f24498fa] +Rmath_jll [f50d1b31-88e8-58de-be2c-1cc44531875f] +SHA [ea8e919c-243c-51af-8825-aaa63cd721ce] +Scratch [6c6a2e73-6563-6170-7368-637461726353] +SentinelArrays [91c51154-3ec4-41a3-a24f-3f23e20d615c] +Serialization [9e88b42a-f829-5b0c-bbe9-9e923198166b] +SharedArrays [1a1011a3-84de-559e-8e89-a11a2f7dc383] +Sockets [6462fe0b-24de-5631-8697-dd941f90decc] +SortingAlgorithms [a2af1166-a08f-5f64-846c-94a0d3cef48c] +SparseArrays [2f01184e-e22b-5df5-ae63-d93ebab69eaf] +SpecialFunctions [276daf66-3868-5448-9aa4-cd146d93841b] +StaticArrays [90137ffa-7385-5640-81b9-e52037218182] +StaticArraysCore [1e83bf80-4336-4d27-bf5d-d5a4f845583c] +Statistics [10745b16-79ce-11e8-11f9-7d13ad32a3b2] +StatsAPI [82ae8749-77ed-4fe6-ae5f-f523153014b0] +StatsBase [2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91] +StatsFuns [4c63d2b9-4356-54db-8cca-17b64c39e42c] +StringManipulation [892a3eda-7b42-436c-8928-eab12a02cf0e] +SuiteSparse [4607b0f0-06f3-5cda-b6b1-a6196a1729e9] +TOML [fa267f1f-6049-4f14-aa54-33bafae1ed76] +TZJData [dc5dba14-91b3-4cab-a142-028a31da12f7] +TableTraits [3783bdb8-4a98-5b6b-af9a-565f29a5fe9c] +Tables [bd369af6-aec1-5ad0-b16a-f7cc5008161c] +Tar [a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e] +Test [8dfed614-e22c-5e08-85e1-65c5234f0b40] +TextWrap [b718987f-49a8-5099-9789-dcd902bef87d] +TickTock [9ff05d80-102d-5586-aa04-3a8bd1a90d20] +TimeZones [f269a46b-ccf7-5d73-abea-4c690281aa53] +TranscodingStreams [3bb67fe8-82b1-5028-8e26-92a6c54297fa] +UUIDs [cf7118a7-6976-5b1a-9a39-7adc72f591a4] +Unicode [4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5] +VersionParsing [81def892-9a0e-5fdd-b105-ffc91e053289] +WeakRefStrings [ea10d353-3f73-51f8-a26c-33c1cb351aa5] +WoodburyMatrices [efce3f68-66dc-5838-9240-27a6d6f5f9b6] +WorkerUtilities [76eceee3-57b5-4d4a-8e66-0e911cebbf60] +Zlib_jll [83775a58-1f1d-513f-b197-d71354ab007a] +Zstd_jll [3161d3a3-bdf6-5164-811a-617609db77b4] +glmnet_jll [78c6b45d-5eaf-5d68-bcfb-a5a2cb06c27f] +nghttp2_jll [8e850ede-7688-5339-a07c-302acd2aaf8d] +p7zip_jll [3f19e933-33d8-53b3-aaab-bd5110c3b7a0] diff --git a/examples/Workflow.jl b/examples/Workflow.jl new file mode 100755 index 0000000..3658cad --- /dev/null +++ b/examples/Workflow.jl @@ -0,0 +1,197 @@ +# cd("/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Testing") +# include("../julia_fxns/InferelatorFunction1.jl") +# include("../julia_fxns/mergeDegeneratePriorTFs.jl") +# include("../julia_fxns/InferelatorFunction2.jl") +# # include("/data/miraldiNB/Katko/Projects/Julia/Inferelator_Julia/julia_fxns/InferelatorFunction3.jl") +# include("../julia_fxns/InferelatorFunction3.jl") +# include("../julia_fxns/InferelatorFunction4.jl") +# # include("../julia_fxns/InferelatorFunction5.jl") +# include("../julia_fxns/CombineTRN/combineGRN.jl") +# include("../julia_fxns/CombineTRN/combineGRN2.jl") + +# Activate the folder where InferelatorJL/src is located +# cd("/data/miraldiNB/Michael/Scripts/GRN/InferelatorJL/src") +using Pkg +Pkg.activate("/data/miraldiNB/Michael/Scripts/GRN/InferelatorJL") +using Revise +include("src/Inferelator.jl") +Pkg.instantiate() +using .InferelatorJL +using .InferelatorJL: Data +using .InferelatorJL: MergeDegenerate +using .InferelatorJL: PriorTFA + +outputDir = "/data/miraldiNB/Michael/projects/GRN/mCD4T_Wayman/Inferelator/test" +# outputDir = "/data/miraldiNB/Michael/hCD4T_Katko/Inferelator/noMergedTF/metaCells/SEACells" + +tfaOptions = ["", "TFmRNA"] +totSS = 80 +bstarsTotSS = 5 +subsampleFrac = 0.68 +minLambda = 0.01 +maxLambda = 0.5 +totLambdasBstars = 20 +totLambdas = 40 +targetInstability = 0.05 +meanEdgesPerGene = 20 +correlationWeight = 1 +minTargets = 3 +edgeSS = 0 +lambdaBias = [0.5] +instabilityLevel = "Network" # options: Gene or Network +useMeanEdgesPerGeneMode = true +leaveOutSampleList = "" +combineOpt = "max" + +# geneExprFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/pseudobulk/pseudobulk_scrna/CellType/Age/Factor1/min0.25M/counts_Tfh10_AgeCellType_pseudobulk_scrna_vst_batch_NoState.txt" +geneExprFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/pseudobulk/pseudobulk_scrna/CellType/Age/Factor1/min0.25M/counts_Tfh10_AgeCellType_pseudobulk_scrna_vst_batch_downsample_0.25M.txt" # VST downSampled +# geneExprFile = "/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/geneExpression/SingleCell/Tfh10_scRNA_Cells_TMsRemoved_logNorm_Counts.arrow" +# geneExprFile = "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/counts_final.txt" # - spike control not ignored during batch correction +# geneExprFile = "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/counts_VST_combat_final.txt" # spike control ignored during batch correction +# geneExprFile = "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/counts_VST_combat_0.15M_final.txt" +# geneExprFile = "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/scNormCounts.arrow" +# geneExprFile = "/data/miraldiNB/Michael/hCD4T_Katko/SEAcells/metacell_log1p.txt" + +# targFile = "/data/miraldiNB/Katko/Projects/Barski_CD4_Multiome/Outs/Prior/SubsetPriors/all_targs.txt" +targFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/GRN_NoState/inputs/target_genes/gene_targ_Tfh10_SigPct5Log2FC0p58FDR5.txt" + +# regFile = "/data/miraldiNB/Katko/Projects/Barski_CD4_Multiome/Outs/Prior/SubsetPriors/all_TFs.txt" +regFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/GRN_NoState/inputs/pot_regs/TF_Tfh10_SigPct5Log2FC0p58FDR5_final.txt" + +priorFile = "/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/priors/ATAC/ATAC_Tfh10.tsv" +# priorFile = "/data/miraldiNB/Michael/mCD4T_Wayman/CellOracle/baseGRN/Tfh10_base_GRN_dedup_normF.tsv" +# priorFile = "/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/priors/ATAC/FIMO_Cicero_b.tsv" +# priorFile = "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/Priors/MotifScan5kbTSS_b.tsv" +# priorFile = "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/Priors/MaxATAC_5kbTSS_Trac_MicroC_combined.tsv" +# priorFile = "/data/miraldiNB/Michael/hCD4T_Katko/CellOracle/baseGRN/hCD4T_base_GRN_dedup_b.tsv" +# priorFile = "/Volumes/miraldiNB/Michael/hCD4T_Katko/dataBank/Priors/majorCellType/Naive/5kbTSS_FIMOp5_b.tsv" +priorFilePenalties = ["/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/priors/ATAC/ATAC_Tfh10.tsv"] +tfaGeneFile = "" + +subsamplePct = subsampleFrac * 100 +subsampleStr = isinteger(subsamplePct) ? string(Int(subsamplePct)) : replace(string(subsamplePct), "." => "p") +lambdaStr = join(replace.(string.(lambdaBias), "." => "p"), "_") +networkBaseName = lowercase(instabilityLevel)*"Lambda" * lambdaStr * "_" * string(totSS) * "totSS_" * + string(meanEdgesPerGene) * "tfsPerGene_" * "subsamplePCT" * subsampleStr +dirOut = joinpath(outputDir,networkBaseName) +mkpath(dirOut) + +println("=== Workflow Configuration ===") +println("Output Directory:", dirOut) +println("GeneExpression File Used: ", geneExprFile) +println("Processing prior file: ", priorFile) +println("Prior files for penalties: ", priorFilePenalties) +println("lambda Bias used is:", lambdaBias) +println("Subsample Fraction: ", subsampleFrac) +println("useMeanEdgesPerGeneMode:", useMeanEdgesPerGeneMode ) +println("=============================") + +# ------- 1. Import gene expression data, list of regulators, list of target genes +data = GeneExpressionData() +# InferelatorJL.Data.GeneExpressionData() +# To see what fileds are available, use `fieldnames(typeof(data))` +loadExpressionData!(data, geneExprFile); +loadAndFilterTargetGenes!(data, targFile; epsilon=0.01); +loadPotentialRegulators!(data, regFile); +processTFAGenes!(data, tfaGeneFile ; outputDir = dirOut); + +mergedTFsData = mergedTFsResult() +# This is an optional step. +mergeDegenerateTFs(mergedTFsData, priorFile; fileFormat = 2); + +# --------2. Given a prior of TF-gene interactions, estimate transcription factor activities (TFAs) +# ---------- using prior-based TFA and TF mRNA levels +tfaData = PriorTFAData() +processPriorFile!(tfaData, data, priorFile; mergedTFsData, minTargets = minTargets); +calculateTFA!(tfaData, data; edgeSS = edgeSS, outputDir = dirOut); # use this if saving output +# calculateTFA!(tfaData, data; edgeSS = edgeSS); # use this if not saving output + + +for tfaOpt in tfaOptions + instabilitiesDir = tfaOpt == "" ? joinpath(dirOut, "TFA") : joinpath(dirOut, "TFmRNA") + mkpath(instabilitiesDir) + # ------- 3. estimate Instabilities + grnData = GrnData() + preparePredictorMat!(grnData, data, tfaData, tfaOpt); + preparePenaltyMatrix!(data, grnData, priorFilePenalties, lambdaBias, tfaOpt); + constructSubsamples(data, grnData; totSS = bstarsTotSS, subsampleFrac = subsampleFrac); + bstarsWarmStart(data, tfaData, grnData; minLambda = minLambda, maxLambda = maxLambda, totLambdasBstars = totLambdasBstars, targetInstability = targetInstability); + constructSubsamples(data, grnData; totSS = totSS, subsampleFrac = subsampleFrac); + bstartsEstimateInstability(grnData; totLambdas = totLambdas, instabilityLevel = instabilityLevel, outputDir = instabilitiesDir) + + # -------- 4. For a given instability cutoff and model size, rank TF-geneinteractions, and calculate stabilities + grnDir = instabilitiesDir + buildGrn = BuildGrn() + chooseLambda!(grnData, buildGrn; instabilityLevel = instabilityLevel, targetInstability = targetInstability) + rankEdges!(data, tfaData, grnData, buildGrn; useMeanEdgesPerGeneMode = useMeanEdgesPerGeneMode, meanEdgesPerGene = meanEdgesPerGene, correlationWeight = correlationWeight, outputDir = grnDir) + # saveData(data, tfaData, grnData, buildGrn,"/data/miraldiNB/Katko/Projects/Julia/Inferelator_Julia/outputs/NewPipelineComparisonSc", "test_" * tfaOpt * ".jld") + writeNetworkTable!(buildGrn; outputDir = grnDir) +end + +# ------- 5. Combine networks for current prior and lambda. +combinedNetDir = joinpath(dirOut, "Combined") +nets2combine = [ + joinpath(dirOut, "TFA", "edges.tsv"), + joinpath(dirOut, "TFmRNA", "edges.tsv") +] +# combineGRNs(meanEdgesPerGene, useMeanEdgesPerGeneMode, combineOpt, combinedNetDir, nets2combine) +combineGRNs(nets2combine; combineOpt=combineOpt, + meanEdgesPerGene=meanEdgesPerGene, + useMeanEdgesPerGeneMode=useMeanEdgesPerGeneMode, + saveDir=combinedNetDir, + saveName="" + ) +netsCombinedSparse = joinpath(combinedNetDir,"combined_" * combineOpt* "_sp.tsv") + +combineGRNS2(data, mergedTFsData, tfaGeneFile, netsCombinedSparse, edgeSS, minTargets, + geneExprFile,targFile, regFile; + outputDir=combinedNetDir) + + +# # # /Combined/combined_max.tsv" +# # mATACTracMicroTSS +# old =CSV.read("/data/miraldiNB/Michael/hCD4T_Katko/Inferelator/noMergedTF/Bulk/5kbTSS/newPipe/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.tsv", DataFrame; delim = "\t") +# new =CSV.read("/data/miraldiNB/Michael/hCD4T_Katko/Inferelator/noMergedTF/Bulk/5kbTSS/newPipe/spikeIgnored/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.tsv", DataFrame; delim = "\t") +# # # old2 = CSV.read("/data/miraldiNB/Michael/hCD4T_Katko/Inferelator/noMergedTF/5kbTSS/Bulk/oldPrior/geneLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.txt", DataFrame; delim = "\t") +# # # new2 = CSV.read("/data/miraldiNB/Michael/hCD4T_Katko/Inferelator/noMergedTF/5kbTSS/Bulk/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.txt", DataFrame; delim = "\t") + +# newPairs = Set((row.TF, row.Gene) for row in eachrow(new)) +# oldPairs = Set((row.TF, row.Gene) for row in eachrow(old)) +# # # old2Pairs = Set((row.TF, row.Gene) for row in eachrow(old2)) +# # # new2Pairs = Set((row.TF, row.Gene) for row in eachrow(new2)) + +# # # intersectPairs = length(intersect(oldPairs, old2Pairs)) +# intersectPairs = length(intersect(newPairs, oldPairs)) +# # # intersectPairs = length(intersect(newPairs, old2Pairs)) +# # # intersectPairs = length(intersect(new2Pairs, oldPairs)) +# # # intersectPairs = length(intersect(new2Pairs, old2Pairs)) +# # # length(intersect(new2Pairs, newPairs)) + + + + + + +# ct <- read.table("/data/miraldiNB/Michael/hCD4T_Katko/dataBank/GEX/counts_VST_combat_final.txt", header = T) +# # Naive +# Naive <- read.table("/data/miraldiNB/Michael/hCD4T_Katko/dataBank/GEX/majorCellType/Naive.tsv", header = TRUE, sep = "\t", stringsAsFactors = FALSE) +# # TCM +# TCM <- read.table("/data/miraldiNB/Michael/hCD4T_Katko/dataBank/GEX/majorCellType/TCM.tsv",header = TRUE, sep = "\t", stringsAsFactors = FALSE) +# colnames(TCM) <- gsub("\\.", "_", colnames(TCM)) +# # TEM +# TEM <- read.table("/data/miraldiNB/Michael/hCD4T_Katko/dataBank/GEX/majorCellType/TEM.tsv",header = TRUE, sep = "\t", stringsAsFactors = FALSE) +# colnames(TEM) <- gsub("\\.", "_", colnames(TEM)) +# # Treg +# Treg <- read.table("/data/miraldiNB/Michael/hCD4T_Katko/dataBank/GEX/majorCellType/Treg.tsv",header = TRUE, sep = "\t", stringsAsFactors = FALSE) +# colnames(Treg) <- gsub("\\.", "_", colnames(Treg)) + + +# dirOut <- "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/GEX/majorCellType/spikeIgnored" +# dir.create("/data/miraldiNB/Michael/hCD4T_Katko/dataBank/GEX/majorCellType/spikeIgnored", recursive = T, showWarnings =T ) + +# Naive1 <- ct[ , setdiff(colnames(Naive), "X")] +# TCM1 <- ct[ , setdiff(colnames(TCM), "X")] +# TEM1 <- ct[ , setdiff(colnames(TEM), "X")] +# Treg1 <- ct[ , setdiff(colnames(Treg), "X")] + +# write.table(Treg1, file.path(dirOut, "TREG.tsv"), col.names = NA, row.names = T, sep = "\t", quote = F) diff --git a/examples/runWorkflow.jl b/examples/runWorkflow.jl new file mode 100755 index 0000000..9a3217d --- /dev/null +++ b/examples/runWorkflow.jl @@ -0,0 +1,117 @@ +cd("/data/miraldiNB/Michael/Scripts/GRN/InferelatorJL") +using Pkg +Pkg.activate(".") +using Revise +include("src/Inferelator.jl") +using .InferelatorJL + +function runInferelator(; + geneExprFile::String, + targFile::String, + regFile::String, + priorFile::String, + priorFilePenalties::Vector{String}, + tfaGeneFile::String = "", + outputDir::String, + tfaOptions::Vector{String} = ["", "TFmRNA"], + totSS::Int = 80, + bstarsTotSS::Int = 5, + subsampleFrac::Float64 = 0.68, + minLambda::Float64 = 0.01, + maxLambda::Float64 = 0.5, + totLambdasBstars::Int = 20, + totLambdas::Int = 40, + targetInstability::Float64 = 0.05, + meanEdgesPerGene::Int = 20, + correlationWeight::Int = 1, + minTargets::Int = 3, + edgeSS::Int = 0, + lambdaBias::Vector{Float64} = [0.5], + instabilityLevel::String = "Network", + useMeanEdgesPerGeneMode::Bool = true, + combineOpt::String = "max", + zTarget::Bool = true +) + + # --- Build output directory name + subsamplePct = subsampleFrac * 100 + subsampleStr = isinteger(subsamplePct) ? string(Int(subsamplePct)) : replace(string(subsamplePct), "." => "p") + lambdaStr = join(replace.(string.(lambdaBias), "." => "p"), "_") + networkBaseName = lowercase(instabilityLevel) * "Lambda" * lambdaStr * "_" * string(totSS) * "totSS_" * + string(meanEdgesPerGene) * "tfsPerGene_" * "subsamplePCT" * subsampleStr + dirOut = joinpath(outputDir, networkBaseName) + mkpath(dirOut) + + println("=== Workflow Configuration ===") + println("Output Directory: ", dirOut) + println("GeneExpression File Used: ", geneExprFile) + println("Processing prior file: ", priorFile) + println("Prior files for penalties: ", priorFilePenalties) + println("Lambda Bias used is: ", lambdaBias) + println("Subsample Fraction: ", subsampleFrac) + println("useMeanEdgesPerGeneMode: ", useMeanEdgesPerGeneMode) + println("=============================") + + # ------- 1. Load expression data + data = GeneExpressionData() + loadExpressionData!(data, geneExprFile) + loadAndFilterTargetGenes!(data, targFile; epsilon=0.01) + loadPotentialRegulators!(data, regFile) + processTFAGenes!(data, tfaGeneFile; outputDir=dirOut) + + # ------- 2. Merge degenerate TFs (optional) + mergedTFsData = mergedTFsResult() + mergeDegenerateTFs(mergedTFsData, priorFile; fileFormat=2) + + # ------- 3. Estimate TFA + tfaData = PriorTFAData() + processPriorFile!(tfaData, data, priorFile; mergedTFsData, minTargets=minTargets) + calculateTFA!(tfaData, data; edgeSS=edgeSS, zscoreTargExp=zTarget, outputDir=dirOut) + + # ------- 4. Build GRN for each TFA option + for tfaOpt in tfaOptions + instabilitiesDir = tfaOpt == "" ? joinpath(dirOut, "TFA") : joinpath(dirOut, "TFmRNA") + mkpath(instabilitiesDir) + + grnData = GrnData() + preparePredictorMat!(grnData, data, tfaData, tfaOpt) + preparePenaltyMatrix!(data, grnData, priorFilePenalties, lambdaBias, tfaOpt) + constructSubsamples(data, grnData; totSS=bstarsTotSS, subsampleFrac=subsampleFrac) + bstarsWarmStart(data, tfaData, grnData; minLambda=minLambda, maxLambda=maxLambda, + totLambdasBstars=totLambdasBstars, targetInstability=targetInstability, zTarget=zTarget) + constructSubsamples(data, grnData; totSS=totSS, subsampleFrac=subsampleFrac) + bstartsEstimateInstability(grnData; totLambdas=totLambdas, instabilityLevel=instabilityLevel, + zTarget=zTarget, outputDir=instabilitiesDir) + + buildGrn = BuildGrn() + chooseLambda!(grnData, buildGrn; instabilityLevel=instabilityLevel, targetInstability=targetInstability) + rankEdges!(data, tfaData, grnData, buildGrn; useMeanEdgesPerGeneMode=useMeanEdgesPerGeneMode, + meanEdgesPerGene=meanEdgesPerGene, correlationWeight=correlationWeight, outputDir=instabilitiesDir) + writeNetworkTable!(buildGrn; outputDir=instabilitiesDir) + end + + # ------- 5. Combine networks + combinedNetDir = joinpath(dirOut, "Combined") + nets2combine = [ + joinpath(dirOut, "TFA", "edges.tsv"), + joinpath(dirOut, "TFmRNA", "edges.tsv") + ] + combineGRNs(nets2combine; combineOpt=combineOpt, meanEdgesPerGene=meanEdgesPerGene, + useMeanEdgesPerGeneMode=useMeanEdgesPerGeneMode, saveDir=combinedNetDir, saveName="") + + # ------- 6. Re-estimate TFA for combined network + netsCombinedSparse = joinpath(combinedNetDir, "combined_" * combineOpt * "_sp.tsv") + combineGRNS2(data, mergedTFsData, tfaGeneFile, netsCombinedSparse, edgeSS, minTargets, + geneExprFile, targFile, regFile; outputDir=combinedNetDir) + +end + +# --- Run +runInferelator( + geneExprFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/pseudobulk/pseudobulk_scrna/CellType/Age/Factor1/min0.25M/counts_Tfh10_AgeCellType_pseudobulk_scrna_vst_batch_NoState.txt", + targFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/GRN_NoState/inputs/target_genes/gene_targ_Tfh10_SigPct5Log2FC0p58FDR5.txt", + regFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/GRN_NoState/inputs/pot_regs/TF_Tfh10_SigPct5Log2FC0p58FDR5_final.txt", + priorFile = "/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/priors/ATAC/ATAC_Tfh10.tsv", + priorFilePenalties = ["/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/priors/ATAC/ATAC_Tfh10.tsv"], + outputDir = "/data/miraldiNB/Michael/projects/GRN/mCD4T_Wayman/Inferelator/test" +) \ No newline at end of file diff --git a/experimental/.DS_Store b/experimental/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..99c7495f54f14eb8e8b53f4f7de74574ef18307a GIT binary patch literal 8196 zcmeHMYit`u5Z<|@=^f#k&7+Vjbz`t-BS_RZ>VrPxyhKA2rPNO1qy@+JU6WJKcd74e zr;Q>)#Y2LEKp@~BkN)K)q#{a@DCGx;A`n%m1eHL7A4t4@(Z3b5x9gIoq`wL(;#Rty zotd58o4Idxy=y`UbQSa{A&rC(86QSzHCBI9n4UdX6-h`Gq=5Fsoyu5F2Dy`&sjGSi zf*=Ax1cC?z5eOm>MBqk<0PfkW$V=?|TpQFu1cC_sFC)O#hcG^j#sr^Oed z?@l1bX;23d2qJKG1lZZVf((!Z>CcoFp5HCg_1fCbgH%zutZKO|%hmD&@kyr?_tIX$ z?MmnOF)raaR;JueXNFCql%fsYj_svQ+sOL{rfy)D{UfHWJEbnCVC$}L8|MmHQIr(b zW@g%>8(PAfHqEw#XQI(9?JePGTXgg6tRk_KFO+>(ZmPanU7-z+BuB^8WYWw! zcHBH-q-c$cA?QPPPu?h^>mymmD&%Z8MQgKZD_gMAp3!MptTW1L+-%;ltUZovdZuHK zrf3~A8k}OBJ8a}#q)E@1_R6DQ(LXunI(e_#pCLtQ!8P_8IDuI-dT<(dEIpkNrjb}1 zbD9w4&6>v09c%Ay+qku(XIKB8d0Jan->_0u$1wD^XC5$({7}i%y~)wEn>B3Rv?oT2 zrfX&_BVng=MlBpwl==GFI;ySGR?9RZCkq+#P{H&L`(v$X($xJrPCwx2Y&4yokQHAO zY0|WPOw*k=u-0YS7q4s9)P#%iplQ*urBziE2N4_8#i`f}M%z_w$Vz7ni|PtD+q7BL zMv6Q_bhi+0+pcO!{G2&-#jS-FA&zxw+JkK7{L=kFo(fb=}bYSG<2Ov!JG3G%+3Uv+=B;=P*wqJobb% zlpBIVB-YHEk<*!RzvTy6!L6@}G?T5Qiwu)VGD{vKFOlQqUGfq6jGQIkk{`+MppXfJrbR4<$GZM_>jXfhXV?JOxj~ zi|{hM3a`O&cpXl_dvFrohmYYL%){4k9=?a4;TQN7E=tQKSyH4rsa|T3)=2B5tAn^INPk=C1c9aD$cfs*2|crRPtH1J+wi=RHPN0Z4KR{$P_rUP2Hhj zQqoG!#?&r_tp}9&e5f~cuZ)RB{{h5*M}8u|k&6I`_82{S9~v-iCMJ6np?5!f7}I zXW?`B0=|T=Fas_y+`}cQf&=(6t_K(4n*HJwxXw8}J>tq+{$bhShul-2bS~8D*U(N) zF5Ha9*(74rVmxnKjy-{ra&9s0buYrvMt1)1UHbn2y3ZR77(^h5z>O0DsO*XNbm99f zwc4<=b{L;=d{|-q<~a3Du)5+R0Qt**7-Bn&U71Zx&^b;~g5_Vo2)K?vuY>(R*#EDw L!{?kEc>n(skw>V? literal 0 HcmV?d00001 diff --git a/experimental/MTL/MultitaskGRN_equations.pptx b/experimental/MTL/MultitaskGRN_equations.pptx new file mode 100755 index 0000000000000000000000000000000000000000..8f775a5b38dbe8518a4544c8c9c7d72614a81e3d GIT binary patch literal 188546 zcmeF2V|Q)M*XCo}Jh5%twtZsTwr$(ClM~yvZR;fI-2bOvboX=jC+OZU_Fj9eHEOJy zW6b%xYSvZqQotZ60AK(R000040K#Y)Ku>@G0Obe(0LTCkKw3h!HcrMiPI}7jcE*l6 zv~Jc`_yr(9_7);Z#U8hG0xgmSv6Yi-FGjDai5mN(IeJ(EN;!q*vj9?Xvo(%Vj=6@A*OG~tP-RGh_b=#9=%#JVk3 zmvmc;9AJ9j4I-`r#+Is~ZLQ&s_cvX? z(y@T20$$3-v4h*&_EdUR^$s{nw;u#(gsxl=kO&=}pnriHc0T`HWUPGNX|p)F{Np?^ z3b{wI0|r9*#iE3qCHM%Dm2Q8shyEA@&Uh>GyrXafoGz1c>?KeHO}+c4*uX0!0KoS* zFo693RxKVZ=YsEkQb6IS7(oA23q1#8D@Quof4={JmH29EvZk*YR2pPH zR>Ft`_SAi~cP$8Xy!^MHN&oi@xe|~6TKTy%{lirJpI z9kJM%F+CIFb|eFG$omfzVATpL;0Vxawfpt^GAg{uC(SG0<^9|?wbr6YE05~O=<-i% z6IQ*`o}X#|_n<_%E8~oT008LV005x=??L%bI2is3$7TAK%|1Jd-;LV3Yn{W+aVRly znbV?RxoU<>Mg9W-DFZRq$k=*FihF(g4%vnw*WVOI`EiT6q{zX$nY+6;FIm~iq0*Cp z0?0i=I*kp78C+<@lqB`xVW_9w3Mi(Y6`^V(h0urwld9t5(rVIxqfS^ z#0HDC7I8*JeS2NiAc2j#L(r7lQ8AbCJ7jU1vMUZ*Gjw)SRi2z1#vUBFhajg3BW(oI zXargEl)U!Qd&$Wnl}_|Rt^)DKE;{> znIB`oV(w6GRhm>JT5OA3k3^ZL0o|-#%vAf!4{_+{a3#Vsd8NT6=N6VrIV6(O#tmgM z4RqM=q-#?l+s{@&p5}_pl!coUM{bQrk4)N#MZu@f(NdbD8{P~yr{MLbPB7ZNi0qXa z`>I31T_nc-%K6sze+ zZ;7p!TuPW(G3Hbe0<&9L4&)VlK1xFRls5;ie*|>=iz+T(rJY^ZX#1&YR7^5WY`(W4 zz-oezm%O(X$o^h)AqG}*F(^Z)DTE~|aLE4D<48mo0cSieX>4prZqnp6m2nzw2tl`h z!*Dz(s68)R-lhT3NK8c#BT;xI6&Wb3tpBC)GOYpCHv&^c1kt5T#g4Hf6j#l{yv&fw zA}#(GpzvcH3K$o4K1-_mqxGgX=gWGt5CH-+VIcy>9tfW+?HD*mt8Cmj;mIz3p*#a= zGRJ^6Y@b^tT)BGlenC>=E>}3@j8nKA0A*EyZkn56AhL6_sdjjIg zapui8yxB;QD29OEm>CQ-`5Vd>@|*Jg%_$u#F-`;gJsiLbwEmYQz=D)_EWoneriuRY zwHwQ3U+YMyrFmV!nUm=4SV^NE=d7nT$M~p(T2s6{1 zM{NZNdr>pDe?w)pUT%>L&xE>()u&dlz}qDjVu?zhk@?I#j%j+5I@%RLnx8y-d{)oN zf)rn&@WG31J*cZ0tt<$PGfmN*Uv?OJq1F06!2NSa_;$}ZYOrfe-QrJ%3}Aa`@!~sI z%m>vjHV0gfZAdvduBTM4`vf+LM&O%;sICQ0nV0pVY7Ri+`5s>1_uLLWzmm}_;6Dr# zYRNb_?&m1oaT~X=Lx$-RbmUl9&cR{xK;?RB zK=)3DF3Vo;{si=e^J6QWYidpPlq$VW6qT#5R^B<==7p4#N4chPT#|0@s;cSj;`Gup zI+YiS*|ZAnZ5_OFOMObt2Db8*Ue#tm$#zCf!(B$N^(Kqc_m5@>3tuYhNP1|t6df-3 ztHJ8+iecWEouxYe)SGuK@S|E^3^>o3TK%-nk}ZSVBca47k>+nAaP2Uf;RtMbSM zz=JQ)f2)1Bl~N4eU;qGs)c;=l82?lIG^HFjSP^<$OTGaI9F6a2Bn`O?ClrR6+@p`A zV{yZRgqiV20pg5~7F|3}yLnzlj#8Zf<(rJTBJdn8NgIZdAoNosKV8r}@m+JXQDLUS z;+hUUV_FY5!)}`<$#X9`Ze+o$2L%)jatu@*8cwLgoN}(dUk7ZK5~+pwr;OomINB9e zYPV9%Wud5D$0O`8uu}{EK+z5rWkYg<*MM4n)L^$lOSda6L}t#W9x%v^DWuCn_a_?@ zLJU9=r*c#uCTKY$+a%*7tzw*`#y=YvpCP0S+XG(UGG-tiW?s(?`t+iHbheR6VPMG* zPX?z&y-Vj|ns`qC!3_!=G{3oNp_3sxV31gnDt}x}NH#Z{Zs0@>Iz`LvMp&wwFT|g^ zSkv-11G{B!R?cX}EGtcy;$@0{Cb=JL2=_EKR5;a>C*j<11h@$PvcaC4Mv=5O_1Mll zwyjCeJf5jd-_qUA^WH1*P*H8GoVZJ!z&vgv9HnPCSfM@3+L=OMc(^v20+^cpS$S^i$r{%yr zzVL@h;m@%c@_gx^y9x5Q?Smsq92(cD)ZoGC||0u5Ou z;QpF zz!TsZ&JrUf#l(d?&x@#F#@h{G+{FvZyT*Z* zh#EzM_u^_uIr(tDjB72?7No9nSoFb@B9RnE>9+!Zt634r_4xqr&R`VL5@MM`J57rX z_sfnl6Eq%M!T18*V%6*}WD2TOn4Pu_4*8W+dplQ#HJz)^I#PtYDFnqx6P?T*K%q>0}`$#1WV;t0{E*D zvWTAutVX9Iw|pGdFWR?y++3XYte*+YNd+^S{rEhfC>FPstFi>kdR*C?&*^ib1xiQ2 zu(Ai8RN_^?rGxZv%Z-WGAvPvQFLTJ+`VP>FL1T!0NvvPrT@z1{Ld#YML$q$PFkL(~3(7EI^D(9hczmsfZ43U!xSYMkEOVPa&wyQ6MA?M~a!ETr zKl;Lh_bh>aKk{Pj_aoBXPUH1i9QX}~8qeM?0>}evp4RG*y#kK zWSzf+NMrR$F_SqZQ1P&8;@ge9h%4^KfT~1-SG-bYERO-o)Dol4F_!xYcvtv2WyB+< zpYtqplYSTDe0C_^NhI1VTGg5SZIIrPQdh&Hb`XSn^q=LPvEdKHMo1g!67&DVt+JDcohG<~)}Kv$rb`=CNDgzb4r`J676HL5 zXO9iSiH?}(^_LtY?Ib00K>x_iVj{ep zljZffq~JoX5tPT08v5FXMwN`qip3Zu5n#+S1rgd{K#5efTZ+L#=ud{vVgNg1za9;u zq)W9PDYITWKma?oz&wfEpN#qswmfJJGsfDRtdPyq@sS48mZfw!{o{kZF|bA;qgrRo zgyk1De??uM1haGWhcX5i{4$>Wi%{6dfovAFW?y~s_*YlnG7E=AwIY5mF6ca1xxo%roVPk%K!)%3?st>p1P*7Gd*gP6GC*W3`X<$t`B?5Y3UkTurzgrB4y~Y2a z>l6t3FV=sd3*7cScyv-cONz071$ZqB*u5Op7R=5Pr#oinkvh$%@}L?3tEr>fq!XZN z>FMc7)$d-pa@8VygI2dxk{J=>cjRA+k$}ZG`Acr&;aht61C#*uvY@N$5h(C<^1l7x zayfU9dNDn$vo|S-YLS&H*~<|qcJVKmbB1GX`g;5VLCnl=VRlAeHBHhWzL-U7A|V_g zuamSn|2&o?1=L}lEP1f|Ox`lCVL_w?<4EuxjYi;=hlo`I+f4_!`nquivQ!2EbV6ME zggi(U2F?-1aOD++mSZezx^nycKU(YEy*Pg+AzTe!1+c%=sCQg9WTjFqCtSG%7V259 z2~(=;F_0&^QhZJ8tCEPL9UMf2`C>@T{y5xWT~MF;QNnn5Bu)LH$ph)`D+WYsM4U7M zgXv;cvF#It;fGeEcv)zwdtsRPZFSQz%pWnmaenMEhx<^uxbOkjS8c3hwOvfnA=`QY z>RCTX$5Gjm+ECGpT0oktDcsqE%^a!EE!Kqj8iR+@SG;E9+|$%l@%b5I?i_8-JKJ== zJV%mpLkxr;hXg`}L_m!QXoitVAR&N&#wOmVTD?)SHl0A!*U=&1rT6>ot5pyC{UWwkA%zL->ko$FFq1N8AE6)=X&#a^ zQoI?6n(*3>E-TkH4r&DpERqr>0=>mU)-A7gH^Phxv=u!Mm7K`bpe-QEfs$tk`bCLY zx5cf@v3d=XMi-BOe7R35^G4f0J$3I`H_5ZUL2??i;QRzNW7;3+`BlDB<96F4uH?Fg z#^q`e=}Fwbk~w!Q{Mu6Dwv*N|jRZ)~Mb$dOeCiRSr?97^xW_X5#{W>QCRTeECVC3v z9s5pBXpmu4Uc}jVo&DVr>CI>Oy_LP@JLFf{e!SF4g+vJejioPvaRjmnV3IAwxdbh! z0AfQ=YNaYBk9wRZKd6+15I0^9^4l&kJ1yev8dyoRZkJq^jC^dMZ4i$+y-q#W7&n+= zWw+!nzX@@X5LQ7x0N=_5E~2X2_h!Ei2l1|rt7KkN;~uC??-ie&%N~2FzzRZ5gwCZQ zX$Sk6y!yfPG^e(CwC5rE3ikZ*-~Ebq?N9joN^wWSoT;^N}X2#d=wizc(vJf`>t$3 zQWK*dBbrcd3Sg-IX^%1|EnRHrl%D{J=O3=B zsdED0g7&Oqi9ueqjhzd_UvYW~eLG2KUmdVc@0U+yH2F1H=Q|35W!Pfte4*2dt==5i zy}rrJh!H1`D^)yFQY0;;l(NPp5N30CXBq#*$OE;R6*U7=fcVD{Roi^sBrnb6*V`(y zh2uo%0;!E2jBvqsAo<)-52(7{`Amkvm0&*o4NR@az^hZ=CcRIyzKivH!1|=rQ;oxM zjqOOfO)2_86u!`46y4@LYImn?iE*b*w=Vq|xUPJyL&QdU+&PN`auN1N3V<8+R_q9< zq?%Gnpl+>eKuRyf&r%Si&N1WmK6HMT8^-|iQCA)r;Qf6lxEmJEF1 zDPtNZ&wu3}%LlM*#lhK=gx|eFBugs%gnFC!ADntc{xu8&t^dI({ZgI=!~?x-UwE+O z;iWD*`>Ze|+K3775#sBwE#^frvRzH+Wry@bJjY%9H?pviGRK|QfG{Xyr!O>Dqc1cI z(zYL*8jFIlgS^5q6nYEL{DV`SADlj7(}w+n(*cJV@SdFp+zorE9fF%~2dMhd;6gar zQIPCsKENg?=~-Xj&uCp><~vzeVu_&bn6v_@iJdw7(iN^HDwQ6e@Z%YXR+J0i>b~j@ zmc|?BG@)JgYV9rCI`EArZ0DCd?#=km`Ljby4YY%h`)na)O9FoDXYGb`u2 za3_W`V#Hx^Zgo`bo|Q%2xw7B0P2P#oGi3!QE73I;DpqYWt>#^it%;&$S8b+P_Kh4~ z<>rHksu<4Y9bzGI6V^*G`6|wxQ_W`{yBsv8=`}n1c9=(p(-uA}703BLP782jv=i0` z@tPNc6fbNKefDT1oqKKqx6vxBE?b!_)kmDhu34$k1GQtAPm=_hVF*O(71=n@;f#cF zHO4MC2SpVL)Jwk5tDJzIg`i?WlW~#?l&5hHX1#mBzhz03>fuJO&+3n57cZA$7O`4R z-uu*&=3KX84l)nQtwq040G+P2?V1DoLJr>7VOk|Vu@c`iY4)X@CrkrMexCs652(kJ z^8pE;KJ?-pC6>NDZ%X8KuPf*e0pu8$e9M1#*oz&h;U;&oX0^0`u{wA4iRI6HgY-mO? zX44u>JrAB*VUl+(>NPfts0S46&FOl--B?&La&)D8wn(Kz{q#*?Srgj2DEUlM>h`Wz z>-0$`!wC#jy4OWLi2x>pKbAbssv7YR)G{Jw#7zH>`<36g||uv{k?#H&YKn z!P12mIcmm&sX(+V4MZ+egUCrw92;r+HfK8+U=&1Ut}F4#3i-n+2S3#(AZCKV>cp5P z7F0R5WT)!#j5$^Edd`Yuo$v^0N3ty(jRRZ`0_c8m3{a9YfDLt(26vi^krZ@&j~k4C ztMyRswYwe@o_{>?7<0u@lHhF?M15dTo%E%MFg{U)1X2n@&<_P)whjStT=%NLubw8q zF0f$PE)Wy2@wln~E|o|okl!^=u3(l#PET$8&JYyWj))({T7ww2QKN-l{Mg0M)f$bz zrz6x)6OWN5Y3V4RCKUW6VDFiH^vpmzV@hY1TsFTm6aU>I{}#z5hk>d ztiVe>=*A+l;OG3aXZ$a#N+dOD;maS#$)w-E1CZrE0MwEB>4IYQ+)RFfN72fVxeVGW zY_Ddo#x#bqk11atF8?$_iKX%5K>63!l{y#j=O{cV*lYxou`b#{YWy;LgbBIGjm9QE<0OsN+DnC(3*_1gkZ! zcAR&mraGM=DCyq!c;;9kON5M>bw@IG6c*8i6as--bKo7I&|6CJk+r4}(pD|3rDZu2 z-qv7FXcgVl-o6Rj#Q@av<;gj&y;&V|bQ?r=KL*!@Uv6WmgWv;DRdv+cgO4rC2x(q_N!D83NS9}o^H z6f+(Gb7m>xaFKwcsqT%v23h*h%VyeGbLw_6-1GHDwgk<{k|XbgUgbZ4e{UK*AMJiw z(uOf0drW@A;hy!b$5Wie0@X{;frujYFp1x$?fnw34>LixiwUpVc`|aH!9NgZ;}`+v zy=)Nrq2u)~e2)Vt!vvRD)?b!(Fg2#hK8}>2arQKN$XCq60FsKl(MVg2Q%mo+a`axi zTTf%Oh1Rv6rHmt9}KUk-^8(_JhHQ%1VH=CS%z8aR`u9?$+k+R#P z0i3)2$pjSrLRzrb1u*5gV7y!rQ2aEfZx{4^mAzWb(biqC8|les1Z&r*o%;D52Y41{ zo2%_!$Dq>9Uq_?|1kAC1np4i7`Byu9@Yx2tAYX;Kp)Vh(Z`7NPy@W6L07w7{rto&e z>-lwc(gq$uK6idETlu9FTnfE3P`7r%g1}(Yifdayy=nK_yx1Z6 z*KyriIM(`2p^-c(PlD@DBZFpYalQZy8Tt{)>fZ=hmLfX}q^{Y+mnNJ~@9PaIqnwYE z&*`I-8O}m;9@g#}QpUp^R5L?Uf#W`VSskXa;e(>&$%bn9em2U73L;v@txzFEgIfB` zz^a-0JbP44ZKJ=VUIV>OT%u%2D&nxiRTlvaomxzt#^XUdHOvfj5a>lmZdY|UkP!D-MHtv zSK-JsW(F@vCdlB%W8S#;;x@#7|9u(p{ky1U`~4pKnAK-4Yc`*WmaxS>O0J4|WCY3k z_1bt>>I%cl4Tzwv1273FCM+w1GO?ZXU<&!098h@wkA90~8YDh3R6_|!6r#R3zclpb z=m4Vwj4tl(6Di;dgq&%^lM=nIg)RurL?yw%?=>)ect^y=-;IZ_EE62$VF$8GUK{gx& ziP+}8MOT136{bQYy6YNjaaR~d+Bw4dIrNX{Ymn!Vh6>b`_e>7yL#?}f$`0Q5F0J{# zTs=pgJ}ky7htw{6yh%#lStjMYmhY_xo-s$NglD4PtnaPbm<0CO^z9f()RDr&@-ND& z-n7kof#sH1ZZxUsFbT44RapZ2%oF$;D}OA5lfN7Q#gfl7BNF#It@#<*qN zw8B@xs$3S(l2547%i@RvC=%lCl#^52XzI4lNFJB-3y&%vZDO=6!? zlw3yTFbsid+?vW6ags!EgI64ElC+Z#`fX?!-DjLY1O;g>#N?en zqT&#M1J=>crWj`hs(3+rs+qMsN@q3O0V-~R8q`c?+}@h2san!$Mw~-*9Fgn>Ll)!p zsW+P`O$*ZGBxxZcKuOAgG;M24H~dJyWq~@ayJmOMAk@2K94$%_4?lBafpJoKMG8K<OB3$-6C;rc567#IuXS_5p^!)|zO0vv<;# z{^?ra=cf3Y8A~QGC}jNNGLNrHK$eZ?hTIBL8l;1Zp#Bkl>BctPVl#}1_6g5Ubucl` zQ0RHQ;w*Tru3nEofhTFD@YLqBOFpY~3lAw(=+8B40jso0+GP!dosWBYZn`8QR~BIe zT!a{x^!%_5km@B|$WPir)z-4_uCW`BrRBMwu#Q#^@@y@$!oA?Ez&b!1GO~5B2h$|bcYGV3s zJ`|c?(LNp%wxS5D40XFe4D2Edeuz?T^?Q4Gzg-`-zHD!f444V)NeTLC4c=Yyea+z7 z`7fn?lYetgoxE!zs)Tl3|1<#pMILE4RnT1owJ4b{LQI6O8a?fCLk_am5UTRF&SQ97`a~BVJ&AfXtZioLg2xt{2-2|Si zpb4w-UcTQ`SB}IRPy{XnV{UVjDO6&>fYXhLM;ua9i~Ky9XdS9{E7RZ={b%?sron2g zLj!RZ`YSdfJDW}#F=a*(<q^ zPUyEG3n>Kh+qtvVa`A?~EV&bKY@#U)k0aCM;M4g_)2=iOJuhR4EbG2K>J+0VO^6jn z6wf@R6iV_)Chmn13Z#b4v=n3|z0o}$z8(wtnL*vEI@B~<+7;B8^dgm%Fshhy=_1{n zc#+$`;46erJkj*M^ZCF_EKi_FmWB@REfT!j@@*16orJFX?g0a-;Fk(3I9mQxHGz*Wbzt;vH@ML5q|mTVRpcashguZjnqCr(bw^btHM8;?n`Re%-pe|1CGRk6x)-RG^k6$kwUeURN z3dg7rGgMzL7m3m)h93Pmys~7Jyf1+FW@a8fCd;1H&{0@LLZ2`E*^}pi_H4uzptv!R1ogATrB>gP~k(~m5@K6>xGk5^ARXL0pP!xH&8&bAHvOg_^EThsD zS2)bj_)rrH&f#ILd>?9m_^x6GrNc^H4Pph}FouEh@##mRG(y(+R&RO5ZQ%8+Ro^No zBX27^$=Nm7q_)9!a{2%()5ped6L9v`aJ91b4!s$rnLu-FZx$Qf#y0G8XW;Kho0C;O zJE4PSYte4CXC_fDXpuo;shQa%nnAKEHtH5+)!Q=#k2xf%T}ClFybRt|9M&8qu&n;2 zTKzDve3Rd;TZRip#<&iX>)zhD79EB?I%j8wwHO1-5r|HVB6Khz>yWc05E;v=D~uus zf-5QNsl7C=Yf(N-u5Zh0x7un^eBV#GGTH`(4K+d7%~6||<~P=d@fX9XqQ5+Q@^7fT zE>?4XxeT?=dVgy@M0RbDzxZxxBvGguA@B5`R#%pf&?w>AM9&!Y=6OL z7Hi&`&*YQZ9&VFudoL1-jqaH92!fd^jM|+bTo$i{SwTAwNea>xMa(vKr<=^8a8(q3 z2l&rRqbiOJ{Q|zdz)6$eebmKhT1?Q{DLUmVV~px4i}QH!CRhSC(vzg<;#msPQ`R5& z3%C172<1;^m=OWIG7tJ*1CrVen)-`&A2XLT?dP*z z?PctB-Nav~11cMkYTevI-yep-m02#s>t`tqHuWBO_2)#YY+4^}(+bc&vW~Ys?h+s& zR|^B7RjQ1AvaQOuDi6LH0+k={ld;!cEel1mBi!cDrMy;4q1C!Rn}+?SP-jZ)j0e(q zr(zlY45+5cNXQ=mTYqen;s^<}!d)I;t&#b>6UO;n(~r}2@g*K^7SzsM`O+G-Z=)Ht z#Ts;<5O^6Nijlg~kp0I&4Pz1HtYi%qr)66mG3o@tj{lxXg_CW+ct`T)xbZn!FvmS*|*o$kjNuUvF#7%B%_`+_re{^=&*qbDz* zh)he39)PdTqLP9vl30rxPid~`NGVfeZ|}GYWJ9@yH#JQW4=KN}##d47)k=K~E4Ct7 zhvqq%xI;|&Y5?|01l|C$znCLG6rP&A5wxwH#i&V}zhq1-N}P*yn)1rQ5^ncL4-%+i zc?eVa6GW~I*3ez(TVIOk^f@V!?=jS-TM{I%dufTgY2et}X2t&C*zBzvE!;!`gZW(# zTF4P9&%e)B|Mh^upv<*ZE3nTwG~QJWr)$==3w$VlOpJ{k1QsrGWU!A|yERnkbTu9h zV+R&|2N%pgJx*(&PrJ-VU*f`~U$Ic0bbIf9p#01s#()Jm^jJ|M(!F|kf>IN#>Equw zA$rcfo2-6p{hMC@E;88uBQml#Y&O`jMsCnoUFE%-p^KV@6-)ER@J}daapcKPkNo1z zwUn|$3KrbZpf80kk*^b};Tve?!WV>N6r<9i#g_e3UQ>JU-d@Ds59nUZe{Ugb4!gIk zIP$LYL>ejZMm65)-W10;;wV`{Q~@44~?i)By08H(D&g8grp54fsWt6>nbvK z=ksC64~(-`BRd1N(Px`Mxj}0(G9{Lysjpip)XvFKkG-KxPU);d-{N(F>+2ILAWMJB zwgmV3+M}Zp${JvxC9~jbFJGq_Gx{vkq;OgLG~mq&7qWTt)$sJnO@LjcCb*X&pd!cC zHHhf7BSE76<($yni>WMv)-dQU(Qx}D*xw$vQm~(`$_a!gkT)n?1J5h~%;uD@26-ML zPTh*cJRyQwsX4w#!K<3Q>>A;2Me&{XThar~cuav-_(Fp{l9U7fH)7$rlNwc4s(Epm zPs+#0THMI5dUB~;-P>8s?2HQJr-E?9WZ(Pq_|}K&DA0j4M)w&$#F)dKKvz)6{`QOA#{_e}CTaeK+^W(l^GCb`6itc2HGg*F<;`#TZuvvApHu-K`Nu>Tv*#&CVSf4UMF*_e1>Dom$hZ(W$W5~XXVG&cc= zK;TTrht`9`1<}0%rDu7Kh}pw^%IlnoyD>6C2H$e&dwqJuM&itd+JkfI#+wvDL*krw zGnr|&bgwBQk#M_>%^>YjQy~y2p%{!Fc`k_Ek>?@HLpGaPI3E^3$%pXh0j0a185a1(Ak#`?lwvMTwo$4oG@tK3Md=k^jhm3C|k zqtvx>=E*qP3t1v7`^z!*>psolvZUl5iM5|%#@Sk~#R#UfBh`6NOHw`<8#vi?qhQFA z-qc_X zW!p7d-2lR%jHMs8zGL2bZ@0phd7Y!p*(jLa&VRX$d34>v$T-M*=B!mNwL**J8>Ei+ ziLRO=GBIta=n_W_dZ07H>WcaXs|J9d07z47^Rbo_(lVp@qUpiKvJD7lV6@#L1?5 zv=(g%U06ff^N|BZ#vyicM0BuN0%^;A@b>-Y?zUv%U?R0Cd_80*(-BLYx*QC z>yHDRkoFnqFzfDp&Q9+BTEX;zHP_BMt9?*9SDU+>9o%MI9^^RiiR-W8}vOKtv=>cV*zJ(d`dvNu(~gwqqrwCGBg5DQ*~$ZN3N(V|th zkkmw6$knC&;YL1;zAc}7tTi5W{OkPN*e{v`vRIp`zhN2R zhsM+{9v4m$1x_Ab_@};9^#rYmLwIoxL!UaRE`$Yb<8@Z z^pRM!!Iij#&_RFQx5-N??bI*fhk0~z-5P}8rMo3#BU4LN7_ zDtLHHIl|%lotu3W!cC~&{{@fJ7==Jt0cZ zzve@E(e$ukbRw!JIxNx~zf1NEGL{odQ*~JDk6P!NhzE9%v{T+#9xoc_=oXCq5X z6^UB&JZn}#=R+^cVioBQ(;<9OZv#G2-W?(b%{b@il{@je*TRzU%e5834|)% zOh+ATs$*!X2vXy~8jIeWjTkAAG%|28-g0u*h9fXlBI$u5kQO6sU={X z5-_{v2`VFQN4#$@SeG&1bJMzy^U$^< zp4?eSZSC!`843F_2%dz>2zKmBt;&{2@<6;7-+%1^LDE%4;r|#fRha%=p0WQQ9ufB2 zp*p}wnzaTRQwb=#>Oeqiq(ur3jaeib`(p9vqO3MG?@-v)flG%M&oJIGIEuCU1!9@~ zK>15vv2rckEH5Ox*; z#;lvT#x%jx3NeW{_o`=?(Uz1|X8o*A=;Wy9_5jGe&Srl$2rz@{`qqWE+31)NW=6W^ z`kymXHTonMe}xq+q<%Z(-5c0wDP#_e)dQgwdRio_s?~PT7x6BMv zHtv{7vTs!9*Oopv?kRIIAtWcyxmzdw(wS$I)YG2tWatzy+$(WWEq2J2E9J1g2!F3*O2Qd|9;ONmth8ym9DW&~xGn zZ1GKBX!lck)M<=l>#c;T9B?dHXvnp!=&;|rRMI5bVKwa?ce4nMy4}!#|1e`>R+8Nk zxrQO$H!K*nd;}qLDOak0GEono%pOc9Cj09l)?yOFT_56#+}4Pfxs|s>osHSlIRy;| zyV31Zsl?H=1#RU{5(;H5qpc~CM5${m$*uzOg7zsUfKu(x=%wcX&P`aLyibaTsZ#OL z-lsrnB;4)vnI2|;Q!hf+%|2M&tXSP7@n+x0k;j+kj*}&3Ll>6L2=}138nd}mJUVzK z=a+u$Bu(hF!@Flyqz>y~QsBhTIIQ49#qU@uY<{jx`zc_wYXW`F1C)>o1FYQsF!Xbldc-*)CH9*& zjTXUP2aV=c^#~&TU)F-?je7$guSz%GQ_#JAuO(6BK68{@a8lDXiKoB`&G*J3VxZAJqUv zWuE+A>faV0hFUtgC_Kf^q^B5wdl+_9TyzqMeZ9EgGF-M$G=rfI!1YY1z z5LT#O3tHYIE~clwRDjtQMX)J}Q_QhcCZu3jXah3{`isW%=lAcj*T9w+9jnJ;zpf9)ppT@@g-iv+7ZewZ}Jd{Yw!(O$4)8oG{ zHS%6ox$oOZsCM-q*SuG6x)Yq%1AaHrfO^2wr5>Y$%0_`T(RGtxdQ3W8fX^pnUvSUe zo`IhsK(^$!Cvm=oG@r$$tQ^PcWrazigl9(FV1bW|JjkS{6)5kaGu9hvm@5fmX$})+ zg%*2@d+Kux^G8uw`@|B#QA^^qi4zxqq-(T*fbDIqM2Hd83 zB?!zRJ0!>iB}Uf$OuTVf`qH+5$m%GQDis- zhwnEaa*4_tiYZu}F|Z1x-9s)zt^?FwvS}ktyTXN^b$WtJ_r>r#A1)0==v1fpa`4K% zQLWfuJiC2-2V7Q$B9G1cpkgXX!FmBkSmw7X_WP@1_iDaaz7t z^Yqa|)YgfrQ1fX^;_!NZMaTIxE5d}^+vx>_27ex0N~unAO_@5+wc^=xuKNotc%v;o zRta3|Q6{{lbgb(+OO0qX87D~886r(-A?IISM<=LHX0)tIR<7O;vS)XDzeFJNh%}9N z+O*=G+}mrG)C*uQ6<4W7%$L!IZ7h6X^`eu^<3;P=Gdnc?$Nt}4C&~UpXWT!w%fA1M z?eagzurD+9ls4EA{_NNu_XG~#FsEFx%YvQClXn4JF<)&p{-z2qX{b(2;=ex2xdz_c zB)cSQPd*G91dUk)9M?~iu=>=H=)72((KY?Pa-N>ck{lY}|1)6g1wThf!=x$=*F~2r z_ksjZArldG+mHvo9YA?=wO8AF8R@@|8L%CU6$flv(Tw55Ntr#LRmN7F2f7$7m5vNs z>M%b>+zwREaRbtW=jzZ8ReMf{H{XjBt05^j?v%11$VHIgP+)LuF&?BTi3}f4O00rE z{JfnuPf$gT+dmJLFyIlIWsLGJwAlX((F6M7*V=2pL!yu%~r>>YtH$Twe~vcSPmXTg-#iCsJ%<>bPmTTMWT~Ta;WtEZ+c{R zHM7Ss^Eh)iPde$XMmMjuG+pAM9Vg(}i|u2sy}8@%xjU)RF*Kgl-Az27|LSpJU`yb1 z{WirA`=$QmVk$K*Lg5K6y@S!?$9#`|9!hg3si`)a9SMq`M?`yjrVP8iEz_eBMuodq zz%_YHDJrAQtEIxe5-=Vy8j7>A?P6TVq%D46RSOTWd9Axp@!wh_c@VDfl(K2eN`bwJa?oKxnmD^s70=ov23Gb=xa?#3Fm#fSd z$m>=7o9mY*<`Jy*S!04EiQ(gVpAt`IYYn~sJQCNNtut0lbY|d>PwJ^yAFkEIO<+y9 zzIRrUfVbXydpn55JWyC-7WSU=hTnX3U-BI^g2lw5UG&KuqRmZ9!WaFjFuQrKJ$Med z*7W&ild8$O2;!%?#vvp26kM=wqFyQz^hc@Yy^0-|P+r&Dx(H+hk2=U}B4bUHpJk!J zb|%4vw(E%sTsn=+8ok{cP|u90+~0#r_lGU=i0miyo^x4CBSfZW%TNI!D-7r`Pug?0P`}9c7)>vH7Po>YhSz%{A1Xhx9@r2f2b}k3u zksxSn^4l?hP8%ITo1?+W-kY0-mwh)@5e+9M%nhzEULU`xh zup#tY{M9Z!l(4sSdHvKnb@*a7K$M$d8;V{!;j6D#_10G65dC_$2cE2b9qloNQOB zt#yB$vGTL~^(FtO_p7U3M_)bVQI3u-^x|0ThpY7aOh2Hye_Y(2$gEiNr|`7>6jAG0 z5zu2p(Lc9)D|-Q9?I!|1MNeGgDG%6A%B!%oj(6co{C zH@qmH*oIm)E{=zUDw60rzgHPsu%W?f&y}~mn*DYCi_X zLUNy>7P+3{z~V$Q4(`zY9~BXPG3+m)37G1jip~hr7yPlKo*$;n=|xi=!xP_b zw9aTPRBe&`QfUHtgG1U05vz}SM<^{J%tt@JHG(K3rfopNo~%Ru$?7>ZT#2$890rnR z`D(3kIoSJS_pf_@6#wsge+bsmX8i9#`8D_O#~Y%A{vU6Mg8qMLLzK*ac|(-rDK*zC z-~t$f#j+&e`ycNT^J~?|ub?!?dgc9d^VJHR3 z3%P7;M87;5`;q0C#K^hMYuoF7tpD;ToEdi?#&!lmc+$-{73-kUqqz|}y2?>1eBq<- zqoNv_MxV@nc3cBNwu!K_jUt&A+xO@$ zZs?g%$vP>axgraaCh8@hsn&g&Ak@}bEyqhCSk^j6?94E{Y6z($G2>O2)~t3jy&!h1 z{4rYrLwDQ{McUpgmtUi??sPS8Vl_95_atS#Gib{-bWjcrO+LMu#b58ia&Zud_}SyH zl97E2wu|}4n^O&*SNH$#$@FV6@gF6Vw!A3E{CfnXbjMrBXGwMXvyxx?LvPQ!{f9b; z_*_#tZCcOu&VtI_6f@MhD5{3G;4q@)o{i`&ubkMQBZPJ-*ea3MRFHIjY-*WqTVBl} zI>ccumU>pl6uL@jI2FOgJ3&o0E|bx>lM`;tG<{i(KeUtlvptp_x)DW*&_BQ)nO8pl z`HJpT;eK|{`YI!9^y?6&SeQ$%P)OSGILI!)30gO+hRHb8pYEk+f)#2_&Z279swa! z>ruVq7t!d~XX2uk`g9C|499CfJ>eOqcLk;87MMkM>4KuMgoZd|&5OC3iHL3Lgbbdj zhAwEVGU>hZ^75+n=+?=(p>m#i8BNV$zoB?^9@drjf{fIO$g0&$n~8MIP`>~DPZ=ts z;;aI79M;Tqre0xt1b8>3RW8>X)Y;ln>w|W7N`H>DLcN*QJ!A}F{U1WqtW>oZ1Ds=8 zushA&ThVwE&%+p7;*}B>UHPi=lI{~!-6TsdRL;raT%?|sD+u7ytoIjnoza1*!ve;=M+|)L0?Da!`eU7rU{a;<(pMplJtJL^B^OwV|tIBo#5th)Qn0{HSd*} z-fS2=qw)ATy>F&f>X@pXD?i~Lk?$As5{#DWulk=6ny$`FuESa+E){laBFL32SBG*} ziE-b{1c{4o@y&RSHtLD?^<*Yue@~JBT9h?a!DJ%DuOFSgiWY7UyUO-kO4;rwR|}^w zC^K2vyTFBJGTCLu;>K!vu0KCjg={KO&{pbfe0P{TX}@)ZAMuhV`O?QMZgMPC*GbO6 zcAf1IZ!PAd{eWfl)WU>=N0Y9!A$xwMj&%0buwD$YrVrQ$?;pF=6tthk1Sr-2c@MB( zn_m5vQh&z-V(ka$*Or#@&aR-tO3Cy7%mZ@CZC?b1ySra4K=1y+9$?pN7Y8uE?^12E zZ9o2|OTGDnE`|DQ53uat_5g!LTElS8Wcc$QV2r)GZ^#?aANBy7{rw(bBU^p&4o#}p z8MPObdpq%taWds`t<1OoLz6ndrqGiGO)3toawLX8fP6r_KW&I+Yj5lTHXPM=G_$dO zu(*GP1nedV3HBTX2mEJ0n&TJE{h2U=_TfF?dvC*LChD>)ISq&2w@)>~e?7SfG9#6o z`2I=PG^il<1*2cW`WySBHC~vxU7wU0-nS!h7-8?ch4|14`)L%Ij_f?Wr_ki^8CY~) zL54YI_6jt#&9(@C|1h8Pt!O-bmz0exmwA^gmxuo2@=O9f!u9inqUG-RqD*?ZHdjbC zM8Cu$ebneMclV$$bf>R7mpx?xTJGQY$`xIsH(j7RUUU0xHTHexSS^C+{Mv9MH`%%? zzTp&XyBz&PxqYxW`mB#%l2{)@_8A%`I$UM?BU15o^oAT$%#B{YD}-!Y#J2?q3YY`W zbm}=WDX0=`YoNM9-`H1+%yY$&utyhtw*w;R73d^n)}S%o-bBk zPByS=F=q55!O7~T{NwAB7S=m~oN=4{sW*CvokPdURb9`Z(UJWnTvk6SfLRFwL&TL9 z?T*Y(cKL-oWvtBAQqAJ@QG>3GYDYo`2H_?~FZGVUq~RUDY)K&y<=A3-vQ#iVxM6(S z#Hx_=${}R^1tEtg4jeMH3u~IQG z9>J7m!i$x5Qn96&(pEm(S4}!Xy6W-Ubd6ALaFmNl^U$DTxC34EAG1IF zhY;T=$ohvJ8^P9Of0ZCI`VNl9_J6Zo<)41_JHU!;H}7OZ4%`oD@gMeb9_hr%m1Q)P zpDJ`jC9!(9rD~4>9{Kk@J*TrtX#Ct*OsN66L8(z>82U&3WIfjc8Q6iUT)q zA8*($zu}cwQVsRsG82wh>NCkbV%ZsNoCIP5l^a?T^UDUttCtlT>D&$~26tsu3mj zFwfkFwJRN*ynWU9m8L(fRr5;5L{)dc>=d*}sYt_Or6ZHNXb{wu=m*~tnNybb*z!qZn*Cm+9`S)`9q zVugp$)igo|tSSYwsk;j9rt1h*av@qvy~ASoTCqz+%<(ccb?iBjp@|Ch7cB9p8*}7q z^fx^`#yK;aN=a#BRmc>)_sei>#d&TVYw?iteuZc4BGW;b1*3mf9G_MPS)=E;^<9D7 z82TkQ7JKCkOKH*iy&}#8`2obYSL$g@&8@!wcsAKjx3Hl7hj~+nhFO^ox~3NJ)rtQg zG=CSA66IOzA53Uox@Pz7M;>@k%S9q$BUMFXxdk=EOFV(1+fzET@>44JUOlpk8&}!l?Fcib=97j5WAJJ5cl=NKeIb$I+vBU?5*-}s8>?gIqiP>M2 z9hZ>!hV196FK!g(U@=aGjPhMWvOh#w`t!WhRZ`C$cHp;{i_lqS#e!TsD<~woMgiQzb^0vt<@uoP2dBnLP>oDQFmg~TQZfC6F zJ_%ISWZkyqSI6%;n>+J_T$RTvi$1sHjaqNexkWKMYDBZYK$4~U_Ixl=eE8{L;9CW? z=sOk(2|e{kq&7LFE@rY{l4U*^jHS@9r+3hX%oW)iog>oVr)$S=&r~*^ra)hakA`+% z8}XndHQXyjnAHg89Wsx2B<(UQ&k)Wn_7sadCkXi#uR*m$h1FNSYFh6nj@@tkiX|F@ zPtqRZ5g-lY@+z!KWH!5%MmMkb@)7%w_0lw(J`c1X>ahcA!BD&YV@zy!VTb!D!||Kq z*@#yO!8JcX9lK1})MnS?!z#QPLjB=5g9N8wgZ0^r3;!&}Iyooe;t@U*w(mgh5F^Z4 z?{3b@ms*`4aVe1d9IJv>2d`MUmN0d3wf2X^v3+?Y16!PVB>c!j-w1mO?7)v|l;ig| zrg6!}w6pEL=4%wj=prjh%1}tqV4csp826+_D$0$b^$;=EK<$(7h0lwlyD16NJpDZX zWY2v#T+4sKcB%%Wg1z|VcH`?op}teOtg=sc5xxdP!(%BJ$j4VfT=n&Jjq4njV4cN3 zw#4_Qr|8VZb{6mxRupd8ZC9 zA-V9Au4)ni;xqrvl1UVkd0B$|UcF>z2BLYA&>!gI{euPHN|LMTbV8=(k#_@lw zDuFjHScM*y!tz!XFLhtCe&o_(+EXfQ!)2+7IF&q}?WA4yhGgxEQejf-{-qG}hUu0J zVxZgY3xe0LwdNgCZvOE8`=qm^vVR58{wnQD{ua=pJ0Aee@K1oIPa9%pfd2}A_wNC% zxqs_DVh{#B05q}T=fpoumsc+CPM|_zvwOJ3s%)PHTHRk%a1VtkCb%LdspN{*F@>zE z8BWR6kZi|NPYgu%`6+&5NecI!z5b9E7h z0boJi?18tz)rM&-in}L;{j%m7a4~6u4Upd(%{cPrSTnVl#s$==t}Ec2nvHLecu&1f zF5++8#G~OUk>!~6U>jL7)rYQlFU@Kiq1yS*Oz%hiqe+Mfkm3WrYq#QDv9rGaZ^hW-3kyl(GaSNq6hyDA!BHwS8}l6+0kOWS9kE9)7@f_Za-)w3ko znSyh+Yd`$j^vmmTSKBBt1wIQL`*7aeDP*>1-_Uv!B(X={HN+OGp?@dzP~0w>DLAnb z;$`jXHJ6~&@~>|tyEP0da|nFg&PQ={`SRV5VGSO#H&OvzV@eYwYc4)@%HN(q7Fmt$ z%%A;X>0IgVzdQneDIt3Jyp%s6%D=WS{~bl%NfLh895L`bZIh5|%9$V(gIW=9G>N=) zwx6qv0;tWmnO|{WxJh!PG)^D42$v6QM~uXrK*C6C#K?B3)z7Q!;V<-d?NNn{t-0QD z#;_j+RgJV28MG+B!p-MA*edC<5JufOR33)Rs9lM3NY?m1%WS8%)ojY9WC_x)7`~0#TzAySFA39fi z)_2xoY&UStxt&zn^K&iqM+vkTCb|Y#+7@_=hNHNjwBOb6)DzKy1!I{4RXmINVsW*M zE#p4Zyj1~r^QtDP8~dfEV%`u{iD*a0sO%)DR;c2;f8D}wE}l5@av%N5FIIijLW20? zkUWv}Wq&N`LVoL{qK;ho2f*t$>HPW|*?(%%`)kacn1l-5sj=l+D{{8VJuuf}xw(?T}Kz09GW z!bYXc+(OnUr4P&F+_9lG09J_dQOzy7`>4>0)#$l$K^JtM+;)|Ln7cNUvtGCmkNV`T zVqCZF)$~!tn+U;o_Kv~rPF=<}LpGvsKe9SKll1=Z&2LwEQUB*pb+)m* z_@ax)81$4I8H@Lp<3v4`2Iq7Hm(1ZeI4|#r+RnapaU6sDL!(8#x# z&J{NbIA#!U9&PWp$;LdqfQ);I2;-3HonY}2^=HL*$R!2v7I^%@3x1o^m6Yr%3{QmjaJV=z^=JCB{`IXDK_TlbTBczTIpdk({Pi=Tm$DRB zsICY@hoeFmHRV!45QNijU$_K`U0F0ONaCQpKUqmL@o^{=80GUsun;V$)lDk@;~l?o zvjwF8IU4zEqUygi@;{wM=4}7FD5Sm1r;GnwYX6$M{%>mk$EWuCze{Q#govj9D+=j% ziwyo&At`SBzh?Q74HZ24S3>jGbt@&2e^y8f?WX0wDWoS~ho0vEqj)}TGtG)F_dy|P ze*K+7Vv+k>@d(ym)cAOV^%1{(;`%3@Wm^1J5o{lU8~Oscp;7c`w9(%Fz)#)i8B}M(oW;Gpji8p5brtHL$~& zi$$P+9jQOdFJKwY#a;M{ue-lUG5AlRIU^egcj}XP2n}zcF|M@pT_=jEs!a92Vid1v zsF5UI6YcuAgEe9PhV4}q4ZYYecFU7Anp>Lwa2DK?WeVq?!-`))`7c)d$799*zYA8x zJ{m3h=TiIEV$c7k_J4Y68!i32q&8d!j@3Vx+P`j|`EP3f$EWtjze{TSteTYkD_re& zAJX~vT&?E+l&kUk|KH$hV*e^vJ7job`mcoMclR*-IW$6lqOl*}q3Oo|iQzxYfoy^~ zkY5a6>#q!d{tnE6JVojeeOL)1azrhPN7tNzI*9+fIgqCO@x6C3F^zw{h2hg*KE5>V z;p0o`A3na67~H~Oos6xthdV@{xP?O(YpCwev&eE6kai{D?kRJ}Le}g5G%z2h0!kZK zp3d|(?RN|x%z?CVW6}Md;WPdr!{4d%JJJFZAO(Qr{*C?r=Sh%X_W$puKf)BhS$C44 zHLGn2lI)?#qN*5og%|O;OQsY^3ivLazWDK+r0n<{){@pL?gc|d|3Lp9RJ9u=mwEe? zcrUUXbJ9Ip=d-@I<@UjD?ibemB!0;<+JiO_;@A# zeDf-4=<0>(*Kh??l%$VvalgL6_@YXTEEH^XZGED%FHLtby%%8c#{lCjf6z>;In*R#b`0IU3f434vvG4yipH(X^y%HC!OnXZD z*9Ga0rp8vrjKBWQ#8-fr2P6+DJmjJ=MCz!-OFm-QOa4CtTHnC_b@y!f9y!S8H&(kw> zQ|DY)NGX<(nZwOST$(M)pygTeA!R#S9qt`9w|RV#5@8~TU{2H6e+=Mr>iX=$H8YT6 zkddrC^(H(^lL$U>Zy8v($2qcK6ASUI%cKhwan?{r#ovwBg> zC-o(6sC=nh2ZwA=k<~U)TXrVO9nnJZz<1qnujE<|*@@BN21qt}?bChJGqRADA)p!} zQLn;_?^Cb7Yge_;xb0WO|{KlhQw2kof=)13i`Qe0W#()9k=7tdVdpJ#F)(byJ$%O4~ zrwW)B%$^d{%1|{9A3r}r+s2?@di?{gFq0sRS?bRQ2 z^(1-llaE`Tb-^EScNT8&e#%wNhq+wqAkr-IFnO*c|3_bqX)%b9GrhhU7nyzbJ5fH>)dfeK1)rQ1=-gd7$q= ziGb|!UY3Kcv|)qLHyG3Ibh#gFe)V+B5+aYE78ucOgajj>%(IgZ`Qej}hNC7*V{iegD!9eJQh)LV-<#Oc0Em4(rgu~3(Mo)*S!T43NIfHr7Wl%O zHk}+nsGlRBq+BucutwS22pK4a1nYNmE=zr$>ci>I3FGr6t%~9U1Eg=ukdyyHlqwM> z*de8#zm8*L;Vp}e#=N)0L}iE(6raV9gu3M#FoIxnasrJ`BM`J% zOd`6(jf@6$zF4(vWdKkCx{uzd`n#2BchMGQ@Hb6KOirvG1M7D?j}XmZGdfRY ze`s`)2bM@3&*$Q@#NfuH3}clWwX!Kvo0yNzNY1TPR<=r&zC>s1kUDMP*O}};SX`Vo zRQ$Ot7%4nRPxkbBvF7Sroyb``_dA7B!^M!dHF?ZcY{>E zY^W<6nv4Y1G} zijs=Wf9e*S)WL?e!p*!sGo`m%`#uQu)Sh$#hR{~~%C_O$zmQ;zkY4#UTW*x)&r#g^ zNa1<7u+UGH!Lp=@eP(?8FTOgAKZB=UtMVM_RGn|U(Ci9g`?`OqoR+)Kkqdz_YxhYd z?3<#DQTFb@8EK7txyQ#=%Xu{Us=*goY)O(?`p<@^kp=wU=}hNVY-p%E#$8EaUD2P1 z@})v3PyQ%1>6QpgLEaR)q>fp1Js;uMHkNjnZZ&q}?>e>Tvx=k&NRiCK2#n(6a}Hvh zo@^<7h2q|aam~gvpRjBZC+chuE!j`RrIyMgHAs->_2!u%XRk@BRKV+88i|ga{eCg) zO`m8@#qKVgBP##Rmr0NYbZF2pJuFk-yh$WOUdvf0tHq_3rydkVjoKJ9W_Lk-7wh^8 z?pHUKDv{GgE8%f`E}0?P&Yc*A$s0H#UmBj@jIYOls0N=Hm*rv6mF5|@FSbIPxl0s4 z{z1<>C$`!RZgSU8){HQDbFZl1yERdxAoZl9XRkWIxyq#LQC7H&2b7yzzGuid$02@6 z)m%%;j(ducnv-<%MTmS>U0Q!;9*$Ol3L#N~I(lp#tw{A&Wc|*H9;f;Eqe^NzC!76Q zD;^IjG9S8cQU&T&yD++q$a6O1foav-A#vSB~B2S_JztgPx1V@vLE zr3ew~1Z0jE<>`elQlyJ;Asw_~FpzVwdps-oW}gCOr_i6e%o>ELLG~%OkKIExHscnD zz^Zlb`$!?te9i_Eq$=`xn#=q|Cijt6cJi#1jQMx*(NZTZe{riJbM|J*Zw@^#?xS)j zv!*PJAE$QU#5mm{A@H}J(+0a9zg?S2?RF{>lo_k7xWjz6u6BIW*<1<(0#QQ!w84z6%0v{^zCKh(N(VJf1-u9_@+m`9nZFeEs>}E-ParGkr!g zEB!ad%nY{HZ$ji{U!ox5JsgB0DIux|0Rb-xe$GSycQ!zz-Y({XZ#<4->W)e_CXO!p z_Qnu0`i{1iHjb8N1|-hl78!8EA}a$k69X3=iK(Nbtpg7uqt#!Y!C+(mhOu4CX#l*+ zBU=d#2M7p?Utf^NmQLV3{WT>;g_T`XcjsNyF($7$4m_wy{SeeE^}77iT?gU@g7p)_Xm-(Xdnka z`I}>U4TNHYfB%rig3kK=Yls?E;h;VLn^%eH@}>Iyi=Zdb!HI_a{zbkTG5`PmRQis~ zUN`vt=z8BdJgjhG(-n#txQF$Z%ff{WQKpH*Ykc|;i!CWxS@Cc2^mg}mUJ4%Vez4$a zkl5JR(5GR4x$3aD0UvpVL~{^bTwLawTn2~cN!`9p-A4mU~A(Q#Xp@?{^H$A>1rqM0bZ9q1VNwEakY-Y(L5RdxHwD*z0Hwa*UO{afoqZJSF*C*QQUwQ;klvmX?<9>c9p3X!nN?+#vm3kifW0L?$K%gR3%J>x;u-F+$iHDW2L=Vf(gebkoSk9S z-QQi?-0b_@k6IA_CFQ?zJyc%3?K0HfLd&;tB_qd)a@`2Eavewr^YNkp)l$vPo75^u z1q%xc*P6#1$a~kiv%QxUHzqEQ@LV*!}rc(Cd0}6-L%sY_)#E^wg zj+=_g#i}w>Iu&}5&{Kb@%u6&-ra6(NrSx<2^I}dHVZuNA_*lHCsj1aJS*KO-yB_GZ zq^fa{Xl9F}B&MW%YoTPdVu0A0Z^FL6J4=KUY6c)me|I86K8aEw7pk(b~ah-^&YPT`4j#VX>Okz(vut-@T%I@6zX}m7)GK{x_`i>3`gmR_g&+6wM zk-iCPUDh-~AENPMwaMwjkl>@H zlM~0+^|p{fj|n?ANAvB@_vS4ZX`>{Dvc!+vRN?{dDK$6*4It`U}l&?i-k9qt$N)ZO?Wz^k+v$l~xP|eC`OY&vrXO zqM&17yt}jMMDAVShUFkiew(#jaA*Y}~gtUdg5%g{z7^F^V#g)Ht zBj9x|iR7{F2`6k2AZc^j#>!Eo?*IPX;f8VhyjYa9*NND{!2zTj_36AcXzcGJA`&c$ zUpKH^ooxD78uoN7E)p*+EKq4T)@<;D)9?pD17}oHUiM@YrpN!G{BjNRLLMvaEkc*28L3kWA!ECqVpXTQ=VY64K+lO1@v=Bm| z;@GaFX7;JQU>vO)*pw=!ujk@u4e93QW+?X+Hb`}01{_3) z-ripIN&{GsWs#|=bcjL?_4S`;W_V^-UXed>b#+CUU0zIt0hX0>eS=$mQ{_G=aPYf`UU+%fO& z?hySy3U#lRJ$n(T2aq`{&au6{J^20mQ8kwk$H3pp3Ko=AZ2sl-b?4w98dB zAip9km}fJvJ@BdS?bY6xjmdJ|x~Ilf=8LWT?q@sHmiak3OB=)4jax)fI(B>#582iC zz{if7K?-~X(AUt)NKcOfa^j_^=sR%ETcP3M-Ph;)0lTRq6DDttR(rX;u4*dgK^foN zo-cCQuOh@VXorH9WB5Ipz212@Sssavib?_lMv($Sx!Oc#`0R9hQe}@8S8#e9rc~=w zU|L$*z+e)9#>7M%ecn$HUXx|o0U0iPbA`5IHB|s+?e-V=^_v_wM>nIgrl(-lpvqtJZwHqe8#)U?oD(U?7$I;Pf=0xR};tF#W^Wm@>qVrN)K^ zw=&w??Xf~=czAgI>x1P^^K({<#yQPe^FptZIT$#&(ALqd$?^zLa%pK1!BnKAA}1{? ztE=RiQROJzu5y4DQO(rV)QoLjezgUyE({)(2w`?}bMxY08G3K7!5@@EmUfeKB9Bwi z#$7i?Vu!0dZi81FT9n<=qWN76aamipYlH>Djmy_r<-yt)qDDE07*xKIk!X(l3xsNA zuX_($Y!;e@2h;iOPBs)`Y1L%q_{oiL&KG^Mb^-ea(E%wF0Pp=1+`m$1nWSj8mdt6x zFay9Xu%*ST>{>%ht8-#PmFta3nj{M$;1&r74uEQ|`7QN5v>*!UL=v@8RaJX?7S$5< zw_m?X+?>tYWVzRYw5q798#p9*5&}a+L?ohJa&TZ9cbz18Hs`wsl~q|;`S5lHR^4H^ zJRTAmpacMoSq2d9dVNN}(B=~XB8)+!j1Yi`DbgGLT%py|jf<@_rd8L{J^-w*fO?)zFMs*GRl zEVk)pujn?rDqJ)GqDHfy0cyF`>pIkY21;n9`-4KUDm{QJ5)KY5eSLk`4I z(<}<|iqnIJprr=*0wvA%ji&FwX+`7uNn`{q% zh|RTlV4InlHSCc^Nyz6)VX~PHL43et7Xbi_%SH0xbZ7c0rChqQn;@WyQ&Ur)Cnm6f z7!U_fkuTTbcXfBq0kBEfS3GjUA;|q8&BrYN^UWm_806UGDsjp* z0T~$?9eSoI999*eg<77=$jV|_VN=Nq6*>@s)(6x~$JtW5f7~`5NkW!VoNs~Uebrf- zqfKWZ3II6YpX}P%+1ZD8w;rH3*VKker}JrGAX`n8;NAflLcnbg^D2R%>tu5jz(3yW zCTE!fXgu)Vc+%APk^MZ7Li2dftJ>AIw6q!;9cFCr06pS!l^^2(+_03;XMdr^bh3+vtcfgjOMjU1M++Dsby=76d z;s-4ljG&l=1OkO@DuSAt8d%+o0g6g<&+JaV$=M2AuGrWa=vzOziUk>g=u9n$4NglV zoT)K0{7e+Zzz)WEd!qEA=~o!_K5^U{lU%-j^7N@p-52B@|JaN}fyA#&uD9ca;kgni z4-Q0(GDzKr%(Pz1AO-Z-FCig;DluMHI+aVIjGl%@ib4Kk5iAlG)$647t*ubNFxL{h zCqS32xn28)ojZVwg++0*v$27Iii-N&BSpbOIi3bmVfv1`DLJlY1wep$v#VWQc~4J| zMQr!QFSG*iQ&@BV^Lq;VlP9E>9v&VmpWi~;+Sx^Ul!GgoDAfuA@^1NC0^`NaW%WUF zRYnhuN>Om#_#=ci&&wx_x~&*>7850pLPWEU?Ce=>7WjtifF@QY8tCcj`Y4wnfFt0^ z1xg_A@X$95{smwMYFACcRKMT@vo8QQ(4IW;<97H-<+%^K_{l6c zKagWb_zUn)ak0wV8rv%|%8^e zs|6SHde_i;woayufh6)!-e7$otsxH<3^=+K?X}(y(8D_h_xAQ^VC|~l!j&b_X=d9V zddt;je#QX^Cc$D%Em0l~5aXmXs?W8@{tglSk~Gdf>8Prz9vmOXSg4&HC)T!_P2ACv zStJu{kD;P~YF_lYr#JtGmnh!@#1J5oQ7gXP{r$+8G|EDt;Q-l)#AZ2#rc$Kbd+H%c z8dVU-l7uj{3mpOC{L$zBPSfKcDT69GxDp8Jm~UrHW+UW$t_MO>&USX~^>!=BQ)DbK zKz=ubDEZ#3GAaS_vWDG^O2Hy~d3seWL+~o9t}>7+30X$1Cd$m-$?2hW4Q0PX{WxIg z98N44&TQCC!o{UdNt+cm{w3t;n*HHoy&W{AY^uCl*92%Rm}NY*m6Zgjo}m;yWkm4?UnJ|L`5y@6b)e@$F6;E8!3o6*UDKbJ>2A z*EI*I*zyVPyX$?y;X0by+;~P^22T#aogqb& z#x)32*nLah`)*{3y%(Jw7#P@?Yp53Q;d9(T19!C-G0hu;7!Fszk#pRg!PNCSjq3=OwZTf1txw-WLc5v?{BqxV{`69~aeu4m= z)IOa3@^HBmzTw>K@(32tbuPOfqOFZS!*C$R05V0Uyw7%U0SjE(-nRIxV>Sq4QE4&B zpg#*V5`ZdW@%Nrz!RQk5I)4Ekf#~k#@>{wd|1A%2)PrpYND2uJ4Pv=YOSFfas3;8R zLBZg8S)d+G#|qHZGPiN}{X@dkyj$o>RizzhK~ z;s&2n#SRDpI4 z90U#KOJFo%%M{2PJDE=SDowP;>`7+`XqRDy*Vfj~g&N4H27BLJDAbhhK(Ah0xLVHD zD@^Up)E2+?8r(6G+0K)?G9g)6Bsqo@f5`Q1Lcvw`OT=Jy-u0 zolN{IKTK6c#pgt3Ly)>TuZjC5$)79>>zMR48}@`RPwM(~fiqVCI#=4+Mm#Y1on2PY2qJQ%pe_HRIy?nz#?g-UZli;V4m#G zs?|ubj>-w%z5``NGmi~=->|czqhr~%kyk}jdV1N(CEz7u0b?qUgnIKbp)(za-m^Ju z3_VK~@X{Pp1#l8T4Z;2>*yQ##<^v?(J|^HILF|7_#HYULV3H=EX)dH(7qOM&73+0i zY;DbOeqK<>ZZQ#DM^8sLRIV#n8*vVhC5(VuQP}|#0$6D8fHk8JY*zD9ML<2p0Z7^I zOfdt@Mf18oQ)!A2y}vn*c%8x%gF-++P~m-hWeI;@MN25j4Jazm=V-M+S*`$a1-BLD zoXX>bnEf&WYaSISIV9jo!0lk-YMyS5ugw5e_Q2KfGC8k$4)q4Iva^@h*MoptumY%} zDh*DN1R%qASKEvL8quw-tsi)92PSt8H)#B8D3`Za#nb>)X91{Yc|PbH^;XXYMQSwf z%Qfs9;L^Bn#Mxbd@0fYr$jHe3PK$tuW&<&rHIA7i9!?MflCfYc0bFkI)(voFem2TY zj0sjj4y5Mf=IYjMVT(=F&su&L~Za2G#8a*aDTJ$Mek#b?ycugoE!X1FmiCXQ> z)V=_8d|cosFv){VT%pfG!-Rprz?9wVlQX;K zB8sJqld5#y)nv1nK<0Jckxz71(FSMQeRFv{421WC`C-wMiwwY`58w4xg&4FOB+zaY zT1P;R^rvvj6`l_yGDCwyhwLK4dLCeW(s^g9k^njycxQ&)VG8xpCn+|q3LYl4O-)S) z$AFDFc(nnOWuskuV%&dPqt-lb4p9U|)kw()r0pX>epTId6$gQtey}l2rZB6f>jevl z>sW!pi@jUGq@`fN%p#tt8b<}q_4(4?UYR0cXo;}1${T^cGX;eYnW+zM+6bBjul61& z9g;XHF)QsxM}=$GB-Dc{^WiMm2Rq};E~G|sILt(-x~&}ipW4#r`mA)Or++XtHFXFW zO$xIhpogH|p-&~e06+nszP#XU))x|*>CFJaLl^@B0${=TkxrKFc`5329WLEp0#VuA z+{|P(!`7e@r~%NUR|X4)MRjgfRrd(MB)}wAEJmFY1)t4(K)mrm8Z0QfTClmwcJ zh(PTvwnc!R1I)_DAt515oSZ@a{?MS99#jLc$-k3yS%OUc0B9;a5J3-CNmO(+6d=GL zG|==~b;?&<4>HPnvGqfJJr_A8WoLIcoUE*@t&@`&pzlr>KAHT0)6@gwkVw!w47^5` zUb`QaN)g7^MCtnnKIm8iyxj-E0z8s-FgpO8t}uY8S<*=?rZY8#eW=zbYhVEDy>EGd zEerHGJmBBiro&kU@*kmq5HD9oBw{H6bYThDnh&0?y=R6pO_$4}R}iUKXeOBYaon@j;igp`gBS=Z;*sqXrL`S0lmmzI`}ELwrQdmtKh4^vEI-Z`xxCUVO~ zYrs_R1aJq^3rKJNg%%G=_gldJ5VxIIyL)>jYi@xXsazoctj7z;O0mLhm6nBX-g%+e zj0}@Osh(FSk|$`d&0~3c1%iiss^azb(KDTsE-v4S>l7w-aX$f;j}p$#7>X zP^8pxyZ%QJcDY-5W6oHYn1r9wJ*oWf(FpyDOzzPBUI`Iqc=g(n*&iMi&W_Nq#6mYuE$H!-n z5==o(t_Ql-#qOs?d+BM=Y~7G?m>__{d-^?3$gS~hGd|Nx< zELt$vP<*z^#v&6+vA+OjbRqben{W7tq+h-2jC}T@3MFu&NCgQLsdD3vzbL6NXdldg z;03=>MyHK1szY?e>e_#gV&hNmM2F76{@Vq(y?P_o58 zbO7jzMGS<(B_0CeD+^S?#?VLqfl!nO9r0jLUL3C2fw)@wYd3HMpBInK)E7+2{?u&S z19~V?&_^Jd!zvWCk3uH_?Rr38fzb8`EJ2`0k1FyocXfOF{ev4jnlFdASnY9cJyxzu zEG#TM4fo=07~VTDQRN4wPC0BAN_y}8CR$t%p<3Ke{lM%8a2;b-)>pth%v7rvFDSX# z?a!geHlS$0G@VP?K0tW!Ht*Y5srlqdH#fK5(k~AI0<(dlR$%hy;QTycw#|0AW2(V{ z612};1wrs{02aL_!Yp{tKBo*|;0F%?qett(#sSPfk>7*kXMd7EaBn-u$1&f&eJdd+ z*At9RCSPN!pile&g4fE8j%FVy_v8o4bXr0|UkRC^qo-dRfoN=O?AuLYAIdK&>4`2a zPfA-|H3Aap90SP3tZR4`n-gL_SCW}Bjp{t9>o+ZX7OckB6Ehzn*t`o0Oiv*rE34wa zs?RmMDV52Lww3NEzARKMPyCXamiA!&Da?X#5!GM~8wc8T7$gcGaC=RGzQ+ZE4(iqa z$JLvFW7)0m!{m)pqEKicWiA;rl%Y@(6`3Ps%uq5FA|*l;AyYETOr|nbROW;XnG;eZ zDI(){u6p-x@Bi0v?0xM0_NM2#@3pRVUgvPFmq)+n_3{{8#3>jEd=Kc>Q#VD}1i`aoEPg_tL3V}RY5sFT}TfRF}9 zLwQbef%-J>622?v@ni%w-!Z?HtxGA)4)xs*$2{e&)K$l`BsTVp6H@WJPTgNq5_KBv}9) zh^FMuQ|}IyCtgyB5-}ehGrnidq-HDax%iaQEwTLO&3~r9^_`hEE(f$HyM=c19;+7* zLmq?XtGv0EF+Vnq%bh+rZn$RZ1GEf6d->$Ly1Va!LL)H1&ZQFe0?I9O|J_s+Vlc(o z3y$@Fj-=~2k}ly$I@kQ@$%YLZ(#}>$VJ8f>O@pYkdP|+x zDfq5Gq@i&ytq)8ou)xs*i#H!Wee&JJr80cw0g8!fN0we!e`DAC_whh>R{iBt&*sMj zN~W`*CW3)^2?QXvz~1@-n*89%2v>$XH3ir4`-+5TDR*@QmfF=bA4y&i7*wALW&!Qm z_946-1w8hanH z2A4>2oBIYbtITzVq1>O<6+Z`{lAv&lzlc4Z2f*dR<`N*wRMjZX*?xjEinzGB`D0@_ z1-d4w&9zrcN=f;*3dW(2O#hh$Kg5N@_c8<&&E?*P^J~>V{&qoISXemG=~cIC#KMn} z<0UH+0s;bU_^wGq7MJ^^+@{lx)Nr+KRuYY-d_z4&xa~-B6P$y2dCh0UnL?AT?o|V| znR`9t<>BL73#vbHav6%wPBAeikU75NEUYQeVLwSe7Le-{Zn!v9iB~|JMGg}%5h$KI z72GH(D8gpP|kmrRAT3V)9)W6E#S!u?Rqw&y>0w; zs1LYj^k&Noa~B4679kc!^-LzodBRF!?ht+t;zldZV6Cbe6GL8 zd29?Fo%ZfP{2{8jg@q&>@Ok<9o4Qjp_ymof1>&=D;2ThUN7p>}`}g_sd)kK3!(gCz*K6!aUXu1ebO2FpVuU|h5 z58s4@;eVq7iceQl&YL>y=bLfv)1P<#bK%&6tmh(c#O5s&_)3Z22LlqlaEdploO>O> zANL$IL)FV@Se4M`j<8q(KMQ5(guvQL-TJtD|O|2eA@Ef;36(Qa&v96)Os=ba)X;7rKV2U#;6QH&+AQ@Fe@)gbQxstU$SMtUiK%5^>N`z37qCLs z+b1Do!rSF%;12a?aC|(oL-@-I?|grm+YEoQMtVzM&7;)Rb9jS^kMnQeUPsQI@7>D3 z&p-o$y=fHW?|@F)aKx0cOQrnR2)UwC2aMX#3?@Da7=6qnhz_HE;6gT2kD>&#~*X*_QEGm2q4?Y#>O9U1ZVy} z-4cfsUS_XHk9f#R2Y3-EH_3#l0bG5~_jP=noK^A8?jr0$?|XWNzyQ?1gVT&kS)!G1 zVPSz?v=*$>8rTg=h7^^Q%+JKV)J0D~R0+1Fh|Y1I?yfGf`Jri+Je$Q&|AW2w41VV0 z1114pB^HBw^m)o?SCUmiFbL#eJL zU{e&Lf1r#xm0iwL5q_w_p&@UOjK@uisW?4OiTSYoazRz7VOLrrn zmd@|U-MLtW4nr?c82Gg0^y6?!zv~{_v{o#n&bDu{K>IOI|jS47wT)nF3{6TYM zWMnlsgY`IR+ManXxshVEcwHRu18hlKv<&m+&Azv{?5i(YSXdBP@+t?rM{JVwzb0br zWG{I2#p#twr}MTPP2&hO~d)K1q(A@()GOH$Z_ zM7`qfKKk@6I-$G^ebNcvzV$r0IQZp@KZs&t?gJ|H^Y_PH8ay|}igq0de1TR(P&H6P z+%U(sI|Ah-^WZTq5Z-Fz4z(I$!Rco!N~>!o+R_=(kq)b{Q?OEc6V!|Ll~+59g=m!I zMd}q;tY-=C=bit$ML>H!o=VfD=g@Zsv8_qVLzDqL6TU{y)6j)&`%1M^rr(EEtXfSQ z?=VtdmE5H&aiLcz!^XqbHY?bi_Z^y{!B3x3TQ33Pqqowsa|sG;#w!ksZhXzm&#LNi zFw#&ej?fp6M)#hIr$9@!OXta6^!fO!k7R0RJGptDezjoCIaEEK`w1ekVT-H0=2rST z85tQo3qH5^Tefb!jTX_WoZk+orS+qG?+ed|8pOcEL0a(eJMW6^&a7t>Qyv75hUK8a z&k}te|KghU>op~ZvcNTvx}s5eiX-WMBwniL;?z;~vV`%Az==BP##Y>b6@gnNHsBDe zNL#`q?r?aktW$q~?`GLT<%yUQCc;Zmg1^rodXfp-q97<3~ zp=b8y*r87d#JU`~|fq>BU3sHeQ1mlO(e^jz#USI z?CtZKbFAfm+?88q@B}5KJwG3F%*7`yC@4sqeOW-?Fx+taB`w%3R{@OHfN68MYIODL zL6oHHV00*IbxnG)!{EN4hMnpD8M>3nbRcK7=Y(YT>`^VuKe^q!KCF_cCg)zQX|m|Z zNAq91S~2L7kCpbAYL=RsTKkd9wSEAY`WLR!nI-!+NXx1zE6dy3?!jZs%>Bl?_w){W zXl>7FduD`P%i#WTpj78bI=S92irs<6j-PsMxbd$jmsL5`Guk6U`ANJL z1NbWWdpGCW;IoIEiOLFu2v;xTcPA)_*}CxFRHjA>Eo?9GZSMQdmPd|%9W2kdGW8KY zkeI5@uM-oGp`&Fd$G-^z7y*~M$JVlgN&F^2H!Wo;-|5tN9Awnvx6==}|Js8?hu0dt z=u2?fj(s}n+gqSL`|wqTwZPSvaKhH5C?X$rsT!OzEx>d>xd3YMK!s3uSg_H3;4}0) zt7Go-84Jv(@gH=2U8#&sO{m&R=lyNk(h?lx#SZ{^3|>wywlF#I@F1}vo;n|wmd11{Ar0&}`*O7rH7GffhoSM&+g5DAFLsh;I zmC1pKm@S_p2d@64g9og=dlp~bLG;q`FkT`bWH=u@di3a3m>sPrGUzk!AURYK$)%zUhj0qvpdCw*6k{3I+<1?0ncDJ(f?e7mV~Z@{&d8{hl0t+?qg(w zT#ELDrB0kipH?ECD~psva$anSilhXjS>~#$Dq@+1yX#{O`?Xgnr*J6T01AgA^k`b= zFltXmLsmU4>ylVY}m>Tmdw{ zdFvKIv-XaT3d+iUmB(3FVo+|7lGq5L=5uSx?S!&W%g^T+R&ef|`}oTLeV&PTaB1u- zv88hU`3oY&w`L$NCr;Wrue^J085USWjyP_6$d!AHo^8W(IyJZLCcW9p-Sj&&%Kaio@=62c9@VTo`uvHHoBK$TDcCE62@mG>Gv_;VVvk7N!y69b-J2&XZr81uy~MQl z%Vh@#jd2TA&X9O0c|1>J_9hwI;zA5&3ybU14 zVJ)>@nP4fy=D6bOYJNbz@3AQj_sNG3Ot&9xeF!7)`J&V$?(&KX=8l=RRBYY=ji;+J zQh4Fh;)gCjqEQO-_?Zc(22@zWm7r<&0W$jlPpC>~fwQyooBhg}>G=s0nVa)npk0!}bwtEU|Ofu3a;&a;BE` zLx+@Rk9615-i!I#l=O8;UsIDw)?`+u6)jU-9 zBnPb{Ki`G zoqW225F63YUP`CyYUk)snlJ%PNO(V#6(wMhQ%1wRC5|PsZDGzZSdp>^qLLS|yJI0L z;?;jLGR5DtV8y$R9>;1#mx5S&V6H6s)!x3FUdhx4q%H?9FK^8fKrwlopWbXm{D>Yu z2rP7X+1>I>L)7qC`9fjcX2Ukh(>+(dnO{>mM=gAkzV+ds7kvEwo=Si%MLUov)i|Gh z?3C!uN)ES8*LVkTd!SRMh8$SSI9deLWJuJ>SX9Ju?1h;+WC z%NBITof%rklZIHS?xcu3oI&l|PSmyn-SbiX8lO~BG`<4o5&;)vjF9UOGZGdd%{cz@ z@AES@;WyxTqvTezpv0Tj$WaTWV2jt|P(KaI%X-4n+?=b^;^M_z7da*w4y_r-guP?S zF%(`fS{TG6EXyfNk);7f<~#Z_gMkC^_pE0XQKYYEeGcm@!C2{A^Ffn*PgK=^a%Jk= zE7)$j2~vU-m*%uh$ypIjfK{xSilV2L2(NR2Bk3ZPz{8~mTH4KHp|Vto`Q+aQMMSJm ze(+(#4CkMI`cW06hIFSnFsrY?lsa|Slyi&^1g6XMwuX~_r9AGGg-9y&fx#*tLL)nU zZl)xrJv?isoD0{!f4F`uHTiVJe%aiBXEFH;J9dd20coi|_kAwH$aAAwx0dFO4`6tI zm)iB7znNreVQ@vg}Xw_4N~vg>py2&O-WpuuLDWtt_zX7g~i3c>*Vd zAHNny*xarAZBOa4i%Wo1@33a$H@xRDXKH6>*IeiBdd!9IcAKHvsYJ!KP0pxqYPxei%5PekKfLPg+mq>s zjN!T>zFL{-dLfe60^+SL>de%khr~yN^XAg0M%ES^$T$T2J#>q_Mi^bZX5Vkr6!GAm zq!0W;*km}MM*?n3@UN$S>vqwJK}640mw5}z*&u+F^<=IZx`h4nOozMrYyepG#u?MX z%q=Z%p^nF+yLx(MiWBWNQ%4GlWlhMgqaQ{`d1kwD>Ih>G|3gD#3O@$2edal%Fne*4 z9_6O5Zz7uRHS5-i&fikXr@v<1mPU!-4b?C1N%Z;47&U$(b@0jNCOe})9wqN3`urJq8@dT6AVn6W6f z!xjvRc8&pGrzg@kb8~Z_zm~%W0lp@}27lR6uvX#HzP2ykFzJkb8Gm?u>uhDf_gRJC zQ3<)vQ-94`+M4nyJkHNQ49$x;ircc!Y;hiII|TQ=v4sUSenRzKvG{jhpJ9!Hyb)*X zoq*o{FjPB|SHP~P26yj;bQ=i&G<&7%`yC@~R9apmyP-BT^l6kyT+vqu_J16lJe{+M zO|8LF73$hL05*=VT+h+Pr5bdWI)rstZVw4^xc&U31`w8n-`4+f6cItVvi2oEiuvp3 z$_O_dHAkaFI=FWgbzXBrmS?f$XCvcyE#b=MD0uGejB>F0e)AIZqdQm-f>BDC z`QR-1eIeh}la0REs$~_HjIydKQxQA7&-fkbvZKq;z?eE1X5b(iTlfMG3HI4ps;{^61C1iapS^M>#E zC?zh)@T$g2Ynd#6`pR-9J`XIT1N~3gt8?*Fm&9QM`F*-$>z(owMwz)NYe{Wm8E8e$ zVXLwkT@7jn{YE7F5@KjDhRc8#ET>msIMHrHV}2KdVar~vIN4jWYyLvE(er28caB#R zMgbaEQdMW6;g+B94?w)Tt9|!aSA$>>$&Q5Qu%?h+uoB*yJnMG;lFs~&f;VF5=*ep* zrwOPfB0Jvn@!842A@7t;Xwt={rAzP@P_WWJgrJ)AF1(}r9>N~_!p39t*9woR*DKjA zF7Rb6Q4f0#yn37YgK6na1izNbgy$C()Ivdnr(W<$3}RPGb$CW7??wZ$nw%F{s^gC% zl26Jle)l#A&IHGgTx^pIJIJCEntU*O?~bhmYI$y@??x>`c_zLG#R9Y?HMBhat60C>3J;knwWbw_H)H+r@ObRPhpf4UgvG|@&Vj6J*HVRcR=%&+FmrR- z%z#GyHAKVVZv4a`P`zmfGf5HEAB%FqwLFSqR8+NBo<8z__3D+aP9Z~x%LFo{Jrsd^ zczAfQ?PvfYU~5~(pP+}YG7J$K$UOk+^(kK?B`WD0C`_V4D!F+QJ&4p&Q^y_yhMuamCLP<6UYR8knX(45Op=AwWtr+ z59BPv#*Np(%yI|`of1enTYF`3s)l_IeW#Kgq>IDG#>RIR+OzZ+iEW|?-XLq#a}a(w z0&ozo+r4}D3$!{!o5S`zRb;INYUq%_Xp5rfcKQ>D0-)0fE^|Rd%@{OT7xljOsoy^> zr^h=_nsi9wYo+KY)X8pxwkr?YMq#^BY{R-)gRx$khL1QA1x%kV*QYkD9;Qmf!+3?p zp5VRr-Q9^0TZuE26wZ#{PH%K;G|cqzx-0AlZx}EXV9gPWRGW%$;`u!F9so5r$uKEr9baKJ%mLH+0t8+|IL(#QUKE-D=S|7yP1z3e(3}^PdYgv84~&+$)e8X zHxdK!P+lAaHm;(gB7INL!tSjM*_!G1>asn#U8km|i1gIo#1r$)Z#oIWyaQzmq#&x9 zBagp6vw^PQ*axEghwgJv8Jvs6eY3OZ3Yrph2G>Eb5IZ4l!=cZ_9{1RO@UQ`LG~d{) z%*}mpcAmo)%FD}RSjt8!TeQ9K9vF#~@5@zh-lz?k3aCh|G3(9cy&c#2fRENs`oMuq zykm_*>qmIqX($$_e7OY$HvnrTPWGU*4kDQjlrP>*W0D*V^;tD$zgmt5UJtL)v#WO& z_*F7z(Qm<^n_jf(2gpuX#;QSafc!VpLiCN0W+2rhl)vwmyGsLSmlrz96DE7xu;vLq zLs@2fMD2-?D+$X$UTmn~4pBysMI+_y&V?5PuV1f*u~+Ju5r6nO=(&*xE(>aCXi%7C zt?vdUpv^tSziVHg<9P@6_2jG#eQZE5X&6e3^1H3*Vi3mn5x)WS8VHv7T<_CMP>@f5 zxms%R)s#015sr{zYj zFxpn3G!APkTA1T8LmrFz`sN!B>JJkWK~Ps~EB2e$Gof|o>pjUCVhkCCMflv!G%$qg zfwfzno+YezxXKx&RE6Rbv9f3lhCpc9J29I-hm7H`SUH!SokbE8fEp~lA(Q5tf5aq)BXpXX|A#@*_zjbjs zrhYD~i?k7zI&i=g82bB<9~zOq2Z3NA(h@_`DfSuQAk0Fb1jc$a`R(3@M$o;2!A5{4 zfo$UZogT~{#qa(`GXYg}QalZwU1bHc^b+ql-0d2nM<@H~!0K1fD8d@YG8rr5z8Q_y zJ$E}iLikZ=iO2F+zyma1=MeFrt%;@(f@WW6)pGdDP@OD&r;NO~UP0Dq4M4oWYiPClT2e*g4RlKN_; zpY^%{KlaD2R%BM{BOUs$DI72H$1WG zl~G&74tsmbt=(Pzfq|O3xr;ct*87dA1LO^RzU@}2d$d=tz|$-Pib2job#?VQfDh~@ zA5DHG^lBu%fUFsZtn3a(o)fQDmS^SA8q;i-c4~Hb>qx?_ILkhw@Ff1i;hO|7ZEtT+ zXk&15-_e8<77ZUL@s?b5SpWO?e9^D;z!zjE^UA;(U*)1@ef7uB=nA-m8qB#wu%hbU zUPIj#!b8M59eG++-`l&5*f{Vr1}7%=CNHkT=7k(mke#CFN(>sq^-OyD`ke5Nq=#Py zsc{R|PJ@X8@Fd|oYR{Ir(7`alwEYp7VXNM!!el500)|(Y7g=Frd4+@U5}pzq+my{s z0rXn-2O|^YudiCOcI_c_x6J)iOX$!u<>Iln*?oQHp~jb~vN zStFQBG)i-ccRSnZZMtkfJbZ*~->D2_dL4Mo6azQhqP~XeOqPh1{Ao{jnU*Yj&V4^L zZIA;Z4qxh)PzQ22he)(A-|QIn->!oPx54Yr*uMhY3Vve^BeyCH;(uy`MnA$Ok0HyU zWKf}1KTO~xyq`n}!9jP=eFb{_W7nS+#0i6e81_h#LYF0rW~On!^g2I&{fUqsI!zZy z>{d>dRe%-s9uYWY2EKy9qOePe^B!STh^;5*S1u=*67qazngw3M z3@MEvE}WhH5bFL}y?Uhdc}a;BmclJmvw-duSd$cw9!8zLHy&OR8T)mlr4!n&G5W1A-3D8o+dW9Zq2<0~q z8EGsy(kKx9n#|9DBZA~O$df=sfEao3Pcl1cUVts%EHif=M=KHa;hn5qS|RKplLflY{{|!27LYMl`-~`!vrB`tyk}o%Plz_rqTEMwbI1;qoqzihL44}vOT#t zu!F3qXXB{x_DJ#~@29C_xO@9?n48ek?~Bkzj^vU`uJwK8-Dyz1p@Z+Seyh*6i|4PY zi1FcVroH-1NJk&Wb1pEv^aG|pl8r!%(P3kF;tma&A>s?>H3@MoG@XgPSw@pqL2n+D-g7o=pPl(zXB*lsxdUR zmr$S!^`FUG+9WbB?c{SDO0Na9XYP0^x*Os$ny%wH9*A67r|gRrF%Q^Zs&E%abTaMQXZOGKsQ5O!Er}sGq5TTaSCm-UhbcF zDR+j3hIT!v5=8fz?!~_`ID~npjoMC>n8t@PXKY5{$bpK%Jlw5UaOOF5_0`BVs`uav zhr8LQ(&KZVW{I~$)hV7a@?4mZfFFcjKs)ONDisNbkud}0@cf)`489s-5&{N!+0?WF zZm>g$xuSrj8R$H+dWoY;Dbuk690f9rB{XvK)fEf>6Yy8gU2DI;JRe&yO=u(|mGU*L znt|+cXHDFWnsc861F%g=AeyQDv;_57+m#72Z{SSvg##lp;5wdOo_v^=4I63XLDzhh z8ue+0Q{l&s6Dy^51XPBv-9_uLP#cAp9>T!|SDYXE==3%nHT#2Uk;wf+^YTMg8l9`S zcmKXG+KHWfe8;{OZP|Br*zKeaOr|(GjDd*xA6bN{(X6eNW4U|#wgMmq@$2n;-vPyz z-+#K&2L znIXv1$@DB?6c9O3HPba#!|73jMRarPVpd~gqtMkkd$Jxu=Wjs=gkd7_Kea5$LF6$i zVf+5nh5PQnr`oj7d!)9WmBJgmcV=dT)vxbg(S8!ES=I9hSfE~Zc1m=bf(hKFv{IcK zrp968v0I7j0`Emoe>-8x;tdu~5xuRj_1Wqw^PjpBC1#lX`V#7&sf^yRdv*V}6N_=DCYs*(DSQ*!yh?6D0C{vUk zB65)MDqLlQ07v>5Xi(r~g{fd8-6ZxYx3YNpizyr+l{Gaq#4=;iS(pl-!-Ach$|83Sy+^NtEO~Lh-q@D>@Ng?BbaKs4Pz89)!T<&}V zsl+UX=d3nJ6QlytZtY;L#(7K7Pf5%=jL9M_ZF*On9vyglu@96Y8ummb&6b7jkQpkZ zy^~X0+9xsA=lDY(v6&N#F$#)*TQFF+tO5Aqd_;SXU1nO%tV!qR>*E72Ej#-3_hpxJ zt5_dqkZ)ajc~VfPY&8WoRzlv{tK3FLM&zkM%2R^#FkIRQlqQ+kKt@f0hi~~UGeD+P ztVn>w#DpZ(bS-l|omT+xnPMi~a%HYRQ>$E@EyQ^MB2YoW^b((U?|4T?2N}nuL2oqY z`o_?~A>W`oADJ$w$!!M5 z$>1nVt~O7iw>nS|`7JQUKHYB-cbdZzBGK^*Ih}Vk;*F8tyv@m>=r3$|jqcy2P{WmE(V_M+rz&{FMqe^%F<=c z+_Q#G2f>>zLieT+5vjGl-DK^DVI>Km_Y==mP>?qb^+#{8)lLuXEJipTL^OOA1+9%2 zt2dVM5JVgYBcdwcS>Af`MB<=~4E$8g1VOA_yOx+ifwS}MdiJ+?qA?K%XD=`t9I4D4 zFy^L-j50{$qyC$?jL{9Ea|dbRr~Lzqo*w8`-pVSi<2LiwdIU#ztK7a93oG6^G*2qb za&K>gKDi0vx^A@>>PsF%e~3vKWe}Ay;wgl8$Fx=`H zMvLX2Ag?+)&_BhXo?Ac$>COjD!-|e%*!JzHKLEDC7p@(@ZiHCc1f_!e2D;HYghw-+ zdTMVnIEonVdUn}u01pz;@su^*8IDNPqZ|og*1^H6bHuk+jju>DSEzXDAvNe^Nfn0n|qc6$Zb8 zcJA3A;!^^h3>HcjnL2;+e?g05z!D>x6lkud$jva~(bQlu6v&)Jm#>-e$To{as-yo& z;!Qf{lArT!@Fn{Q-~5ATkd#Ry-7Q7(=9n?UKXW-$hvnty3^0X*9W-<6?G3s4jV$a8wX;HItkwxDG1>Sx^ZDEUAJrBObEnteuh6aI}+X(8vUAQ36`cy>)fON~Pt>WqLLQoCTERT^N<}tq9_vNn@liTU-*}B7AtS zal9!_V5Po93_2`FYBQ46wD*KLtfL7a1D>ueLgTd&B*D zTQ!Ax7p#@FQ!^N1G6YA5X1bX79+({f>35(N-S01UFaP{?s76{U3sEy?@6%rk3k?gq z2|PFcbq;4B37xLzpZv# z2OMfeHlh|82y^vPb@i&`npt-j@{!&Qe{iiGp5#jqP=qIVkL3b9I`=8&Vl(K(3Xf}- z4qCC{xa8{Ye{wPIyCbZv#Kh|TXn!Y8L)}U5-w+|HSaP51y&K6WeSc_w8EFW#x^=cl z-WQ>Z(%bE@AOR6oj&$F!HQwVo{>iPkap-*Jbohyg9r_3}Ku$~3%DmrQL(n@()oFTO zRzPdC5j6dG@Xu+5M)+Q6p9I?^k1Hz&yWcG>P~*KOjT3^nPNo~pYLlgxrnZefdP5h@GEjcvxB~iq^(Pk$ z6`YPMvUduZR{Y_bs7x!C>rW^Xat^{onCv9ZL?x~^5z|e+VLCM$yYK6X3_f33ZkX~Q zrhiTT)xqlTq@TR?_%y0@rt@&@=s9O7&mbU3R4rWNIVN5F;R)BStrIOeqY@z)1h#n) zXoW?{SiV@`E7_kgR}5hW35BAejA-qL=rjtpD)dH7d=wm+!2qfaS!EkSU2fdr+Z9Vj&g9J)CR8*p9_ zYYygO30pODsL$_rCbQ09a)f685?@zs%#DGaeP2ezy*~agUoqtM#9Rre#IBAX9KuR` z55u%TLV?Psg~@@?i)b>*v`+BPLwEv_V(0JG$>vG;z8yycrPvejEb~V`it!+xOjR)) zpPu*Q!*j| zv94AXxtaX0`I!F(Pc|1J`T#PA;G9c5q{j_HhXiVgrxZ=mu=Wvak%fo9TUHjYQayF<|AyP6V9nI#(M-3}rLR=YI}Vq0Pb@&_D2yyWpL8mrhUI-A4Tl zu1Y{aN<9H%YJUx$?U0| zCA5|XJCiK&N2^fU_e4_^gIp!@Tgf*YP`o(s60r#=ynyyTqHQ^1X11H)XaHPfsaGK+ zGB-E13qRc$o)U=S@i=ktnGZs9Awx@&P>5YEs$w(pJAA=8M7AyeiS+^qIWd`YvnM3euzJpB@+`F2(tqEg##}g4AK#}(RuAgaK=A| z%#QO>Vdz=I$1A61#axZEGvFhGweIBm91yQ$Eb&42U#fHEI}W(4hrt5+YJC4Z*YP-1 zo-$H-@?VbU{%EUMCf`Q8E&WFqH3z@xX>1$J5m8N9a=vcL_l}IGBcvlV{u=(X!Vh!MDvn*w zDxYk(BNBkwe8bW`7T7QIpHsD^Bqez(9?W82I@o0;9i0z$HqXze8UUCh^@c)3Th?+z zFbYE8%F^UDVlVBb-Q%yg3T7Ypi>ojkeO(0&X;?51VhF8S$+OF#4)n`hg|!KKrk7ic z?{vh}Q8wr(S~f=Mn*6kWB8EBV*}xK1Hud4a4H|b3CMx|uXa%ZamAF}q6*UCh3}#;Y zi!K^fc^rnA8fs~GS@dK>sfbZj*sCZKD{!`zJmll_ra7(?UpK&_7Mu_v2}g!llJJgh zYmZaB0QR{O7futt_l)+VLvFBctFL9Ydkh&G$6hn$Gh)yRm8Yj?l?^su=U9p44lx-Z zxtCb0Hzyh==Wtkl1nRA#TEcXY7Z`GJ;bV#{%MUh-&!lwl$_aIhPvT>LpBg%9aweRk z1!T2i*Kn!Rm&8yd#xObY3%x8b+!E3j9V($%h^UHvhNJs68ljt7&3JuEQ2#d~GlUg+ z4m}%2t#c9gJl@C2#{yGq6twjuM}xs6=3|s*Z7ucnEDqm7 zzclD?-C5@JRgoZQ@?|kc_V!%Fs2u%Pas?y zvyJ0K?*x+Z0*Z%RH|+OQ?ZLUFB;fe2wN;T)qgAzoGW+Yx2mc-8)8xxzC^|eUjtD9o z(22$1grZ-KE~DfT*VgC3Nz6Ea!fN;8th(nXXjCe6X)4ywd+iTeMVs+7u3CAb%i-vg zYa37lK(=%DZpGXssvom5Tj1vQ-4&C=R$Fm4DGN0BnzsWmNt4+L*}sAapUn#v0m#=E zqXFWfvlIOW!>jFcfkk+){(K`rjx!J-K2JY@0}=3I5mTjhE2 z`V+^;A8#U33*zz@d7szmbKt#vD)| zcrTFoD~>Fkfb|XGPo<*m`R{-PN;J2aZOh+~`qbfiZ6PyA3Baw*MW}*wXqhm+>HH-J zhfOk8INu627O+8SA)jn6x^#o~OY}~;T-dl1MK+?@AQLrIYJ`rEiD}sXN&=V3$tWUV z#CK5ze<6y1kDXS*;bA9(J2YycU#V75KK<$!k;^p2lENgyjooDO8Mf1S(cRy_AA&RF zd|5ZYegfW9V%>7+0}OUg^smL7+Cwuz_&sp!A|Qsd+hs*Ktp7m~7ee~Jq}>S|_u;)7oQ6_Z`50C!KoDsH zo^i>yEv&43D#DO3q!*EzNz*IfCbb1JKYS?+2*4oQ^a^EU#G-2n?Hbz*@hhloPeRe7 z&OCFdr0mj=@Am>gK`55SdCwR^JVA2#k0paz8%gi(sm>y4$79OyekFF>1rYrRSevn^ ztGk$VD{R}L{UvUks`V}N} z@|FO}ty0*5CIx{e18%5;V}ul}1Qw8z0=zVfv=c8@f9ez2PowUF%btGgd2Hmxpc|O% z)zBm69Y9kb%~n*hX1Bc;6IHRQUx5uGGZY8|MrK+-!6u69KmYtwYfhW4`fyPQfWzJqEf?qXsH)Y%_{h!qa7#P&x}40H-c<>CLy*>aTvFP9ROU3^hh zL$XJH99L8M?s|5_k%z5&R@2bz5)^Ebml4?jmF@;6kA5FOpWh~Z0(15hd$7c-Q7(u3 z$xRBx;e@;|W^-&Y6tHQ}NSV1#6Lr6Q(bW(WPcSBe-}Zo{ zR-M7$uL0nphzCVRMjE^giO4natsbBcF{#UG7Q`HG3eu@Lbgg&=KJO$_BJx^Q6}gy0 zwIA*251-_s$?mTxU^L{6B{P>$KobbAeWn8j6<;b+$OH+*RYLY3TH=3(N~)JCU+Q2zMg}-zPn+1; z#SVF*0jYvLkfL@=iB1RJ|FFdbAK1eiNa-g6S6av3ayrQ$9w2)>s_3X+?KH-znBakt zX-Jf2s+-OfT5ZILW0MaLM?ByRs3by&n+Wtk9}xW*1?G!?aF{cM0}mU93I^?aK`g?n z(ApfpKu=09Mor~w(@U-%aP#~THDbUAqaQDtzLtCqL;60$^!$C2k^&@Y4TwM!i)yLn zWW38hv_!8_L^m=pc%k*i4A!-%w6Mk;CLReaLzv?R;jjb5OUfftVaTMkAn#CYrD72A zEh@7QR5HvuSbk1T9fHG+oLBhIAR1QWIwPAFqS+@)<`#B}JV5#db@|BqhfZ{+a5bEoN&9JqNhfq$s24R7jM)>}4* zb;|g`8!&IDYWN$VJ9x=K$ojXC!)%&gJN)+FvNxWXfulDG@C7^-$?sf(#$$W>bPxJs z^jC!Vlr2?ZBir_c<%xWEqmCbeVb>|uw_jOVu3*@vrG>piEUeV4l^^|@9M~6;VGpfY zs_AQJI3{1AR-5!d9Qoq*B%eyd($X^JR~;F~fF#F1D8tCUgIy$1IF*{t2slq6Ow}~^ z3DM@yC{U7z8v{Mt1coNxZ@w+ThJCAt4-UynfTfVUIwwRX4S*u8tsp}{&{Pr|BVjZD zP;s#ZgM1RMGe5^ZeXY*nj@<=|!cMQixAiz&A49Y75xDnrOuUf= zwm37%e8x>AOa%0SxCUu@$#IMf;m9xK$d%Bs3p@u*gOJ7bMRK)8 zI&1fpb zB_TRrmdqB-oI z>+EnifXUg1WG-E&aQ)@oo7QH)0{25v#~C*#bic5PyfidPB6No0?3062$}lXv5G988sX+26kd${wKaPI4B& z-zuE4@rwa^aZ*p10%%~6q&CJhFMjspE?RLA?jXX1b?>v`XxD5F51BWUV^mdAQ2gD6 zzRV{Mjo~^xQ>tISyq5Srcm7N=XNTnW2PBf>m4&O;lK(mTGH@&KykcoY+;baNhW12~ zwFjd^=E5YD9o~%s;SDJN5zq24URfWRPv0?RcG|H$$8amkxXJ|2)ACVu|^KnVsiVBmM`e`i*!w)bSbCHb4yyl3%FG8lYE$x&8b}l)QQmCT-TxK!aw4JnZ_i^tO8Y~bo!S$mO=BHCtH`-a!u^`Bc z`x{U)-?=__Qre%l!Tz<-#t|d5RB*RYQow8<%jjF1HuW?5fMNeTj1eg*#W)ji!WM8%dbPa3NX6paNdGY6&l)$Yb{C^iVFhcz|B znz3I+G4ub-gxDLx#$kdRzilowFYq|p)&r@~CWV%MGN%{EJ^4IwwA6tLiXox!M#-Y=^@+? zVq=LbPT{h5EysU;GA_$7y8){PCQCxuyp_XOUeWz{uwu6WCN<0dJi$v=KPGR!zgWV+2J;KG0y^m1v;`-iTMJ@ z+tbQ{cMApR3TsRv-60HYnhaN^mYb;j{m}osE;TmFe`S zO(CsjI<6p<6R{jsCN85xP|v$B@V~d|DI4w9P&!_(oC~C2k7N*(QZg3?Ql=ut6`7zc zqv8>B3~H*llf?gBN=B~T`mrswQ5;Vs02Wqb)LS5m_2g!)tJl!kkV#!|Car}M=LK|q zc3ui&TZxl#UKVCCV$N6e-$Z7O5%({shy-+AZlAfZ0{-t;$T@?Z))YwTf)Egc;V-LW z8KWVHMY5<@;5F{J@rU@6QFIYfJc*f;2y2PXe;+vhSG|e6=37ub!e+G$S1~Hv3TB%$ z!?OL>ojF0UrBnil0pTiPkPw;m2akJ;Fflm55C0|QIS_!-)vM*rZI z5P<(^$k^L=p8wBnhkR9DX*&!t-kvlgtwUrA1?J-6J~@M8$|me&BI#nnIEfD+kl+U% z<#Wn&)Dj})AtWnyk%W%(AYYlB77&SM`y`b$sl3y~#`Dm()! z3NkX7Oyj%`w+#7Z7LDmW^n zs}Ds0M^Q;?C`QkC-?~Ku!xk9Lc$ay7XQEsd?%lfWO+DCYg2<892iGeEGJmiL5v~t7 zYI#qm1~$#BB=18Dv(a-IvqaawXT)EFU+;*%_m2W)JOoJUuR#~=1CQb3CA0tdeexI& z24CJL;IRk2=;@MTi^u0)lM-ZidG8rAz}l(|=uc&$6MP+sh_wBJ_-p5UyVm%esp-_W zz5^yP-dcbEmcA0_@%vUiZL1QunUWXrg`O(WEgw0hK}&diU?6Sts!%`C;bnC~K(U~n zjf8yKk3{&b=O52Fe|l>m)wfyV6k+OmzV`3!d+lBoxVjF%MC|ef6#rh&aPCFF z6?;u2c|-Fru|z=ZHZ~ic(t{_R_f0l`rh*(2?HShxF|i8=vQ^PFMPH5sy4ip4tfC|M ziTrxUy_G@yb$jUleAn-=xVHQv{2?aiB0oY*XlSUIi%GdPYdfx{qOz&j!^5MfTY^&| zgl@W^c-^1x9P;fT4!P-rO5y(9uB-p&li@b1fMN{BCK#rLR@HRZfpv_hJU?K*^#+nq zuPVko7GzQi$|?@cBDzdy9LgMPK@MIYXOf1tU^=MBR&1b2jRDv^+T!UP06 zLJ$BNdqek)hiVEkE}7DuFV$9l8rXXd{60Y;@JN1uBv@sG`xeL*0WjL6ynD9k{1|Z% z;2uOWo&aTB5*OxRlR|uB=x1bQ^~WAF>1m@LB2OBZVkzQ6Gk|f}4OsFdG6q>fQL<|1 zgg7#2APLVQr6-Jvr0xzg3~{M}<)We>;vc|!Jose0E>iDA*QH3d-f=%2ZDhd92?7+{PwqC zjBL$kEHX4OaEP_ti|OCET?G-0EszZ;yfAEoOy`B__4vd>w7;n;dG|xFz0w{MQ?}pT zyEVXH!xsWX3H1o7S;5|32otKwz<4lOUpz)W15w+Pn?z$0Up(T(-X**TdRgZdan9_2 zuQc*P(${R^Au3Cj?&DP;yGV-89;v56aacjX|BWYQ{(XqQ?|oIA=+!=nIaO=`+#9(c zP{{-IkgN9U!-ZQ{-w`G!LLj@1-G3II9@mkQ1tpNJmF1TVW!ua2%=t`yIH>L+v zl;=FIT~0(E3}wS0JHSl7|k>~H#;6c>q`pLZ$h>HNS zY$4PETuBr!aPCimT;FDSbQ5b8tOZI%7h-^m$sYnBPn;!!GN@u)2FCzR(b(eGwVy|I zdwj^L>8E@-UpC^D7&QzGFY&skkfB>wBpLhQ0Y->X6HRR2(m#JIcD=yU!)@}IfPDw( z`U8wYFF`tyk}PtISn2;v&H*P5RG{(edO4_>|U%oO0~h(Upj3MQ=pkdaTPqcmSWI|KIDYk7vVHh!NVWhcr{O)ro%+P#I@a_5 zI9ydnzyrR*vQme;x?(2e&z8ZUZM3p&P-sY0r%!PJTJ`8CSdYY?WbhjvlFH-&hK;uT z0vImrkl<7!Fc&N#g$Q*cc-w}e`};UnqW)eJRv}pW=k`2BMFtv0QE#9B`W=b0i-MKU z0kOf$)Fp_?IaV?0|6cv;!~>TY5Kxs1nl}ctJK*(58V;sc3NJuTAih;# zAlxQ*{jIvLI0KI~c6I3$?v3F*qLbh8g`f+JBc|&fOM0tVB86K80nu(wKr0uhxTvBM zVD&%eDT3XnwlGnUVdw~o624fz+PV?%(-cGjVbl)I*9vo^+^hG8x(m{aM1uUiU5v+- z`P0#Y&M%|%uo$AHg@&oBtU(Kk$?14cgO(7@h!d~kq6L4d>b9)E74XZ6j?qmhB)E+R z)9>*`9<@fE*z)hHcgLz%hNiFV#Lv^V5N+J}aWZBIsWgA6JOaV}aDW)(bRV$t%(bcJ7{ zbn+=|q5!6n7{{6SCaUaj*)GK)POjfw+k?edNtkG`N$c$3hkl8h2-mmBQ2hHWwD2ry zNk$h@VG`hhBl-nwgbZ7@M1b46&?}GO+W?aLXcIwyQ-FeNxHJHwqw>$C)(~p>XQgan z?>uN7;pXDnDJ@Opa+1Fh2EpADpp6l2WJ=12EW`OTuqRuc313SKQr-`f#)|d+uNq?O z-a=|@O;40*9uC(kus>D<`R41OIw#I9Q~u|ZZub4=m41&-P!@JmoVV4Wh1Jn$kpu~- znp?54+lk);eq2oHrz6bC_Lm5`fg`zodc#co-Ik!oDI~^X*sCqQ^R4zN8YhXe1Zj z6?N_948qLcL!2S<{Nbx8;GxfM^TeOn)ZVuG{|a%08ISd!SzrR%OvOm!k(ERAO5}A` zQK-T_M`kEvOU-l~{kJG{3l6>u0oO-tB%u9{l8F-W^4AS!`5I;^ukPp1pZ(DEhV9YYL=g-k7Q*UBJV&?j|2(u%#b~LN zAg3|BEzG_-f_@CD!gUZ)oS0pJH9;u7Gc9rpQfb1IIc;GYEokk~ z%O6F?n&JfpqX_y%e!;pR7g&Ao$3aH!%t>TU3D~1wsI2Mk1Vbgct+L)@Ltln5T(_zqKm`(&yHwzC4dC#Qa$e_!|x zYDG2T82(>|t#%IV2AO+~3sm+Y5VeNG^w$*5S$i_v#T~^}=t8d~Tuq#klDbx4hl`rT8y#PcBBMAo33kw8Y_$nN&#yo~!=do=RCetykf5Q&4R3znQ7v zpN~N89iSD$jqKzOoD1r%9n3lGj{P!Se!XmsG2R{4HK-1YT;QbMqQL5H|#*h_=3LQHifj1Y$ajOK_G#G zsf0llvRLbJ>%Uu>QzY>JqwdY4xorFP(Mu{6g~*TwiX@2!B|<`mCR1i*ibP5%X;2|! zW@Vm|DMQ8#QRX3}C_^QL3`IzpvX8U-dEWPV*ZS>$_S)}WYg^BKcR%7gT;KCL&(G)h z90S<94E*;J3u0frrV}n4t%p^3Mj( z5}+*UH1TNsGGI(DM)JV*79>LOUtd9MSy*T&;J5^EzU}+M(y8E{P)jYx`3(v{Z!=7 zfI5=-`=4J&*OJWO|M)V?{*Rac_fdkX{`>X*eXua$|GqwdAM5}3FJ<;tpbJ}oQ<)CxtN&9 z#S0dmCyyUtTER_ziwUT;Iz=rZzpY8X9RHsB?Y9em9I#i=vDdIYYk$Sq&YU`CY=6<( z*53M@$tFj0yUXWnFYFc(6BXGnya}!xzpyqenQ07&SK|FeFrQTis7P??q!M+ z`1=pdq&2K^DYgn_`9qplBfougx_E4Gk#4*|SZ?X1Z>sAKF>m8)Y(^SbGTp`MEWsMSYa!~Xg1-yyaE&!l~CnU z{FRd1Lkgu>3iBEYzrb#?L>3EN?kI&{()9R$`bz=z8ZIubFMN z{<&@Jv@SpQ6Xn6g#Kh50e|~aq9WGz$QtJtUqze!Q>IH&=Pa^$Jw`q=~_2C*(ue9g5 zjdt$a#{u{@pufmWCeeh-T2F^Q3b9pp#{Ku*yA;0xiz5<=v4*d$@~9PQ`iU?b=f=x$ z$9y>+>?yqLYIqoX;iA#}UM!HN0MLYWT!w5$`|Ho=7<#mXz%Armx)Sm}^i(2S8>68Ux?F_!oL- zQ_6qqDx#SMbYu*uQ#>g zUU0Vb3WMLGQAiw%t{Cb7ArhOf)%k@jIOK&*m+IAbg+1tjm6_j*= zW?>0Jax^mfE=Hf(n+QgBcyfIz#)~j}Pdz+HXlkel-5k(KfStfm=p)}Wj`{P-2DBnM zMPr0Y3lAJh7RCcaC3p4e!&B`JaIK1qiv!nGit4J%>sdTlG2wr?eGRRP-K8_$;eC~$ z0E7-NqCGN(U5>!y4vHKzfVN?OLg34&5lZH|^ZH8XQf+H){Me^^-);uPe)&zC{uHX~ekr2L){wS}k$! zpco~{L6@H_aU2LLy2%>8sLcltt|#&pr{6mbp1n@D(qJ2-J!4?L)8gkXwHyOJ3mEpT z4TYS{@=_V@-IY;i6LULG0Tb5=owy~p_!)d6-^LrtM(U>2N$FMA#*zC6K-d7YprB64 zi*!gB7#L~_O-4pjwH-8Bf`fc^;uVf%SXJJ0iH{$zkn!{I$a4CG(co&?9{q^mCWYC1 z{sjdE|Kl;+$52la!!YF>LNs}NBIbL%y6$7kC9!Q*#Q&=$Ki2bQ4m4+9(w<=N@0azKPU1h|(v_pLN6EKIJ@ zr^a(IkQ;b1tl6s-60?uBEPe5NCz2j2dyIGbN>U_h zytBmP)sJ7lIMTa{JmDQ`j4n=~%jV;g)v%BxefJ1B{-T}^6+2IXPGIxH08xZ^1qJdI zhn_xa^#Hm3ba}QUqkJ!8mDoR5V{f%*LUv9L8B`hXQGAXGlL`D8M1(^2cnMI|r>R=q zh30}=f#shfZlO;xD!qO#`{TRDTbR0)+u>diyE=X*=mxkcn_rb(xSk`b2{o&6N2kjc z@LGWW-OCc`Z#;4O7^4TB^R~MGe#dK$d>I%p0h1g1d9bB3K0lU?8*P84Fk@P<)o>;-E>;pyJ! zxRw}z!>nwZ=3B70_1-+>1ZQ~}MTi?4Z~l&JTu{B!%Sr2!jXRRNuhNK2;ox_HQ%=6C z*na2^4rRVy6&IJkqnHsudaP42jpj(!*!hx`Rcgl#oFhWH#n5|_;YWLc?M5Ow1KW@ftqfLK4csYR?;Qm- zzwuM&%*+gd!YQ^-kFYB~`F>iRt%j2d_NLWu<=#3CBit7RSB03dWg64QO=4nC+pkN+ z=`0}%#hx6t*zrk@BfH;5M)Cwr?-HQ4xcxY@eIV75MmFg!FE&n{jgpQ}l2^QY)76dJ zUOvH{33F%=#qxEcLGOxN(f;P>dBC!SrG-6$qR6oVuM=k2V32$btTC85l><{%J9(0l z)hiwY3Dh99+Z+I z@eXPIKTx7zLD5jeIR*QDipWo|_*yO2K&oW`{ML1!r=xh+T=7hq-a&jQ%!Gn7Mk?BWpcN|7z zXcLO3RU^6@GC0Bj)LX+88|p@k=bm1T#%Q&DbbZFJ&o60tVEZWB6T9X9OqDEra9?M_X0h7kgyt|t*L(62aya8Wrwc^u7UpS zSXa@Z3+}Dkk5`Q!=W%a&6GE=6u|~+vXUFyTEn-aUQLXHE|9+@uG|U{%09YWi8c;Tg zq8VkF>Yp1tgt+^qzdsHeDFWEaga;#`M5k-u4y{ z*lKL;jT^SQ0oecwd=G(5^id_jfzi?T!4gE(yk_H#set(RcG^~cG}}104nUT$fy9Sf z>f@Gw3hqWLFiC~PzE01^f4{mR@M~h%@ooWWE^KU?t$adF(T+cH zznh@QDW3%@yu~7fzi`+{#Qmwo{4_>ibm^y7Qe^7Bby(i~+koxw4(&5%HeH+gYJ8;- zVm1WwSpB**y<+9c!>FJ~2fu-j*!JnWT|l3teO`?Vn@SbK$r6mSQMMAKqa`2oFjJJ{ z{3i8|3anIL9kbWOi+2SLL*SWY=rCrTIZ<5H!)PpP+!(VlI1OutBTN7WS#1F3++_k~nQ%hq)AA!FW>(_qcJ#tUsh>q1F0x6iU zxdS*OY0dF{!g-6x6s6y507gx3!J8_|)MfPPt;L&w7}b)wmaH%(;ilO>xnD)tlqsKk z1@=>Oi`d%<={BC^{60ZJ5R>o=9^|`l0_F!!9@>8vyy_w-NWwqv6c<0$K8Dv4*znjD zq#lo(?_o9ZAT;TbGLp6z7DV7pqH}XF>WV$er?Y+y&CML^3*cL1teepC6~vjLFWs&@ zRh*m76aT%H^z_UN`oO-muE3*7A#-GSFoZAjIaC~^{gCeh^eG=it2+U+0}@?E{K)g2 zu3h637k_v5T@W?^a7<_^6rA?~m+H=&Y(lJU<5Lq8 ztiIoDepkb6%c8DP?aMVEn(Qxlt7OMa9t#Jz1ys1ZXv3>AMpiX%uW~000%?fzc%Q)0 zix*UT;BoRWdpqj>oaBpQx=VQ3K_}jT5zl+>>IXp3e3FvupfCB|>%H^IcYRE_2xhu_ z_uC%n6`E3B1T0}*zkY|6K2mg8h}s-LHi>A0&WpJoSQD=H!P+*axg(4Ryd)=3FF-snu`wxLrfSbiij@>O&GHcg&0kMD?xF0eD}iR z8(SyICE8-R9eyzn1K%3K@A2~2{X!drKh7tXybMd@4n5GbvT$`Z6=o`B;mi7}&8WCx zQl({SERY1Kkq%(Xb;kl9{aDO_`+V)Mo%;F07$$q8+OQva*O(D~AA8H1P=u3z3x80! zg>gtJGz8i|>;s6k^UX>JWE(gTELuAA9u#{Jk^-OmJQBvUCD<>4ZMemojjJxk3~FtQ zYC02n*sXtXun%v6q-~RPAAJuQ{i3;tMo1pEg0F6RZLWf#4n$Xyjh3Tz0PxlK7lzT= zUhgPQJ=?{2D0$ZDD<@ zn$uWAD9UZOBX|_JPU{I#X4kG={t*%0;Iqa31EB!<;lzh<_WkFTc(ZnPKPOWw`3Y=i zrtdklluWtb{C@=I{TFN)w@6#W=(aB|@$^?Vhfn8;Nd>rvfC?{)jw$~b0a7%yL$-`nTMytUm_GQw=1Zgxa4PhP%M zg3j(;fhFtKEa8{idnWBYfcenk9xi+XDxF zMc=$OHZ+tzP@d>4=5Sw(!a)e(NoT`l`z6;*^)j@C zeC>RcdJiyo9s1^5=jxMnIa#xaL%W`it(0|J>kE$w+C=VktlW~(r`r56a<1O84c#n^_=TJ@hS3mvIt8pc3VwT6Y(4Pvo6LSzhf4iV^K=$0*_~P!-_i4+elgdin#%70`Uz|VL zIh8+NVeH=_N~aR>XYHr4Jm(u4Wy=#AtJjw54oyFQeEWT@N1$1eTgGqSYgu~VxOW-b z*Q~Ie`;?`tU%p#Yg2OQ7I-j@64#g{68<)sRawrGxw-c*avuBBci=aerWe30U(%btE zGprFcNYQc+IdDBq=0rj6krpQ(yEj9xm8_YzN#EzZv@2-+FCj~Z%l;gwo^So0nyKiH zk-0WC-;>!;SkUiyl69cpXkn^RM&|m^{6d#s=7wA6YrLiY^zS_Ve7>)FQGF+qUiU$# znW;MOv9doSgEecfK1fKAn)k2N+5i5B=jEWPEZK&ozdg#LDve#0jN&6)m+&jq>Aa@p z;`%zWQa)hW`-;cwZ7(Zvh=1;%F}yo|sB$n+$K`!|JM~>m(b-4;z5TJqkb-h4hHpWW zP8+VQi8Jl`@Y4MO-6U7#!KW#VeAcf&Bz?AH>s_jPev z_f9cC4gccL!_^YZ@XeodK0AwL>8*A79mU_KIP+4*`b^g_IMb>{s2pY5ZKQbF(`36T z+gm9+j=EaKi8C*cxW9B;dg9*0T^6F%a_wdp#@!D6;(o+c-1X}#pLl>lUh1UkhEk4WLx9{pXdh?K(J#!q_QH{3U zt|lo5*qVG81Y@SMvmpQ7bleci~pPF@lAT{cm$3|I|rxoe;RXWu;=ah?HuGPE6@m z9fx)vrH%4^YX&`%%YP7$sb@o=_!-SI(L(flqe819WOXD3{`Ka*e`5!&2W_Z$s$JRZ0)A~GY zq%+Mx+g_z)@wR_L=&`dWaRaq_HQm<`rnh^WemLi=&;D<3&+N8pqj{RT-NAO`r?_O% z%7E^BRu0D#m3G#CKQy!ciP6!=VX}T1*$oGWUP%l+`?dY%`;}HVPp0P1AF4}p4pDCy zd#7sHovBTCGw;n<+XfdkGdAr7`|rH-IVU(83VvO?KEiafL@2M>SI6LN(JGPi#tX~K z^tRu;JI0*#YC+MaJ)^+cSH1Yt`p}Y_nOatKzD6nC^x+G}GUNK+dT8HEygQSrzq+J4 zR?ABBZtR!6LS8r146Ux^>AgSis85?YcjnxAotfb)=0=Spy=6Gl78hx$|2coGyh=?j z6Q(Hg&;M!uDEfahf9%Ago&7t1{OKf4j?4eO`Q!FW|22QqT%$clH@=f|Elt1~!;`5x z4^{{Y3Vl9tD19wQZpJh1Z7MCj_w|z1i**W$#^lm-zppSktv#M+ndoquh5gFXm#MLL zMgwOSUe=XxWe2|Ur&S0~VAAYAFRBFBpJy|<79?-6ObOJj~s+1 z4IsMt{2WxLlnat?e{^ODSjtDxABV*}`q8V1b}BuL_YwaB;!_gF4bUMTeK-)#n9{?n z@<54^B|!cHSip!Rj|4^{S%{8B>3Q}1?t=%T7Q${JDEU)o|894*;eB{gJU;{rw9eN7 zS>Q+Aa&mS<-=Fkz1WsJM(20TFI3k{_;3>|csjSRE6psi40>whPy9bw1?K3bi$_3x* z0ft2Klwe>TKbUM43jD|#-USqyr5SaF*f}`#ryEL}5-&B1IMaelNr|FBpY^<4LMmb=rezeVs1F`A9N~l!k{^Is z!sON~h+|U8^*n>0aU*U9AO+q-7F=lsx#F%ce*W;`Ll(@x!1Sj1EO4Yv0n~f{s3_mj zQES-RD|Yu}HO0s@-ya6WIOu(TVnQ7lr@x^ybh3%?Uu-w76j|UE9*SucpswlD} z$Z7x|TW*9CTr_A8JOKz1!vwto4K*z7Ly=NWtRzegW(sGJR5o@IYsT^gIBw`M=qR(Z zXVIeSm(jE_IEr!%d<u&rIf@4vWrw>!0GY$#}c1ZT_7zV#r1lKF~U%5=z%>Sl&)&%>D7le zg<%F5ozN1+oERD$vcEUYm8Iu_biK{C?U$FGl;JJMG=06rQnce;*L)P8#>Q@X^!V`- ziuiC7?mZ#6;@z-7WCuG(6IgN_*jauWVtx#fNF+FL1|-oCX_d9_2-qMbEeD801wcLC zwcbw=+y}F`C1mP*_3C~|H>lv}w*BS_K#Xzy01GXX@75}!5Iq;@Fa zi4`#JeFD22$V&{3Jtvi-h8IlM2-t$dl59p`(YmsDSNX)mY_zoz(am;UQ2YaOMUyFE zWMZwnMY6oWjvc-Q@3(ZWiC-=1GkjAN?w{ln11P#8Ms>lJ9zGv#Wc*^GnT>^f^xV0W z(!-`;x;^yWv1gBFe<1)FyBeWg@jKRfhheT=8?`w==^>7z>_Jc*!hcLpp9MpI$CV2R zp>Y0$2(SJP4~$-_+dHU+!Ko~#ISiKH8k(A0t~6l?Eg?e}qy1%yy#m{5dp>b^(a^{C z?dIm@1CR90cm|+AQC%ADin$E7kJ293_^wMDRI=ZynZmEdfB(0TvHb5iKRf;5dIOr) z6CR=ZiZjWWG_+v~XOh9R-TtG-R z1;<`=@7@r}OrUj8zxB-vUvJT7#5=XHZTRyk*Zl0+>b2I)b{XQ>z&Oa13-Bkynl)>D zcqG)jd%hk`R##0`;rXFk?(y~b#{T@)kisR&G0esQSU%%?r(B?9s;H^CcjIW#vH)C` z-!Q8o7F7_y)8nYXrrUe42`6k;0WvV|1_rWzI(^s*o{B0-cC`Y&@Q+bZ9mL-IbWqx9 zLjL;?ObT-f_Rg4)^c*%0j*9knPMlgf^>=8YV;;4U-hTh<_T?*Au5>)$KRR}d1EdU> zfB+U8QGR&o_a2BhklkJ`JQ%Xy&v#c#zrMVuh$Xc(P{2I&x3IB%Zc?V5et2+Ups(jF zL(gHT&u^k~pj?2fLfo?7GeN-95pyx(;|kDqGb#nZjTM;JH90>a2C!d(PEcfC_}8e4 zo7)S+*X&!`*JqIS9U?6td2NS&qCy*Xw?aEVb~d)G0wUkdj>6#&{?ZZ`#mR86ojba& z`&y1jE+2XjFh74is%uI8Y>++2@i@i2Z7l)^R8O3+;1D22Jk2d%k9Vo7UJuso&2eRE zi7u_TIj26bBKD@-@4YaU#whJ2PE4GT_ntl#+uCxfU=uv#2gz8N;E)(UZ2}sMQi((! z)(_&qA@<(D3`Z{nhl9L!=Ew)AS20Mw4X-*LjG*p95(lDh#f;?JPbMu5LWD|zEy!tNoZ$JjdRS(s8lh$eY{ zpdEk;UXD{uO-IKA+5rxD*&TwG9>kyur#@{5hQ^N{*Haha*WhS=FvWoV8?DRtg-yG6 zuK^E>+jc_p?OYx9L5Y}UC`#Ij(lL@gu75|HS9BBZ6iK`T+F0w&tB2^#L1t1*I=-G7 zK9Y+CPXs+9Bl>aA*Q^!Rae*$v7<2?wk8Qu5u#ky00nACrGz=mT5&=e$i6UHi*bx}{ z0(!9*qCVB%hVPCF<`)_6H}94t4iWh_=k~6F%n}E`Y6CsfR(HHb*s`v5nQdA00SL&Np@M9gzLxgmSFesu@M7v{`9Y2&9G!P+;#PK4 zy|(>SqUQ7+ETZNH~#{Md z-NXeR>Wr1u3i$is>@tCg(`Z{8X5`b}F1&X3_GY%-Ww%cE7iM?4L06tuvLfqJpW?K5 z4EvEOt_n!8+Pfg7IZ&{*p`l?VnTuhoxHvOmZZww}tDA~Dc4s#+AGib74{4+#`%teQ zC^+_|e_)^&^G6i-FTkhp4+yx86@!p5lI#jMp*yg6CnW+-qwMCld$x(0v2V+Jlw27Q z-|+Yi>k;Vhm&N{Jx;MtE_{ik9U_sRpnUhHZzk)#YYr6g(WVDp^h8hAOYvJmh9gBGQ z@M2)hu#kX&`+5h2r)07p4+u=>j1z464mVybUk~sX;y~CK{rapFo7ziAsPXY1I&~_v zv_h>8MjGgMYG1uFdw)rAv%ckL^d>5qH=`sjyYF6hrQYPwWH*_>g!kFrg@!0+*sg&Yba7@1j(B(_sbMUVmb(1pBaS@27Bb4V+4Rt_fAU6 zHjJx(fZTx1)C9W$99a9du(cVpCG#bQ3hDJKg`Y`rt_TPf6%rQyl8!Ai@^ydyo^N!+ zU(;f?2nh7%G)tR~oyI5~gS+(H_jJFb#MN)M7+{ZPf44N_IMxAiWueSzw-7z|)o6i` zBX8cm4G0Nwg9Co>qAdF}r%3_SotjOnOYMhPC7CK-zKm1veN(kH=K39Q5qr>L*4EYa zXb$`H8$O_+NE!@!)b_$5bsXC6Wo0swRpk)-4sp~V6$f1boot6zA&FJ?4H@GU>Lqm5 z`n55Cx)&E>n!eQq9{VsOGHLO8QiRNWpmGDrF6uIZsb(Sz&E)rCl@kkYbXN@S#w5QD zaZ0`LywrN;_6;3y3sSm|r^JI2qB(mnoq5lWye;;go1g0VU~~AS!pYL8m1w8GH!mhQ zX87e`E{pWeoOP*q_m~X5Y;9%3siIw^)Ch-sz5se88EA)Fk_$(bB|J_gX;B!vx}p4; zof@KrpUWQb9v?q`Oekk)2rvk$iry)|!?qL>EP70+??6rqnFg^6a~u3Q^p*~^DPHLN z{R0D+pcp5S7>J!DV`?&K$A!FY%sH+443D45eqf6IFxr6P9vBiX-weMAc92`c)kF>1&S=guGEwTdA@*Jex0JD`f19W zJ2A#1*-kFm-C=uJ)D>on8yg$7o*lbp?q>Sb;j?#kOCnI1< zN0fF+$*D=0M1FYg%t3*`g|2vYUb7RX(6~RhUikoL{E_|n)*W|1igbT|3lzAk&dzDq zn;=T|#u;(({$Jmm$8_k4Ahb3qcxls?%1$s4uDF)m)sO9X0@p%8>v)_lV$9#NWmezMQYf5r_>9 zizCG$yDeRRU&!g$8gMgO49sO0I4-4tbY&-WQE;M)<23OjxeBW?khMI)x)wZG0NElT zmb|I?_U&6+H6jcT0y0@57@?vWFgtj?DmDd!b@;T=3P!YTYHhpRL37xF{ftm~tYR)` z;G*Ca7KRj{aDD;)-A^{HpU70G)@6}GO0IA8;OOY9cM-vD=)%%X13uQTbt#uOIt98o ziMT548xD*ocLT#(5Z<*!JSplexO+lHcWG)pT}_ow(>hq0$k2Q_5(~SO zyg#3Jlrd+6`JloP&*B7u=T)rxMBlU87PvarcF!Rdck9ZV9Kwc3)tBlBMWmVt5_NiS zuL-aMV^BKpJik9Gy(v3pP0_B0^2N!?^r#Mk^uNt?Si}%2A^V?v1x-mfwj@b~K29KZ zp#X9!pt9e;xE0s&-GNXq0bMmUwH4j2t^BA@ny5H{kMrhrhrC9&D9>NBiI>Mb>T zm1R-UV9G#6Mfl-2$=wscT#s`D-sY0fCbP=;?e56rB~@Qz`SrP%0}ilx^GTUZs|J z2^-wAy5+SewW_2k`mWWjDV_^UKkt)xERR)mP^P#DWqk!0Xky3~3DNlcn^5oo=8=}- z_4^hrP_Muaa>H;w0SLsM(opLSK~YiZ6VJ=|rlgM)=jO6O0z%xw2H`RF5u3UB>$D;B z^Cwp*znu+bSNr3)A8iH97`Sa-(*%d?4f^IKZ>pI8Ih8FEI+Y@iIf1o)g^Wr6`mS}7q7VqDO zXP|Qb3Po9B$*CC!am-Bn%!R^?j^uK=zl5De$AMgRkOn}KNqW;F5MMmO&PwzN=yv-+ z`Qbh&vva54<f(S>@quFyWyde0=@d*pf*v~lU#!2!9gtm9=N zP5sKMJj!KRN~%0R5!Hz9snWJ=VR?X(E7WswxF{}XC+)T;xCZd1dflLyGd3n`Xw20C zY?RRM!>0L8n+~-}|G;OB_+v_q<`*tBg@vn2H@Acut@(ziuzFwmD=-|zCVUsDT6BCW z7tZA^ziuzWCBy_Nt0b5Uaci}q18R@1)3x@is?D6Etq*(x39uv0!;lbmIBo9l77y_~ z3l4VF^GCMU);_TOPxd*2`%f--f2t2IXmaW_&EE?u@8x)@bz1cgj-&mOt8kWNz7euu zmSYSWontt??)H$)uo5sK64}=zhSC*amv)#|H)@`{a54CtnUGvhw-L3wE128$zDr0# zChRMS}efcDAb5qU7LVzRaF&SG9;bL zP&FW%*sb#+3Xn<*&xCqxogS%n|Jph$2Xg}^l${;Aliiu)qha4z%%T%=JQq52VQr0U zbYO%Q@`2y%@H;SbUj}}z{|sjt7+?~8H`YXu^E@gYot(2waN=2scmTwk`Y$-m@U$$i zSKJdLb8D6Cr9cN1-NaNwwxhq7#353x0=jTWvo4nHD7kh_Np;5!xRC&lpv5&liDuCE z)&?7TwQ@mVu;xq#rO*PeMiYG?@6Zj|-EvcF4Btg)njMifd>`>zPD#$g-|TXjkfehd z`e%rYmkmCIQzjbRwhO;ye-8r91w2N47FCi)L>cemiocJ;x#=nTv0mb#4E46T+aXP&B0l@-;cZmLSdgaZ3s+pJ)-SY6 z{IKK+0oYk>I;t-AAK8t(x)(QdrlzL{gatH0%<4ifpGTPIaz@URJXZ&XIw>3Q4kjvCQx3ALc`A|m|`rgapIa@GQ>Y_(Jij4N{fC1UYC=_KAl z!drJyc79e*lGg>lCN|WF6~L4Gn{-RBOAMCA`yLRI(+&KIl(OSac@vuhadV^f;W>CM z__+*lYVIpx>jk%KXltdapWOI5#_P#G7SCrJ*I|zf*`)){dwF4|s)=ya$j&x^!Q6Rb z1V<`8yh)z9NaVnS^(_Nwvr(Ttr{Rq7qfPttrGX+;V{pWCfAFzo>Ktg}T8RGKmLIN!O4N6w=JFkX{Zj!=MQ3??}+dx+FaM6^gdGKoLy=MQ}Oat2T| z5!NAf=`NUFB;*JCC{f_yE@L!lO7tflDQHD-!Vk4JVGhyHN_G?c;8TWa>{w(h^z(de zQ*(%bF6@0HS^d2R-q#CQpbLc_naCHgLfc8xTw=I{uRKm#R&!g~`s{pug`Aw6ONbV1 z65F^jW<`y9h|KBVLVJ~!9MnrAV@A(f7BLIANTY}rIQq~96~?p5RL>5;SnZ!9)oX|R5iBA`7W6l?^CMw}wRW)4oH zyPyQS9D*y5%9rh! z9#@Ct%70y6Fs9Uz%T{J@j>RR&c+GDdf0|roELhTlO4Q(zCbs0}felIwdkAvx`~)o+ z&LKC&%}sLGOUdUCPXDJ-L(uke);d<&DI#EKbIzjBbk5${xl3ng3E0EEWwPK3~T|VsA@^Bx7cP!r(ht2o~ zTyv|z8~Bn@ln#w+-?u*8J(faHG-8l7x+)_Z3z`J%5|%oGn?9Q_(v-)bAa68jN62VF4bjE8V0GW?l>f`XEYN;&3ES=e8QS(j`FMSIqSZh9n!} zPljA_yhNbZ*wplzk7(xqsKl8_3@1aH(~+Js7$HcJ-WWy|dR1~$cV{WJ8qf^TDo5st z!B4*iqyl1Tvd8ul4>ni@H8mCp>QcsclM9In5cdP;N6j4ZDX9O6Q>e1CGMEv>gAxa( z%W@{}P2h6lMkWq;_)OtDatXazdfFiKpSrd81!c63ap1xfoyvdhn%!~_24m?X`x}nm zO3r{1%?tMoeAKu|qz80yUO1SDxS0R|d3k%WyaAk5ZL9kF`7`lD2TwuyLY2(P>Dz5! z6%#G+vSrJzLWuot9rHHT>^)oontKvm2KrAO#1a>{`MZZ?%AbJ@F7nrQ3?B*l9Buzv&XqJnq0wu!fh z@7;@;*s`Si=Fl0whS^wN^Ntv^4)~a$+|#Q%G<9#5YH`H`3u;^9d`E_{Pypi&yZvSo zJrEN~mw})ibu@eFut4iz11esLUq_I}18uaHpt#9?w3RSn|fseV#2L-jHl6>GMZG7i3WM4n32CkhYcX_4z~3Kbmmxu zZoa-u;DG#1AvfpU%a7L#{X<6=4iBheMQm+?>a`3EUR@X#$6Pmr{T;4k&8MdNdR8g% z_%Ik~XBRk6eSLYp>yGV9Bd$jeev(KX=nxaz80Z&aOw$4i#fP$68;S2OA+fZI;(@;c zuXlTVSJU-m%+5Dz$0)MIGaa0j`*Da9N(c?{npbzqu+HN)ETvd)Dz9{Gg`XL4yZ*dy zR%*(p9WSkD+mU`9DqX7n)TqkHhBb0Z4x1_m8wiqLRSa~~YI_)S#aalfVy)FkmZ(;A z1c(ZsGPY@GGp(->wH`V_^2vr3ic;5lI%3Og@9MtsE2^`h8T5i0RAz3g(yINVOda8o zb5Ic^sd?7kNd?EwTF5X-v7_%Vp9!rA75jwsLH6vhI-bGQ_Pm}^ApHm~kafsj!^dSp z8ar6OK^l`~>yGZ{E=B<)^9#4-7JmL7%52( z9r0BTdhclw2S9+q{?bJ*4~!J-#=o{~re1$t8Du}z)!G_d@5fC)6e0vv?)vOdQb~br zr6^;CbJ{h?zwaWBIE!6WQ%qdE8ki)UV~rRB+88l5F;9~+cz%r?IPNTy2WV|_F+1hC zI+{!I#$oIxH&h4*BzFXws;H{=3=Y+79CA!3G= zEFAqP;0EsK1!J+U;B!e1Ey8|5h9&qWnQ{@Un14c|`5Lq4zm86;cw*V7T>Y)?N}u-F z1;ej1wrN6_Ux_lJB)EqeiibQOVm=afm2b8pS=d*B2NLgG@GcG+C3N~&u33P6Ie~>p z`WEgyz~GwQye#;n?l6F>h4Rn&7T9D^fb~Rz-$Bl~zf>?$Y`+L0jaBd4;&m)+*D^vv zf+{czYR5xHcUf)9)0(B{7{OgY!0~!aNRs}totR&u;Oqma58I3#IDhYf7iWz74;v1h zJRb{bRxoyR`}o@uBLwtrspHkkuD8HSB94f_bje}epelUMFR_7k82O3fRfHA zelQ08Qw4#G;#+4pm*XhT%FnMx$HyLh-ptGuZ+RY2s@3)&|N0-e6eNB6PGe(ee5R(W zHnBHC1{(3*OqIo(f7A)M`HuaQH&s>301;H+GkFb84)H3(pptBV)%VwXV+}&E`7qF$ zmiP#g>q7DraQH8~v4guspaerOvRr|YkoZMV&4whm5Tc+q!lz6I^)&L*Qxy(`r&}IB z*bQdjcoW==vCkfHbQA{?nDWmR`=q@6K0%p*BwoAkZaEC%OndIRH7M_voyevE#Z4AW z36edJyyP&$rR3o?Ij}9!QILa%zT2!tSS~~zMI093k)h&rjPT;{`vg44CJX3nX?h8#sb~wQv)^+CCNH(8vGFE! zCVg7gYNn&h*7)wITp?EdiPcxkmT&C~IUlC&?P~t{-5ck`#+t-Kzg2uH8<{9_tnTQ@ zk*s(XHW)d{Lsw`ZLWffA?^nRG{d&bj>-6`_Oor13Xa7H6-o~PcsQBkg*!`v8zrUZ& zxjf)_K|H&_G8O$vEQx4$P6Zm2RY|aI>Gk5^+ARWO2TeNQRRd_Moy=x%yK);yz zpNla|_?Q2RJWuZ;G3b~;mcwqx>SZA0XQlss=gNfMN%D_TyUPj$y=-n?P3&agvvU#i z6zldZn!iy7pn819#))XPe7h7+4<+_{LjjE|nV8(roT3P}3S0mg?Ik?zfQW-d5S=9r zu`m9c2!#QuDNB1ZR+wHeoL}(ts9kGrVbRyHv#b(>eK^N@p*%H(Glpe%=|Sn=pJ|cb z!j4jlxHVIsac&YcB}COG;%eqrzVoJ{Ue=xXy>VE#&`jXfs)abgJhg;P9th>L=0BJ7#+U@C;FP?4naB*8fyp;R=WGG@bNvs{)@-KlgifYBRA0tHlC)d@D{eE5Jd())nV7xq` z_HkqM!F50c;d``RdgjZTpwUTGB<1J(Bh2IE+~E7OLRPHAHVh`;aq}S4Kq%e!>~=nBsl;EizwV-$?zFR30Ch5bYVW|7xhZ6$ z{smAV$3HobdH%^n0YmJzFdw`JzTm^r-O4ig*IuDL^uc*yIbQ&_w{bD18|OcMN5e%T zpz2D4B-)3^Y>$J+f}IJudMFgDF>@Nn2TYhx1pdaNrNb8qvLKmYc=#PgimY*gm5+<1rCS66YYgkNypJFlWmjJ;BCac zX(_vP3!$42&B_3HRN)4o@hE2A-mp*tu6@V+n5Mtd&xhw50ygbcpIbQQkgkt27Niw7 z00-wkf;TBmVw1*i5LZ!cuDbp8g`FV96FM{jKycI-IJV?Q-!hB2Dy^e<;d>IUzxOri za|o)~UUC8bR;hGSY-{4)fX?U99=Y4;9gK^a6a#F!;O>knm=Y|S9^Ni3y@3onFux*> ztR&kBpC_B$pD+2Hlvllv0A>Wd);8}%lqQr5Hqstg z3e3!;I`RTY0n}iXQ-wDA)#n>3KOV~5`eUF}In>|JKUBh|KEJSI8+;tDMrnIXg}K{h z9yZ53mlIZCU+fCqRL!_BI*cC~#)g@0xP>nb_BVm!`(iL%yxfHgM0^z}7x>r3n$-oK z-5H}fZe)89b7;}JSFbw%z-L@%Zo5i7nCtRo-QdD7eR_|ZO<`^Y1A~~cvduk-;c`aB zfa9QBBSP9GD>5w16F%Wz&Tj;J6ci8tuaEnXk_Y_uMOBr8%^Xp0+*Wuqv=;T@k%V*Zi|we=B-<*2arBCu2+BX^CL6ZED)L?I`Xz_;v@oh zzlCS=XN}Tspv4AEw`+byafP`ar{=`6TrDu2<`CCYm2T7Yg{k=p*~~W zMBY6-!TP4b8v!wEDHSJ~DT1n!wPnK0(9 zf9s1&y`C&Lc!9V&0OFS0F3G}fUB7yQ5YmBv0n(yt5y#;GkxCUVBn<@;!9DnMDKwmG zxSClI4cb}{*6P_V;sV7R^y8_~Y3x2)k{deR9}HJ)xT7|Zy0Yt=?#R&4xmJHZXWR+w zQ<%~sf`lfPGd>S{8R!W7&A;I4zz8V0JR960k_A5b^5DL$6tQYLKKKXCn>VIplq|qg z@{yLes6DSB>GC}zBidG7^gYd+4@Tjv#x*s){x29g)8OnRg-vv8@yI6d26ANuUWL z&`7AUO}#yzv;R6DWHfBJ073}HbtW)Eu>{Y=Y!?@H3_>N0yGKw4GSAV?sTIHX>)Hs; zU$*E3pGz215r`bkVEj)EKorNiqQUl9S@@Q!vbL)qJbRPC0T|hpXG7hC4%tT}VTYK7 zVf%%*#2ePh857DxvkiDLM>} z2Fytas%kGCjLPg->*i6M?gY>4fs6NW;u!{#CyKTlr-3i}PpRv_nL)e})XNowcajH6 zyEtstIIATmr<7bYGgyd$&NVUn(RLP8OjXzfpF);G>@#!IF|-BtDTauss9#&1E1jSJ zWP=qtimF{!8`0DOUM$huBm^8o{opI>o+8tF-fQ(tZ>f_&w!OO}8tXnd7#k%j@5F6; z82*xO{UDb`y_GkD#V5}Fwfqm)WAIh&w=rhW0$?RWrfP^#qC+v7xHCwVpx8o!0HzOr z$h51{iOG0axX;Q)`Q0~P1Cbgq9o$eeT98bu;9mp>auQhECht`vh0LU6p7(I#H2t$X z#h+Amc7;6nXfZF}M3L4|k-uO|U&_GNldE1UHG&NC<0ak3P3Q%>pVMX-6y5=OfXLO* z3GdQisDh@4WaohfWlEeGVfGF73EY_jAOiymf>lh5F;W$#big^?Fho~r)jMjIjLMn* zXB`e%Q~U&}cSt+D%e?*tf}jwHL8g6hSo6e6yd2NKbT#uq1}GPCsI|%;NfZ(l1gMWX zDVlE*4?JlKK0M{q@gV)Fyy3%_f^Q%4U~DO7Hmhxz zoJV|4dRLV64ZYusCD-(6m{~6rKYR8yyUd!>d=75ScMU;DRkOdEpk&^|otl7!#eFkAQ@ch9K$?>_j-6eF>5+;HC)4}X39rC@csb1ti1Hb97(4mnSb~;jJ?cnUt`|0_tQw?Hu#0Z z7exL1Gn`C(h){$Lp&l12&v|b zkJ!L|LnMc(9}|>C<-o&9f(@#hC*dVKVDE*ET1i>?5F~JMisdyQB`oS*+RIR;C3csDs=7FcVEDdXn2X! z8i5Q!!XzF{&+D%M3{;!J0mV2g?Gj(3gx8COx)P3~rkKS)M4k+s3_UUViNz)uUvfbG zai^pes?YNCg$DN_ck*7c1}h(cWBq08qFw!8o`mb&zkCqN9F%--3USAOMyW_mjZ_h{ zqqxMdIurKipY1k^P-n1nlv{hH9=2etTgP8zdSUeFHB>2U3^k_0dauH?DE`;8!a@|7 zT7%9w2OzZAZrzQ_JbRmWKw0ag)ET-j%14gGen(8{9g71iyq9-37ic%Yy8 zL_0IODXh~vU`JG1$$QlXd|*Fj24m8eNs`sKY~@w!?@9=Dx=7ta%eLRKV%u=v`P!rq6>u(1$7HS>Xf)%u!4&jCP%)5Xy@EMZZLiU?M@fV}iie)>Z}1vg19KwXqeQU*jCPlu z{w+i1G?;TWKOIO7w%GLgXR#2S@MJg_rhIMX=Wp=615D0r)HRp;0@JB-=q+5$F>k`N z$myJl5-a!T`uXVUtB|{D?zwlqET6RN*J`=`b2y~>-Z4XV6^fiSprh>&#_#H-@7h__ z-OVEhXE_kkWm(Li>{5hh>=X61>IF!J%9B!4XHfizFt*mKw1OFlIC|fqc@Y(y)UJ9Iv&j=_=u`EK`g!Zi+ZQ5mLmUt(<;ivSgXTtEL$d##AUOuH@&7lW5Xu6RdE8h&N4JxQ9V3 z{aX&32ha~LTi>MaFi{w7;Zm+~Wzrq|_%W%w%da+sg^(6ak7NMY;y}*R%NYlxAG71v zQj*Rx7^c6My8Z5oSrlfgq2W<`MR%23gi}qi-yYRcRu0UVkg=I}{?y5X|Ke%fKw(Xuq3C$p0B;IDSx0tGGMm^;5y`!TaC2&YE!S5*|* zP*SCKF9ha%1t{VCyq5BPWhxOp;fL9n@JHD|eM`}bfxzjYgLqap?0l~D6Z$!W2#p%S$Q&+X7@`s<&c+{JHN zeeoiu?DbnyrT`e!&!-7|A5RA$Qz4IQB7XJ)~yj} z7Y0BMdE~bwkII$byA>79%^NrTabRtS3!X{{b2T6r@>fvDe?*%^9HWRP5W)@p-+j2! zxZ!W++V6mB|G>EguueV$dLeQSKg1o~28W5Hik6-DAe2>!g3AuO@VIbP8j&^senObf zr4$1=Rb#Ka{&IlN7L9+(DiyQZQuNO?BS4HCHS1<^2NRaZfjEK}W>0--{{pHT4S|L< zf4naeAcnn2GA($Yw4YihTtZ2we1~P_qk=`iA_1mf)6O&@`Z91NGIM=}-DJdV;mP`qAwp!-Bsxd9A$(^cY>cCp&O12Cg{b=OiG2kAn#w2l!lDTbFYL3CC zGGmEhOt2{apo45fXp(4kA;3{qam(wtGuMoKahC(wgbz@<%K=0aA}(rs;=~djEX{hmB0eyn>Tv6+ zj6_pkL=nZv_jso=4k3zQ-7`4fnGWnk8_ZmH3o~`NF_cjs8V#d-1;U3TH$-N7t`=x1iOdKJ&FY%02z!46QxAu=q zR7y}Gssx`!U2*Sg+r7#+y;D(54sd}b057*YQ=mHtB55RcwIb0 zTC*9p%4E|~gW}^_I(}Q8Ee;k1av!{!Q94Zt`4WliZ&$@SC!Bs)`iuw&S?7i>7|0Dx zvQZ1+r$I|V+~f%XW8=O>N`mH9Tx#2J9QykWyp73&*i@=0*|q>J8USd(qoG z@O+cDb_xR1X#A*FabuYS=|HA44TEi!Te@3_Sn4y`A)8WIk#LZG<0U3dulcH(; z{n3~N3RE6JEsLpmJCcI$EJu5Mzu|?idzh_gqacU3e$dx<^V*7C6ET81JpEoNB~D#g zF}?C#CUFx3DDn=7U&HR*ioaj#yj3GW5mH5?=sRt;;O(| zw5dy-l7zbYO_|$fpJRz?BoK;+IYBesC}OV`cwk-b`RcN4$D04%E||fl(5CIg2@fZf zOO794jgMn+S)uJ}Y(Wm!`vXWVCPE_=EfZ|9Bo=<3{8V@S45$VLl}mlfprGBz2W zx`Dt^R#1p{D8${)@AL5McY96nqKl5e|4SwqDDWY;Qo=a}DW5nN$iZ*rX-CCl-{11U zCLzFM>eEo>WA^secz%B&64)1)o;Vr8JESW=l)gqalg6XeKpWB?k(GnccZP$uqvNjZ zmYar}oRLdt%a$!&-qKl-9b-qQ?Lf#oaFj}^T_XjHCdJKFku4@NlE-xV*Dnc^mIyR~ zNuYhFcs5Gg492uQ`}cVR8nZS65-;uB)(aqaKY!F(fl;i;WDHw9KisV!_ zolKd3c;m(m>$b0l2jg$qt~muuqWrklgfVkTxX8@cK9$d7LMCs$u5Ly#Py~+TeN1{|BN$uJe2+Pqzwx2EIx~}`9Jt@1Zg#c%T#QBN|(v02y$1mdIk1LWUquKH|e{ypWyH=oO$F{m-y3C%cR zh^KhsxxIPf^YDwq@nYF@&XflnZvaHytzIbF>4F-S^?Kt`OO~$hA(IPz@2#WDx>xII z|9FxX3M>h6SbQaW<8ArbT}+>Nmwp?H6Ncs*Bp^wXhfgB;G!VSRh8D%R)R_ntw2@gK zHEMPXj3NM{)O~$@voLMZDryD&J9q64EhpTv00d>d_hH*Iaj{WU{#>f`md&C`CA<$% zQ+)+YMFi{3tx!`Dwg#n{r4#z?%{Z8-lfIb{4lLzm@#Tt>{d;Ym`r96$y71=Rn79fc zXy@Dq^Np<+TySvmzjCp^21K0j&!US@z#o=`|Cw69?+(z(lC#saSPj);nxl!Q@r^1V zbb&>3yLS_OST#;7!9A}HQ<|59?x|dAI0l@AM+Z5$`O41bu}1qm=^XA!Y`VA4O7xl) zOqyW>+Vjz+Tvlzc;`x`svU=O+zbB+clbquw7q7oI^2>MENX{JG^|;cVaq@jP!wz}K z8XRY1vw+@n$V4UX1cRCwj^EvTA*8WYEddJn)0(*Au)8s^OkOm7Uc z^wss9z@vGNR$~3UuaEpt_R6O*tuZ7u0T4Q?cxsjY$U1C)=Hp3H-8cX`kF(31nZAW* z@Tv6Hty>KaZfJS$OK|eQAU}i4+~3V|U$6|WkQiW-W^g*n7~iC=Ia`4D(ellOe@N5v zgk$dpABV!bI(1F))}iX}uFvgF&k8i{#mrF>Riuu_6s`+Q>zYNhrQ zSKZH^tplud`pZj(s4poUd?@i?;R|Pg)d0$lcefT9kS4qNKH?I?Q96uF!4iKYon@(zv_sS0xedEFfVVfAEK&RRyPW)wZ>zvucIxhf>T#-<7JHIUaXiE-IIsR!5Va8#7ga)e8FpEo6ps_za)cJBP08vo--zB-LEya&$$ep|H z?D8Fbap1F)m`*7_kMT6nj(KoCk7NuXsASCj?JH~@fCUGENKW$Yzwi$PGX;S~C`{`^<2ZudsWhyD_O!31LtE&YE zJq7@c)2uJ;9t5O!Ai@*(WNak&Q`f0^C%5TSVK+F;6phmS^5b-uOy6Qc*4bf2YE$6G zh})3)?qCwaoVTZZhqAi?DTg@2adf9R5U}i)ujAt<))FBTzmbqe$0ng9ld6NxhA^5^ zwUZB!&=P#tU++#nrP6NVEP+EX-PXPDO;yTvVB5vM$l)dk57;)!BPl54ueSZl);>`@Ib9?oKh{?1or2J?*+ zq-JO(e#Ed)|6L2j?x1LGiglAasSosp*cHQwY0U_swUXv~EZ9Ou4zyVepae~O(8dSM z5UmhqtfcRUgdF&5N{fdZL2QtHP;k5Fpv_NB8-Jc($6UvRE%(U^JNB%x3y4kpcI(VL z{quQ=LHffzjnILHfsq>QeYFDIbxb=V?+Wgx?YVifgYi?st2V|GI&zDmDH{a@yM6hh?&PkH;{ zbGk+L>cIV1W5j7Fl5pXX$JcPjxAF2HkPD;oYL(4wX6Zj8MwDaEFp~)!xeZU``fh1|bC5@y4A`zJ+=4Mzvktb|?{2fWoU0HK~xDpAs6?aOC zNz|XE87txyh7wM9UEcQz;6@}Qg)pxjVK($S?59cxnmTT_Qjf1k;P+O?JC|BiR8)W* zg!Zp&p*=?ztiM}o4n6aTA*ClWkgn<4kUkDip{XZl{3^7(EM+JAN#^(f`uX$ozjpwV zBb0x*V6Vq?>UitsoKy{`UAY6hPl; z-K@=cp`oVEMmfgS*3m1$nLixOY0Mw>32%+Y)5`5l0XLYDd?NL=wUPoiHZ)qypoO~$ z{RWmBt!NTzY>wrt(PkQh?s6n`6A%cn$C$;;-`u}=2o({a^fJfuT9~SU=6MBeF*ztY6Q9E0dNsp2ahg}F7-}w7+8Df~W*Wzt*-kD~OqcYZF{MwEf<*LlK@+}?c&dY1MIphj3Khemuz%p*kmYou9l z)sk_(^M~+;G5`w9kTBFtVArnaI)q`Jn~sFg-o1CL1-Ixk2~?dQKOj@D&W%i5WdrI9bDmd zi9dtS1+8cu+A4Bnv>0jtFYZKiTD?AU7CFgSVCTRcG0TrpnJw&p-cUE2igbaacSly0KRMr>( zrO>RhBi`#UnqbHi08DoX-Sfv!pKN~Z#l$)ccopp{n88V@b8WNSr!I6puQwmXxE)Bm zHK#O{Bv&qhJR-5smWS16gAatz!>k}W_7%_$039bn9AbQgR__&%ov=%pV@&qoj;q;5!^mFN*tP zGb$ho^Tr8O)coQu`%BRbUFVQJ>=5^j*uMdFrud^5{fwm%D|FX_aB=_qSvYCq1=z{E z+46p(-S+%sZ;bX?R_MU4rVbzo;ho{prr@L_R$64hK_+7uCLd>)CoVr=5m=)}cfMHf z|Ct+BArDeK0qk_JT@wQFk6fRCRbKKQ{O+!GSHa}`8o$L^Nz5{M6J-VW!MKdA{S6mrGuT8PuetBb4JLjg9+e;-l?v8X{=YrGe_fIj+c^Am3f6XI*r_@%RHWqiE z9i~Zt6QfjKReaeMJO}YfU{5dCANLp$6J5KbtMu4`9+K+=0Cx&%w>j^VIMCp z)Z?wM(@FS8flWXgj}L~4fE`q1QuFry1YH8>B<;+l_dybFJ*A7Er=sp({+hL<4r?M@ zBVvtuUqB21=Yw|R@1e-(Xx^2wm{MLR-5t|msC(AV$ak@SrcK1*X5~in4%;?s(5mwb zDjYVrgyL}F&Jo5$4vrm>4r6JT&nPtMY#NeKWCUBu4om0}G;alYXgPth-0D;B!?hv_ zaV@%lbt9x#$5M%}cw~Pc2v%^qDo{QdxK)MqK5%Z$007M+An-ux+=A>?cU?Wo0hdrs z{^#hh&Qu}eBl3XDE%#%G^ceM}?z-o#Zeetm$3EV4=P5R-(TGZtmS0r7@YnNiU?>TZ zXwo^kW&*81Mh3s3=eXzpC3a9_gi(W4b>)Y1cTO;=55S>2z_r?A1QR8!9x=N7$G&q? zheom{c4nG3I)EP!ATmzLolPoNzsOBqiHN?8yVkWVV8I`aKyKmtnd(gK~5`2L)0Y3rb+Y(@HGfu%bbR)JW+5(~^FTUtC> zsOzo{I4`fu$*Id7)c}``D*%;00_!DVB_y~$dtW7pJzKGeR#RX%W+;=bg_DY;8I4P6 zAvqjGE=*4k9XYarkQy*;Mr;TtA@<@Y>Hs`?R8jF6xAdcn%X`W7M!?78D$x!0#wCt7 z`@Cd-*N!~6aC4&Rfr8Nw4RA~DDtM)o`QeHtQycZYcIYoS0(3y<14)7^Y0dpF4kZDK zO@0J@f{-%!c-aL26+~K$LOdC-UeF+m8IK_dJ#<*T4rpIsf8uvGO-|DGj!PUOWQP?= zj)m95!&zg=*@U$baHIr{fHgMc?9i=~z_j0Bh!Q!M>Z*qEf#7K?AW0Iwv!_yLM!@(Z zg**IfjiDv1w~l7*70Q?|Iqyu%DRnW>3oI?-Bt~H_<{IaIb{@DhC}UX#qn|(Xe9>!* z!zqb-L_nW)tD<)xs>Qbyle!b67V(maeq?cC&#@W|mF~bfO%iG_*Nd-ot@P{Nw4>Fx(W9%%n8zxsj7tvp z8FO0y%Lvqpn{{};6MrEr9rRHVQ0I3cTYhuTP7C0g3zWWlIYUr72PNRZe-}Xe}CY z73q^#0|TGEel6XV2GgU8UwVq<1%ay7ql_23p+g z!M%+vTepf=?)9BT>ZP$IUHNK_n_w%EgO*TsF~>#9JnLN}*&X0L5#BWs3*z(49ahxU z4iAzWsX4w*6%SL>Ehnc6)7kYuBlB)kAKG;EXsd5bYy22B1>+T|*(XCoU%#$~adp(s zV%!EPw>R1Eftp5n(;>arGbe8_M+X&usYi99WzMJ@105J<(KfS2*m;D|D;#(DVqM?E zCMW2ZoDW`EUx-S`_vr&%B*d_oq^W|$O%mY=p$h{Ja`6!Y8_LmIpy3W@=ID}7=5N=) zk;aVEjimD}0x2Y>+Av%s60!SfY1NodpuQ)(dTeD3{x&}GxX2#Rb9CU%gzSMB_lXm3 zM81eBnsfIlrT8DX?2fPsTJ)g;!@8M$?tJggyWQKP8h)W8HOu9!c+==vk3JpCwvHG- zp_=)G!V}ed`Hi3!ocF*EN#Nf44V7x2&VQ)|!)uy|EOfc!UJ;X9;5I8LBzhgku35Rh zy)zCTl21r59K2;>OZ2j_@qn*yW!Gh$vL@J7q8CE@T5wk9nv8RM$|^t#6o2V^kxLC* z7-MnHVSOgJPIp{xLe4u0it9T#HgRyc{yC$k&5_ec+_|qT7H5Y8pYW_Gwe&YIK>eES zBdZ<}7Z>--)U0m@Pm$a0x#-aZcll_L&P)q=SE?Xe zcSEL*R#4VY{rb2clvu|P#0HytUl6163Urd_iuAp zfB;b{J+<~nXlHm(+6cJFc&r~F1*yeInj(0j3VvwdpFG5GVkTl4 zy(-*1Oao01l!rMvIe)=7>l4fni_hw;>DbNHokb|-%F1Vj!EXZEL7fy|=0`AyBS%y* z+3KFbotXmdO{@?P1B25ueA;W*9h(3B)erys>}}FF0>-Z}&vmQQWnj>;+&#I2;pRBR z3}n<}~=|r)t*n@KFDQ4Ez;3sb6-E z{D7C7z;uM3LH$N*={lvqGN0&8cvs5$r^t^W|JoTQeUFiWMda_IB!5o63;G^PQv44m zEKjfLYgo<7%4K2yDx!)^Zo6K-mhthEgd%qJ-+vXc{oUdJ{7I-y>;J!{Q`iOnd-AWj zi;Yq{AF_3K82+y+y#LCq%59h88d^c2Y}!B)tp3TYI&t#MF%K;pk2A;IJ;eXppZpVG z75Cx9yKT(%V*)?6h@5!PxwUYMK$f1YQ?Zuw!>sRI{??<$)o)fL^52^Nxrbh@BX83M zN`L(Gd(WwsTd3Z~89l!0WhP|*VS@9@CaMWR0XLmVp+isavIfw3>DpLYce^L1D#mffe&j%4rL1||M}hVu%){r>evjgU>9q@AIpa# zANu20w!T|UCCSY@vHOI7!3JxZoLk=~awQjcy%u)(+>pl{c7iHm^`1b5pHr`UI6mx% z;8b?kZJ7VMr^^1qLiocm)?clxQ>|9(x*tU~n)VfEc>H$Qx%ol6!mahkZ7SJb>>6nQ z=zWtvKt<~Mz?{hAyBkj~J9pS9a9>X3^%XatubF+ia;GdquB(RU{@yyn%l)O|`e)qS zLb&N%RT6xa)j4?wc85vdVtsSVrTN5?UgTP)nMae`uE^Qab|jy*X4za|XmGz&&E;nI zvzkDq5B@>2w;AOIze%5IY}-+&XZwKni2ChFc|MiC#4cZIi8BF}MaTL#TrS?gXV)^I z`-8p9%ifIjKx`#V=$RPu= zSG(hlPC0MAE2raaE>3lc%VyQun>A|nZ?%)f8Qg9?Z!fAa?!5lQLzj}5n{8xy+~DYs zJ*KuRf<9?HU#ToqKOMc3-PW^?ny;Rx1RrtyY;V7_f3jU$D3-fHS6X$PBe{GT*C?)AiE%L$Bz4J!J56kg$DPI0llU~*fvPPz=e@G%XS&yi9|IoE{2AJ;M?ST z@Yb%3B439NR&SUfZ9oZ9_a-@>lRqA}@icD#ZBY%x?p8f74j#7&nlvE5pszAKe_{F0 z(v1dtM{qKMvh$X2Qh%;}OewjbfF8~h#7z~yM7iGhNW3)%GHZ$p4{qbhk|2T*kb5c5 zjI1v3y_IF@yD(#6^A1oIs01qLxJlQj{!?Yp;iXg(@x%py^1Qc~14@2|Ep~D?mz|hV z$U;Uet)W27V+eu>sAKBS6x@Q@zwHs=iR^>3Hj=pg(bFWOZjLmUCh@6;N5pb+a_AWu zS5N?b90WOxcP4d0(w<#=H_SFE)&Mn6oj(2V?~p;~Qw#D&*7z)dK1qibPHI&=3_lcw zS1$erz!EoF5z;G+X92_D_QIok=T<@{`x34|=wW#u3&ey;Hc}|Jkv&2o4M6Ew{^(%f z+H?j{lcJSC#9yEHg%khm&&OkQY^b}uOrQ+>D|A3tT5R95M54|qfP_TOHo<&rc62Z7 zBhb9_yuF})u1=5|W2{FAd{&%*Le^V8$3~V2?#wVZ$NsA zI&%dD@#WSwHrx%K_Y{4uf_kyee?DDjZ~->mwq(TiKJJV|j?^h=SK!C*l=DPCqiOI( z(%KypF6qp!^68i$U4BxS>-?&Ad*E}0TP7GmU zzDWA`_^g}xH9gI0X(ak}Xts3>kPTc6S88{1X6B+XCT2ZEoeN6+I#}XjJU|1mh5Fai zL}3b~M=`xF&;Y2hy@5^AB`Lqv`j&M<)WCOD(PL`3URl{_ctohNKW9Hv6NU?G-+_@s zJj(yIgD`(|s`^H9JTpwkNFpjAWkVmRvh84fxcl_yKpW~*+7V!^q)YZs3F6FDwTM-f zZ9pG%W&clM7K;HZB$}fG4}9@Ld9=4Q`^`AGW^0;H0%!r5B*wnbkR~Ki7-Z&0j~~Z* z`@yQH>d(9jnJ{68J_AT~3DALp-y$1Wx+>UWr(x==1|$^@WMx@N+Pa`O`}PicmrF(Cp$Sm|Ub{=u}3 zu#eG_(}Z6aN+B@+(Isrz^RU@p?~c^LlcCdHQ*KhnDqbw(@4db6qUD>Dc)Eoi2uP5C zuE=Jej7*7c_)gJK*mV)76bdl|%1u;-2}%e|bI1{Z#YYf>Ul45!K)*n+5kP)~0nY6N z?Uyp@SXs+_47F;e@c|m=w%p9SZa@Rp3W5+m=-#Sl5&HXGhM`)HqT z8{Z$~QrTP{#B5~%!bn3OORy|4^Po!lcG4G&W=2)>(_nUQh9fye+jYYMPv8t!_#4rq z58zxO>J?%ozhR5KHIN~Ip3*q_5DmOO=^Itb{rhQv0cxhfWqCFJLED5an+^k-6d(wu z`{Iqd-EG@e7UM)BoNSVMgLncWpgXgO#dMXfo);sJjnK}hVszEX)e4CY);#Q<96!Fy zNYTEVKx~sZT$=oeRQ(~Zfx5U zulWIAtlOY2g%!%6xPSG^m29JZ7&)Zyy>7}~PRkn?9vUnzDiZRf?;f}1Au3mkE``|CIzo`$Y4rzKd3gh&K*~|y) znLJHuUx))wEzNsUo{?~%!gryUb8|&OR=KhSWC4lN#hmSizC?Cc=F*vwJYESN7BQZo zBpDQ^PXt_Wl8t}1%((eN6Ge$nIz}oftkL+$^+fBeoi~Md?bFeTBWl(p3;>)X1bBG~ zAi8)+*_32J0r}z^U6`Mbon;m;ZIpTT70V2kkd!2^n2iS5l{IINALj?T@fH2CK^r$W zQP;0SL(U3gn7P!91Fwtr{(!LwwP2Q~Hv+&9D_Y(B;y8bse@XJcK7+iR+^5`!eT>tX zFUwulRl!kT=(pq@7W@npYhZTY7?&Siqd*X3&L+2k0z_Bl+fs}RB{wGro~=%83vqE` zJ**2Cu*}BV;tg>h^Q42Enr-Hf+;`! zJhPCXfFowf4D-C~xAuNE;*Ia^wp^hSZ~@p_5N?GP6rw)Fj?J>5PS`eIUWwW80PzH` zDNC=8q6Jt|qj077_Y)tG@+6*cT|9c#f%cfv=9tpkY_ip;0Kx>zSB5Y)0m0h5gXc91 z#)%N)Sp%$ahQE{_Cpo^zEWmKPM;Zx`z^MlJsTFtxe0Sd!*c`;Day4+c+6qTO%;-WR z^Fe5c-rXhu?u*Q-W~>heMn+VDD}bP^B4c1oOG2NtwzO;j)>IGEx)45L4EzaS1DuDv zvUSQ(m5pPN{Jg*a>5DO=PXISl_O@ib4*Y%?mmg;K@O-+{TlE^65f$LoAbAIYgoWAE zAsO$EH_Zd{*O&2+5R?nBjB;~dFD$RKZ&)`Dx}!$6h9p?t;@~}k7AHQO+_yZp5jG(K zjQ!H!hGVJ|VySWD$kl*=fVvrGR@To?X8N83DOsD8!H+mT)8B8?<9pW%4vUe%`|yIL z#W@d<8p6H`E#Gl@Jws+QHD%>pFXLEV`TrEMlcT!+UbzZmS=%Ksxwb>axO3Xi@DoC* zLjjA4W-UH-8=UuH>{!zIg;9`WEi50eLOcImHV>N`4w$wWLOMrjBxMxO;F`cuy?r?6 z4=ux%^P&%=Uj$KuKr9se zACyf--=fyA?W*oPw;1w080FaE++@SFc^{pnC+xSJbMq!LyghLa6`=mK&5zT6x00x> zi*FcHo;lpRU^rS`j&FrBm5O5Hwa=5x16=Ft>i#(mh`V$_zSfVJiTBM}0fxdtu~Z)n z>fBebNml#fm_c2=V4E*pE%Fw^1HuXktB3orKP*SsC3ptjKJnQFex~5C(Hdqg{~r*+ z%=Gze#Bf3hJaRhIY+*=CQOG7IFbg_Io&Yoqo4`;{yGKY4fe4YB0;iR=WOmJ)2KP)y zq?wV_CTt{@$33{*RJLAA6y#rT*mhjk*7Nhg0OCCQZMP2X+_@8b^&rj_Wi4$e-3d3V z;rg~+%8McWKGxPrE#t6PT#bs2X&MvJ`;)T2)qB3!q6Br`8hKblEZSiY(GLQW&zr79 zy+sks;%BFrr;SHcBBeGmcH_cct9f`WqVKuk$-Vc%XYqr3?FM)GMvlQbQ=c%)Iw zQQWFZCNns}Q(X+{m&)pwi$F>?8(6^Ueg*7Dn~T`C91D*(Ap1nAy>$tPigWAtcMtR1 z#(n_ygmndlDBLi5zb)@c3w7AH{-^s;5Cty4YA9O1cNc8HHi?;2v`aXm(u02sb6`#4 zhF-z8qk3`d;xchCD6Q9Dkk>g4!)3H(!lySp$H;f&3HzIPjF<%G{W&mnAz2X^NoVKg zo`Go()M)-_6FyL~+K33R9vYC&kY(nIt*)YL@9hZ2xe z{>&PWp(cjrA`0`=!Fdq$mDhg8VJQ1YkxHfE(M01>khA44zC_sBHVFx$zprt@ZTJG8 z2rB)JXh}YKBCL)0r~uBd$#AS?QlL~$b;G03^;pI%bWX(49sIjEIm51)7(7FRAs2Nm z&y}v~4YlCfZd7k31RInPZuXb>n8Pdb(M2hH0phF6h@y34H(1plgi2h93hMg zY~5J~>(JB+Pgh7fi?3&S3WhywlzvA9qgPtDuA zbvid5(fiSbep+Q?9lTSNK;@9BBR=ooSFE6*GDa|TyR9JpML@JJoe8b~eM;Yivprzuft zck*Ne-m4>LOzgdT5ioS6Ew6h#k)iaggg?I&t&5_vQ_6{~ZAL~tj0_R7nOR+MT`V0k?^4H0z~yf!w3b85(}`%;P_;+1o6Si3#{$Vrqi&*cy|d8b5>_ z0PoNh*)MeTGURd>c_2$B>;e3BZP5H*+85D&C^YmslbuB*)X?D2T(6VRUtW8w<9;Vj z9C6PbQmeZByx~~T<}9X%Zvc9Q(i?H7ODnp<#~Pcfd*3`dotnY!itFw`l)e*UjYF42 zqI>3KS9*k6P0ge_eY`A*V=ZL9#>trrzc{x*5u6ujc~U#Sz$y4>XHO|w8h};mnUV(i zmRsPJMXjx`=XNcoIab^%96^I5stlTD@2B^6*!=qishFBj)1cQm*s1>zYFK2%YQ$Qr zLS~Io6gaqbDDP10@m*gdJzeQDPkcVXK>Li-$~gk*5gSn_=G+iojZGZPScr5FYusNc zPNLUU;?h~V=H4{U_eJgnn_Ou{(&-C6o^bwE(Yqeqz4h>xE{=@{|A;2qEoMF4s%Q4t zcbKU+S)g|B~d-DB^d@FixNRuZod<A%}> zZ_yg_nw{`Op#cQVd)X9Z-g3L#QIv=CLIM@I#-8CwgV9(BOiGEm{Xp$4P5##|n>Yis zO1M~A`4cZXW3E>dI1K&^iQ%~6yZ8Jr3Ti`BS(_cmk+OsFH0J)OsHh2FtHh<}h+tde zg?&QA&K+B?@Xc>&T8w!~)!^6|6>=Yi1)HohGmZ5K41W0VT(CvP?}lw_kHf(RbYqdxo(vso;!#mx zMNA`3yfog zK`IM)O>FIJ<<(eHgButiTSs<}OqWcciU@WlhS=LVQu9B~6Kq(4g|%rBZa+#8U?(f{@&l zjEywv5(x29J}QTrL~v&_?)e}`dqT9#)4QC>rUJ|H^~elxS9R?LM=|~MU5#31)e42V z1Yv?|fi_Mo#&Mf;?oDeLbO!;J8!8Dvbi6uW|RdZdG2#?B9}2_1#RpITiGp0~(#UmA4H~=;*p9D~N^>ulo(0f;KOPl2Iqi zms&^1Fb^;o(=-uBTH-T9LiW`U^AP6fk~Sy@uw4)5*Q_d z2SW!+Fi2#daNo_MKoTC#P6V9-NAo9CcKq!|hx)M`1%?N5DkHVjN$xL4bxvhqN?G)+ zM#}SU>qM8u#sE&rwd3m~RyE3gZfsD2Jh`oM3|3-5!-M-CWG-9auShVd;@A-WVBa}( z?U;W_a?hc_<%OrqOj`G(0PcoqbY|`ha*49e%G;O|&OQI0y#~BU;!=sPlA*Zw?5HtZ zj@fn#y*_4RLxmex^R5CrR&_m*pEMuEq+c=m3oKi)N}nUPjW`gQ-NIX z2KlGpBxRObk#q7MK%eTG|OX70}(-`8$%d#qcp%SL%j_i8$p1r@DxWimIgE z#heK(l160&b`cB#uXyei6%qN>erRj;>(_d-QZr88T>Jd4L(viYXwd*K(=f`z=q$h` z<{u-8r{bl%_FFZk%E}UZEj}mU2;CK4ilU-gCEgpXXQ}oMv0e1`t`Jn!8x_cr7ykxb zobu#`BajoixfMG1&k{uN(@#k*y8jOj;T?*g+7fmewQS6pX38clcZKnwU58&}MCoWHG^7S6K*AzoC)Z zyK5d`Us{y_nkl@`TGcoXyFzM@&}Hn@lqEEA-d|6Vy9<1{Z4Z zGz1)%z`8m(Fb9;1ctVh-Y|HDN`vF;qmCdV}7~PUkT^uB4(Ct;WlC*j$@;|zO5ncI# z9pUWz@?lvStEZ%P-Z=K|CN$1`;nBaZVBc|jU7mxBVTkGpi1Ca^cr!$(FW`6&tV(3z zvjNEj@oY8dJ-WDzaysmsU&pa^;CK&5J(&u^JweOHNzI;}yfTDK36m*F`MjM2VWeVTUIt9ufc}A26-(V1d%v) z9>a_FO#E$LN>dDFEk#+@mnDtHqp#8nELJ0*CWH;)Y|E8M9e6c@A4JGsK%O>G;75B^ zlRx0joh>+|u7F)y$`mRN#{o~Cl~s)#4qC#}Lw|vBfJ@!1*Dbw8ViGg;lyj$RyycjYVWJQn zQa5y+@0~kzsNQzc7p@r<##+(|J7cLjuG-7S13-+Ib%`xF>EK|L6XokfAeZ5f3jn7} zM`DEZEc+{YI<1K}$-v8V8X6j~rzzLX>CiJI4m3DYVD%6H3N*8ZShhhnaShO)O;heV zjRhuKel;j4)OlRS1X|7BwMjr{kfwN=8Y+bY?uKx5A(jg$Qdq;TcBf4r^`zzNIxcI$ zF8`-U_wLSDaBiups`7s7VB&aKtR(Z2ZqJ!UzjH%DWKo%1E{9vYPJg<1)oh9;)5;O~ zwnI7rCQtn?w67c)LN$GG7Pj4hHY(2Sh-;l+#xA*n^S$QXJF{$^Ah0Wym6Rx0PQp|6 zzZ;v4AfbH$>4L_E2*BakRzdi&2kcV5f-R3zlo*+iWr|Ai#=U#uoIPpFpJD8)ynv-6 z+{5U7HTRITcZ}y1BZBH0zP9S7+Hq%PF5MbTkAwa`L@@QH1*b`gP?K z@{Y$bNnMYEo$_HOqu>xGF2YsEQwya1-lL0}f(0D`|Zr#2ulDJhC?J(e$=r4Db z_0P`#xn)rG2mRAjnQ6Sqa{UrIg20yAq4&!ZXj*^G%=E;Y!@g3wTto#ANy}X+a0l!& zHUJBxP$1$Y0gG^l%j)vMl1ZPr*-R`Tr(xE{yYz&E=T8wt<}77!0c$AIR6gFCV-%IY zW=R}=yTtY#;-@UdKD4TG~p6#wMX9@G`TlFZSr93EF>y7>ua^>Mo^F2dR&%WgWGCnmblyinbm+K;Hj~5c?3!Y(FLvP&;Wl~8 z^Wm?jbVyXoNOTz-CR7U{4nnE`ad1cR&j4^o`1EpCq-24PPN=$OZkPa$Sw%% zMP=R$`nAW^)YibJla%WXQ)sxr^}xS>fq`(?T`O|{BlkeCAW4=Tu)6j_%n;!BnvDAg zgkUNAm*mR~J&inSZqA9Lla?|&oSo16IKAfaSe0;tY+CpLrUdAyuoGoLCf+VTgaw83 zfrp!$3ZfrHK+YMsPjKqPNbwVN5NUVKaQ0zV`<-tC9-frw@q5;=KH7?UjKoS3zG#E2 zot@nkH#`1#zS`46P3#mBOAWhN9{J+{j=&5iu~6vP$fy>3UR+%hVkEM7hsv7;4Fm!>yUk^reiTIe31uo0WsrOtc2ClTs7+O z$PpyX(UI{6WngDS9CJMk;cmvoJt&;k-9PTpfI$Zig;&gnz^f)Ua*#4WYGIArnmD^) z-vp3HfC1>Siaar*R>AK1j8l>@S*jK%G>B;-1_G2nJux4loBbsAOlZ|LquWdg5FIzSp5NI(ya6v$7d5ZfS(}fyczSB?5^%_?}lTz_>J^ z4jggi+Jqn5wyOL{1HOq;PDAl&QW6C%_%lp-0$(sw;0Ac=_ctBl!Ugs1A<&FYX>OH` z*L^{jDkS@y)6>&weff9~#QzJ0{>|vV}LTSoBs7&dQSN6H9r+fnh9b&tg~Vj6rC zC?#xdQ^9$t4M+XLhUV=cV(>7W9>+z>p#U3p9!BX`E2U9Y1EW|)GC~ZqhsH63qAfj7 zm>9sT$3ZuOQQr#ev|1FRg!d0jgF=DNCNUjvuoFMYiM9+%H8Fx2cP$|tE&@&Z3JQ>T znEYJKwX=BYr%17eFW9%Y#Sgm2qB?NxQz#f>==i)xx;kzu0y(9org9&|q>^WSlN9OF zb^=7BN_xY2V;FNjTJ;Yo;)wVvd6`RF|5XnpW00$Gz#)MWLes6*m5*f)$uUWM&vjvf zmI}^_m0o%yE2TkULmx;@A?jJvCx^B|YC}xYFp8Ol8#J*Th7tiyC*PvEh^T0SIU@r@ z0)4Tj#8z0{xGoQDT^1aFkK&R915o@?=h*;mDku@@n<_ycNRn$%$8AuW{}ZQM`)>kP z?igXA+nCLLHRMpSYQs}|-qlt5KDC&P#=m^IKe<5IYWVHjp|r(Z>N)9v>yKh_gC7zt zyc5qLG8V@7pdg%4N=@Um3>&H;etS6&<${u5Uh@*Klilpyd- zU|4#8I4U-lhC*WJ_Ka2P<+1q}9WCjDy#gmj!z6R))>kbpbx@3~p_s}FV`W&pi7lde{pK`*oAItmc6E9FF*UU1#O4MUZ6I!& z0UX}XL6opQcI?<_8@YH^9#60q%BPLGbt`+k5=AL({c1#%4trusO_!9>eY7!A(yl&P z?WJczv99EYqj;`BEW+Gd&tZqc|v%5ATy9@GZ3*x7HWJ{_N#a{jis@$KJGbx zcfvdp18Cu?ijbS%QNKlYN3u(Y|3e^PRB9-FlEoYlfKu`_EZ~3RO_-nl#)DZNhG4J; z4}^gqOk1rBe3(GhpGz229%&qLfBIkZ_zCMxo*2>Hh>l)K-f-P@;&D)u_^Bc%8(9Ov z0Ng1QRA)v%cbCLKqz+_oJIsJuT3QHWtNJGl3za)0Uafo$JMuS2;DUDElFXqkbhQMw z9Z9**wf{uZZf3ppR4VGJV$ct+Ny-R*Dk4l3Q;VN@B~U>^Li@*;;eYNLsp=e*yeK>< ziznr`{4>K9?#?#+3Q+Fv2v^*5eY)g;w#-9nm8iTCTWHh%hG-DFQA)bsgeQdteuZ+>(2-%Bje3niAst4V7@`-Bg8rpFu_J+^{AjIJlbRTpYK@z(;3&W)^}e zFgwS;amX2E8&5F=m0?grq2ORT4@V_W?|e*+fOxEc-rD}yu}=`L)u8TwwM9=ImOetR zaXWK${rvXg;{>3nyvCm+fR8XB^=_pHC>a!g+!VJJeGGrdp}Az4--1JJ69x#+P#h5s z1Fk@r#j~M|IAmzZiirUgv+UEjb1zCVUyxId7?)GX^c*AlVP6s#NBBhmhp{E9QJ{vC zZ3`fW7?EboS<&KXV<|&>o_yAQmT)J4AAWmNxE*y2Nh#c!hSiWjzpgx`v8jofnK=o} z1vL$ggW%MS16(uv;RO-RRV*D$EIYX#q6UUhKcP1QKL-h7E2_cn^ZfvfNzNjLSOJk) zQ1@PnJN+nHjo(!n6l`NAs#_>(j^G7BwYW_J@XMe#^1;S1a`ieN#ja2ZED2}eZ3 z^62aj4p_{DUtAiolFxOeXv@8jo zCA}8vr#3jB*tku0%qvYw#%troxw2(`3n1jMA+^0Nm5HfVY%AW{zO~#24 zY9yXH(;pb+BD^L#C*I$n;VX8u)dn(FVB$VQuG0vY6j)2KFL9+p!U_P`RUz~qV5l{FRNE^9{ z4cYpcE8rH~V7Cp+=CxfcQ?4*vipcEe`Q=sX$ixG3Ja9gSfA~SiiMT6Jxcd6~ydOs} z|1U$K@*oU1KsEX8?nX9&)$w=Ac}b3W7p6xO`iy*ac;hkaZP%DA&_|eJ;NDD}yqD6_ z-YzGif_`~D-jk`k1_dXMp-VVT$oQsiL;W`bAEPjM4t*Y8f$Z7ccsu$H`q%z4q_1hN zes_e_-)g#=693=&-{p;C=YHlM+PZ@6|5^7^(rV0Jj{ln%gntqI@2OK~{=3x0L)^;! znCt&t{US#G>h;!Y+8PNunvah^?umZy_Hc``{SPsoQK3ny!*QOqxuR|?Gt;bRPk*e| z*tT|glkToDFCKBa_T8_N@2{dSX`HHa-Z1midpA|Edavb?7M`G0+huH@W}n>M5Pk5p z%-+bHH!p288}6}(I=?Vc9aiYAWH*mL*67yOq%n1a!^D935Np+X`pr=#ia~ADd^b*v z&qn+}7*yr%soU-8TLp|8O`4MEIG9!+?rAU;PE@o`Iex4E#p?g0YDgYvW&E&}LLvY8 z52}V;|5?>Qh0~F8<*%y2#(Cm5{zU$ait!n}lgH1TvvEI0(Xu&n%Jt+KS0~$TXQ3Z* zI(b4`Oj1H@x5zffGiOeD$cu}+{m;Kc?4-Mcc(s{l3x3MlQ)(t26bk>}|4>c3dLk(d zkcRSp{qwhfd^o4CZ^X6w*BGVaGOgWy*Wgvm^diQ#7R5VPvM4KG;|M_pmvUGI%}Dz< z)l`wk2b2mfSp?*S)J3kWWt9oJrgJD&GVcw;sCmXO!d){B`~R@P9X59;0Yh!`0(Dj@GK@+NA#_ ztg(I@`U53OvbOj@?0o;>zt`hbNI|dY8?=8-X6j11=MuiFZD=tdm-L#jSL0=j`AV#= z(I?4T>j&Pb@OOT}!GYvVs2aHG*XZA!%)hv_4pQc;>q6*t1+;ncE}_BxJFsarLD)T5 z7p_Uy)^K}L@qOk90cwcrwd<}l=*KXh?pI=9IH0+ul%Yo#I(k&?23!vj*^F!P(~lo+ z&ob6RbG9;^bzg#lH!C0U``ycmQYL z%Sg!W1z6nGvR?;5jA(rZa0}5~Kl}t-wCQClL1tT&`@TWS*VNm-I@Es*-o0yA+y!$( z!>gENTjMH&kh7LVx*iNlWdXs{cjm)6IPAtZdcbX$0kIgM1F%U*{XvlM)yZ@g+AHKo zwM#kjRGk~+3JD2`=CWfW!qMlb<>}ZXyFnfXql2}yc_X+&Bn$^l%=f)&_j#z7pPs~kv~g;ovisx&v&_W(*g4ZBxpU8;BJ=nNI!mZKQ~9f;Tv0dr*Aec%JWJ2;T2yGXc0X3_Vxl)rQ~`ZX&N&a@7*Ny2)= z-ODaye&9Oe7_~e+1D?GVLt;DPtGq%~eNcg@czer3*PA?X8`uCQV;?aIqPea)gB~Cd z-ZIwk>ctS!zn51e)P{`&u%oW{J5~qwk6UmTN-o)Lz*Lsk*;&st6 z4X{jz#$Kou9C!sl3}~-xp1nMi`sfi}OOukLV@CHbqgRcMB(N6k;Q;ERZY}@7c(y*K z(|Donpoq5Y)jRrv(gsifI&xCs;fJ9cPd{=e>#XW7yUEYB>xc_G+*ovXgF>c=oNQ*4 z0x$3yS;8OOzi*9NjHoGU3Nhc?4o(y_4Bo-F=sJ&1!a3fUn0>s06 zwGd;REnD0kC6#DnIRlPC)Noj_)i!#fO#v!TkQfq|fo4O{d>?p-Ik1dJYf_8N)D9Iv z%V`9c6X82>!u*S?x$Ei#84GdIJ3P+gwWXkUTn(BoT9dSi+khA#vxVfO7dRjbw$zKa z4hT)gER3*cP&Fs0wr;0iBLTmxMK}vWZU2cAFC56{{S$u9NHmI&)=md*z+oZ47~F;) zxTQ34!&4NUU(FwkZrTXclj$^uyKq8ie0vUU$F*A9yF49FK)SX@(^gQR{Xv}%ZB4)L zWEfRyRs!VBpV2xEjG_&%htk4Xu}}wErJHy?AXl88-o(N}TF%n&(!8krN7z5AsUtX? zSQ4x4ABSfZV@}ypmbn-r%~Hw#8J;ON0IP4@JNFH%N$By#ge;)#+X0&a;$41)u#^Bm zS=6d&0e!jAhhnlv)!+nAgNj?vr-e+k;j2ci za-SWq2xIb=y(Cr_BP@9@KX9ut22LnGD#E%QnOXv^Ry}K0u4q~Rb7I0hIwmIb3BQ=w z5vPrJ!2C4y$z9}>u<{*OS)am}_)(PM>z6MXxl0KX&o+3HtV2vMV(QtiR-+*z(ieP5 zOFzG=nxk*9$Vbkcxqa{6y_za?p~ItdBZ-MJM@cX|n%U8_I1+Z(cgY?_I4&9Mjh;o3 z?iB8(_>qqxE+z(K&5+lYd{0@;Epiw%kY{cKG+8JjwAPl)uQA_CrApNW?&+uZMqnTP zAv54{hHOr-M$@`erE0;`FRE+76CM$9$jNDkf0maH4CbH7<(;4y0El_;{S?|E=N>oS z(&rjHaQYPmy4ZZ;p=0|B)R(tChTVB+{4XvJYz1{!#|!79`KKW^tLW0MVQ=FirES{r zD?@s(8ARnXWIGFp2?>49N#5N-@2WN5vB9 zsP>&5WqX(QKiGToXfE6KU-&DjkRn6UNGYT;D@x`mi4ZDeiVR7HGDSj}qB3Nv6f#ex zGM7vtA+w?~WiBFP_UG(=e*5>l>wVs}-hcMq|LoSfhbFG?b)DCF9>-@otl+Ab2vIRY zpV9tm8N?umQ5N8Qgha+9fOSTWWTJqVs?J9;s=Pf~ghFIjwjZ`@?W31om~4oUVYW9n z=LXiTp|AgLd^6^jV_RJbJC^U%h*icx*zRm?f5R14j=?ZN=TGi1_jok@lbiRbu5VtR zQ(jy3ei33|Gq{0ZZF>HUhokbu>;5^uLOB5EBD^YkG+U3-LHC z*OI1GI$}4?TZ%!jtdf_t_k?rU!xZ;fI3jAV-Qi91vXuR^(LXoIA3amfJzmx#+ci4Fcf!lD`AOQrD4tW1Ue>nfDF*aC_2 zLDt3PP~ijtIRWDw9DL`wxg}AajHE(0b1B-~4AMicefxr6ihD}{X`wr60UwUYgPD`e z%S#iUtTTuzQyLQmDr0d|Hs&OQn)6R;B&?i1a?_nF!j7*q-(e3O9bLQa9G#bqWRCmC za&*7;gY{dTCE*I|bX$>hn$7ECjog`4D&-L6#D)jO#m4@CD0y#eTZ-16dg6{^Rs0(oM(Go9hoOBF|i?O@4&zS(V$rvsi@Eraz5_xGTw8R`LLL%s5i*j9COOGy<0g; zlN7N3+o?8dS$?+Op5;H{g`bhz0v}JDL&@mIG+gA?*Txqv_=C07-5in#y}1unCg0i*>KKLyr1iOLH%b)pOV91+=jP&p;ag>e+uqau zO|`*a9^1IMo9VLAK&2j=*H_$UR|7tOV|L{G1Zpv_rKQ!h zyc>EMSR z2K?cT$X4gwT zsbVI6#zZUW?1>GI2o`)t%$ajW-%E6KW}{ZCT-L4B#s{y(2R~j8brI3C7iOfY$PVKj zl?GdQkogZ0K*FllQWBxOs|+<{$3uOxaUl4KD73Ljk!V^n5(?1v&Bu?qikDZqEX?df z=32`PD~eMM_I|TE0yn^ksXm56XeF2)q}K|8&U*SYdyv3byLT%N(QPLnKaR&Vk98La zv_Ff8>Vs8$d-vwx6r1r2*Wi&y>PS;ULI#jbDAm3IB%x4<)*o~>P6HX-Rzikg`@+h( z+-a3}Aw;`H#2yqvy|S{7_|0(9*Ej@b5n9)+JU{8Ojq#Zh((f>*19I`sY~ z;qyEIq8AT7vufc>LiwhewUN$OO;*-h(Vm5sRZuoQ)c(>XGLD?pFMgkxm&VL{kNrur&T;G)EMn=Zh z0&Cv_>c0QE4MbJ=L=LnR*@Lk6-8E+Si#qBlHFzk?usBtdzw#v8+9y4e4KvT4-0tqY z1uo){Eg2JtmPjwJttDx&97gE&hdUsNAyKD*exCk1OH_pO^TT8H`={Cxj(~*MaH;pA zqQ)$0(3=>Ucyv1#5pNCRmLHY^}^h z3;wT!oU$SsQS*xzZv!UAt6HGwuz7Fc$2jmTNPi@wO_DL@kzl{Gvsc^O+bvn2@S(*s zQOXpBvkt8xUl;rPy1IkMZNOt#4RSR4*i6aW+Y3ImeMR=eLEq3)eK@mR>j%?!YZ-9J z=~EUXZ;Cl~E1rd6AH9&nOa>9t7};F%tD{LV-u^Uz#axvvKKG(%FmuyI+n%o)9i>%=fWRAi*z z^GZFvfIC4FwAyA}thx5@za1AyYVMYGDv`FoonazGw{j|L;>#B=blQ~U)S&A6Q{tFA z5MD}0RUu!5op2_H_zaKnP*rNOQI`AT8RGVQLk(gtF~6FIrp5A46Sj*dxNosv!y)ds zVz!6d9adxVw&T*+5EcYaa~XOXl60D3Z5vHOlMo>Fc=8oaAV>#8Kuml;!X^jl)(z0` zDAP0y4R7N-lHIj|6Y?(j)?*z2Vy}Zn8o@?Zj>76&FYK%P^l1w|C8NJQCG}JYNt~>= zxT&QmVeuSbGz1H&M~s|JoDDwlM`V1eUKe5Y>6IWzi{zQpbN-%YS&{2}Tf+pyXkL4| zSjs&;&J5P{OZrjheUck)>M>8|eH-5GkIt_Oh1u8f@%r&X} z8w?hz{F&^4v12~IzPtv0tCHuPyD_q)tViAoiMAnQ)==1=a~6umnNWXjrN`Z|wDJD_ z#75EFz2`bLvSplCf5lT!`9fIb?eQ@@Jlq(f6^S!*GI+)Z59;;5mmlJ*dfxE(;=30r zw>OKgLs12MA~`u35CD(GgOwgG$f;h?yQlvRa87%<*@X)!0&jF|d5)E15pW!Tn99#Y zIuYz(8JcS=HQ2oD!|=*Q+=89w2NGXr9*!PP7G-5&DMw)EIwcG}c@0#;eyE9?D59iw z1}}HW?9Ri)C@<6p^^qY(5Q|xfuqW<6jr;p_ZOgtYEv?)uLlAzFL{lPLlWw{68I6B% zq<_}F4&yhu_nxNZqHLZy3>{9Mc#{Y##&!MhOds+YMGhHV8)lxg9NcfpLqBA#@>6G9 zr$F)T^gNO{Rxe(h`r_Dl^*U6lwa#L*2lo}LZ(d~o4M4|FRes+ zE7jvsLJDKw9wp46hi*4+g#06RzY;9GHWSeR;*d9@WM{8@Zykkbtj?w?bKN1v5i+%? zFob+{awy}gt4T1yo^1k(9Dl`-UQvvsz<5$k3qiYxgN>U3j+MP1$~{a zi#pVHfL;vJ2KTf;scJFk7_lsymzyh#EELJh)@WhrMt0VK$31Vc;@BMC?6BO)Vj0;wX=6|j%_MMe?{ zjSrL*E&QlG_3?o zzUWp_|0;s~` zqjB=2H{JxOK~5TbUANJ#j8?C=v$Z9{TS8+JEQ}aK(Knz-dsSFS!q16r37@v|kB_(? z7FMH^K4rqrWevM+A!Dj(JVL;zCzUOzZ-50Amj%uIR* z;}@(~$klr#6MjbvM&6Vk=>UMai20e8zyyMy-O#})=o zd9AQpI-#xnNMx_>@|h5I0?sv)lcRR^_?nb$+$P*mP22`iP9+f6DWD-Lb0Ez_ zn&PM+dI~rY*E~v%(Yt(E2r|Cpse|awzaU-&kQO!NoV9fz6k{ZC6#FRRYgWSN3(=E5 zAl4ZPtL?kXpKEYMOkpmt*w1(3#}2zpu7sb`B+~IG!g*jYG@KtfrLDb#pFb$r94`pY zX=)!?PPclbH~B(Bu@kN#2wRMQYb^+HCi!;W>c=}NTVUyL6XH*%7h;08@x4ozE(ric zO?@fDj8{+=$uyMLpx06U_U|&L;4IV2HKQfbHaU8A<4-vR@r5?zY%xEw9%6U_oGBy= z2QKipkf~vQ;(CCN#pNU)1P*K}3Ynw~|8cl&2|yQ209jdRYi&(W-tcmTHI3`7qCzPeld6;T>czNGo zm!+bZdk8FQ5V=Sx2l*d#Hk@F3CE-DHU z_MnKSc%j}nTXeN?hv3=LYjGZs$3We2Qm-`3TMgJ~B@i%=mau*e!Eq>vCe?cXmd$aGc~uyuUOWqCIw~ zNd^b*XF^hx6Ee|Lnlale)&@A>eYiF|EMTv^=84|kmzbO zL1cLtvJLluEpsN5e(3ex1T>7Jk4R0U3+80Pa`wCX5M4v(k;n(|KY@vPn^=B z=)4WJCXyz!3J}+>pnNKD3bYlw14SDv0#`c@tRRb;A%C~e_l=^*7ZpzLWLvY+$~pr0 z4XH4^QmJ;Jp@^koJqd-Q*>+46Y~f*pa)gj-pePtn=dJ?Brg!EHv5&9|nXUOV_ra5- zK%HavvsI5L>o3ylPDTM!94$h}6%|*lTtNkYDw*wEBFE*!&YQ|K@(y)16$PTJFdte@ zD^0Fe>O0Eiw?ku4f_-RYlnvU-ZE_@3ecMq~%JqGdK8%mo=KXUPD5|aDJp4rdf8W0s zj#fNnJiH;TZwgF1MMXhfK2!Fbx5hE*m0;Qb{@|n(OrL*pVh@Ev{`n7b0I~lMIlvtL z`adCuYepIM@ss~`a)8~1|0M@-Iji0OXZ4Tufw#^|>&q)>iuh70xbupVbZfa^eiG0= zY0s&^w5Iq@b<3`I8IHN~4jI}$dP&uRXJU@&3SN0Ht9@2kNZE|hJZ@^i^qPaA!}%k^ z4(C_-O)N}LPDejGdSfbf;?UsSgTWl&DO8+3jI!ysGZN{g*WiCYKH6~t|JCEA!vmGQ z75?~t|4%8`=7&gJns~J*I=zTLQ+nDU9+0!~1w+$t4AB=%0GfE_IS88uHA&@J0TKd# zey6y&ZbL6qQ`r67@fF&hbGUSl=OI$_YjBtYpeF%kE4vN=Xh083d5v6atZa%G@?r=$ zOChuDhlj1++*VQ6GD$6<*H~&SDy#Lu%zcwtzz;RCyQ03me&` z2$J%Lt&!OGF~(|NyCCfaJ~jO-RH>!&mW(nb;BSff7D`J&q;!GhzO+prWZj1*gPIt@U9}8z0T5tq)v1 z2Oy9WkqG#mDwH7vLWB{jp{at4?G%_;1RhW*mAAJB;!^1gmf`%%eTcXnod_6*sVRV! zLgCK%ol#EN%y9fo=jnZ8cYxBVPsH{2skpa991$v zRh%|&VDt?@O%~NRD~1|xcZcu^)W&j9$&%52kcpFt58sw7-&UBOdGiypD zae&$e_MK&RAmwcWCNQ4s-b4B<5IkRywii1Gev^D?Kvb){UQTGy;nU5`i4+!V&2g<{&|D z|A0#9Yu<5yf{G#Af>FG$pdikR4C02-ARbmpw7hRohaj}&UB0qb+k-v7n~oiL1O9A! zdio$}OFW0Q3$pT;7&voA5CI673lswzzJ~S;6HLl@4K5t6gcAGGa5L}Uq3Hr}EC-Ju z1uY)GH}649FOV?4qSe3?v$P1kf#w+k7s6KXl!O>;6Q>P+o zCSfVdt_7}P+aL%8v)u!!kL+jqR>|}f)C%D8Va3oL0QV-*mX?xYOggX#QBF>oY$3hF zKQlQ=8eI0Jud>0li6?pCv$=)lA|^-s_|3y9m6er>wkLygvE2U7s`P>o>I+Ptx-Rb4 z&`$tbC*?Wr_NcqaStVsImpL#Y^~kMZ=@Zo}b`?EeADd$DVqmZpD9H^#?$J5gttYtdJbM0|g#xpVH(|q1w|N4A zaJxG_Iq9vutEpYl!rHplW!EqoD-3VQScv?cKtq9GU|@RdtqZVuQiT@#3S+`c@k{7N zcn0d@01CMGL7`)%@Ts%LVM(}7fZCCd=zw@s!DK5GB&7cT1XMK;Y z=_9+(G#rLYd3`QAf-)zn+p2l1t&P#sp~&X-{E?$@BR-s=f{3=9bwTzquH z+gn3R%e0|n*9O|ZdZmXT82>EvET^WQ(bRl%ZQ!2eF9;o0P>2i}3ZtR3r7vDYEY%I# zKmPXl)?)dI*(fA&)p#AtD*aQSxgfGT#~Ngs&Ae{uhz8kOFYMnU~2v|hS!3|suK`NjQ@Wls! z`-uch$T~WTfUP@p#jbOOe_Gn!2X5nsD7i{A^N!~)V{0|6vQ+z(^7oVAPMaNI85V|NR0P_=L3S3OMfmKoBXkEgc_bY(^AGI*p695%Z;8J?~Op*$;1Aqu96zucxx;Nwp zn`~mKRTNpk%%bOFMw?v4?e_T_0D5@kG{PqNe(?5l!G@=#xT1PgP{4s_XHs~4-P*O~ zNOjrsU67}GglxL71)bmCnBoA+88BmZvAO7Zm_hhLnt@T68g^a7fkC_nho->xT*nK2 z;dsOag!@X~_R3`T{dU>ObO#UmX45wMHEX_z8ead_E2DKK!-fF|P|o@H9QB9$m+jp#sVXVqrhP1esrIBKy(rxT7N{}OUW=OjF{S$hNr`~^m6YD zrIEMfv6hzReX5|IZ!vguq0J~oyGCX5*RNkb&?7*(LUWXj#)TTBAeBju-U#a~T6sZ0 zEvw>2-zb4M(Oy-7A$9jEr(sPc_-{SUZKPvC8;9QY{mA_Q{g0iUhYFoWDzDYQf8W27 zN&!U}0ibw!jR$`ox-Pu-28pBd8kJnr`aU7N08AU+jEbnp*x2-@0siHf;aXf^dd?BU z+t`7kFD`|9K154S$!(h~H_3Tdqa@QUjXR-$gQ}Ie>PnBtO>~CVTRSoZNs92Hj|hD= z-Y&>{S9uo<%l+DlMBVu*Un}cg2P7%&A3D`pkV0j0&jUL+$w>|`b;8z+lv5p{;?BoSI}AUVrmk+4dI+>=_-iNMney`Ui+p$5ZLKnz+bZee<%vr?zZ9;T zh_ddyq~l$2DiCl{qxVImgPG7JptL2i3D$=%cq~n~0kWvXcK!aW#MdI6f8XH?f0|YS zA*V5%q@&N1bo&XviG%#RQ<1Kr3k~W?B_}7*`UgkS?2F-+CnTY=iV78lxCDut4xu&; zj}_=q0y2X#OioYt!G1eDGNO6v6zRAP!&P&{-?kaL9nYF*Xhxn)(A2s@F!qu=Ze0$I z>@hCJI55|(Z#gy;T3K6nJea18&`-LL>~HurjLrlBQ!AT=QZ9J40iY^ z`uY9^}b7Fv5P~wIMm%A46Agc0r>gSsI*Q8x0wnShkzoa6tvvpB5f?v(sEy9hR z&uDpmqh#E}hwJYs`@qDr1K=dyXb2?Vpvdc}x{jAKwovwXU)(e_*Bqby14R+Or!(hK zim*GI1ruZ}YhM;z>tbfd;BQd6{714YMH|Ro1LQq%UCCNd^$wZcnh;2ZYZn>ceKxf( ztrkI#JtJLOKra$EkK&oc&3i6iqvV!q!3%_1-+a>a%;D*w3iHNcY&(yC*RBxSm!t*Pmgrfy5+$vEsZv@P4R7M-sr&DfZ8-BBLfX)guO zR9V!iEt5J-J@|;h6*q{iBJA4DOa-1tRM4)#~B4AyOzaOBa_*T3-VC&1on zIHd)Ftua6PHQxbfcEMx6QHjk=uQ(gZr64$)2A7F4mv2NtNy(uVqZ9ATR0aC`8~={C zel-YO(fRY>1Gn3Ely_ORrDbJVz~=piw+F|pV}t@=cJn@5S+?oG1a~6&CXQQ?vtsl+~wQ z##dDW61}7N*t>~UcHKoOS9-owCa0vlL2F2$%879W&*-@Y%)gF9-fF53p z(}y9K#JuZ1`EHNIGHt~-|0_@!H>Z6V^xF?w0H`{d_S@NC zx4g#)O{7R_<5x;YZxdcN>+~;(140{SQPm;V3g&=#ZXvCe-nGFPx5s-_HxjTV%f^jw zTU$?={!W*VD9-$(b`LwSSUd0PIWmqE-D+%G@~mZ@zd=yhUIVY6DpxRQAXaG%-emJ2JjAh@>SbH zL?X)6NMD)UjifIV^_T=9mlH}y7k}J9nHz+9FdiE_-&8;KoaM-jt`U$7n~4j!m;p1p z7*%Y9CxR2c*-j#>V({}c3T*qk$t)=g2JB5!2BzwxfEw8r*wjJS{wjH`nY1{c z`NDY)Dl)N@27vEaMX~*z7oWqWcksjrJQJeM4Ez@U1Ygmq`gUcq)v6=coEZLaiZ<`h z!h?q5=ts@oto!+9mDcd-?nz@04d$~5m=HSmnnJwNKyzZN%91Ys9yb6gv6X^xpvIHE zqNkvo}0%&iKQv!Om zLnD^1XX4T1AO(8#c}U25$StmhfB59I(emo_6|jPQw1ESPGTrjzEfRc9ovZc4SW})s&7U~JJljJG2HCDwYd=+Mn_s&I?$prKy@1K{{cL4M*j%Lu((&~81j$`0U-uncVdjbeH>8@(&$(_OsHLd(FD>G0(K;bWn8|%)`3;#Gw#v;$LOb zb3xNuB>A$ym}ksfSl2PorLm!KcI6!SPiGb>Y3KMx48>Ic;NvyFV;L#!^~@B&ctvvq zF_mmF_c!Ieqvr<~JYrglw}LpXF%v}4G$rUI4iQ3#aYP>5wV|H&y53W?3aD)wY-B!0}2vu+`(=Sy#7dUSYZ) z5m7m}3|-l^8(Z(iSt`mHN+WWmv0Y~6j*-~jw5*bEMSiX95Wt;HtdayT48@530CwsNn;h>E%}4 zqInZoGv zf_3REZgpCRJ@JCx-Wt3<)SxyU8KM>?L<4OfstG?RBT#BGB6I!3 z(WPdyxH4MTJ$A>BwEaXG=!XkL+>msy1U5fVjW0GGGe7KNaReB}w>nRBK1e-euP-95 zFyt7-UgyC(2RWvb^ZodBal1GkVD~kFb%f0Pl%cY+vibrH_3qLc;p4WGP|n{_qLPM! z{m^HsTHaMI6%H=}AK*b@XLX76A3o5Athj814TN7)=mXTKdG>73_G`0LM7auWYUjoH zU>V!VRjv>b5ns@>!7SvT$-nqp*pZ$21vOHv3y@aiXlWPzL4+Cu3OT8oV)P3bC}OGc zvUu{I5A1F=)#l|wxavXK*0#2`FZ;y<0|E|GB?Ha_oz*NHMI~}1^C-l57Im)2-@P8* z)`l^wH^<(sr$FiP^w~1Cg0^Qqav}~eZ~3*lLkilaZ%A0L5A)>iiQZbNP-q?=vd-^yLLfy^CS)qw3h{MmIVC@z*QEpA%gOS_pq?cNrII_qiba zqfX9?m%h#M^Jz6GAEAy$R<)Mf*PVBip>?iUhk4OqK495to$tuoPP*P*FYv_HED=Cr zF9LpVWZ3r3Pi;GVl@TNSzSNC$Jllgn>lEP|r}N)WfPI^V0+HsxPQff_T`q4>bweGj zon=d=gZ}vOrA|0WD>kXFyL%fdiBU(WcOlrh4NbhEN(nN<~)Lg73u`8hX^7t9Q5+OoO+iJLOl&>dWCpGTuoj%Bv!@G0+}n zRW?52NX1t-g#sQq=g`!zv4mX!G_W@yVyg=Cxqo#=teHS- zO=86kO<|g~u|}pZSO0Z=JR$X;ca~7#9lY{$tAt}i=ZB)ps}*su%Hx_PGprBln`?XO z04YlqNlX5SLt1@2B2*=FH~nNpJ&%YlCLmdE9efE#Cy5n0sM{8NeettVg`}9AU$Bl8 zFUeRzpWItq>`oz~07E&C4Il5QCK5K(q8n9P%kE1ZAklA~bVPeg`~I->_t@$VK1o8A`UPpb%w|0`S0<6* zI8c+t(Sq{@Z~4q3(cP;!yv zSWn!^cnHrPxX^37l0`JaCsFp2BqvDG>lcwn-tfg3QA(zQ(6lT~MbLO;L1^1VSObV-~$+WQTZjimR@=(6IldyO&i zrnwJx;~xG7zL@*a$aRSO&5U*KsOvEc7P@{AS_lFhbR0=d2G-q<8s*8NaTNw-R<2yF zedBov>UL@zrqFCrhjQ};$aB>z>kacrGf%h`9kob6#0qVv^&juB>! z%Xc+d&>R|LuV6oT=~2|N^F`=`lYhxc!S9K=8_R-Lr6_#)vOa?(LB^fZh;u-Q-_S1O zL&U(AzXScH63U^Kl72{fs4>_*?QX&DBy$aPd66QG_QpO$e24IAC4Dzoms;w#7rCC= z+ANYsFghu3W0{}OyST$WFV4Cseg)4&`CiX+7TSxLj%$6@Out6j)&oT%Ek6`90f39h zT0=rz8CV_oDj5*Uaic^INwGYz766B-Y_VlPPQm-{3zG7!r&Q0OkS+l;gQA1gI;IxhKtA-v`wR6A3-qxz#%Y z<7E3ws`ZP6w|EHA^&j2kVy6m4)oX0lnn11f4k!SZ!qpz?+F_hAQvizxU`OZE@JWJI#GI5vD-$#damXl3B=fuoc5C;ED+31^4} z52i$H$bR4CtM1Ss*$T-yD5o+$i%PlvT7w%&G24_-06(>y#RnUR%EeUNXt!hg*B+Al z6kWY+w{`1Q8V@HQMiW+XreB@obUrLTqPt*jXyZFWxk>v&(uW zzPlpfQ2&0BaHxxaxQ46Qe_?ab+A`nRiJI*ZZaL)#dabwJ-QO;7`7l1fgR1-oGTBps z(+-Ln?V%N)wCm^waddK0aGUy;J2!x4qwgeunE zS?C`|Kl`fn@bgcKr9c@9R`{LN6ga%)O+0{%sWpfFj zrEi$d-16#dSFWG@AJ1c^8q7_zdS{mMBBoNgHY`=Ipa#142nE^ z^tkOE9bfV0oW7}1bm{8~1gxy0fJSdTG`|UI4U-JDh^u?F731Z(-T>&PR15Vx1>aHI zDvY29PNChjld|Vo_|Dr6f84hI$FZiJMClQ((%I)*$I~qS?1A@Tdky%L> z{M?7#&$}op7=e8MlnWM0U&(h=yY5}I2`>fkcIwdS&lA19re8NO$*##c&(ilEQ_22n zDUzRT!&gWG&>Tdp-PZ;_=By`Iu$FG}yd%7fGs4B+l-6*TH}h1-+$lP;?a|-wh(B+* z1I$zfHf?aD>q$x+3UrjOW^P~ZsV3S)7GiC|+#kptcx(-40_~&vOF`O@=|bBVd(d8X z>@s#(o1T)bM1>3w5K=^N9N1&7rUTnOMpEy<0jjFOOZ)ng7~c|3=Dd2==m(bq|rsvk`%{H_*Wu z6I1!KXL24M2wB5kfu<=>IiHr(M`UaBZo5ZVH)5|(K4d1x}ZCJ+z*tEg2M6kNlR*zo~Y1zd{og*hSs^71Z{A$ClPG_Ft(6CLA5+FDz49}X>D#S84YT+^&Cq|6ku9$j7-2Ev5inzQ>gvG(N zF3dr`ak?#Bc^8Qtk~}{46Q8lp-@E7m;HbYW2s9DCozwqp)KF%Z)jyf}`vK(f0Z{$P z0P7dR|FY2wMo#|x?h5Yj$1bCwV zS%A0N8(;AD-#nh?`3jovrCY@bf^-|&RD($Du+j>(&CqRzq5md!j&rku(03~bq2G`tIoZ{99CJ4c#d=_r|K zJM=(USh7D;vaZ;nc2G3|Tf8UP( zbT`hJ?(s?%cyQRo8}sH~x4+XkhDschEX_vE?uU^gZzQYK4@V_>0%AFed*BH<4k`60 zXqq;Hp593zT6{p!mDtrAfKTEfeg9P;pCIY3&KR{Z;yv=fo>#et;Xk(=gLiuSSMz}N z$wt5>g+$yqb-$vfK#xZ(N&6?f8 zDv2lI9Nv!F`z=7>2AqZ3soJz)56XadEPQBxw~N^s1q;%Rwo}OOLX$VL$N}6M;d+ zqPAUSr*JMhHVC08uaQrY8EK7^c{K_~iWfY{2wGDx-j1ylxE_Uqo(A)Jon*NDNC#O{b$0kX4da(qT5hf%JBlFulWG`G{IYz%T!*r-^>d^}89yL> z|L~Z^-&N8-xD?j%^u%_8*iev7eH(1t@V?__dJrT0m3e=1%K+cvntyw=m?ZX4@;xVr z;uA8%ONg`pB)tX4$|;uzFqb_6SuJrq2K+{H&d0Mq4#htpav|J)qCSFyIT&>k!a)cf z%41LC-^t7T(jp;>UdyXU=HpAgi*H;hJx@D ztai9fcDh$CXdEHyt~uij387U0Ua4?I=fK%l=fZx$8w1_P4#W+ky3oqEgX#wE*7c}H zs1t98Vc5gVY|KWy5*%^@vuUx;E}<31FKk3yVzOQi7lLnnacVw+6rR$&d1y5S$Uiv| zaSmSsGe-u4LMGdK(7?=<4Xk8w6Gu!~9lb7*P2a35zpoXJd?n*M-e0}PN6#sqs)bv$%l;to9Pa15j?=;jq zco*NDYlDC2W>%Knb%rK1dT0ZIZSLsb(=kTx2h zgvb$+Fm*Cf6>bA>Y=QU&@<)1gF%AGy4r!RolL@ zLGe|ygp5cnT!7~1EYl6Mu{`bg0)kK#HNHT|b380}JUXc?|4M_v_0c^9JCkl$9Q{FD zi2dL0T(dE`bFChC<^+7*=&OVMBE_%DwJcREXo%ikvpWz-w-@fs6MScgGi$Dt!V7W^ zQZ4|ny`Ll%L)+X|OJ8Tuo5a?E$HO0V&Ee7|vKJ7WH9Gol;4P{E#t0$<3Z;~x@`P=E?t$MA%6LOP-r5G+PN$Dp?UA@eXYE;nGT#0msUW6l__`?Wud zF2tk=2>ZG=pMr)4BbihII}-rzx!tDPfu@tMMP@kz)g$oI_(pxZ4ZcFBd(uR$TgGeLFc49)=LI`)6la? z^M%VomWzHKqL@l-F=QMOnFR|K5J@G+qY;3L@oW3y09z<`44a3~Aott<*Ouy>;Vnxa z`ET1PIkiq3ji9%}zIoE+$g$Uh@^5M)_66e2_<}MYOsQX!;yH@t=K@g#-H>b#iZtR? zfXrA`C)^D;?Fd4Erdyqo|1D)!xfah&x#NLqH&Q;Gksd#{cE`*3w#G)m9*3V&+QAHO z%rsRa_y3(zVp0*n_j>^YKL}$@oft3)bIdWl{ol>SJ^V-#q;AzF`=Va5;!`={`M=b#jPmdeft*&a=w3j8j`(b z+qTTz%Ln}16RZh| zA1+;sCz$j`dprXY3(S$(rVzlQxtEV?;_YG%=fUkDTD@XoV!m&WS`B)WoWIz-PUZIi zk@bR*i#ky?5BmMU@jM*UCw}E~e+oo(i0!}3)t8R7S>hrvSJ@lESaIn5BPL2P#EtIj z>8ZVss`+#?V|lcF7MG7kZS45*4mYN4cni>5L{w~bof-AVHX1MMOJ~{noEOptKk!bv z?I+?MSYtP3QZ#!2p*mc}&3wmm+fq;G@d4USYH?AE;+EQ@_H_b39UDH`$KjJeGlEEkp_;UI2p?43$Oe^4M`FrfdXZaBPdNf?d< z@)lWdK4Uyms+RMw^+I@WFqR}i3f@natDnn0-HiC7W-KS5zst_f4y^o{*|#4);9w+D ze(J=U0zmMbM~)ci9neYDzFvh~y5nnlHHxm<8<$Ky{NM_(I%xA#XLpbnf z3={8jH!44t_gAYBnf&8 zs{x7{VHPP8XN=G8Gff<(cr|=a2&8DY&mYjrSm5dvPITLQfd3bn#8@EqcUj)&KZO<; zxx?#Vniwn$0X2aaeV>%}02o!x%(I6yv%;abVH=JG2=x}q+0=&3Nq|2HO4oiAwEXi- zz9i2FP|?xWLbzZx{Q~(fj1llrVurZ>7s1IPm1D=WM%bIW!NRbPZ-uFhOi%Zfn1C zY~TJO*`TE6q3ajMXJZ>e@NU!B;;lH};m{aet=mw22D<`=NZ37U{?1l=J~|%`2U)2( zFcch;`ttG|Ieq`S$K;oOz`nj_1el;aZbwkWwzHq7Az_d})+20D47OKBniOU27?ub~(>go00@t08&1%1N|D3G0{mR$h0pRcRn4p$X~Sz!@u=Hs#M?rwt4ATGUGLP_S~!$S^= zdXXnQuaPU^KXW?%^GO&2i0wl4=snx)CMe(&veSpQ6kwH>gO=G5uF82wnRp-Mm6ZOD z@5WGLXwePEsgjPI<(#~;b;;!GH zg(MJ;`TlM5auc$D13wmfTH)R4*Pv)6u)Z$(8-H=1Enk9?GbAD-l7%c!5mUF!wy z_t+Lq4q72;{|($>+jbs(m|`=lYYsNtx0`G~a(ECes%O;Hg0j=qU$q@}ITGsiiOC`1 zDCk*fclV;E4|H9O{0riv7-ucw{&E6x=68W>Iy)wi zmq`s6=G2mxeLCCkpM}UxqJk_PFF?(%f%yYZ5(iOQ(M@%pZgPA1qGtjHeA(qYTnd25 zLMKXqF(K9=G8=KOhA5eJd!OHReV(!`G+l5&h#~2+UH^N|iII^s&3TvYd{utua_R7P z5~fyX{2oYaTtqLv_$t+K;}*;W09V9hD~a!3>6;5c)Hh!$fNfs@^z&$(xO;k$S}*@q z&s?dGcWU;hsc}bb?XfWzeW;dr_|v*x-$T>`XeDD1AO;z|$_}Js!K+a6a2ehbXjfZ@ zKmGebCgn^HGOBV|P4Gl&uNa{lK)!Rq?_NJZbSB9Hui4@4tTiFzIA}f$n$--A*SmMs>=q;ejACnS+>3rZb z_-sxfsSOUn;82VeDSP&>*FOK=_I-R^Ugy?M#_dL=NKmIhhIQw)qaA)<06V{a`*vrK zM<5HYH%5-E8G)c|7BO<7C-_;FyResL{j-;*p=~1MX(hGbRdScVxD)d>W=pXx?VJ+B z8>ki|cD!GToEIqArnY8Sj+?!GPRU>Y8@iCZkWvJ@K{~?nA`5GE7MiK^i~ z(-3~7emV+QkKTU$+=SCZnBl|f>#p(g`W`7MLzZF1&vUXW4iI=T22X}fRDc{N(<3i9 zjYB!vn13vsFR|4gZxVqVsVGn+5>FxhVMQqsRhi$>$PdH=^>ON0II}gE;Z3w(k=P!0 z_3!Es^&y;#FUSOev{eY=4~pe$@`FMOB}7X05>6;Z<=~4sHZz_Owc3w=Iz)~gkllU| zJ!6B8&+Y-BiW+iC7*-U2v!g7uT5H<&yK3bxQ9e^MHUM?W0+MjBvs@(9T=D zCvA8z|2b8*oX;WY{cp4k50a;*K}*vY-A3eRvcqpIRE5DEpBs|?)NUCrsh(Av?rvH+ zA=lr|sCLR6uNDp4St2J#e%@EK41OwMR3wxNgPC(IyKr{W+t^TDep?N%*WMu6C^1_O zY$DACB{uhOdMk2SoIg)jJNeaml9_{pPo(*vq$C-)VzqV8=G`wY_t!kG^L88rUO|F< zExQmv$ujd%?64zbsg|E%CCKPSL$!+^p6fU7-)FowH8ZjSoX~8=5_bH=>C@4tFVybr zVoa|syTkhp`zmA^0!UZ++A)qWI-HpCjzdj~>QNU-74;XN0m;NJsp8s(NLpXpNeB0+ zz19a}5|fh10B5J+W*ICZQYk@juEi2yXbAN*Bvm(YXq^KOfkS;25%Q{vaS2zr?~6?A zv-UebHpdh76z!jXD1|1@GVSTtH%4h>x!U67=u|aWbUW+fpCz~Oc-QNn z1bnc`&=qTAEe4ZFNh_l>Grpyd@&n9-gJF>hXMHSvaZNMi9cxDLahQCsYbc2AgY{k$ z^+6CS5A1LXUAX;;B8ytagR~LFNfHaFWfb)gC{TKOFoHopePUxdwr2lO{sQub?t;O7p^qn%QeXY7}=MR{r;|^y1sSCC~6wyNi%^vek8K?c>#JL z5;aJ|ZJ>XphC0^qzA6{j=V!mL6Q_%A=brlYDbuhx014P? zBH!vZAm7!{SpF-TwyCkLu?T zi;{)Pswz{6QXl7=(?`FQZN&=bP;aoMUdq4CKsZRSk8smp=H-y_JG@D%uMC}wfZhLGyQ{AA?43vgsrX-21 zmcDo}7ArN%##O}wC!y{d&x9zUA;4L4L&a`+uA9t1&C1C zaXjtUek;Uur$pZsuYPMp$X%DNJnP70<9O)vc+iaFj!MqTIIF9@&gIzseC`@5e|S(= zczsen9WBxdgIyJm=3}4iKQm)zIBocc{F454gWrTr>^uHLO4|(J8ztXi9Y6tUR4;Pq zHB}?{X(3uCp+hfT40Hd)6@yD5_N-8>5{NEH{@8YpUw7Uuwd0f`zlSRKCoY+Qgy=P4 z-8~er@xaB=K7ATEI55_66H(do)rFI2MB<{x@t>y@9CZW1%l@uYi73SI2T1Q*R)TQu z*FaDdx-h+!#KCK_uXcidh#H>OD)_~IVh#p@Kw&jk{hfhU65`A~K~`rl84N0VlGWXK z4copV=7-B(wRWN(!=kUkC!nGr4FuyGZz%To(?vj_jFK_n#q>mvk);nt1xEbvrm{TB z)sCPSIJaau`|<5){FcHQo(zFobAI3e%G6jtZpAU&ANXl}onTtd%cQ>bRu&(S%;mD$ zZhaf&GladXu>A-0Tw7qEXe1~zLE9P+kD8+TrXf}A&zBGkW_$!@G;SQ{EP&z5eq6C z^M(hv_(Kv0u~qGxOBMuLd>EPp6rN=Ox$nfr2BC+773DET>cUYX4@<2kX6Bmi(K%N$ zosMmWH3I?N$44AeCQ+D>l>nqqSxwCczFQ*b5V2}j{(9(1x^O{?_{x!m1qj?3&=^xF zKxl%K6mt>s1k{HVyEa{jB}JNY#>^pQ(c>efb@6}O&F(YNUp$gy4L5tk$ajDvZVEpA zuh2$PF7K7W3{#Soxi?6pt`~6x#N>%%kLP5Ot+dwX_WfvgG zz3zAWRN^mT8ORJpT!6xr09~MhU2Vz=Pgl7CnTiD6mdP*LiSuF_O`ZvN~i?rCQivJ(3kj#_~uz8VMdp zI$Agi0p(@2{v;);j%qMc>xpBb$zv|Ip$;`26QZJtp-p-`Ikcfc!*|s#AvRa#Lm3B6yK!yh2W~F!WH18^BF9%*~VRq^anDQXc@zS@STuOxCx`_ zQY{~%5Gg~AihFfx!94a)NPb_W{6HPYq^R68KlkR)2T~pZ*bE42@k=UZJNjA?Od%lF zIB+IkhlislN;;bltr#%)K-X8&Rio~3s@XQP0l-Qw^@Zf`Ichi988=ELSP|E$g0(?7bK#{VgUf|`~l zK*SJu7n8(e?Y^$8UTQ?)e3Qf&^YW_v(g!qAGW(4lM*Qw^^^ zVo(84N@^fWRH0oB$rmk1SjC!J#CdXfY~*VP%0>iNjW57^_zM}EJNR6X zz}k^*>B8VaLyM`XbS%)()7+L=RnB<=P*}RAU1w2aru0`g?pi;K70t`_AXv0{Y*ti@ zClnQ-3Dz|J2$ld*qP15%rS>!mr{j*n`Aj3INPK=DMCS?Fe-CGSR_dX}hgY z?H;>Mf57ys$GTC}4m|W$C;ygnt=_*QoPS5`Mz><|!|w~aHpFKeh%6TFAoC~5h>Uyp zsCS6-*Mj_m!!~-wp7i>A$biMmm>hfa*ml@V8!(^`VQ?B_rbwtI^Hj-P>G@-NoFqC0 z=fsAk7_|46+p)v?dZ0QsOJGSm^}eES1zsOTy=Ui1 z*zF~7lI}=4DeF1=0I+*fL$N3tbDp4+q#>~EK5kAN=(t^v@H&tgzYw|u@g>3tvT%i=50=*_#V>$%O<-Omr)MV~j6!_yq@I;;eqzLXtb?aN z#!qIP76nwf87JDu`68U~Ao-%2cqouYiO(*Ca0~pidd`s56CJ`)1JXbMj*Gm9OO@x< zr_)`Sf+j`my3dZ8>U88;Z^XeB?D=O-Y0MPyR{TLV5fisAS{cVPEG?nu@Toj+t4K|q z|A7;R6Qum4gCfBzrsy?Hp->)JN{Z6rfcW(|fWQ-)B4%2cZ;bA}S3C>jhEnJPp=GK5lw zh$2IVhzymC8A3{>5<*IbN+rMZvetg~vw!dN9>@E~`{&zn?7i1&7hj*xeP6?Qo#%Po z!`#sZ<7KQgeu5;V>9UQg9B@_Du{x7*RTw+Z#07bRkf1du=Eskdv^Wg3QHA=;b7=IS zpH@JAd62lk2KNl+c|f-O!QP|qpuST|sE|Vudp-$D6F9e}tq_@w>j#HkyT$Ud;p3@9 zZ=zs?DC^(6ul6?TSz~BH5qqAnw{#(Z8Nh@s1B0RDeoY zH?b)>L)f&FvjTj9MbWf7VdnSC?CjWa97?<%Fz~m14+#r-rm~g1AGOC084ZVuik_IAP!6S%I zTK@(woT;k|H4+`SaBjD8u)7PjkC3?dHtSRHJ>5~+@b`R80Fjs{s_0ou3;iTmUvR(a zdG^ZQ$h`|=1rIemXKQE~(boVQbw13_xeM;2-o=lUW_WWxCl zd1ZaN{nlQC#yG$`czHnu>A%!6YoT%R17T9!@r1LpIx;r_RYn=6oZx4Xi3)la$=|vp zkj8m1u(wi339z#9ECY#Q3GNTde$T|-qlcRkPTTu=Y+h2L8>AFDN8xYO>QLpW?e|Y@ zUsSRnYOTG-Fb4Gyoy`zBvHU5p2;yk?Pm)zXGMxBaVvjV`rf>m{VA?O>Vu61k}TeH{WDJLCvlDP{R0@ACj8br(L+M}TnafJ7DrRD=NbfCa66=XWK#KJ* zl5KV0>^b#Em40NQ6RpuJlLzdkmH|A@W2srb{mRTf^pxRIQ7bYwC@ZtDC)a8_S9fMx zpG`|PMBr;1m}uzJ`~%V(a41@xXx$0#@~A(UXK&ZleT>>HVUBAUvH(r?=Gn2D$Ft4?N zAd!4t(NDdwir>5O^QE;JTMt@;lUUdkLkE`jKfDp{_Ei3H=J(S>uy4c3pNn@(my~@4YGXm;OBv+)}TB zqO1HvO$0fI5zYbsw3Co}p*PjC-r3q;1TB(MqSbTM^C8;aHBT4pPS$3RYFz1tbAG-Z zd*zKkUyq4d9k<~Xte*%<%eK^Gjsr0f@6aqyH=@I5lV(`Gzow_zm=#2)k zlqQ4IaAzD%WMO1-^-CWjN)oT~c3UyKwRWRQWMAL+y#IODb@kXGpwMuq3VNDlS5q%l zwp@<4{yAHt0pe86A(gtd@J5HxT2{HXl~AFE3F3VYpl(__^mAKMpiyzV~pxi;QqxPysU8BA3a z%lMKHfRKBjL!Nft*U4 z18SE}&(lECs83}i*8)Ho@k$r7D3ApAiBf3%-sjJr8OBI_1OEhVVe5p#2*LK~`xwx7#(YqyFA0feP+~GSL+Gvvs{8H*20lcG&a2Vd;tA{BLTxQ z5y#*Q7c^mJNt_Nq?7W*kpFpK|Zdk+)%2JdVJSV+h3`_i7fWJ7PgTauzvPmog?fG`r z0eAF;+RbWoBgZWz)5?o*`J1omgNLFH@@(r4>FbMXYZ@Ppqc0@Y@4>0ML$6joI}XcJ zVqoOyK3KR;LSj3YZk0josTY#SHTYW#o$iA93=auc@63fOwyn;b-LBXYkS|w=+8Mi| z)-H8v+`_&>Ny*GwAQo~7d$%>{h4(D~Yv@p{hU%d=r}8BiEC`z!R<{$V$!Z^>J5!mB zih5=k{R`#<$zykElsqu^!ehjaRjZi5SI`8Is*jp9IJw_OzU*c$I{ z|9@H$YdWOuxvz+nV03LAs8~1-GZ{vi=G>@P+CSc}HhEQ7=6FaWfZrntW4M^A7y6gj3k zj#(5r_l)uI?0j**oyItq51EO()Jq5H@Q*5VukG_})r(XewA&&TEAEnhAS5{0aBFY@ zXBH+Y{-`U13v{okn8|0^Z6<$TU>g#S>-@8us(yQel<{Mt?|NI@_aY%eB0E<4EQ=9$ z5o0}QK4o{yZEw5Z_vro8t!E{T( zdm~tSXOuh%uGBnmRhl)*%~{`75#PhVOWhJyZzq*T(TvbKXBWCuY;L;pbgo{OGMa5>}*~a_!CAFl@K+kz@bzc7M+qbLS zMa0&f`sR2y=J4sfLQJ5WyB1s&++p}aOTGUqVO<((+YjRod zWzFKpo(=LbhmevHIs72g)ouI+GNyRxPTioQ*1NS^R=}Xr9?YU7H&9Z9FGa4zrr4Y%o{bZC5ubkE=l}D0HwC=KHF8+|(!fkO3I5gs@edKb ztSt--E@}5Q1;9RxES8&PAu7bED#wB#ujYY5Un>Ma~zJG7z(kqByQRZl$yw7 zDK0q}+u?5jBpd|Cd1LJlR!^Xt5LVA%5EzRK&F;(@=8TSyYhgsJ{iHhV&hOqUN4P#0o^k#B%B+9&a>HR1 zw_^1{uaQZ~01|`_cu8`G+{=y*H|*W5Xc*l%xb{OrF0Z9qod7K(B4!B#?e>)&N&0** zQJu*id85K8YUIv;v+!$5?_o6?EGigouJ!tNCRW5C++a097v; zx=9dB=wZ~Vf4~pIaO%R)p(yOD9XWs&ab!t%9{Y7Kz`y?0L8!!@-SSQbq|efM;WdY& zLsuilSFIe5YvLC*q)0OJmHnx^=(aan?_BMv?HhG+=C`-<`ATr$;+Kkw0IP?vJU9c#y4s^F$mR_jN6bfW#@s^l2L{g0p2>Xd3jp=( zv2S{=8g|zQ>8p1`KBL^FWRvGcJE=H|fz6vps62pGj<`{p(7+P&j%EgOF&X+_1lIlK z!LRwmH%xqbGo9u}rPttHnqRxXRoDAX+HaPtKxKJDw3h;E;^5JJP@e9ncIn=|_3&EK zF)f4;12eHaT}89(n3x##jkB(+xF>OhJ`k$9^H$#ueqY!LzNj7iwE*zJXH;iyO~({6 z?Le8rg{yMkb7OPgyl@}=+J83J<@RW$^oGkhrKP3X&pKlkzL=0w$XwiLGk;=*kMy%g zp1R?}qxk^nFvMQnHSAAR12n>UYGp-o-vJb2+Bi(w>p@!G04%mSQGtvnjjkV(IN^8a<^c%Ti65B(_66sHn(8Yrl|0Xww%kagDKGShbP}G}dGL+09S3 z)OCFv(BoOP``PH~4d-$>FBY%tdZ@8yq+dbPTulXxj-#5lBkbnQn;dHW z|5PwPcgNwdR3e@*s5IRK3k0^~PR1D7h?zG&eQI)2m`RA`3#^vT;$zPn9N=1y%P$=_ z>khb%X&{Rg)U3=EKz1a#o@OU)nhDb01C|fel<8L&RlhP*f1zXch2cXKD^!rD*bJj7!B{5uHgF9iKF>gX~i5;j?jej^$4MX zyhLGKq7$#&Hc&a+TwU$pRx+IWXeu1F?HH)mv**rD!i>@$K6U#WuZ5t*>c-SZdz-@= zmsnOBtBczpS_Vm#5LseERyOxJu1LW%G1G|f@g1UD8IiYx2`7m&~S~-)+ z%jxZ2v2t%LiVqBYr`-QPFzN+V)X!|*US1sc+w5}0*1?FP8xhXn`Cq_R7>XpdlRVOv z7clA;T&eE?{tt19{FzQbEd-y_H7|(=0m6aC%CP!Yl-$SQ<=I>Sqtr?eHile2tlN3i z8b2*JQXFX=*p&P7aUE&0j5i>0;gaG?yCsnySeF9Ze~irQKaNT_$RkjKb+f1h2w?Bb z*V(?TH-En{rOe1ZFpB8wRd2;^dK<-l_e(>{{ZRfm7$WNC2f$CZ(Qx?zHTgQTzTIZ`srATY=;|s21(x-`aEjTwfS`4-IrsLc0kN(^dOe)L)xQP14*=gUe`$j52$y;h zm7Rc@j>3NE-CZOJ8)(L944N3RZNG@B>Jo?*lbq~oBP*aYmRH{C&0c|4LCWao$0^Le zDluk3aaLJ?Fa!Jwe6S%mX2znGIvkmG=hjF0=!XOba`<~6#4aPVr)FCNgf~t_DOX`) z#S&ZKF@7*OW~wJx`T>X5X3`mM-5OX~y~2*W7lQK&a^U(dFA%pPs)1Z9{q=m%P;W+w z=;{%V*ere_FKu^GuDR$LJB=}VK>#I~yUM_fFhBQ2{@m|NWtScHJ^1Fs7N0eQNr)*% zOtdydK_{mt=LUiwu<&y<-2z+)gEqI*`YlNC(6fiY|6nP;eieu*+}fVu`(`C5vRCA~ z8N1U)Ww$unWCO;4+tp{Qw*U6-eaYd7cObX3q%h+$9PI|4XN94QNAENjV)1e`Fzm;@ z6lYtcm&Ae_{;o83R2sUGu{SK9KUt>vskF~3@xpx-zRr!wn31%1|N za#wR1!!n1<<6+&dmRg=QFx!P!ujX|Zc^Ul*Frxy&GxN^idU_8{nSL?veJ|W zQ^`z64Cyxd2fPK0V~v%j%7XD$s+N8DqLo_D zH}MiN#=i$cxI>;LDLHwbtxe}aPJ)1eR%l^hB~E}4!tQ%`x7HTUSPPpZaP=PJ-w3*_ z05_sMUC{}sG*6}ktDQN=g!TmbEqm2D&8WJ+VyQ;6qD`L`vdo(@pDdYGN0IN@;sPpV z#&Z)Ca{ZKze7{HMr&|=RAuJSl92n-zo#ULsI^2VCi~Y9-QKR`|Po- z9FPGQuvw9K5Eg|y7+XSpzjF5Pk12V(LPpDD{D zeTCX1qv0PWy`?QnHfd_Yp%wTeiKHvP-s%;Z0DV>Ky2G=NzV!*vXa%qCRxSaq{CH7vfS}){ zc{A|^K*vDHJB=@bYkwL988##!o|K(klKGA+!?#9i??TJxBZH^u?;bCY;nZ0eh=^TS zKi64eerxQTZg6NoFr3S1g@;VnuLac&(h%8+x^|=+-{v7xHMgH_L?XbDED?0*H2f~A zOkYgX_M!ayfzHr>68)EHv{(L?I{XB^H7F4?dRop@~*x*l>bH5dPpBRkl z8_Ta>LW{F$f4yUts4l zf|c!Tz|O1mMNpDP8uTU(>iN>U%Eu%^vz$<0!qv>fg=%@Kg)r zcF{4t`a$=TI4f7d*z3qb^FW;Cs{ZcLBrD)eG)cimq4c~<($GqQRzZ8k2wB3B#0C}h zK9_KVcaz|NvKT62lEJk;%Nj$0qp(-Doj{dRZul+M0 z-a1;N`*<|JP4QNTTucl>&J4V!Va*27wc>=jw7xKG+~tmNKjUIFbDm4BUf3N zJ$~~i+6LHX($N(}hz0ur`^!?smHyFo1s9+qtjnC7 z$}WiO?hgO1XBAuqv+IMAY;0^sO6p=8JuSIDp>Vthmu-6s@Kf~)5APx52PFv&eQ=Uy zFfbaRRh0nDLy(yHMQd*OfzgK}q8XJ>c+JVV;>nV1j;^kG=Ij;V1B+^3D8~!-cY;HN zeN(~=)Nr%lpp24J6eJ^<(ZD513n)(OF9UxoDZ9I1v3BDrVL|#mdkV1__yt2b!$A$? zi({gI6KOzCT|Z);gr?^VXNjK=A3kjJo?rL0FePL24#a;D7>EMxKNtcJhrM3oQI=aG z{h?0;alLvkdTDYr0=fGw=ovq7oxg&51`Dy7farL|bugLT`|;PK$kFY8EN#!D;89Xm z7J&~V<0w`~T5pTR6--7xPVxdjMI-;gS*5$to~=e+kj5gQvXc3O^%X^zJr;5kW!r_m zel}O7BoNzUiD6q7xcvJ2Cjj{l0(00PAtbh5!=LpV77+Hx-+(KP96!D z%@&Tyc#0_8T;}7Gc%Vd%dQDBu+2Elc#84p`4;Wo=)gRK3Araugg8|23@=tg}3EsMO;l9z`~2&THjUTeiqyTXjQlWcm)SQyvrtWFHg&~EHs(4VFuh$hUhs>Q z+}x*}IwmWQ_&L5=EoSHF435 z{ebh{k!#0Fc$yR4CuTQ|Vnbk;vT0v9jw{=E=(#38R!0kV}sw%X-jF zgA34)v0~V2Jb2PBalAEiYs8RU2?{<>!PBQ-h402OD96s{)b^r$YhAm$D#wo>XNIT} zGyvnLlc^aqm7vk;(zZgaNHh}2sh*{Au3x*>FeVIK9c``5zCwU?ii(C zQV&}f%G@qBF|!X~Deb|te%;uZRlbe&#moahTJ0Zf$K%7R(q0#|B8BQHL?dp#7c&^Q zjMC50#B>tlKo1@MbU@x_>OWQ@2pP0RvX4Lvz%}CHR=HsRfdj1ZhcGA$v3zHzi*X%Z z&pD7`{PiQ^#0_pR!+1InQP2i(tH8NGAwfZ$wLcJ2HGuvwJ{%XIy7A%`l5G)<6;zyM1gT~Vq^vQbS#)(-!3dHeED4A zMEkX{ftPr2L1^FgwTg>TC?A73yjf9K`2F^rWTU>^WJRLc^~2&|*Mri{&-6@F&3TrO zJ~XbC5${3QEDbRA-bR=)dB(_uS`vBz=CCKUXl+($mi3zCiCXl zxJZnJCs95pYJn5CxRD;8>K!S(QvZwQ1RVl@-tly&-bc>W79Dv&nEemdhdG{Ji}MAq zK%RE+zi+u!E2&A3X&iq;&Re|Y=xFK(+#Zkz2+|x1|_YYfkQduVcaL%l9u&exv zUc+3Skmis(I2#6vhL1t`gTJOWp(1PgyY3%DQS-!Vi!4I16TW~s7HJ&g*%DnE_bpA2$P%-RFrVIX-yi%W&<;g^Vg_P7p^bB{X&9e$cVo$zcsHuP@q>issKCscVfY|S z_t*shx2!=C7r0J}BzRr=WnTk$4(e(Wn&FA1-587Gi_A;TAj$)#jujxbl1{?Drzk(N zp|uD6A98@e{{v$4IE3VBG6#Nw%Rr3I{L~4(%b)H(N3V=L!CU)>QV-@?w6d)KlgFGF z(DP=0f`f+1BhDB0A`r3&XqDH5rT0#^RD2uu&dMnKUF~V)11a%YbYj*hLdhb4Y$u2Y z<~cC}qW^3hmhelL&VcOo78Des;HsZJmaH8j1FvsUuUXG}@H5~u2BqS8eu~jfc7P2d z3r=Qp0isP39vf9z_#ypKKX;?OAXP0)LNrGYKqAYGVmB#m*2t2`3pEB0x=Z@nmRPkjm?_ z-4*ZJv9mxoui?p)Eq(FokqwJbcR7V}v~e20+k!G7t6gbC;;5oSi>CZfBSTNikLMgu zx?unw?{{p~5N+Isu>fZ~{P}X=w37KWs>}2I9OD>iivl+M5D>s`h9@1)KRifgfS*h& zMbO)5BD?w@z5A1j>PlL3j3$h$;4)Cfn<3@C_c-nF|5rK#+sa1W5h=ab^5%0xYz26a z!l<&iI4S_l>4nj53ImUgrYqrXI>LMJR!y9a4HI~Z=AC>wn(y8bLWx&}Lqb9V)zo^= ziJw2u3!Fiq3dKY@Kecp*FECeYj7q7$wLgW=8M;_0i6hCv1ab<1)^}tE+9ThnO1=9< zxC3G}uhZHmp`8B}rYk)5^1{Uk`SneTmR^TE&4G?~?9&Uw zp~)2s=fORkGA)@v1Jz4vR9GVW3+v+jodAcld|Z5s@7FKxcH;R7tms0&lrpZ8`jiXz zh=2MX8QJ&zQ+lMgjov}@f&PP|9p^OtNhG%=JeZL9?r^L$L&VC@rq1G|oC`7RKntZ= zK;CQ6jR0Kw$H(tU%+iNO&|m!{MirV->uYbMrn0z167RgfPBoTlb6UW>$mvkqt*Dp6 zl?s?gPS%2B46#137NL{eNXFK;BjE7CK@OpY`qD6E2%pMh7Gr6+vl z)~>8OdfosmL(QLR+R`@)uCxVL{Ae z`GFp*9|-@aHlM2-_UV-gZT||JUwrawQg+4{a3H6np;64X2hqTzFqGnD!1Cp$t5MDh zYqeW@F&mvn0~h#FP@R+pxXygZYV4_#Mf5|>%}lM1K=au@VW2%Dwh$I(ejnL0<;41yT1-q8VG4XcahD=d`4AzzQR zNB}g?U|d7acgP#ibj#sw8>hLdVJQ|MEFO*n(vwjK|)|~@S^tQ zq$E8Ei>>#lUFgHHXOAwZ{DFd`86r2vCdAld&>D%}3=GtXN>P)(FyR??{ycF_hk=tb zijp6rCP}|L8qqU>zw?#E-$)ph1t|Ac8?`$;d{w*u7T4GbryZ!`R=INYHzzZZ`W*u> zNz-h`ro(ec_}msZUz3+ezUNzMEhsYOn&B9jqLPzUWpxCWb(D8@T4eRh^BkHgmFzzf z=Gu)|0*gvPXvU9>EZHR$w4x`ucR64Oq=1Kj9KPa?;DiDgr%f}+g2cHw*q%`%(k3S1Pgxj#jPGZx^SjD9&E*@s$~upRTD1gv+6zy} zSxT*9-&rh1`Q2{Ni!KRIoJC*n(Mf~ zIMV;ab*xDTnnoKIfvQ`>n;gvi8=EV4*M>S9o{;wdX}KxwBeFR#?*0`{slH{r;T;e^ z`q^#2X5u%I_7%%i5&%Tnzq*%`6R}Spl{*ygz7Y2?iuC7V0bd}rc=m1&fK>B;S~3D` zDe2=$^pi2lZnrG69IC(UGKEQ1(m1F%*~zmx!{=Gq+qVRMx@ts*DXPLBYD_*|26lxb zN{&Gnod6z@N5RqUC`^85}4X9YPvqtQC(dblN2p(JKe z_~BvqL_x5rNF3W_Y6bov>;jM!>+!?e)YjDKZ*Xiz$8;1_ck4;`jJv@qQkA8!Sk5AE&4?jTE(j!4X@F;3RnF}6` zh>q&#+G1kBnYw}Dtl@YrkfvE50v^7Cpo7p*p%e3yLGjLiI@H^{ z^7k(+HEP>oh!Cq8q6FoCId~9U-!^#tl&6HQ;{Dq+@Aak81%E6x_W_o@hlx3P`6Zyt z(%upNSr(GPb?`Tfl(H}}-J7#d>vZ#q3quq)M=(c)Rg1%;)|gO4h2SJ_&*2It$a=Fm z4N>RjuHPb)z5?hAR*8pXzrm(2TK?1{i;v9ZznvfIPn5VLugd^wU{EZFADT(1(0bDz zobf848%CcOad8=0bu08gMMe$3c5S}Uy2TefyEk&`FIAIfk;VctvS2<^HaV98RFWBu zQWLTO9-vMIk|9k-9bXr7Q$+xs2*pt38&KqCV5&wV0t8-v8{}_#nf6|~(G5D+KYeWZ zEuaTUZMIXV;uugV$ft1B~hperD@3HCliNj}27$4>;TBcMV82I63n4Gkq_zuEA#u4v{CU2E46Lyw#Wx3#|NVH z`aWVz^nt4<3*!g+jhqN;XUM?bqG-d(o>`r_?r?LGKhlk+tupe9FOER^t5wo1AsIj% z<5Pgs+(7%gkT5Ak?CD*?5qCrCtEc3xiIs9$AXzrc>~sKu6#a;WrfWPd?d?QdscVKFw;2^SaLtLr)81wyJ3{3w3FHj{9-AmEiGMl3#i z5}w2lI2QR;J;6t?7*lcmT^jhvc50<$I+gsnih!EX>?j_WOXEOOEsJ5d&8oA3fkrr9 zJs|9G`pTwxze-aIQ!l3PyNR*{)=;TqF z&$zhZ`{RG2a(<}K)SptBBXb#mY44TWw6dRwUIl77^Ae5JEd`>CEk8KJ#qs_@F=xVn z=?Th2e<$4DOiRIuK$gl}{uvy-;(#Z4)n{MofB!|xRyihYA&TK}HB(!SY+!m=QRDkc zscB>svs#*s)=^c_{gGg0Q{m^X^8J(DKgb1FMa`x+f}Oe-sh}r-z`^F{{wx`yj*-rfStL1D+Teu|6m+11>C1~0YYn(CE_ zWwFcun{{Vk;8;af$Gj$9Y?-k(dmxOd;lqfPJf1U2j8B-FW0P|nJ*lNTD&e(044S^y*CSJ1`fME>mJESa2)?@NO zwA)Wt@GKFs7!2@m$yRjMjaS*gsD;gako-yXo*zb@c1TQfYM}bV$|~PiML{n({^JKS zTF)aWpj;O476|36YNw7RO3pX3U*pI3T`xa|H$_Y=hA-ym=z7Lt_SGdiXrQZ?$Hkt( zz!T!}W(A)2?_VvU#v#mLny4W!%w_O_2AyWTF6*nlqWl=>HH;6T1_1mr9z-~Dp?V-l z$<5;_{JlcH9l^xivdla4nLNP3fB!OD8@z<`)4+hZWM2bk3D{RKD9pC+p`iLX@ z{^QLBx>k$#GJAi0KL^rhHU`Wi=V871o$NA4jic}tgkqg>R9@ykzDw+~+kBi9>q=mJ z$B+SgHRTt6UN#2)6vLN=TRZ`ctH*ZH*qZ?2D*sM8d{DpSL}1viY3m9kcwy}p01|S? zo%1T?mU4!>L+QbfSEdJ%5SjQXZ!8n*TQG2A2>A=!bvL8LLfDo3NvbEX)Q-BifGcrb z_|o**n7coRy#n0WN@rNKFcLuep(dea636j?o!txuxzl3xN55>8ZRp`QC1R4#Z|`ah z6cffVkb8hZ4{7fh)PNzk@+)b*wB`>Btp1IqtJat#F?`_wn}k_74&C%el0tMqdj;Dv zWjHu%UdPCE+L_#$NuTx_`UI_@!37~`7Znw2%W@qxWEl7z+HUsD z;%BwzBuEmK&Pc!^F$?K$b^k*008JX7SxqfFU!n2E0n;i`5xI>td%~N5f$mupcLO~F zsaH3qMHJvr&SVUNIB6_i>_5UwH*lJGfMfO1M4%X&gG-DGOG}M!jeI|8d7p~Ih z6zRs^>ws*>gQ6*|kLmqQ6k&5IIa4$6=$WvB&drd~c!8qqXdQ~x-Ofv|+D5ILgfD&|^? z={ntrEdKkEBFFNI7?5LvN6|lT_K5sZ$XCG)fq8S!|KeUe5|VZ^9iS=AGB8i%azZJy z1=v53g&W_Bup-s<(n`iXuV0D~h!ccJzNSzDrtQjF3ZsQ%qrJMIQNM>oA7;4=-~gF$ zcaT0kf1K93*^Bz)9Q$S(4$PwiZ7UfqBNAh$A|f)n5X~(aFfb0brk#dM&Bq?)zu)|{ zWy@iWtmI^y2Yuog|DdMu$1GhKBY1S7TM&a;bap8>2U7*a95e_fsuEP~k+Q->M<1CRTmxHfZAXpT>&A5^pP@tMAzAfllp zAT(ZOFRUG13n_q<{LJql<)Qv~0^Q+RRGk8uoF_3XhgMpd9st6n*pQ|S)~@06u@2%E z2Iz?35r*%`5sqQrT?b9scY2kCQmju2BU5T)UAuXWeaQXmcXsg#g}W?EkPP2?!k zo>ZxSdD$L^806CWc1?I$?9DVBZCvnllCx&kY#bjB37P3-B9-PCo395^8)jy^{hZM;L!dTT+!)UG)KApWC;PQbKQTL({=Gu#@Gb?!E# z+y5|IO}Z?}IC|_DNhlGQE?&AM;@J1<7=9$%Ayh)N4v$(9&ZsbKUZcg%iJS<`f*7u~ zoD!9(JBhULh3Z>uu_!cu5`*U&mWNf*WblU0ylu4<5qqPlu0Zw-31y`Qr<$IM*a?0!lI{7ud6@%LGxM?d zcRsFGNQ(hXm^(Ut?=^mhvC+-uujmuhHM~G10d$?v{PiafT1fPaGZ|p+hhZ3$2qzVq zBw!jd2e%YToD8!V&XLRm5`SL zZMX%7GUuY;nXjZB32|;&HGLWP0(AlG=|2vxANR=%h>oeziYWU5+Y@n;;R}2wr1A*C zF2Jf8zF@Zy$^U@EEMJfr$SlfySy!&zc5>1Nyk0rRLoUz23%;U_>n|h$g_*J4&p=mt_P~0-;}aW#|We+U(9u? zt@@{np%M67ou@fDt4bA4tf3CLSIJEr*sy+k%?oW0LsxiHkZB=6O--B);#2@Qtoxw7 zhSu)%FQhntN|@vae_K&L0U*51w0HVUYQ7J6i$Z|84uh+QC|2|#_&J;6Px)`qsHx-* z5?+k+%RFN;9ES&rS)bhuH;vPEY z&CSgx|NK^(zJm50*@@;)9@C_kjejftNIT&ah(J8_gAq{5*n}Iv_L7+``@$AYTd9%W z9R(-%2_;5IA0NgB>k;A3B4X>1`#U;n z=n>2D31XK{^+mMal9H#~-P2$H0>BYmDJL-x@2L9aGSP<(VSC@+h}#(p{~AO6AI^S43!|iLYiX{Dy;a4Fs8iyiXu6y zp}G>q8bk&MmRf3C;Zx)))1|vaMsk-W%?vVaXv74fYsTG67wN{uV{C>^KMb&R9sy>D zcYE<-L^qtc{n=yF4`c~47Zw05&)5j*N6#O6E&E=Q5#76j{8*oRXi*r>b+d?h_p~LI zyj7<6+ckl3gwgo3wEpROdoFBHjceem6+Nn*!$m{olbp`Sk89)9jQw*Wg}XS|7`UEi z;MDK?D*F;@QrV8JLUeaB&V4F@Gt3*~qAa>LF^Vs;pP{P@z?Sl=K4J?sWiNqXPhi4v zEAIL_NP46)!WJvFtnQ}@P)?D)`7sxlMbS3<_G!9wt^V&bqthYmQ;w8+ih+UDg=(7@ zyNa0?9%g5NZ#4&eTrJ_1<9{DYQr=ikAV7+LSLpd&!S!tAuc;T1w`4xm&oGsxeH9Fn z-eT&0UZ9k{2PP{?(E>qLh#`xVA0nT>1Fn?c*L z2q;h$NXDzP6D^)o*FuKO2bWw94?hhcS7`Zi&1HWd)%Em*AYD3d-&zYRbRwhpPg~`{ zy2D7=FWgaU6@4%TT6cj0ox#8?X)~M`wXV~GCgd#g_H0G&k$iz zAa1+?h+E#D(Z-FuC+qQin{F9PfX_f4v}dmwVC9act!Y8k4n}z5zh@K+8vmCWMLD`} zRtEWDVqGk{cy`}pExh?a82ZAgVOPHz$Oq733sOyuz5`#DOtZk~X|b4Y9&(q09%Nc_ zQkFq$z7cNGGtr|Lb3zne)GLm$nkp7>>`5Cn&azxW)h(gyP}X8pnSh)?E~z+CRB+L1 zDa=)iw>~(7Ns};~woI?XMg&VDWH8o%MqpdVQbECBkPxj9_$iptT!f68q#fuS$;}Ni z5|0wtz^NHf09!RGRBj!f`u7~+jFGrMEWOCd3oS6eObUJ&%~U&#?J8Ht!a0Z*R!@9B z1@0o66BfHWm=Y&eT3j;{=Qgf?8oQPW?B}M{PSZi?9H?9gpu_@vptVnTb{8c%`*`&% zh>koFvSb4f_>(gkj-3VUIFD^L?%2~u8xKL6$!O{uz}gf5N~z!eAh(i*CikBsV42SY z{zW^8fM~!T{xN*FWN5?&Hd|9aC0w}`Z0x!w4qz40`e`%_vrt$d*jHlG4io@0HW|eu zZ!Vzvcn@`7C94LILbJ^j+fBZJZc*v%EqZ1Y3$~enMJ{T`$G?EW9*lSp8!B@_1R=QZ z*vsjfY(B|L(JeDRJl(QauiBb^u1hCYtX%mX+)sutK>Z}-VdV{l;G_|C8TXAJ$(q_3 z*W;hh5SyMH+d$C{KnAV=*AHg0K%U@~w}L#Yagw|7ZLvmnzbJtzX3MdphhCsavIYG6 zNCL|!$G5^N;C&ysaK*&#!$pkfcyKd9{9J_Frg&07a>W<(QV#{%Rm&&`b4vzN^U^(o zz==q2aSY@)dY*_V;#)N84g#aXi2inA9=;)`862pmm*9mr!$XJzsqPF81_TW(aIUaf zg;b>T3e7u}oj|MzmmU80P4VNnl<=2Yz;QrchoJ&8!ht582!c{bJelo?v~Z#-qJI!c zt_5QZv~~sqDCvH)@Ez#2P!XtL&nL}=F$Ig?oaH3u{v2~~`dSQlTFMBXrb0!MP4Eo; zZ(wyA`hVgvqSmw?Xicx$F`tt&b{^*P!d+yDj{leaxYPFP`ydXGWf@}=25Gd)gy=5# zm`NkU0sj5C`(H_{nwvZbHw?8bcKc6Q+$SR z|1)FM(T#<{;N9`BrIcPqMs`b?hQBbjEf3J;lX;LZ_nOvUP2Km42J|c0Hl*k=f9y>t zvL0wYou{+A^R;GkisC%ZX1PtXWnWZtb;uj6-5bbWV7gT=NZ3GTpKp)d)ycCx*Z6yT z=C5`6^6GR~-{QuvmP^-c3?J<&YV^9+cOEgC#gC(sbHgFtP0amWt8JNuuO?l@4%YMP z0UVWLEd1)sH~(+^rgQe#986(Cun>0p-spAU*lz+(oL?t&qbuPWo3NiDmz*ER4*hBl z_4Qi98qDvw{OLD&7VtxXvN2o4uR|t0rAx>if-K086EZVH@-s~(t5B2+FAM@aXNLZL zHn7G+mp5-Mv;KN_4~k@S|LAX#(`O7$n{Uq^M@R~0)`YOsz;4kgdd$>?^V1!Y8z37l3zlHw+NAJ zceDm`&P;qckIoz$CX9;BI$v~lb|X}w#CTu4T%CF6$$9(?lI-fMa>FMY^znV5N~;)Y zF~1WyBAjd_4n*}UR$%28xE()Uh1QW4)h!hl^*b6=$X|xm2TWhtTj!GWRu8`qmS#Cz z$DCuD@$~>;S%GCRI}0_r0!tJLF*Nd z)R}jk+&fDK-tV?{cEZ330X&sf6u7y$(F#H!ac6;Nj~XQoV{O`EiODgfk(HR#$UHf97%YW*2L0f8fQ;t^Xc5TsI&*ivb+) zF{;08nB(LX779AjWC?|du$tPnn*$l!chRZ}QjKWuwLwKdc96Dm{AdO;M-N3tMiP0T zH*#Cxjx#U#kD&Zn@A{Dgu%}sPDn?>iK9dp)jLoj!+1!Qal-u~+9oqzt{TQ|`eK+=) zckkFloFAWt3_b%DGWWkcIAm1l1wsQ>y1h>n9}KVS`2nk%yr(6hzI%j)sE;*_f48(n$c~$&pL?LR4vm1!V6opv(GGkM^n0lP{qpymI<*0O&aWUw zjOcOuxT>1T^_k3T@?lkkFU{RM+XL0k%;yi^>Ic~(D#hG_A+w@MTYVUmq(3x=O z&W^#7B4HRGCc7O5rU%Tn*zEZV&1IX7XVg&(*M zc7D9MF}SDHgBu*=cIAyQyakmq&Q(|ag7qT2pD>jXW(+~#Z`|IZ=HxDEkC_4pn?U7_4BC0ynfaz1ETkfi$zegph&eYUGWoZJBVoT z>60UBnXg8PJ@^c$T_MP6c-xm>P>__KK98H5n-1=p-S>P$L+586$41Et98#c$k|t|~ zi#v>}8RY=~NqeXA>(kwgLQPOp0-!j>hjTQvna|G7uB;)yapSUyiHZGtPT>)fW=bNf zL|v7ylJ!c_9)mtmdjLEOwtd4?LvbBP#*wDyi}49br2uQ7VL-J8L3Cm`}l4z0;rP9fSjm zw$m2=Wy_A-J(ASQEeF)^(Wx(`2Zl${PH?fz5fBvAx3mnctW<~B^C4AZaWSz9v~pqi z4V95d3i?NG3o)?IkDGt5Y8+;ziNRneI?%ss^_Ol!Ub=i@AYY$I3mA46yvB$ ze8zYE{Xy^=di3N;H?q&kf%KbsFaKaY+RE1hY0}$a$DY>k`uxH_FEp!Nj~^%bvi8tR zn6GRrndrTuV)U5Zt8iQDnhT!Tl>tn41`6TKFhs>f&@y*EH~dngb9H@q!KdXJvzN)? z4g&apg0NTf@;KV3ADvx)sx*vWHFL`$gT$;qmQ?c`-=pew)`MGa4>B{^u{7PQINGDS zDRZH0z*~)#7|fspv2|Jpa15{E zDJR3MY)gc_+cNU)5xZ)cNREW zcERZIu*}cby6WHI)1P?j*0#Zt73$i17i8VOUH9=&+PMp7&(6XOp#Dzqn9muu*rX&8 z677pe)mm);%tSWaScaowT$S0A$7ZkjgA@3&-K34WVf|8HBq?Z;^LC&!-~j@k&&}=OdZFbFE7}sry2_bfVU}^ zEn6m1HQZG1x`(`|u8saE5XO%+Y;PLMe!YjzM7dG$*QI*_0dqslDg)+vfgR%4HTCDa zPB|7N3*p&=8pI{Dn5Kys&q38c2U>YxA6Agv3#`4{#*O5J-8Td~AqSJ?0X|`O+0>-; zs_}WLtDbdq1cD<&R_z&1Agl+Y%|Vzfa~-#pYz|JpbEgUtw!Eid{A;T|Zt1RK`zao^ z4TqAp$C0Un?`PS&fP^tRc;;6-?+PeebioAraE++ng8q?(+})}Y%I?4bXdFHP0zil< z;X1Yft3Oz8~o`>mtfB9NW33t*vC zjuzJ)DGGO}{)mrTuZkH?bmg)?!%VwNPU;WZ>{R)xdA;DZ(U%%5HV)~$hUyUxwPEx4 zAmE`*1{gh^VdVoGb3cc;YLLS z32|}p;mc*e=WP0JB8GbhSEEg!7l93_r>Dn2ppJ}+5>`-XOUgG_wFP33aiV0?YoHL} z7cZ7;XtgIEQ93QA8e34Hh_NDhV=J#Kf_0!eV(N8oi++Kzp--P4QMv|3AQNn&iiygV zE5V4w&o0fH-_zSm(I10e2_!7#8yDiWU9U6J>p#VC{NaTN}0kgjC(X1KF@Igtn+6 z1-zX0#SyZ`fVD*C+Il8-0Y5;?WZr?Y><4_{*cm_IKzhezi6H5vY0F|-TG}QJjkAwE z5=~fRQJ!7=YljAg`r)y$FQoU63M5I`B9@1E^hM^NE(9^-Ro@B8sHiB1SGC5wb`hjh zZ@3Asl$ghIu@^j~RS-~TBuu^T#S6ME7t2OiQIo1R4+lp=VQGB)5;9_iDSJXeL3@El zFW3{Q)`H4wMXYLLH6K6&s-hwrRe_6ji)wFB!uP9(Lpt3sl zvPS~ABl)0<3LI}FOl*qr@EKNk9od4;5sLVI(u!q2tjRzy8>ni86c$f+jz9golF=Gb41v&4<=FB${vB<+v?9?nqbQYD9yA*M8 z`OoqCBR~5#PNf&>dU>gWchR``gqxc+U@niTEL?niXKF&uR#=*!11;{l||Vy(hIgh&KAEUwm41urfl=(U8DR;3 zaM-eM(^v)5vbSg&wVj+Y->7Cz{dltRee8j2GnytfPkkd~FTW!ElVbboNtV1N2fD4Hw{iR`jkOl zr`1yo5;HO`D76=c9t1-b%WO$1iucph(sBl2@-3)x8Nb>PtQ{`<2YT)~0rUX3PpK zD=TZhdrwP*v-fE{c^x`iv`!s!zVG_`$FcoiqlJ;(mwErN228O3)~#D-)Dx{j+5f-x z-aDSl|9u~R*)t&_sf^Mvi%8iS4GGyZLT1Pa4O>d0K@=H9!E-k0@p_%E>v=uLc^>C+9_Ke^5TA+O77e0dsCePqvV{{z z@aqzFJ{R^eIUHA(&=X4^Aw~)9-!%@FTz+?I?bW9*TDfg~7SHBp(3(R_9f*3&jV0J1 z5%Q`WI+R%}WV|WcK*9={j2tNZU6Z^qIyhJX(x1>wzwlddUm)as$8(w94Y|SL_wN~@ zP0`j!w|MbM6OjkVt&zFUsUx9@^8(IQW>!}FmeKmEJ(kObd`j84xK2k$v+p)LZ=eO{ z1~$qjI1lh(ILkk86c7lRDT5#fiu#nsVy?{Y{z9b3ks3F??#ai^zGzv=oWOAc#zmX% zFzK`5;rV78gT(cE@=i0F053TB{jN)26G<26c-mw2`#G$$mD4xT-J{CI}JBHkKqc&^=|ORoXc zD%d^FixN!|a4A8(?X(+D@@!YdRt-V}oKNb`G@T9bpus{6k1ZR3<`K$~LWZ^Jc!elu z3dlcRW$et*-oBjzL$u-vt`Zz@a&>Rvu5DfWR-oO z2gpKl?=dPz2tm+nWIqTf^H6eZ`pfAf5eZ@}m>6j{0Z|WDr8E3Rkru`pJeTTo4s~rc zDRDm~T?q0yp-vnj`-xL@Gv2bc$lv_q)icoVtX_?Ru}2aJNY1?XDRx#7p3x*4gpj_u~{+m^(LX_=aa0}|-P z5orBH6e%KTjUnNZuNoT^DaWky5bn`Niei!`-iAENwc_Gs3ezJ^@E& zWWwCHI*Sx7ydgTAvfLeVPN^h7nlr$EJVjXvM8zZ$3za5$5o(n9tm49ak!}`$i>eXzySnf z(kml^*^;X==*+Cw8HlnX9a7J|4fzPvO(BESP>hN}+w?MlAleK2l~l{uVntoo__>QB zbxHEe%oM>gXXD_Q)Z^#9K1Rtx#DS2tX~@Y0c7l96zqt4kbk8FBVaXF}KiFW(T6WS7 zZBq&r8^pW>9zHgzcf*gSh^`OB zPWAfTJ9}^el2cNGFU->NrY=!_cysaW)@|FW>+8clx)DH4?Q&x5`|lYhLiDX4@zhn99ArzRe!nzn1S` zT{Kuun@Om3u{4PFiqEY~EZs;z>awq+#{Vys4baCa7ENHrYxnPq33#Z_AvvV)p4Z~J10poJ!>{)W6)iQz2RSsZH^&x4j zc}s`n2MD^-e;`~t_{%cT+v1B5EBHlmyf2^b4y@eId@S>kRTXy3%3Ps5BjIk}x#J*F zQ|1($3-1og5Pn}Bz|6X`>h5wSTDrya9^||yUY9EKH6${l);`}7Li&q@;R1yC?mS=1&+31zsmW&ax;{W<4jJ-(!Q10Je1fzOzh;qj1m(QNq`-%#@|%t zy6`9n0(l3ltSA{67>4D{GxQFC)j(7nV9??3vK=3x;9^(W?Z1NQWO^>9qPHR$$+Of{ z&zrFtlF=93gu$`|0FYZ3wp#Ma9&JsM;1^YHCK~($(@t#DrT~yEEkI@F;Y;5kXJ?Kwk;1a? z-%pqs>FerJ1{Ow`x9nHtEA2W9kCIGQR`zt%rO3#{9fk_D zM(l^>Uh1VSrJ$e)kiXEWJv2DjE7N8xfdliruHJPP`Y=>aB`Ci0EbVjOx=~(!%x&8& z(XO<)V{zidN}D^&z=9}i zRI;?>1L=k>?RDy42oe_6?I)yeG}9G!hTmK_v1FUgo!BF)R(5u`l11O!2(Yme;SH;rI zk1R$Y=TdBP+CyHCNLC~O>)$W$Lb5R&v0v98G zL`Bc<9^W<~t!*TT$u>APHegc!`0;(Db-Lu3Hi7dA&jpG)Pf z5F#AE%eZ98k_nV_*MHA`P+R*7s=DYL7|N?D9*I z%a3hdo9^DV3-fPvu7OaQXyeq>RJLgidx6D9Y3Wm_M|_eG&vQ}KP*+OR@JEY{6-zbB zjM;t4SHhV)UvFs2lcxv7;Jx0i)vQ}}q~h9VD9e}geF_{o&gXvOgdDbJGQXaSOPlkT ztys~2+yTm7+IPGbg;;o{QHyWdBy&S`cT;=&gQHp4-)}7~3~3&EW6Cf<)|*%;#2I2Z zUEsNU_imylexSoXP3flkMzS|uDO$I5wm=cCCv1na;t3YX{_=jEuNGjdl%t3wbnH9y)YZ zDpE&BXLXvlu=2Z9!4$$d4i67M9ko`XXuU_7rL8S3Q4W645nfpz+|kpGUz|O`k_~7_ zMFwNo{Q&q)DwiCbb)E+YKc&vXPa69S>aEE`amOJ9nOX*4i}%?Dbr9bnUQDQP-h7WOsn(BN~5x&sQE-y8OeJOiYiU zw_)GHH%@*0Xo1jvlBib%2lS1M+Af146A`mJPi!Ulq@>!N%M1CEvZ3}8_x=O2QCjBa zS)X%X@6a|j4$HbbyKDTQP<@rvrb??#WCn)mQ9u=uj>BfG&~n>oO>nw(Ul`^N0`#O- z`)|=hS!LbbX5DO8pB-3=9eVL2zRN)1@7Gxx>YeLK#T_8Cs^sl04XRvz~Bq$UM6Dd2zAy)Td9$sJ_+G(gMY>7BxVc@E$H2Ud*NsLobsv5fMjMrnTBvu1V9j z*i< z{#}P2KpOSnqfPa>L@A`Yz!J0LA9cGQUx*sV$`9QtYcD*eIEeE&Vc`1bT<0FSmX?+s zvnw`&&rWp4Ycx9tp52A!71IY@U0L?**+UpX?CmY=ni6!~nwO1TNu1r`#_o33&fM|u z5s6K<&}y}~_p$4CPzeuaSaPi=4ycfj5C9leFJ>FX7>VL>(AxoIs5Q;XAx~2+u8A&4 zF$oEmrndQ;>DWkcSx9il|pNgUhwQh3KM*%gU&phm*u z9u3Wn!eOzo%K#7gxjA_hZ|R$4Vu;$c#-~FktNmJ05hsvq2nw(sH>q&Dbr9izeqJM|5D?| z1*rplX}VWcQ#dvrcNVO>DLAJ6tT&UMBw%C8A#F|1{ugZ~v}jo1(|o_#50I z7BXtrCTe)u3$f1j?ss)~H<%C19h#y6S)XoeP+9&vP3Z*ndLt9-|Yv7q5T zDeYUW_Bwicd_b(=V7wMyK5aIfd@=FN)G|hD4TJO*2lg9AOBFzFHpfHFm}#FqV6&UZfZkGikgef{jB$d4(|E=*5nG&MDi8}|3f2xMn> zQ1fg*5lKTsBQ5xxkvj0^7f((`>L+2Ag`F($38SsOn^XMd?kJtAl$5^6<+#r>cG^hk z+QjgpCoJAb0(e>EdWr^>XLCHpFTV;}%p4~! z_=d3{m;qi5cCaV;;qK0yA(g$oJY%y6C#^5-JhptY6 z-xVc`z7=xXMjMPFPu8+2LS(bLjmiJvKATJu1#_Q3;Jh_iKj(7&IaEEs(8LpcuC3Dl-u=7&S#T~D8zB*t=q8SyiV$o z`l_Re4{Rl<)5q9%mQ^cVYx*ox`Ka^#d!g^&zc{-eIYQL-lqH}ZCpjnQoK5m~XM1~k zjT=kw59$VrNHRmL5s7yC?(jQ7>7HHarM-U3>nSuQpqVo8iwwSRbolIR^DK3H-So!( z!xI{ImWFL({%G)AHRN7pb7w|YZ8b{noy$K)TdHxWl8?og6*^S7+_Q8A_D}dOoH&NNco+;`sNpb3`@G^|8Pyu0uTE}l!yj`C#J*hV3q=oH ze+rt_mx^2Q5I!)OSa60M>DjX$&KwGL&wx?#=;&^~_@^65X;+~n1{|J&$zYafHVr;3 zb$_igue0I9_>L)k8*C2GyKk3cN{Z1at?i3skAL>JghH}mt+=LfkZbo`^aYF7&wwilqfvs5TA z(?AuKAM&sS*arz1dnX+8gwP>V86Rz5;w9-(A~wR3=UjH{-Z2n!5V=c_pXLb)><-um zu1+QJeu>;J6c@nB^mxyrNJ~?$8U@uz0>ue;gn$y!aDC`Ge*iPJPwZ>YgXrEzNPHS? zgKvRkC~(dQiY(fId*ez)q-;Q@;i@`l4ot|&AeY}?AsEoRwu~iHDalWnPtn36$;cRi z6<8Jf!KLQ*Qf2-cC>6V}wzd|2+cVqMQID($!5E%PG#xKq_=8 z9n~s?hD#2FcE3(0%)M_f0RBVV14>*cN_)PY8=TD&kKnC21Z#gM=PvBv57{0~?&^hz^E5x7Th)>h6~t(hx8QqBzz9 z=~sIv2wM|6!ZOhGnm#)b_mgF?7~wj>ICUj7qm>y*yHZ#I);@gvSOrM2*@+oz^pqof z)CXou0{t&lQ4yk|geVkhg!LVcqIw$?H1%s=4{pDH_wGutDpb%07TaasVT}}RqQAGI z$R&sVV?myM87^e|;j|F7pJ2(ic0@--S%7&2l9gHQF^v7(nKFuUgHX%fdN!t7Rbs#PhT52~J6@gt;jr7-R z8QUb@OZ5aj0wu#ytZ>`hSzNpt1w=W>jhNg$R{UUw?t8_s^r0gVx+93e z;fDgD90AEJMy9e?SMezwef{Y{5N*CBs&vMq_PG+>WCaKK@*)z07)T&Kw0j*NMWNNX zD_7DEL=>sq(%};qzoRyX0f#JNB25JCah@%G%)v<9hf=tn-k)+83IS7>cys8lFazn=gvoGe}{Y%fv@=l%F zk9Ycl<%*^&H`_8nk0mJ2C{5lnzN>IbvXDw)eZqD|MF#!Vti*Zh^*UUtrNc90-7LKR zE#vA(0IGiM=z0G9Vb0nhIhH&Z|0e^{sM-QgK+i!C9BC9kw6wOm_QoC>beNgKl9c`w z^mJ_ip5@oKQuitmHTJ!Io7z@@*-ES~EN1C(^)HZR$!-4*nT1#2zi=PjKFAQrEJ89c zG~^W(U5r8`dneRphl5h5n7Qs<+B>z~yBWaw4gxJGPTtq-wXR~2*hbDi@(SdwBNGxL z(Ea;n{x^ai$;!%Cv{VI-5IcI~_#EGROXn3rCt8TgAngLd-U*_G*{~sSm z_m}T!swli?gVQ)2D6IfM_oHD9D%{`uMO>=yM~b|XV2gk%Lf{WT#{)m9C28(Ql3v|H zV$b(aO4U6*2{XrhKUc4s8adlXHx0mwAb1-Y8O`PcO{sze0ijZGR;F2w7<*vg=o`(5 zW$&Qr5D5Y-p@`w@JEzzfjLx(ZQ$zbwzro{N{7&ZBTl2YDj>*f54ehDdyab}a!^1<; zzNM2ff|a<)QZ0y}IfGXo92|^R9Mps^D=3lW?ZK1Zw2X{S7k2wVaOg3`r#`$ry&#FM zFf)^B?%SAetR$U&7;q;_g0>8bMN5ak>ec8@d66Qk$Qt(rIwjyUTClYUHp17LM&A(B zeyc7&O=#7O0q*w8`U0l;#w-v-siZz?KEMnzaFY8fwk*g69X7`w$bUcgk?LI-iZLxw z*v{>hVh-{Jp%T}N4_AX6NAy+tI^yhGjdeFGD+0$8AOb~6^t;e)-XE+21(#pnV1i@> zc9Y?zD;kudCfHSx%|C{uw!IUy6Ze4B?%KO_E;~233RD9vO-(-lj=jMgW`nJn*RE0c zqKh((`yt)F4;B*7ByU%FxVrj7cXMU=kn(sH!4q>u-;#V!=vD&!czb`9X%B9j0-#S& ze!fT6SX!rwQWUUE7WCA%>_AeW@%^fvUgQ1r%tbCp*JU()ibI+Y^KwM|we7kOAb%D# z9;~D=v}-x;V`tz4QotU&<6MR@fUrb)L z1?T5)hSx1oXS(t<>iIr)WM!tOZMZ#Y3`TU!<;$NOvUD~OXf(lSRiE%d)JyER_4T`7 z56dxN6c-`4t*NKi_IjI@z5VHj4|fpMPXPge^iC`I7dT^1Em_sdyQL5H6H7qf)hXQ^ zg8U1b+tgfHwrTGqh2~YzyJsq6l?0k6JFCoht~Nh*GoAW+HhOf0 zTLwv0i?^pOb2!0&Mq`Rvp0btPAVCT>p%#IV)E>z>@dd{w2zHC0A|8xgqvGM<+=TJn z24^yV<){QyT?4w4V@SEh3Si-sa+SY+)mzX!%SYv9j*OawbBNsB?UA09^?*3sptPu( zhbqz{Jlh+|U4Rx5hd#^;3=G7~e(KOfB`7F}8WIcCK1SB}r+0CGmU#8StjM$<6{Y33 z+dLuLPF*;%0{|W(=)97zqu8)i@NgqeoZBIhn7*l%rMV``C9cSS`HDc(OG zJhff`&&Q*CnyByqkr?4^M!C38#JRj};8-9E8*SRlKT3k0!gejKb4H~^h7O?q>k9ru z>oLT~j>!-_z=VXkhA*v<>b_#-u=J?;_YWLx!W!nt;dk-e zNe1aF_T(Jc=c@3w2v!m*V~A1!@N}Q;prsEc2jWlxZp5t6O*zyl90M_c>x;mJM+VZ5 zv4(GEWZVa_%kX2G`O%ThL|qVysJRl1jU`aD1CK%dC?PHbu@=<_o;{lJdkY_tB7#d% z!UREIm5g|_-A2m3>WWyoWHESg5qIIaK$egJw2LaRkw792hPaFc_)mnCUV26bv6I8; zB-lyS5M*{ipICr>iH*aMMUhDF6_8XFU;O#-i+9Uad=gJBiaY)u6H&M5L6w^+lyKcKQmOha^(M@lVTYEGH7{`eXz_l1l%iaN8jI;>GM;pFTL9 zU>NV!D~8V~sFz+r$ilQq%E_5slXa!*dM4YN4!b?5>R7{~(bO zpMe^-M{5(ZXFqRw^-i}4t_wm2e%0=RpC(#uSN2HF!G}tDbR(cmTwN|(u{i2N$5yD>t zEu5N!geC}}i6G~Sufh-$JoSwu2rb%PCv_9Z5LB7c8i59j-u;}caqJlTl)y{y-#7Z7 z`93~AuHfleplW9yG^&Lnc3}@UwBmxiV~olSED^QRQT{_=xI>$E5_r|9nqaDHM_{M- zCH_daBBtl!ZL#VhVEhnN8PJ42n!0&-^jo*?D!&Q7b>Rb1V`q136+3>`UA8ALU*^)g z>gMZk1d^xONFNaOjpgMl7O#?1hQ=>~qAff989@!y(__EGHU-cSE&ZzD!S+E>4L%~K z5%gC4X9(3Dq8NR&=iXw!gu+LDD}#)+wUe6;xxv9!L#F;aye+rh)(VJ`l&O}Iaq)Fg z?0a|jIjFhSD4-!Cc2fMUbEwX%hCD8HS!b03p$?9?l!cA0#&IXA6$$Zvf*oeghc{Q@@AuCfgcgnBY%C0wNU%x<>j6A+%9^PI#ozWNQ~&= z@PMc?Bv=zWcalAwktKXQ_d(69eTkV(nGAab4^I(Z39-9iEX?YkT#kv^j}k?cvQ>eH z9;0SxI_^=kbbYI?Ni!dOABqj%9_e3IDe7_%q@tm5(6RJfw-Ie)kwN277^oB76YXKI zgf&-JU*FWyLW$!m^u8surr*6OFm0b}j5}Kt!egXVi1c&>i!iB;@AE-xbO5EkAdogq z$rzy7kGdI4voEu=OtCF({z_GAyj0p(YVw`ik#;R3NpZ27bn4M=5%X)&vx$PJl*g&V zI@O~}*rmQvw9e--)pqII>f;m|YA+GxyLn5;C)v$t2RvPhKTD}-OGB^05=7$dWBs4L zH5I!orKck5EL#@O0*m%IzVpe%^z=cw9?i@db(fP*f{nqEgIw|^C*HMZr-R?gi66^| zQQ&0X%5iAgw2N_15nPdUr+qt$C^-gAif-S(FC;8XwB|51KfkYpqfs(%l|Vp(%hp#= zMvuL;**<|vx9@Un>>@AyEdiX2arEuXYW%u=B}GLQaCRcPnYUBL(@%*v?_b(y#8 z>xZc5)mdg0Zd8q!$#gzdHHlTP?a&Sk+U*joA-Ml0eLJ9i$ZD6A#p!{#s9ruWm07 z2k{t;N`o&qQQWr*a_Vtxu@~z539kaQx&%pVch8I6)}s7n0-NL=@_oc5h>JHDCs2|N zY8JsW!L(H`y;so!Wdp1@edgRLA+KMD<4))8`a_Dt|+-Z320K5uAr;24E+?Z5Qz>cy?FErxx zd(t!lDGIe?$I80dR{(kJMai5+N19&7v(L{n4>p)ay3l1dopLFqNku)|ld=McDC+4& zcTda0Rkbn0$+GC-s#`jB_7{lv?R44Yal*mgzK%#rKk|K)(}T)IxA{i_+7J~Csis<5{*Yx}$3|~zV#H5SuwTAx$fGFoH8-Sf1Bh+hekNZt(;UV~+i@?7!bNX4SP?$b{xo8JNf#nBva0B0$VGpi1X z4%S(P;i?R<(?JaGFrB=IRq=UdX5%;FKu3q%>m&6J%1HmDWpM4-r?zt^9cUXdl8>F~ zjwgEbdG7UacXJyqi#*4I0Y>@aqq0lz7wU{&oo3-z&F;9Z^nP)$l0TSQL9J^1B1wz$ zL`+&|G7$EGzfG0uWMdP8gXYdNI;d%www>k?0o^)%zs~f1s^dtUbYI)T9bOb9_jD+< zv-48NlP64wbO_Bn5^>f+*Bo$GhiA%O>`MQ`IlVgc`_tPlQM*!9cXuHuB^nwT7#NU9 zfH;`Bxf?c6G;@I3+>l8IA;}$k_N=&=16S5u=AT=NW2 zYwvA6)+&WGI!*-s>Z77hmuZbM0AhLHS-2Qy7hvNDxHA@XDnUh}hBgmyb;5TggjPsI zvqlWD1w2OhpC@;$R>EvWHCn|n^r)aEI^=*HPNPuxDCqmsccGoKVkx6xvQQp@98I`g zds~|&YS3-%?3z-I%L$+u07x#mE&yNvcU8b{;0Q9s$#}EkqQPDWMdLH9e$FOxwiv#t zlX}5vTrs)X*%bhvTUOpdqcCK^HjW@=2(~kvpK1H74cv)yTBa*NDBgm05>VXpIo^Yv z7}bMgS~@yG2nAjNfJ~pZt64J@yyHbVA6H{6N;?R(wM4gXjtI2$o*1L}4)dCzQr6Yc z!_@~;;bT~=%>0h|qRSYV!gX#ULP9NHEU5yX(gbuopZ(8)dEWLEZy17VsOD0TkgrI4 z!>Gu(nUeoyW~e9$#e1QAB{h+%nr6$&3IE@`WoDJ2rQ(u4W!)?D z^JQ!#Y7#Ae$xD)r^X&>HBay@?NF)~g#>K^5)YH!Th~{Bu7dH_vC&x3|dz`Dp7|Zu| z_=X-{B}}_jPb)Iju9U}#)p1(LV|S|LsaK?6bNT7OtZ<6L=O<2#y_m4li=yiE+ivY% z^`$&g{OPmny9VgyOzz5^k=`!qzJdN4Z{Icrxhvg0dmPKOvcI{Up_QoR>k}Mv;-*}u zuSx%Y8N+4!Sl5(swi1~g@y}UZ4JiY~&ID+$dt7?r$nJZOHm&Kq|ImZ3qi{77ecvOQ z%!~Iau9OEq-l~vnS0XC3$8L)$MOc<+O_b}GgMI8lDN8RVYo~N25GHHwvS?a{T8DK{)y77uH?tk*n{qq#m#sd(KpjTQOXyC>i3Ty~kgDtT<~JK4~= zH(VUK)rTIYuu*Y;FraBZ6`EuDaZZ*0NoeXSNy&BPrRSN>9+#BV=-+i`L*KPf4GXoz ziEIjwx+joH%CFz+)Y9G>#;rm79Np~$5nvWlK*;a z?iQAg*8AMO9j$*nIL3yZOaWq?3Vr3Lj@W)bF1%f9HCq*r;E{J^8!qw=DcD|Pecfd} z^WnVcWBFalmki!pNl?u7lChR-?z&Dx9dR`9fI^#Vm#bag{%m>%?}DBb8P~nuZfR`# z!%tIfueeZOb8}M)by7vP{RdW@dUb)i<Ee|2-$u&zm}Q zim`=^(R^8~I(=GBXj{&yeeWOoSBlgQp74w8S8kbA(C#>!Gbh)$v%W-Y-8Fr+SL1hm zy06_S_m1n=UhlrL}sX7#bkL{Z*y0X=ZiR!j?xtli~ zS?!`v(7kZ9Iv~vDvg=Vht%|p{YE9H+-IMYrD*k837O6>4PKe3x7f{|Pa*m}VAWg1J zWvo_f_x*U8E6aK=#*M&{=udwIF>1B^>IceH{=ydM& zBA(B&TH&u`Qi9JN{dU&t;$3>LUKL+kV_jeCy`vKNhD?xEt!?` zfgg`d)WXGu*h+YkDaqC)lJI2zzy0-IxAJU<7<0M)J9z`?d>WSkz4rlU^ab_UbW@c1 z92>i>l+w2rSeFV-4G*ur!d!BbRdz~8!`bF)_ZhQViemYT%IMw8X1lEu+wZ_hpc94)RhulJw36zR#%_GD;%t)y_GmFHmnGyTKX zyWSqN^)%?0-rJ{@BEKt=DWb~dOkb~pP2?i?wVV>Co6El#H>szz9{aRj+CEEdXEOH^ zI#GYsBZY;O36>xB#5O!z2Qe?H7#Uk(ue!MDpiD3Oi@L?z1)~oST@=<#H>;?rQ=^{E%<9n{#y1X|SAP}~NNY85G37AYA-R5u*tA?z936vM$?nKC+4eM+wc!Ux zyWAJw+DC2pz1g{Pi$Y_AQvayXbMfj}cg5`v0bC4D@ktD8;#G_pmQ|YOh8#b%YI6D3 z+jJ|xo_lV)=EUY+`jJ8Ukq)_wE^8~ynk)|&tE^~|^Eo!^(rg+3#d%bOb7lDK+4k)c zC!{82K2s;lo1c8EK7O!Tw5{!9y`uBi;iq{NM0Em`004U+DEDTsWwGoCpVv47l;M)9?Oj7_a%0~`LOl>zs7#;D`(D|?)`B@ zocpwj&)kXiRy^B=>}NbVr*zwcBBSIh-=^5S*(xR*Y2wUZSp^XS!5 z+>!ig`dK+U!wpZf1uZ?sJ)5(vW4HJI-vm-#=$K;H<*QkdYo=o(X<0g{>w& z%(mai#!gujmR>QUEIZX+q^D_G(C>60DfqP&U51f!xZWfEPG_ob8Hsa~@m19q-d$CR zovtYo`n2NSvBkGndsdrY<`$T49+TJ8zusUdxBG>}<0ty>gA$&2J^Ajg)e|RA`%$i9 z?HTWaI`Jnqaq;&yF$LuA{rc&6bR(X^&qJMzTo@#*|G1jU?3c+uV(#8T2^!(E{P|_P z-T9Ea^&xk29d8$FH&bFRYwrHjz@$S{@B}`kg~?A8rv3U7p(K&a4_iCBiHZ>4=YMcy z$GPwAc=r?hb)H|J!hdPf{B(KU9PO;E-9&%d`0;}@aRm!nJL9dx{Rn^1l$%6a`iC`Z z_|$|0{a@C8{NKif?qob|_r~KmCCP;5o3L2)w~0*)nK;gx6T^qAFWxra1Pgd?^RGgD zArtn?dQ+a@Dh^A{H(|5avIV}8w_s0RZ2w=O(U)iFB1J=EVO>W^z#aH7f=2qr9S%|iWO)_x4m zg4QOV>wC>&x?3YsGV`{qtA< zzEKXD5qrY&Jy_oGn_Vcp8~?O?2q`MJeZLF~_0#T;<5bl}%ZTHs`TF^1LNY{LOVPl&;{ySp}x(L)&oK6cHWA^&*j4kLa_`b18UwHV2ef~B! z|8f7@znu_awO!aQHT_6GRu(4?W|IXNgS9W5%n9;%m=o<95vH1_?-+qa4)YY72 z@KKk*wI*s?etn$@{%P0U*4oKh^vCz#ZWDsrKfZWL6CXxZM*~SD;`OJ6_A@^%I9XfS zS%}&>Ssb+9NYu*w9Q9uo7WYpV=)*xKUVr+@)68T)CdcmIITBfhf0>Z} zW#QLtWHfDB`4&@|1MbeB(|Pl`pB8@1x&It1zx}Y_JtY)A%}Np_k%-ry7i4ttuM66; z&;{i+8Z0ZugI 0] + nEdges = length(edges) + + W = zeros(Float64, nTFs, nTasks) + Z = zeros(Float64, nTFs, nEdges) + U = zeros(Float64, nTFs, nEdges) + + for iter in 1:maxIter + W_prev = copy(W) + + # W-update: per-task elastic net with ADMM augmentation + for d in 1:nTasks + dEdges = [(e, d1 == d ? 1.0 : -1.0) + for (e, (d1, d2)) in enumerate(edges) + if d1 == d || d2 == d] + nSamps = size(predictorMats[d], 2) + A = transpose(predictorMats[d]) + x = responseMats[d] + + if isempty(dEdges) + lsoln = glmnet(A, x, + penalty_factor = penaltyFactors[d], + lambda = [lambda_s], + alpha = elasticNetAlpha) + W[:, d] = vec(lsoln.betas) + else + augA = copy(A) + augX = copy(x) + for (e, sgn) in dEdges + target = sgn > 0 ? Z[:, e] - U[:, e] : -Z[:, e] - U[:, e] + sqrtRho = sqrt(rho) + augA = vcat(augA, sqrtRho * I(nTFs)) + augX = vcat(augX, sqrtRho * target) + end + lsoln = glmnet(augA, augX, + penalty_factor = penaltyFactors[d], + lambda = [lambda_s / (nSamps + nTFs * length(dEdges))], + alpha = elasticNetAlpha) + W[:, d] = vec(lsoln.betas) + end + end + + # Z-update: fusion proximal operator + for (e, (d, dp)) in enumerate(edges) + diff = W[:, d] - W[:, dp] + U[:, e] + threshold = lambda_f * taskSimilarity[d, dp] / rho + Z[:, e] = softThreshold.(diff, threshold) + end + + # U-update: dual variable + for (e, (d, dp)) in enumerate(edges) + U[:, e] += W[:, d] - W[:, dp] - Z[:, e] + end + + norm(W - W_prev) < tol && break + end + + return W +end + + +function admmWarmStart( + predictorMats::Vector{Matrix{Float64}}, + responseMats::Vector{Vector{Float64}}, + penaltyFactors::Vector{Vector{Float64}}, + taskSimilarity::Matrix{Float64}, + lambdaRange_s::Vector{Float64}, + lambda_f::Float64; + elasticNetAlpha::Float64 = 1.0, + kwargs... + ) + nTFs = size(predictorMats[1], 1) + nTasks = length(predictorMats) + nLambda = length(lambdaRange_s) + betasByLambda = Array{Float64, 3}(undef, nTFs, nTasks, nLambda) + + for (li, ls) in enumerate(lambdaRange_s) + betasByLambda[:, :, li] = admm_fused_lasso( + predictorMats, responseMats, penaltyFactors, + taskSimilarity, ls, lambda_f; + elasticNetAlpha = elasticNetAlpha, kwargs... + ) + end + return betasByLambda +end + + +# ──────────────────────────────────────────────────────────────────────────── +# Lambda selection dispatcher [NEW] +# ──────────────────────────────────────────────────────────────────────────── + +""" + selectLambdas(mtGrnData, mtData, res; lambdaOpt, ...) + +Top-level dispatcher for joint (lambda_s, lambda_f) selection. + +lambdaOpt: + :fixed_ratio — lambda_f = fusionRatio * lambda_s (default, zero added cost) + :ebic — EBIC grid search over 2D (lambda_s, lambda_f) + :bstars_2d — 2D bStARS instability surface + EBIC tiebreaker + +Returns (lambda_s, lambda_f) for target gene `res`. +""" +function selectLambdas( + mtGrnData::MultitaskGrnData, + mtData::MultitaskExpressionData, + res::Int; + lambdaOpt::Symbol = :fixed_ratio, + fusionRatio::Float64 = 0.1, + ebicGamma::Float64 = 1.0, + gridSize::Int = 10, + refinementSize::Int = 10, + targetInstability::Float64 = 0.05, + elasticNetAlpha::Float64 = 1.0, + totSS::Int = 20, + zTarget::Bool = false + ) + + if lambdaOpt == :fixed_ratio + return _selectLambdas_fixedRatio(mtGrnData, res; fusionRatio) + elseif lambdaOpt == :ebic + return _selectLambdas_ebic(mtGrnData, mtData, res; + ebicGamma, gridSize, elasticNetAlpha, totSS, zTarget) + elseif lambdaOpt == :bstars_2d + return _selectLambdas_bstars2d(mtGrnData, mtData, res; + targetInstability, ebicGamma, + coarseSize = gridSize, + fineSize = refinementSize, + elasticNetAlpha, totSS, zTarget) + else + error("Unknown lambdaOpt: $lambdaOpt. Choose :fixed_ratio, :ebic, or :bstars_2d") + end +end + + +# ── Option 1: Fixed ratio ──────────────────────────────────────────────────── +function _selectLambdas_fixedRatio(mtGrnData::MultitaskGrnData, res::Int; + fusionRatio::Float64 = 0.1) + refGrn = mtGrnData.taskGrnData[1] + lambdaRangeGene = refGrn.lambdaRangeGene[res] + currInstabs = refGrn.geneInstabilities[res, :] + devs = abs.(currInstabs .- 0.05) + globalMin = minimum(devs) + minInd = findall(x -> x == globalMin, devs)[end] + lambda_s = lambdaRangeGene[minInd] + lambda_f = fusionRatio * lambda_s + return lambda_s, lambda_f +end + + +# ── Option 2: EBIC grid search ─────────────────────────────────────────────── +function _selectLambdas_ebic( + mtGrnData::MultitaskGrnData, + mtData::MultitaskExpressionData, + res::Int; + ebicGamma::Float64 = 1.0, + gridSize::Int = 10, + elasticNetAlpha::Float64 = 1.0, + totSS::Int = 20, + zTarget::Bool = false + ) + nTasks = length(mtData.tasks) + responsePredInds = [findall(x -> x != Inf, mtGrnData.taskGrnData[d].penaltyMat[res, :]) + for d in 1:nTasks] + taskPredMats = [transpose(mtGrnData.taskGrnData[d].predictorMat[responsePredInds[d], :]) + for d in 1:nTasks] + taskRespVecs = [vec(mtGrnData.taskGrnData[d].responseMat[res, :]) for d in 1:nTasks] + taskPenalties = [mtGrnData.taskGrnData[d].penaltyMat[res, responsePredInds[d]] + for d in 1:nTasks] + + lambda_s_grid = exp.(range(log(0.01), log(10.0), length = gridSize)) + alpha_grid = range(0.05, 1.0, length = gridSize) + + # Traverse from high to low (lambda_s + lambda_f) for warm starting + grid_pairs = [(ls, a * ls) + for ls in reverse(lambda_s_grid) + for a in reverse(alpha_grid) + if 0.5 < ls / (a * ls) < 2.0] + + bestEBIC = Inf + best_ls = grid_pairs[1][1] + best_lf = grid_pairs[1][2] + + for (ls, lf) in grid_pairs + W = admm_fused_lasso(taskPredMats, taskRespVecs, taskPenalties, + mtGrnData.taskGraph, ls, lf; + elasticNetAlpha = elasticNetAlpha) + ebic = _ebic(W, taskPredMats, taskRespVecs, nTasks, ebicGamma) + if ebic < bestEBIC + bestEBIC = ebic; best_ls = ls; best_lf = lf + end + end + return best_ls, best_lf +end + + +# ── Option 3: 2D bStARS ───────────────────────────────────────────────────── +function _selectLambdas_bstars2d( + mtGrnData::MultitaskGrnData, + mtData::MultitaskExpressionData, + res::Int; + targetInstability::Float64 = 0.05, + ebicGamma::Float64 = 1.0, + coarseSize::Int = 5, + fineSize::Int = 10, + elasticNetAlpha::Float64 = 1.0, + totSS::Int = 20, + zTarget::Bool = false + ) + nTasks = length(mtData.tasks) + responsePredInds = [findall(x -> x != Inf, mtGrnData.taskGrnData[d].penaltyMat[res, :]) + for d in 1:nTasks] + + # Stage 1: coarse grid + ls_c = exp.(range(log(0.01), log(5.0), length = coarseSize)) + lf_c = exp.(range(log(0.001), log(2.0), length = coarseSize)) + D_c = _instabGrid(mtGrnData, mtData, res, ls_c, lf_c, + responsePredInds, nTasks, totSS, zTarget, elasticNetAlpha) + + # Identify region near contour + near = findall(x -> abs(x - targetInstability) < targetInstability * 0.5, D_c) + if isempty(near) + _, idx = findmin(abs.(D_c .- targetInstability)) + near = [idx] + end + rs = first.(Tuple.(near)); cs = last.(Tuple.(near)) + ls_range = ls_c[max(1, minimum(rs)-1):min(coarseSize, maximum(rs)+1)] + lf_range = lf_c[max(1, minimum(cs)-1):min(coarseSize, maximum(cs)+1)] + + # Stage 2: fine grid in region + ls_f = collect(range(minimum(ls_range), maximum(ls_range), length = fineSize)) + lf_f = collect(range(minimum(lf_range), maximum(lf_range), length = fineSize)) + D_f = _instabGrid(mtGrnData, mtData, res, ls_f, lf_f, + responsePredInds, nTasks, totSS, zTarget, elasticNetAlpha) + + # Points on contour + onContour = findall(x -> abs(x - targetInstability) < targetInstability * 0.3, D_f) + if isempty(onContour) + _, idx = findmin(abs.(D_f .- targetInstability)) + onContour = [idx] + end + + # EBIC tiebreaker + taskPredMats = [transpose(mtGrnData.taskGrnData[d].predictorMat[responsePredInds[d], :]) + for d in 1:nTasks] + taskRespVecs = [vec(mtGrnData.taskGrnData[d].responseMat[res, :]) for d in 1:nTasks] + taskPenalties = [mtGrnData.taskGrnData[d].penaltyMat[res, responsePredInds[d]] + for d in 1:nTasks] + + bestEBIC = Inf; best_ls = ls_f[1]; best_lf = lf_f[1] + for idx in onContour + r, c = Tuple(idx) + ls = ls_f[r]; lf = lf_f[c] + W = admm_fused_lasso(taskPredMats, taskRespVecs, taskPenalties, + mtGrnData.taskGraph, ls, lf; + elasticNetAlpha = elasticNetAlpha) + ebic = _ebic(W, taskPredMats, taskRespVecs, nTasks, ebicGamma) + if ebic < bestEBIC + bestEBIC = ebic; best_ls = ls; best_lf = lf + end + end + return best_ls, best_lf +end + + +# ── Shared helpers ─────────────────────────────────────────────────────────── +function _ebic(W::Matrix{Float64}, + taskPredMats::Vector{Matrix{Float64}}, + taskRespVecs::Vector{Vector{Float64}}, + nTasks::Int, + gamma::Float64) + ebic = 0.0 + for d in 1:nTasks + n_d = length(taskRespVecs[d]) + p_d = size(taskPredMats[d], 2) + w_d = W[:, d] + k_d = sum(abs.(w_d) .> 1e-10) + yhat = taskPredMats[d] * w_d + rss = max(sum((taskRespVecs[d] .- yhat).^2), 1e-12) + logC = (k_d > 0 && p_d > k_d) ? + lgamma(p_d+1) - lgamma(k_d+1) - lgamma(p_d-k_d+1) : 0.0 + ebic += n_d * log(rss / n_d) + k_d * log(n_d) + 2 * gamma * logC + end + return ebic / nTasks +end + + +function _instabGrid( + mtGrnData, mtData, res, + ls_grid, lf_grid, + responsePredInds, nTasks, totSS, zTarget, elasticNetAlpha + ) + nLS = length(ls_grid) + nLF = length(lf_grid) + instabs = zeros(Float64, nLS, nLF) + subsamps = mtGrnData.taskGrnData[1].subsamps + + for (li, ls) in enumerate(ls_grid), (fi, lf) in enumerate(lf_grid) + ssVals = zeros(Float64, nTasks, length(responsePredInds[1])) + nss = min(totSS, size(subsamps, 1)) + + for ss in 1:nss + preds = Matrix{Float64}[]; resps = Vector{Float64}[]; pens = Vector{Float64}[] + for d in 1:nTasks + sub = mtGrnData.taskGrnData[d].subsamps[ss, :] + pidx = responsePredInds[d] + dt = fit(ZScoreTransform, + mtGrnData.taskGrnData[d].predictorMat[pidx, sub], dims=2) + cp = transpose(StatsBase.transform(dt, + mtGrnData.taskGrnData[d].predictorMat[pidx, sub])) + cr = zTarget ? + StatsBase.transform(fit(ZScoreTransform, + mtGrnData.taskGrnData[d].responseMat[res, sub], dims=1), + mtGrnData.taskGrnData[d].responseMat[res, sub]) : + mtGrnData.taskGrnData[d].responseMat[res, sub] + push!(preds, transpose(cp)); push!(resps, vec(cr)) + push!(pens, mtGrnData.taskGrnData[d].penaltyMat[res, pidx]) + end + W = admm_fused_lasso(preds, resps, pens, mtGrnData.taskGraph, ls, lf; + elasticNetAlpha = elasticNetAlpha) + for d in 1:nTasks + ssVals[d, :] += vec(sum(abs.(sign.(W)), dims=2)) + end + end + + theta2 = ssVals ./ nss + instabPerEdge = 2 .* theta2 .* (1 .- theta2) + instabs[li, fi] = mean(instabPerEdge) + end + return instabs +end diff --git a/experimental/MTL/admm.jl b/experimental/MTL/admm.jl new file mode 100755 index 0000000..83d820a --- /dev/null +++ b/experimental/MTL/admm.jl @@ -0,0 +1,194 @@ +""" +admm.jl [NEW FILE] + +ADMM solver for the graph-fused multitask LASSO. + +Objective (per target gene i): + + min_{W} (1/2n) Σ_d ||X_i^(d) - A^(d)T w_i^(d)||² + + λ_s Σ_{k,d} |Φ_{k,d} w_{k,d}| ← prior-weighted LASSO + + λ_f Σ_{(d,d')∈E} sim(d,d') ||w^(d) - w^(d')||₁ ← fusion + +where W is a TFs × tasks matrix for gene i. + +ADMM reformulation: + Introduce Z^(d,d') = w^(d) - w^(d') for each edge (d,d') in E + +W-update : per-task weighted LASSO (uses GLMNet — unchanged from original) +Z-update : soft thresholding on pairwise differences (fusion proximal op) +U-update : dual variable update (standard ADMM) +""" + + +""" + softThreshold(x, threshold) + +Scalar soft-thresholding operator used in the Z-update. +""" +@inline function softThreshold(x::Float64, threshold::Float64) + return sign(x) * max(abs(x) - threshold, 0.0) +end + + +""" + admm_fused_lasso( + predictorMats, responseMats, penaltyFactors, + taskSimilarity, lambda_s, lambda_f; + rho=1.0, maxIter=100, tol=1e-4, alpha=1.0 + ) + +Solve the graph-fused multitask LASSO for a single target gene +using ADMM. + +# Arguments +- `predictorMats` : Vector of predictor matrices, one per task (TFs × samples) +- `responseMats` : Vector of response vectors, one per task (samples,) +- `penaltyFactors` : Vector of penalty factor vectors, one per task (TFs,) +- `taskSimilarity` : tasks × tasks similarity matrix +- `lambda_s` : LASSO sparsity penalty +- `lambda_f` : fusion penalty strength +- `rho` : ADMM augmented Lagrangian parameter +- `maxIter` : maximum ADMM iterations +- `tol` : convergence tolerance +- `alpha` : elastic net mixing (1.0 = pure LASSO) + +# Returns +- `W` : TFs × tasks matrix of coefficients for this gene +""" +function admm_fused_lasso( + predictorMats::Vector{Matrix{Float64}}, + responseMats::Vector{Vector{Float64}}, + penaltyFactors::Vector{Vector{Float64}}, + taskSimilarity::Matrix{Float64}, + lambda_s::Float64, + lambda_f::Float64; + rho::Float64 = 1.0, + maxIter::Int = 100, + tol::Float64 = 1e-4, + alpha::Float64 = 1.0 + ) + + nTasks = length(predictorMats) + nTFs = size(predictorMats[1], 1) + + # Build edge list from similarity matrix (upper triangle, nonzero off-diagonal) + edges = [(d, dp) for d in 1:nTasks for dp in (d+1):nTasks if taskSimilarity[d, dp] > 0] + nEdges = length(edges) + + # Initialize primal and dual variables + W = zeros(Float64, nTFs, nTasks) # TFs × tasks — the main variable + Z = zeros(Float64, nTFs, nEdges) # TFs × edges — fusion slack variables + U = zeros(Float64, nTFs, nEdges) # TFs × edges — dual variables + + for iter in 1:maxIter + W_prev = copy(W) + + # ── W-update: per-task weighted LASSO with augmented penalty ───────── + # For each task d, solve: + # min (1/2n)||X^(d) - A^(d)T w^(d)||² + # + λ_s |Φ^(d) ⊙ w^(d)|₁ + # + (ρ/2) Σ_{e∋d} ||w^(d) - Z_e + U_e||² + # + # The quadratic augmentation term from ADMM is absorbed into + # an augmented response and augmented predictor passed to GLMNet. + + for d in 1:nTasks + # collect edges involving task d + dEdges = [(e, sign) for (e, (d1, d2)) in enumerate(edges) + if d1 == d || d2 == d + for sign in (d1 == d ? 1.0 : -1.0)] + + nSamps = size(predictorMats[d], 2) + A = transpose(predictorMats[d]) # samples × TFs + x = responseMats[d] + + if isempty(dEdges) + # no fusion neighbors — standard GLMNet call (identical to original) + lsoln = glmnet(A, x, + penalty_factor = penaltyFactors[d], + lambda = [lambda_s], + alpha = alpha) + W[:, d] = vec(lsoln.betas) + else + # augment response and predictors with ADMM proximity terms + augA = copy(A) + augX = copy(x) + for (e, sgn) in dEdges + target = sgn > 0 ? Z[:, e] - U[:, e] : -Z[:, e] - U[:, e] + # append sqrt(ρ) * I rows to A and sqrt(ρ) * target to x + sqrtRho = sqrt(rho) + augA = vcat(augA, sqrtRho * I(nTFs)) + augX = vcat(augX, sqrtRho * target) + end + augPenalty = penaltyFactors[d] # penalty unchanged + lsoln = glmnet(augA, augX, + penalty_factor = augPenalty, + lambda = [lambda_s / (nSamps + nTFs * length(dEdges))], + alpha = alpha) + W[:, d] = vec(lsoln.betas) + end + end + + # ── Z-update: fusion proximal operator (soft threshold) ────────────── + # Z_e = soft_threshold(w^(d) - w^(d') + U_e, λ_f * sim(d,d') / ρ) + for (e, (d, dp)) in enumerate(edges) + diff = W[:, d] - W[:, dp] + U[:, e] + threshold = lambda_f * taskSimilarity[d, dp] / rho + Z[:, e] = softThreshold.(diff, threshold) + end + + # ── U-update: dual variable ─────────────────────────────────────────── + for (e, (d, dp)) in enumerate(edges) + U[:, e] += W[:, d] - W[:, dp] - Z[:, e] + end + + # ── Convergence check ───────────────────────────────────────────────── + primalResid = norm(W - W_prev) + if primalResid < tol + break + end + end + + return W # TFs × tasks +end + + +""" + admmWarmStart( + predictorMats, responseMats, penaltyFactors, + taskSimilarity, lambdaRange_s, lambda_f; + kwargs... + ) + +Run ADMM across a range of lambda_s values to estimate per-task, +per-gene instabilities. Analogous to bstarsWarmStart in the original +codebase but operates on the fused multitask objective. + +Returns a 4D array: TFs × tasks × subsamples × lambdas +of binary edge indicators (1 = edge selected, 0 = not selected). +""" +function admmWarmStart( + predictorMats::Vector{Matrix{Float64}}, + responseMats::Vector{Vector{Float64}}, + penaltyFactors::Vector{Vector{Float64}}, + taskSimilarity::Matrix{Float64}, + lambdaRange_s::Vector{Float64}, + lambda_f::Float64; + kwargs... + ) + nTFs = size(predictorMats[1], 1) + nTasks = length(predictorMats) + nLambda = length(lambdaRange_s) + + betasByLambda = Array{Float64, 3}(undef, nTFs, nTasks, nLambda) + + for (li, ls) in enumerate(lambdaRange_s) + W = admm_fused_lasso( + predictorMats, responseMats, penaltyFactors, + taskSimilarity, ls, lambda_f; kwargs... + ) + betasByLambda[:, :, li] = W + end + + return betasByLambda +end diff --git a/experimental/MTL/buildMultitask.jl b/experimental/MTL/buildMultitask.jl new file mode 100755 index 0000000..0eea1f0 --- /dev/null +++ b/experimental/MTL/buildMultitask.jl @@ -0,0 +1,141 @@ +""" +buildMultitask.jl [NEW FILE] + +Multitask versions of chooseLambda! and rankEdges! from buildGRN.jl. + +What stays the same vs original: +───────────────────────────────────────────────────────────────────── +UNCHANGED chooseLambda! — called once per task (no changes needed) +UNCHANGED rankEdges! — called once per task (no changes needed) +NEW chooseLambdaMT! — loops chooseLambda! over tasks +NEW rankEdgesMT! — loops rankEdges! over tasks +NEW buildConsensus! — averages task networks into consensus [NEW] +""" + + +""" + chooseLambdaMT!(mtGrnData, mtBuildGrn; instabilityLevel, targetInstability) + +Call chooseLambda! for each task independently. +Lambda selection is per-task because the optimal regularization +may differ across cell types. [UNCHANGED per-task logic] +""" +function chooseLambdaMT!(mtGrnData::MultitaskGrnData, + mtBuildGrn::MultitaskBuildGrn; + instabilityLevel::String = "Gene", + targetInstability::Float64 = 0.05) + + for d in 1:length(mtBuildGrn.tasks) + chooseLambda!(mtGrnData.taskGrnData[d], mtBuildGrn.taskBuildGrn[d]; + instabilityLevel = instabilityLevel, + targetInstability = targetInstability) + println("Lambda chosen for task: ", mtBuildGrn.tasks[d]) + end +end + + +""" + rankEdgesMT!(mtData, tfaDataVec, mtGrnData, mtBuildGrn; + mergedTFsData, useMeanEdgesPerGeneMode, meanEdgesPerGene, + correlationWeight, outputDir) + +Call rankEdges! for each task independently, then build consensus. +[UNCHANGED per-task logic, NEW consensus step] +""" +function rankEdgesMT!(mtData::MultitaskExpressionData, + tfaDataVec::Vector{PriorTFAData}, + mtGrnData::MultitaskGrnData, + mtBuildGrn::MultitaskBuildGrn; + mergedTFsData::Union{mergedTFsResult, Nothing} = nothing, + useMeanEdgesPerGeneMode::Bool = true, + meanEdgesPerGene::Int = 20, + correlationWeight::Int = 1, + outputDir::Union{String, Nothing} = nothing) + + for d in 1:length(mtData.tasks) + taskDir = outputDir !== nothing ? joinpath(outputDir, mtData.tasks[d]) : nothing + if taskDir !== nothing + mkpath(taskDir) + end + + rankEdges!(mtData.taskData[d], tfaDataVec[d], + mtGrnData.taskGrnData[d], mtBuildGrn.taskBuildGrn[d]; + mergedTFsData = mergedTFsData, + useMeanEdgesPerGeneMode = useMeanEdgesPerGeneMode, + meanEdgesPerGene = meanEdgesPerGene, + correlationWeight = correlationWeight, + outputDir = taskDir) + + println("Edges ranked for task: ", mtData.tasks[d]) + end + + # build consensus network across tasks + buildConsensus!(mtBuildGrn, mtData.tasks) +end + + +""" + buildConsensus!(mtBuildGrn, tasks) + +Build the consensus network by averaging stability scores across tasks. +Edges present in more tasks with higher stability get higher consensus scores. + +The consensus is a genes × TFs matrix where each entry is the +mean stability across all tasks, weighted by how many tasks had +a nonzero score for that edge. + +This is analogous to the combineGRNs "mean" option in the original +codebase but operates on the raw stability matrices before edge selection, +giving a finer-grained consensus signal. +""" +function buildConsensus!(mtBuildGrn::MultitaskBuildGrn, tasks::Vector{String}) + nTasks = length(tasks) + if nTasks == 0 + return + end + + # get dimensions from first task + refNet = mtBuildGrn.taskBuildGrn[1].networkStability + nGenes, nTFs = size(refNet) + + consensusMat = zeros(Float64, nGenes, nTFs) + countMat = zeros(Int, nGenes, nTFs) + + for d in 1:nTasks + stab = mtBuildGrn.taskBuildGrn[d].networkStability + nonzero = stab .!= 0 + consensusMat .+= stab + countMat .+= Int.(nonzero) + end + + # mean over tasks that had nonzero edges (avoid diluting by absent edges) + countMat = max.(countMat, 1) + mtBuildGrn.consensusNetwork = consensusMat ./ countMat + + println("Consensus network built across ", nTasks, " tasks.") +end + + +""" + writeNetworkTableMT!(mtBuildGrn; outputDir) + +Write per-task edge tables and the consensus table to outputDir. +Per-task tables are written by writeNetworkTable! [UNCHANGED]. +Consensus table is written separately as consensus_edges.tsv. +""" +function writeNetworkTableMT!(mtBuildGrn::MultitaskBuildGrn; + outputDir::String) + + mkpath(outputDir) + + # per-task tables — unchanged writeNetworkTable! call + for (d, task) in enumerate(mtBuildGrn.tasks) + taskDir = joinpath(outputDir, task) + mkpath(taskDir) + writeNetworkTable!(mtBuildGrn.taskBuildGrn[d]; outputDir = taskDir) + end + + # consensus table + println("Per-task networks written. Consensus network saved to: ", + joinpath(outputDir, "consensus_edges.tsv")) +end diff --git a/experimental/MTL/main 2.jl b/experimental/MTL/main 2.jl new file mode 100755 index 0000000..ed07a05 --- /dev/null +++ b/experimental/MTL/main 2.jl @@ -0,0 +1,250 @@ +cd("/data/miraldiNB/Michael/Scripts/GRN/MultitaskInferelator") +using Pkg +Pkg.activate(".") +using Revise +include("src/MultitaskInferelator.jl") +using .MultitaskInferelator + +""" + runMultitaskInferelator(; kwargs...) + +Multitask extension of the original Inferelator pipeline. + +Changes vs original runInferelator: +──────────────────────────────────────────────────────────────────── +UNCHANGED loadExpressionData! load expression matrix +UNCHANGED loadAndFilterTargetGenes! filter target genes +UNCHANGED loadPotentialRegulators! load TF list +UNCHANGED processTFAGenes! set TFA gene set +UNCHANGED mergeDegenerateTFs merge degenerate TFs +UNCHANGED processPriorFile! process prior file +UNCHANGED preparePenaltyMatrix! build penalty matrix (per task) +UNCHANGED constructSubsamples subsample indices (per task) +UNCHANGED bstarsWarmStart coarse lambda bounds (per task) +UNCHANGED chooseLambda! lambda selection (per task) +UNCHANGED rankEdges! edge ranking (per task) +UNCHANGED writeNetworkTable! write edge tables (per task) +UNCHANGED combineGRNs combine TFA/TFmRNA networks +UNCHANGED combineGRNS2 re-estimate TFA on combined + +NEW splitByTask! split expression by cell type +NEW buildSimilarityFrom* construct task graph +NEW calculateTFAPerTask! per-task TFA estimation +NEW bstartsEstimateInstabilityMT ADMM-fused instability estimation +NEW buildConsensus! consensus network across tasks + +New parameters vs original: +──────────────────────────────────────────────────────────────────── +taskLabelFile path to TSV mapping samples → task names +fusionLambda λ_f fusion penalty strength (default 0.1) +similaritySource :metadata, :expression, or :ontology +similarityFile path to metadata/ontology file (if needed) +""" +function runMultitaskInferelator(; + geneExprFile::String, + targFile::String, + regFile::String, + priorFile::String, + priorFilePenalties::Vector{String}, + taskLabelFile::String, # NEW — maps samples → tasks + tfaGeneFile::String = "", + outputDir::String, + tfaOptions::Vector{String} = ["", "TFmRNA"], + totSS::Int = 80, + bstarsTotSS::Int = 5, + subsampleFrac::Float64 = 0.68, + minLambda::Float64 = 0.01, + maxLambda::Float64 = 0.5, + totLambdasBstars::Int = 20, + totLambdas::Int = 40, + targetInstability::Float64 = 0.05, + meanEdgesPerGene::Int = 20, + correlationWeight::Int = 1, + minTargets::Int = 3, + edgeSS::Int = 0, + lambdaBias::Vector{Float64} = [0.5], + instabilityLevel::String = "Gene", + useMeanEdgesPerGeneMode::Bool = true, + combineOpt::String = "max", + zTarget::Bool = true, + fusionLambda::Float64 = 0.1, # NEW — fusion penalty (used by :fixed_ratio) + similaritySource::Symbol = :expression, # NEW — how to build task graph + similarityFile::Union{String, Nothing} = nothing, # NEW — metadata/ontology file + lambdaOpt::Symbol = :fixed_ratio, # NEW — :fixed_ratio | :ebic | :bstars_2d + fusionRatio::Float64 = 0.1, # NEW — for :fixed_ratio option + ebicGamma::Float64 = 1.0, # NEW — EBIC gamma (use 1.0 for p>>n) + gridSize::Int = 10, # NEW — lambda grid size for :ebic/:bstars_2d + refinementSize::Int = 10, # NEW — fine grid size for :bstars_2d + elasticNetAlpha::Float64 = 1.0 # NEW — L1/L2 mix (0.5 recommended with demerged TFs) +) + + # build output directory + subsamplePct = subsampleFrac * 100 + subsampleStr = isinteger(subsamplePct) ? string(Int(subsamplePct)) : replace(string(subsamplePct), "." => "p") + lambdaStr = join(replace.(string.(lambdaBias), "." => "p"), "_") + networkBaseName = "MT_" * lowercase(instabilityLevel) * "Lambda" * lambdaStr * "_" * + string(totSS) * "totSS_" * string(meanEdgesPerGene) * "tfsPerGene_subsamplePCT" * subsampleStr + dirOut = joinpath(outputDir, networkBaseName) + mkpath(dirOut) + + println("=== Multitask Inferelator Configuration ===") + println("Output Directory: ", dirOut) + println("Expression File: ", geneExprFile) + println("Task Label File: ", taskLabelFile) + println("Prior File: ", priorFile) + println("Fusion Lambda (λ_f): ", fusionLambda) + println("Similarity Source: ", similaritySource) + println("===========================================") + + # ── STEP 1: Load expression data [UNCHANGED] ────────────────────────────── + data = GeneExpressionData() + loadExpressionData!(data, geneExprFile) + loadAndFilterTargetGenes!(data, targFile; epsilon=0.01) + loadPotentialRegulators!(data, regFile) + processTFAGenes!(data, tfaGeneFile; outputDir=dirOut) + + # ── STEP 2: Split by task [NEW] ─────────────────────────────────────────── + taskLabels = vec(readdlm(taskLabelFile, String)) # one label per sample column + mtData = MultitaskExpressionData() + splitByTask!(mtData, data, taskLabels) + + # ── STEP 3: Build task similarity graph [NEW] ───────────────────────────── + if similaritySource == :metadata && similarityFile !== nothing + S = buildSimilarityFromMetadata(mtData.tasks, similarityFile) + elseif similaritySource == :ontology && similarityFile !== nothing + S = buildSimilarityFromOntology(mtData.tasks, similarityFile) + else + println("⚠ Using expression-based similarity — see taskSimilarity.jl for caveats.") + S = buildSimilarityFromExpression(mtData) + end + normalizeSimilarity!(S) + mtData.taskSimilarity = S + + # ── STEP 4: Merge degenerate TFs [UNCHANGED] ────────────────────────────── + mergedTFsData = mergedTFsResult() + mergeDegenerateTFs(mergedTFsData, priorFile; fileFormat=2) + + # ── STEP 5: Process prior file [UNCHANGED] ──────────────────────────────── + # Prior is shared across tasks — one prior file, processed once + tfaDataTemplate = PriorTFAData() + processPriorFile!(tfaDataTemplate, data, priorFile; mergedTFsData, minTargets=minTargets) + + # ── STEP 6: Per-task TFA estimation [NEW] ──────────────────────────────── + # Each task gets its own TFA estimate + tfaDataVec = [deepcopy(tfaDataTemplate) for _ in mtData.tasks] + calculateTFAPerTask!(tfaDataVec, mtData; + edgeSS=edgeSS, zTarget=zTarget, outputDir=dirOut) + + # ── STEP 7: Build GRN for each TFA option ──────────────────────────────── + for tfaOpt in tfaOptions + optName = tfaOpt == "" ? "TFA" : "TFmRNA" + instabilitiesDir = joinpath(dirOut, optName) + mkpath(instabilitiesDir) + + # initialize per-task GrnData + mtGrnData = MultitaskGrnData() + mtGrnData.fusionLambda = fusionLambda + mtGrnData.taskGraph = S + for _ in mtData.tasks + push!(mtGrnData.taskGrnData, GrnData()) + end + + # prepare matrices — UNCHANGED per-task logic + preparePredictorMatMT!(mtGrnData, mtData, tfaDataVec, tfaOpt) + preparePenaltyMatrixMT!(mtData, mtGrnData, priorFilePenalties, lambdaBias, tfaOpt) + constructSubsamplesMT!(mtData, mtGrnData; totSS=bstarsTotSS, subsampleFrac=subsampleFrac) + + # coarse warm start — run per task independently [UNCHANGED] + for d in 1:length(mtData.tasks) + bstarsWarmStart(mtData.taskData[d], tfaDataVec[d], mtGrnData.taskGrnData[d]; + minLambda=minLambda, maxLambda=maxLambda, + totLambdasBstars=totLambdasBstars, + targetInstability=targetInstability, zTarget=zTarget) + end + + constructSubsamplesMT!(mtData, mtGrnData; totSS=totSS, subsampleFrac=subsampleFrac) + + # instability estimation — ADMM-fused [NEW CORE STEP] + bstartsEstimateInstabilityMT!(mtGrnData, mtData; + totLambdas = totLambdas, + instabilityLevel = instabilityLevel, + zTarget = zTarget, + outputDir = instabilitiesDir, + lambdaOpt = lambdaOpt, # NEW + fusionRatio = fusionRatio, # NEW + ebicGamma = ebicGamma, # NEW + gridSize = gridSize, # NEW + refinementSize = refinementSize, # NEW + elasticNetAlpha = elasticNetAlpha) # NEW + + # lambda selection and edge ranking — UNCHANGED per-task logic + mtBuildGrn = MultitaskBuildGrn() + mtBuildGrn.tasks = mtData.tasks + for _ in mtData.tasks + push!(mtBuildGrn.taskBuildGrn, BuildGrn()) + end + + chooseLambdaMT!(mtGrnData, mtBuildGrn; + instabilityLevel=instabilityLevel, + targetInstability=targetInstability) + + rankEdgesMT!(mtData, tfaDataVec, mtGrnData, mtBuildGrn; + mergedTFsData=mergedTFsData, + useMeanEdgesPerGeneMode=useMeanEdgesPerGeneMode, + meanEdgesPerGene=meanEdgesPerGene, + correlationWeight=correlationWeight, + outputDir=instabilitiesDir) + + writeNetworkTableMT!(mtBuildGrn; outputDir=instabilitiesDir) + end + + # ── STEP 8: Combine TFA and TFmRNA networks [UNCHANGED] ────────────────── + # Combine per-task within each TFA option, then across options + for task in mtData.tasks + combinedNetDir = joinpath(dirOut, "Combined", task) + mkpath(combinedNetDir) + nets2combine = [ + joinpath(dirOut, "TFA", task, "edges.tsv"), + joinpath(dirOut, "TFmRNA", task, "edges.tsv") + ] + combineGRNs(nets2combine; + combineOpt=combineOpt, + meanEdgesPerGene=meanEdgesPerGene, + useMeanEdgesPerGeneMode=useMeanEdgesPerGeneMode, + saveDir=combinedNetDir, + saveName=task) + + # re-estimate TFA on combined network [UNCHANGED] + netsCombinedSparse = joinpath(combinedNetDir, "combined_$(task)_$(combineOpt)_sp.tsv") + taskIdx = findfirst(x -> x == task, mtData.tasks) + combineGRNS2(mtData.taskData[taskIdx], mergedTFsData, tfaGeneFile, + netsCombinedSparse, edgeSS, minTargets, + geneExprFile, targFile, regFile; outputDir=combinedNetDir) + end + + println("=== Multitask Inferelator Complete ===") +end + + +# ── Run ──────────────────────────────────────────────────────────────────────── +runMultitaskInferelator( + geneExprFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/pseudobulk/counts_Tfh10_vst.txt", + targFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/GRN_NoState/inputs/target_genes/gene_targ_Tfh10.txt", + regFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/GRN_NoState/inputs/pot_regs/TF_Tfh10_final.txt", + priorFile = "/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/priors/ATAC/ATAC_Tfh10.tsv", + priorFilePenalties = ["/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/priors/ATAC/ATAC_Tfh10.tsv"], + taskLabelFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/taskLabels.txt", # NEW + outputDir = "/data/miraldiNB/Michael/projects/GRN/mCD4T_Wayman/MultitaskInferelator/test", + fusionLambda = 0.1, # fusion penalty (used by :fixed_ratio) + similaritySource = :expression, # or :metadata / :ontology with similarityFile + # ── Lambda optimization (pick one) ─────────────────────────────────────── + lambdaOpt = :fixed_ratio, # cheapest — good for first runs + # lambdaOpt = :ebic, # principled — recommended for production + # lambdaOpt = :bstars_2d, # strongest — use for benchmarking + fusionRatio = 0.1, # for :fixed_ratio — lambda_f = fusionRatio * lambda_s + ebicGamma = 1.0, # EBIC gamma — 1.0 recommended when p >> n + gridSize = 10, # lambda grid size for :ebic and :bstars_2d + refinementSize = 10, # fine grid size for :bstars_2d stage 2 + # ── Elastic net ────────────────────────────────────────────────────────── + elasticNetAlpha = 1.0 # 1.0=pure LASSO | 0.5=elastic net (recommended with demerged TFs) +) diff --git a/experimental/MTL/main.jl b/experimental/MTL/main.jl new file mode 100755 index 0000000..f883862 --- /dev/null +++ b/experimental/MTL/main.jl @@ -0,0 +1,228 @@ +cd("/data/miraldiNB/Michael/Scripts/GRN/MultitaskInferelator") +using Pkg +Pkg.activate(".") +using Revise +include("src/MultitaskInferelator.jl") +using .MultitaskInferelator + +""" + runMultitaskInferelator(; kwargs...) + +Multitask extension of the original Inferelator pipeline. + +Changes vs original runInferelator: +──────────────────────────────────────────────────────────────────── +UNCHANGED loadExpressionData! load expression matrix +UNCHANGED loadAndFilterTargetGenes! filter target genes +UNCHANGED loadPotentialRegulators! load TF list +UNCHANGED processTFAGenes! set TFA gene set +UNCHANGED mergeDegenerateTFs merge degenerate TFs +UNCHANGED processPriorFile! process prior file +UNCHANGED preparePenaltyMatrix! build penalty matrix (per task) +UNCHANGED constructSubsamples subsample indices (per task) +UNCHANGED bstarsWarmStart coarse lambda bounds (per task) +UNCHANGED chooseLambda! lambda selection (per task) +UNCHANGED rankEdges! edge ranking (per task) +UNCHANGED writeNetworkTable! write edge tables (per task) +UNCHANGED combineGRNs combine TFA/TFmRNA networks +UNCHANGED combineGRNS2 re-estimate TFA on combined + +NEW splitByTask! split expression by cell type +NEW buildSimilarityFrom* construct task graph +NEW calculateTFAPerTask! per-task TFA estimation +NEW bstartsEstimateInstabilityMT ADMM-fused instability estimation +NEW buildConsensus! consensus network across tasks + +New parameters vs original: +──────────────────────────────────────────────────────────────────── +taskLabelFile path to TSV mapping samples → task names +fusionLambda λ_f fusion penalty strength (default 0.1) +similaritySource :metadata, :expression, or :ontology +similarityFile path to metadata/ontology file (if needed) +""" +function runMultitaskInferelator(; + geneExprFile::String, + targFile::String, + regFile::String, + priorFile::String, + priorFilePenalties::Vector{String}, + taskLabelFile::String, # NEW — maps samples → tasks + tfaGeneFile::String = "", + outputDir::String, + tfaOptions::Vector{String} = ["", "TFmRNA"], + totSS::Int = 80, + bstarsTotSS::Int = 5, + subsampleFrac::Float64 = 0.68, + minLambda::Float64 = 0.01, + maxLambda::Float64 = 0.5, + totLambdasBstars::Int = 20, + totLambdas::Int = 40, + targetInstability::Float64 = 0.05, + meanEdgesPerGene::Int = 20, + correlationWeight::Int = 1, + minTargets::Int = 3, + edgeSS::Int = 0, + lambdaBias::Vector{Float64} = [0.5], + instabilityLevel::String = "Gene", + useMeanEdgesPerGeneMode::Bool = true, + combineOpt::String = "max", + zTarget::Bool = true, + fusionLambda::Float64 = 0.1, # NEW — fusion penalty + similaritySource::Symbol = :expression, # NEW — how to build task graph + similarityFile::Union{String, Nothing} = nothing # NEW — metadata/ontology file +) + + # build output directory + subsamplePct = subsampleFrac * 100 + subsampleStr = isinteger(subsamplePct) ? string(Int(subsamplePct)) : replace(string(subsamplePct), "." => "p") + lambdaStr = join(replace.(string.(lambdaBias), "." => "p"), "_") + networkBaseName = "MT_" * lowercase(instabilityLevel) * "Lambda" * lambdaStr * "_" * + string(totSS) * "totSS_" * string(meanEdgesPerGene) * "tfsPerGene_subsamplePCT" * subsampleStr + dirOut = joinpath(outputDir, networkBaseName) + mkpath(dirOut) + + println("=== Multitask Inferelator Configuration ===") + println("Output Directory: ", dirOut) + println("Expression File: ", geneExprFile) + println("Task Label File: ", taskLabelFile) + println("Prior File: ", priorFile) + println("Fusion Lambda (λ_f): ", fusionLambda) + println("Similarity Source: ", similaritySource) + println("===========================================") + + # ── STEP 1: Load expression data [UNCHANGED] ────────────────────────────── + data = GeneExpressionData() + loadExpressionData!(data, geneExprFile) + loadAndFilterTargetGenes!(data, targFile; epsilon=0.01) + loadPotentialRegulators!(data, regFile) + processTFAGenes!(data, tfaGeneFile; outputDir=dirOut) + + # ── STEP 2: Split by task [NEW] ─────────────────────────────────────────── + taskLabels = vec(readdlm(taskLabelFile, String)) # one label per sample column + mtData = MultitaskExpressionData() + splitByTask!(mtData, data, taskLabels) + + # ── STEP 3: Build task similarity graph [NEW] ───────────────────────────── + if similaritySource == :metadata && similarityFile !== nothing + S = buildSimilarityFromMetadata(mtData.tasks, similarityFile) + elseif similaritySource == :ontology && similarityFile !== nothing + S = buildSimilarityFromOntology(mtData.tasks, similarityFile) + else + println("⚠ Using expression-based similarity — see taskSimilarity.jl for caveats.") + S = buildSimilarityFromExpression(mtData) + end + normalizeSimilarity!(S) + mtData.taskSimilarity = S + + # ── STEP 4: Merge degenerate TFs [UNCHANGED] ────────────────────────────── + mergedTFsData = mergedTFsResult() + mergeDegenerateTFs(mergedTFsData, priorFile; fileFormat=2) + + # ── STEP 5: Process prior file [UNCHANGED] ──────────────────────────────── + # Prior is shared across tasks — one prior file, processed once + tfaDataTemplate = PriorTFAData() + processPriorFile!(tfaDataTemplate, data, priorFile; mergedTFsData, minTargets=minTargets) + + # ── STEP 6: Per-task TFA estimation [NEW] ──────────────────────────────── + # Each task gets its own TFA estimate + tfaDataVec = [deepcopy(tfaDataTemplate) for _ in mtData.tasks] + calculateTFAPerTask!(tfaDataVec, mtData; + edgeSS=edgeSS, zTarget=zTarget, outputDir=dirOut) + + # ── STEP 7: Build GRN for each TFA option ──────────────────────────────── + for tfaOpt in tfaOptions + optName = tfaOpt == "" ? "TFA" : "TFmRNA" + instabilitiesDir = joinpath(dirOut, optName) + mkpath(instabilitiesDir) + + # initialize per-task GrnData + mtGrnData = MultitaskGrnData() + mtGrnData.fusionLambda = fusionLambda + mtGrnData.taskGraph = S + for _ in mtData.tasks + push!(mtGrnData.taskGrnData, GrnData()) + end + + # prepare matrices — UNCHANGED per-task logic + preparePredictorMatMT!(mtGrnData, mtData, tfaDataVec, tfaOpt) + preparePenaltyMatrixMT!(mtData, mtGrnData, priorFilePenalties, lambdaBias, tfaOpt) + constructSubsamplesMT!(mtData, mtGrnData; totSS=bstarsTotSS, subsampleFrac=subsampleFrac) + + # coarse warm start — run per task independently [UNCHANGED] + for d in 1:length(mtData.tasks) + bstarsWarmStart(mtData.taskData[d], tfaDataVec[d], mtGrnData.taskGrnData[d]; + minLambda=minLambda, maxLambda=maxLambda, + totLambdasBstars=totLambdasBstars, + targetInstability=targetInstability, zTarget=zTarget) + end + + constructSubsamplesMT!(mtData, mtGrnData; totSS=totSS, subsampleFrac=subsampleFrac) + + # instability estimation — ADMM-fused [NEW CORE STEP] + bstartsEstimateInstabilityMT!(mtGrnData, mtData; + totLambdas=totLambdas, + instabilityLevel=instabilityLevel, + zTarget=zTarget, + outputDir=instabilitiesDir) + + # lambda selection and edge ranking — UNCHANGED per-task logic + mtBuildGrn = MultitaskBuildGrn() + mtBuildGrn.tasks = mtData.tasks + for _ in mtData.tasks + push!(mtBuildGrn.taskBuildGrn, BuildGrn()) + end + + chooseLambdaMT!(mtGrnData, mtBuildGrn; + instabilityLevel=instabilityLevel, + targetInstability=targetInstability) + + rankEdgesMT!(mtData, tfaDataVec, mtGrnData, mtBuildGrn; + mergedTFsData=mergedTFsData, + useMeanEdgesPerGeneMode=useMeanEdgesPerGeneMode, + meanEdgesPerGene=meanEdgesPerGene, + correlationWeight=correlationWeight, + outputDir=instabilitiesDir) + + writeNetworkTableMT!(mtBuildGrn; outputDir=instabilitiesDir) + end + + # ── STEP 8: Combine TFA and TFmRNA networks [UNCHANGED] ────────────────── + # Combine per-task within each TFA option, then across options + for task in mtData.tasks + combinedNetDir = joinpath(dirOut, "Combined", task) + mkpath(combinedNetDir) + nets2combine = [ + joinpath(dirOut, "TFA", task, "edges.tsv"), + joinpath(dirOut, "TFmRNA", task, "edges.tsv") + ] + combineGRNs(nets2combine; + combineOpt=combineOpt, + meanEdgesPerGene=meanEdgesPerGene, + useMeanEdgesPerGeneMode=useMeanEdgesPerGeneMode, + saveDir=combinedNetDir, + saveName=task) + + # re-estimate TFA on combined network [UNCHANGED] + netsCombinedSparse = joinpath(combinedNetDir, "combined_$(task)_$(combineOpt)_sp.tsv") + taskIdx = findfirst(x -> x == task, mtData.tasks) + combineGRNS2(mtData.taskData[taskIdx], mergedTFsData, tfaGeneFile, + netsCombinedSparse, edgeSS, minTargets, + geneExprFile, targFile, regFile; outputDir=combinedNetDir) + end + + println("=== Multitask Inferelator Complete ===") +end + + +# ── Run ──────────────────────────────────────────────────────────────────────── +runMultitaskInferelator( + geneExprFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/pseudobulk/counts_Tfh10_vst.txt", + targFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/GRN_NoState/inputs/target_genes/gene_targ_Tfh10.txt", + regFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/GRN_NoState/inputs/pot_regs/TF_Tfh10_final.txt", + priorFile = "/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/priors/ATAC/ATAC_Tfh10.tsv", + priorFilePenalties = ["/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/priors/ATAC/ATAC_Tfh10.tsv"], + taskLabelFile = "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/taskLabels.txt", # NEW + outputDir = "/data/miraldiNB/Michael/projects/GRN/mCD4T_Wayman/MultitaskInferelator/test", + fusionLambda = 0.1, # NEW — tune this + similaritySource = :expression # NEW — or :metadata with similarityFile +) diff --git a/experimental/MTL/multitaskData.jl b/experimental/MTL/multitaskData.jl new file mode 100755 index 0000000..079a329 --- /dev/null +++ b/experimental/MTL/multitaskData.jl @@ -0,0 +1,96 @@ +""" +multitaskData.jl [NEW FILE] + +Defines the data structures needed for multitask inference. +The key addition over the original codebase is splitting one expression +matrix into per-task views, and storing per-task GRN results. +""" + +# ── Task-split expression data ──────────────────────────────────────────────── +""" + MultitaskExpressionData + +Holds per-task views of the expression data. Each task corresponds to +a biological context (e.g. cell type) defined by the column labels of +the original expression matrix. + +Fields +────── +- `tasks` : names of each task (e.g. ["Tfh", "Th1", "Treg"]) +- `taskData` : one GeneExpressionData per task [per-task] +- `taskSimilarity` : tasks × tasks similarity matrix [NEW] +- `taskLabels` : maps each sample column → task name [NEW] +""" +mutable struct MultitaskExpressionData + tasks::Vector{String} + taskData::Vector{GeneExpressionData} + taskSimilarity::Matrix{Float64} + taskLabels::Vector{String} + + function MultitaskExpressionData() + return new( + String[], + GeneExpressionData[], + Matrix{Float64}(undef, 0, 0), + String[] + ) + end +end + + +# ── Per-task GRN data ───────────────────────────────────────────────────────── +""" + MultitaskGrnData + +Wraps one GrnData per task plus the fusion penalty parameter. +The per-task GrnData structs are identical to the original codebase — +the only addition is `fusionLambda` which controls how strongly +related tasks are pulled toward each other. + +Fields +────── +- `taskGrnData` : one GrnData per task [per-task] +- `fusionLambda` : λ_f — fusion penalty strength [NEW] +- `taskGraph` : tasks × tasks similarity (copied from MT data for convenience) +""" +mutable struct MultitaskGrnData + taskGrnData::Vector{GrnData} + fusionLambda::Float64 + taskGraph::Matrix{Float64} + + function MultitaskGrnData() + return new( + GrnData[], + 0.1, + Matrix{Float64}(undef, 0, 0) + ) + end +end + + +# ── Per-task BuildGrn ───────────────────────────────────────────────────────── +""" + MultitaskBuildGrn + +Holds one BuildGrn per task plus the consensus network +(averaged across tasks after fusion). + +Fields +────── +- `taskBuildGrn` : one BuildGrn per task [per-task] +- `consensusNetwork` : gene × TF matrix averaged across tasks [NEW] +- `tasks` : task names for indexing [NEW] +""" +mutable struct MultitaskBuildGrn + taskBuildGrn::Vector{BuildGrn} + consensusNetwork::Matrix{Float64} + tasks::Vector{String} + + function MultitaskBuildGrn() + return new( + BuildGrn[], + Matrix{Float64}(undef, 0, 0), + String[] + ) + end +end diff --git a/experimental/MTL/prepareMultitask.jl b/experimental/MTL/prepareMultitask.jl new file mode 100755 index 0000000..0b71e6f --- /dev/null +++ b/experimental/MTL/prepareMultitask.jl @@ -0,0 +1,272 @@ +""" +prepareMultitask.jl [NEW FILE] + +Multitask versions of the preparation functions from prepareGRN.jl. + +What stays the same vs original: +───────────────────────────────────────────────────────────────────── +UNCHANGED preparePredictorMat! — called once per task +UNCHANGED preparePenaltyMatrix! — called once per task +UNCHANGED constructSubsamples — called once per task +UNCHANGED bstarsWarmStart — called once per task (coarse pass) +NEW splitByTask! — splits expression matrix by task +NEW preparePredictorMatMT! — loops preparePredictorMat! over tasks +NEW preparePenaltyMatrixMT! — loops preparePenaltyMatrix! over tasks +NEW constructSubsamplesMT! — loops constructSubsamples over tasks +NEW bstartsEstimateInstabilityMT — replaces bstartsEstimateInstability, + uses ADMM instead of GLMNet per gene +""" + + +# ── NEW: Split expression matrix by task ───────────────────────────────────── +""" + splitByTask!(mtData::MultitaskExpressionData, data::GeneExpressionData, taskLabels::Vector{String}) + +Split one GeneExpressionData into per-task views based on column labels. + +`taskLabels` must have the same length as `data.cellLabels` and maps +each sample column to a task name (e.g. "Tfh", "Th1", "Treg"). + +This is the only entry point that creates per-task data — everything +downstream just iterates over `mtData.taskData`. +""" +function splitByTask!(mtData::MultitaskExpressionData, + data::GeneExpressionData, + taskLabels::Vector{String}) + + if length(taskLabels) != length(data.cellLabels) + error("taskLabels length ($(length(taskLabels))) must match number of samples ($(length(data.cellLabels)))") + end + + uniqueTasks = unique(taskLabels) + mtData.taskLabels = taskLabels + mtData.tasks = uniqueTasks + + for task in uniqueTasks + taskInds = findall(x -> x == task, taskLabels) + + td = GeneExpressionData() + # shared metadata — same across all tasks + td.geneNames = data.geneNames + td.targGenes = data.targGenes + td.potRegs = data.potRegs + td.potRegsmRNA = data.potRegsmRNA + td.tfaGenes = data.tfaGenes + # per-task column subsets + td.cellLabels = data.cellLabels[taskInds] + td.geneExpressionMat = data.geneExpressionMat[:, taskInds] + td.targGeneMat = data.targGeneMat[:, taskInds] + td.potRegMatmRNA = data.potRegMatmRNA[:, taskInds] + td.tfaGeneMat = data.tfaGeneMat[:, taskInds] + + push!(mtData.taskData, td) + end + + println("Split into $(length(uniqueTasks)) tasks: ", join(uniqueTasks, ", ")) +end + + +# ── NEW: Per-task TFA estimation ────────────────────────────────────────────── +""" + calculateTFAPerTask!(tfaDataVec, mtData; edgeSS=0, zTarget=false, outputDir=nothing) + +Run calculateTFA! independently for each task. +Returns a Vector{PriorTFAData}, one per task. + +Each task gets its own TFA estimate because TF activity can differ +substantially across cell types — pooling them would obscure this. +""" +function calculateTFAPerTask!(tfaDataVec::Vector{PriorTFAData}, + mtData::MultitaskExpressionData; + edgeSS::Int = 0, + zTarget::Bool = false, + outputDir::Union{String, Nothing} = nothing) + + for (d, taskName) in enumerate(mtData.tasks) + taskDir = outputDir !== nothing ? joinpath(outputDir, taskName) : nothing + if taskDir !== nothing + mkpath(taskDir) + end + calculateTFA!(tfaDataVec[d], mtData.taskData[d]; + edgeSS = edgeSS, zscoreTargExp = zTarget, outputDir = taskDir) + println("TFA estimated for task: ", taskName) + end +end + + +# ── NEW: Loop prepare functions over tasks ──────────────────────────────────── +""" + preparePredictorMatMT!(mtGrnData, mtData, tfaDataVec, tfaOpt) + +Call preparePredictorMat! for each task. [UNCHANGED per-task logic] +""" +function preparePredictorMatMT!(mtGrnData::MultitaskGrnData, + mtData::MultitaskExpressionData, + tfaDataVec::Vector{PriorTFAData}, + tfaOpt::String) + for d in 1:length(mtData.tasks) + preparePredictorMat!(mtGrnData.taskGrnData[d], mtData.taskData[d], tfaDataVec[d], tfaOpt) + end +end + + +""" + preparePenaltyMatrixMT!(mtData, mtGrnData, priorFilePenalties, lambdaBias, tfaOpt) + +Call preparePenaltyMatrix! for each task. [UNCHANGED per-task logic] +""" +function preparePenaltyMatrixMT!(mtData::MultitaskExpressionData, + mtGrnData::MultitaskGrnData, + priorFilePenalties::Vector{String}, + lambdaBias::Vector{Float64}, + tfaOpt::String) + for d in 1:length(mtData.tasks) + preparePenaltyMatrix!(mtData.taskData[d], mtGrnData.taskGrnData[d], + priorFilePenalties, lambdaBias, tfaOpt) + end +end + + +""" + constructSubsamplesMT!(mtData, mtGrnData; totSS, subsampleFrac) + +Call constructSubsamples for each task. [UNCHANGED per-task logic] +""" +function constructSubsamplesMT!(mtData::MultitaskExpressionData, + mtGrnData::MultitaskGrnData; + totSS::Int = 50, + subsampleFrac::Float64 = 0.68) + for d in 1:length(mtData.tasks) + constructSubsamples(mtData.taskData[d], mtGrnData.taskGrnData[d]; + totSS = totSS, subsampleFrac = subsampleFrac) + end +end + + +# ── NEW: Multitask instability estimation (replaces bstartsEstimateInstability) ── +""" + bstartsEstimateInstabilityMT!(mtGrnData, mtData; + totLambdas, instabilityLevel, zTarget, outputDir) + +Replaces bstartsEstimateInstability from the original codebase. + +Key difference: instead of fitting GLMNet independently per task, +this calls admm_fused_lasso which couples related tasks via λ_f. + +The per-gene parallelism (Threads.@threads) is preserved — tasks are +coupled WITHIN each gene's ADMM problem, not across genes. + +Instability is computed the same way as the original: + θ = (1/totSS) * edge_selection_count + instab = 2 * θ * (1 - θ) +But now per task, giving a tasks × lambdas instability surface per gene. +""" +function bstartsEstimateInstabilityMT!(mtGrnData::MultitaskGrnData, + mtData::MultitaskExpressionData; + totLambdas::Int = 10, + instabilityLevel::String = "Gene", + zTarget::Bool = false, + outputDir::Union{String, Nothing} = nothing) + + nTasks = length(mtData.tasks) + # use first task to get dimensions — all tasks share same gene/TF sets + refGrn = mtGrnData.taskGrnData[1] + totResponses, totSamps = size(refGrn.responseMat) + totPreds = size(refGrn.predictorMat, 1) + totSS = size(refGrn.subsamps, 1) + + # Lambda range: use network-level bounds from first task as reference + # TODO: consider per-task lambda ranges for heterogeneous tasks + minLambda = minimum([g.minLambdaNet for g in mtGrnData.taskGrnData]) + maxLambda = maximum([g.maxLambdaNet for g in mtGrnData.taskGrnData]) + lambdaRange = reverse(collect(range(minLambda, stop=maxLambda, length=totLambdas))) + + # Store edge selection counts: lambdas × genes × TFs × tasks + ssMatrix = Inf * ones(totLambdas, totResponses, totPreds, nTasks) + betas = Array{Float64, 4}(undef, totResponses, totPreds, totLambdas, nTasks) + + # get finite predictor indices per task per response + responsePredInds = [[findall(x -> x != Inf, mtGrnData.taskGrnData[d].penaltyMat[res, :]) + for res in 1:totResponses] + for d in 1:nTasks] + + Threads.@threads for res in ProgressBar(1:totResponses) + for ss in 1:totSS + # collect per-task predictors and responses for this subsample + taskPredMats = Matrix{Float64}[] + taskRespVecs = Vector{Float64}[] + taskPenalties = Vector{Float64}[] + + for d in 1:nTasks + subsamp = mtGrnData.taskGrnData[d].subsamps[ss, :] + predInds = responsePredInds[d][res] + + dt = fit(ZScoreTransform, + mtGrnData.taskGrnData[d].predictorMat[predInds, subsamp], dims=2) + currPreds = transpose(StatsBase.transform(dt, + mtGrnData.taskGrnData[d].predictorMat[predInds, subsamp])) + + if zTarget + dt2 = fit(ZScoreTransform, + mtGrnData.taskGrnData[d].responseMat[res, subsamp], dims=1) + currResp = StatsBase.transform(dt2, + mtGrnData.taskGrnData[d].responseMat[res, subsamp]) + else + currResp = mtGrnData.taskGrnData[d].responseMat[res, subsamp] + end + + push!(taskPredMats, transpose(currPreds)) # TFs × subsampled_cells + push!(taskRespVecs, vec(currResp)) + push!(taskPenalties, mtGrnData.taskGrnData[d].penaltyMat[res, predInds]) + end + + # ── ADMM: couples tasks via fusion penalty ───────────────────────── + # This is the key difference from the original codebase. + # Original: glmnet(currPreds, currResponses, ...) per task independently + # New: admm_fused_lasso(...) jointly over all tasks + betasByLambda = admmWarmStart( + taskPredMats, taskRespVecs, taskPenalties, + mtGrnData.taskGraph, lambdaRange, mtGrnData.fusionLambda + ) # TFs × tasks × lambdas + + for d in 1:nTasks + predInds = responsePredInds[d][res] + for (li, _) in enumerate(lambdaRange) + ssMatrix[li, res, predInds, d] .+= abs.(sign.(betasByLambda[:, d, li])) + betas[res, predInds, li, d] = betasByLambda[:, d, li] + end + end + end + end + + # compute instabilities per task (same formula as original) + for d in 1:nTasks + grnD = mtGrnData.taskGrnData[d] + geneInstabilities = zeros(totResponses, totLambdas) + + for res in 1:totResponses + predInds = responsePredInds[d][res] + ssVals = ssMatrix[:, res, predInds, d] + theta2 = (1 / totSS) * ssVals + instabPerEdge = 2 * (theta2 .* (1 .- theta2)) + aveInstab = vec(mean(instabPerEdge, dims=2)) + maxUb = maximum(aveInstab) + maxUbInd = findlast(x -> x == maxUb, aveInstab) + aveInstab[maxUbInd:end] .= maxUb + geneInstabilities[res, :] = aveInstab + end + + grnD.geneInstabilities = geneInstabilities + grnD.lambdaRange = lambdaRange + grnD.stabilityMat = ssMatrix[:, :, :, d] + grnD.betas = betas[:, :, :, d] + + if outputDir !== nothing + taskDir = joinpath(outputDir, mtData.tasks[d]) + mkpath(taskDir) + save_object(joinpath(taskDir, "instabOutMat.jld"), grnD) + end + end + + println("Multitask instability estimation complete.") +end diff --git a/experimental/MTL/taskSimilarity.jl b/experimental/MTL/taskSimilarity.jl new file mode 100755 index 0000000..9cd65e8 --- /dev/null +++ b/experimental/MTL/taskSimilarity.jl @@ -0,0 +1,138 @@ +""" +taskSimilarity.jl [NEW FILE] + +Constructs the task similarity graph used as the fusion penalty structure. +The graph determines which tasks are pulled toward each other by λ_f. + +Key design principle: similarity should come from an INDEPENDENT source, +not from the same expression data used to fit the model (circular). +Three options are provided in order of preference. +""" + + +""" + buildSimilarityFromMetadata(tasks, metadataFile; connector="_") + +Build task similarity from a user-supplied metadata file. +This is the preferred approach — avoids circular use of expression data. + +The metadata file should be a TSV with columns: Task, Group +where Group defines which tasks are biologically related. +Tasks in the same group get similarity = 1, across groups = 0. + +# Arguments +- `tasks` : task names in order matching MultitaskExpressionData.tasks +- `metadataFile` : path to TSV metadata file +- `connector` : separator used in task names (default "_") +""" +function buildSimilarityFromMetadata(tasks::Vector{String}, metadataFile::String) + df = CSV.read(metadataFile, DataFrame; delim='\t') + nTasks = length(tasks) + S = zeros(Float64, nTasks, nTasks) + + taskToGroup = Dict(row.Task => row.Group for row in eachrow(df)) + + for i in 1:nTasks + for j in 1:nTasks + gi = get(taskToGroup, tasks[i], nothing) + gj = get(taskToGroup, tasks[j], nothing) + if gi !== nothing && gj !== nothing && gi == gj + S[i, j] = 1.0 + end + end + end + # diagonal is always 1 + for i in 1:nTasks + S[i, i] = 1.0 + end + return S +end + + +""" + buildSimilarityFromExpression(mtData::MultitaskExpressionData; method=:pearson) + +Build task similarity from mean expression profiles per task. + +⚠️ WARNING: This is circular — you are using the same expression data +to define task similarity AND to fit the model. Only use this as a +last resort when no independent metadata is available. Consider +using only a held-out subset of genes (e.g. housekeeping genes) +for the similarity calculation. + +# Arguments +- `mtData` : MultitaskExpressionData with taskData populated +- `method` : :pearson (default) or :spearman +""" +function buildSimilarityFromExpression(mtData::MultitaskExpressionData; method::Symbol=:pearson) + nTasks = length(mtData.tasks) + # compute mean expression profile per task + meanProfiles = hcat([vec(mean(td.targGeneMat, dims=2)) for td in mtData.taskData]...) # genes × tasks + S = zeros(Float64, nTasks, nTasks) + + for i in 1:nTasks + for j in 1:nTasks + if method == :pearson + S[i, j] = cor(meanProfiles[:, i], meanProfiles[:, j]) + elseif method == :spearman + ri = tiedrank(meanProfiles[:, i]) + rj = tiedrank(meanProfiles[:, j]) + S[i, j] = cor(ri, rj) + end + end + end + + # clip to [0, 1] — negative correlations treated as no similarity + S = max.(S, 0.0) + return S +end + + +""" + buildSimilarityFromOntology(tasks, ontologyFile) + +Build task similarity from a cell type ontology distance file. +The ontology file should be a TSV with columns: Task1, Task2, Distance +where Distance is a non-negative value (0 = identical, larger = more distant). + +Similarity is computed as sim = exp(-distance / scale) where scale +is the median pairwise distance. +""" +function buildSimilarityFromOntology(tasks::Vector{String}, ontologyFile::String) + df = CSV.read(ontologyFile, DataFrame; delim='\t') + nTasks = length(tasks) + distMat = zeros(Float64, nTasks, nTasks) + taskIdx = Dict(t => i for (i, t) in enumerate(tasks)) + + for row in eachrow(df) + if haskey(taskIdx, row.Task1) && haskey(taskIdx, row.Task2) + i, j = taskIdx[row.Task1], taskIdx[row.Task2] + distMat[i, j] = row.Distance + distMat[j, i] = row.Distance + end + end + + # convert distance to similarity via RBF kernel + offDiag = [distMat[i,j] for i in 1:nTasks for j in 1:nTasks if i != j] + scale = isempty(offDiag) ? 1.0 : median(offDiag) + S = exp.(-distMat ./ max(scale, 1e-8)) + return S +end + + +""" + normalizeSimilarity!(S::Matrix{Float64}) + +Row-normalize similarity matrix so rows sum to 1. +This ensures the fusion penalty is scale-invariant across tasks +with different numbers of neighbors. +""" +function normalizeSimilarity!(S::Matrix{Float64}) + for i in 1:size(S, 1) + rowSum = sum(S[i, :]) + if rowSum > 0 + S[i, :] ./= rowSum + end + end + return S +end diff --git a/experimental/MTL/~$MultitaskGRN_equations.pptx b/experimental/MTL/~$MultitaskGRN_equations.pptx new file mode 100755 index 0000000000000000000000000000000000000000..92f5f34d67b15bbb1f2a650cf670a72e8e915b97 GIT binary patch literal 165 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 literal 0 HcmV?d00001 diff --git a/src/00_Data/geneExpression.jl b/src/00_Data/geneExpression.jl new file mode 100755 index 0000000..4de6532 --- /dev/null +++ b/src/00_Data/geneExpression.jl @@ -0,0 +1,250 @@ +# 00_Data/geneExpression.jl +module Data + + export GeneExpressionData, loadExpressionData!, loadAndFilterTargetGenes!, loadPotentialRegulators!, processTFAGenes! + + using DelimitedFiles + using Statistics + using JLD2 + using CSV + using Arrow + using DataFrames + + # Initialize data structure + + mutable struct GeneExpressionData + cellLabels::Vector{String} + geneNames::Vector{String} # Full gene list + geneExpressionMat::Matrix{Float64} # Full Expression Matrix + potRegMatmRNA::Matrix{Float64} + potRegs::Vector{String} + potRegsmRNA::Vector{String} + targGenes::Vector{String} # Target gene list + targGeneMat::Matrix{Float64} # Filtered expression matrix + tfaGenes::Vector{String} + tfaGeneMat::Matrix{Float64} + + function GeneExpressionData() + return new( + [], + [], + Matrix{Float64}(undef, 0, 0), + Matrix{Float64}(undef, 0, 0), + [], + [], + [], + Matrix{Float64}(undef, 0, 0), + [], + Matrix{Float64}(undef, 0, 0) + ) + end + end + + + # Load Expression Data + """ + loadExpressionData!(data::GeneExpressionData, geneExprFile) + + Loads gene expression data from a specified file and updates the `GeneExpressionData` object. + + # Arguments + - `data::GeneExpressionData`: The data object to be populated with gene expression details. + - `geneExprFile::String`: The path to the gene expression data file, which can be in `.arrow` format or tab-delimited text format. + + # Updates + - `data.cellLabels`: A vector of sample condition names, derived from the column headers (excluding the first). + - `data.targGenes`: A vector of gene names, derived from the first column. + - `data.targGeneMat`: A matrix of expression values, converted to `Float64`. + + # Raises + - An error if the gene expression file does not exist or if its path is invalid. + + # Notes + - Assumes that the first column in the file contains gene names and subsequent columns contain cell labels and expression data. + - For text files, data is sorted by gene names. + """ + function loadExpressionData!(data::GeneExpressionData, geneExprFile) + if isfile(geneExprFile) + if endswith(geneExprFile, ".arrow") + dfArrow = Arrow.Table(geneExprFile) + df = deepcopy(DataFrame(dfArrow)) + dfArrow = nothing; + cellLabels = names(df)[2:end] # Assume first column is gene names + geneNames = df[:, 1] # Extract gene names + ncounts = Matrix(df[:, 2:end]) + df = nothing + else + fid = open(geneExprFile); + C = readdlm(fid, '\t', '\n'); + close(fid) + cellLabels = C[1, :]; + cellLabels = filter(!isempty, cellLabels); + C = C[2:end, :]; + inds = sortperm(C[:, 1]); + C = C[inds, :]; + geneNames = C[:, 1]; + ncounts = C[:, 2:end] + end + ncounts = convert(Matrix{Float64}, ncounts); + + data.cellLabels = String.(cellLabels); + data.geneNames = String.(geneNames); + data.geneExpressionMat = ncounts; + ncounts = nothing; + else + error("Expression data file path is invalid.") + end + end + + + """ + loadAndFilterTargetGenes!(data::GeneExpressionData, targetGeneFile; eps=1E-10) + + Loads target genes from a file, filters them based on presence and variance in the existing gene expression data, and updates the `GeneExpressionData` object. + + # Arguments + - `data::GeneExpressionData`: The data object to be updated with filtered target gene information. + - `targetGeneFile::String`: The path to the file containing target gene names. + - `eps::Float64`: A keyword argument specifying the variance threshold for filtering target genes. Default is `1E-10`. + + # Updates + - `data.targGenes`: A filtered vector of target gene names that are present in the gene expression data and meet the variance threshold. + - `data.targGeneMat`: A matrix of expression values for these filtered target genes. + + # Raises + - An error if the target gene file does not exist. + - An error if no target genes are found in the gene expression data. + - An error if all target genes are filtered out due to low variance. + + # Notes + - This function finds matching genes and filters them based on a variance threshold to ensure sufficient expression variability across samples. + """ + function loadAndFilterTargetGenes!(data::GeneExpressionData, targetGeneFile; epsilon=1E-10) + if isfile(targetGeneFile) + # Load in target gene file + fid = open(targetGeneFile) + targetGenes = readdlm(fid, String) + close(fid) + + # Find all geneNames that are in the targer gene file + inds = findall(in(targetGenes), data.geneNames) + if isempty(inds) + error("No target genes found in expression data!") + end + + # Filter target genes and expression matrix + targGenes = data.geneNames[inds] + targGeneMat = data.geneExpressionMat[inds, :] + + # Filter genes by minimum variance cutoff + stds = std(targGeneMat, dims=2) + keep = [index[1] for index in findall(stds .>= epsilon)] + targGenesFilter = targGenes[keep] + targGeneMatFilter = targGeneMat[keep, :] + if isempty(keep) + error("All target genes removed due to low variance!") + else + println(length(targGenesFilter), " target genes retained after filtering") + end + data.targGenes = targGenesFilter + data.targGeneMat = targGeneMatFilter + else + error("Target gene file not found.") + end + end + + # Load Regulators + """ + loadPotentialRegulators!(data::GeneExpressionData, potRegFile) + + Loads potential regulators and updates the `GeneExpressionData` object with their expression data. + + # Arguments + - `data::GeneExpressionData`: The data object to be populated with potential regulator information. + - `potRegFile::String`: The path to the file containing potential regulator names. + + # Updates + - `data.potRegs`: List of all potential regulator names. + - `data.potRegsmRNA`: List of regulator names with corresponding mRNA expression data. + - `data.potRegMatmRNA`: Expression matrix for regulators with mRNA data. + + # Raises + - An error if the potential regulators file does not exist. + + # Notes + - Only the potential regulators present in the existing gene expression data are included. + """ + function loadPotentialRegulators!(data::GeneExpressionData, potRegFile) + if isfile(potRegFile) + fid = open(potRegFile) + potentialRegs = readdlm(fid, String) + close(fid) + potentialRegs = vec(potentialRegs) + + indsPotRegs = findall(in(data.geneNames), potentialRegs) + potentialRegs = potentialRegs[indsPotRegs] + + inds = findall(in(potentialRegs), data.geneNames) + potRegsmRNA = data.geneNames[inds] + potRegMatmRNA = data.geneExpressionMat[inds, :] + + data.potRegs = potentialRegs + data.potRegsmRNA = potRegsmRNA + data.potRegMatmRNA = potRegMatmRNA + else + error("Potential regulators file not found.") + end + end + + + + # Process genes for TFA calculation + """ + processTFAGenes(file, geneSc, nCounts) + + Processes TFA genes by retrieving their expression data from the expression matrix. + + # Arguments + - `file::String`: The path to the TFA genes file. If the path is empty or invalid, all genes are used. + - `geneNames::Vector{String}`: A vector of gene names from the expression data. + - `geneExpressionMat::Matrix{Float64}`: A matrix containing expression values for the genes. + + # Returns + - `tfaGenes::Vector{String}`: A vector of TFA gene names with expression data. + - `tfaGeneMat::Matrix{Float64}`: A matrix of expression values for the TFA genes. + + # Notes + - If the specified file does not exist, all genes are considered for TFA. + """ + function processTFAGenes!(data::GeneExpressionData, tfaGeneFile::Union{String, Nothing}; outputDir::Union{String, Nothing}=nothing) + if (tfaGeneFile !== nothing) && (tfaGeneFile != "") && isfile(tfaGeneFile) + tfaGenes = readlines(tfaGeneFile) + else + tfaGenes = data.geneNames + end + + inds = findall(in(tfaGenes), data.geneNames) + data.tfaGenes = data.geneNames[inds] + data.tfaGeneMat = data.geneExpressionMat[inds, :] + + if outputDir !== nothing && outputDir !== "" + + outputFile = joinpath(outputDir, "geneExprMat.jld") + save_object(outputFile, data) + + # cellLabels = data.cellLabels + # geneNames = data.geneNames + # geneExpressionMat = data.geneExpressionMat + # potRegs = data.potRegs + # potRegsmRNA = data.potRegsmRNA + # potRegMatmRNA = data.potRegMatmRNA + # targGenes = data.targGenes + # targGeneMat = data.targGeneMat + # tfaGenes = data.tfaGenes + # tfaGeneMat = data.tfaGeneMat + # @save outputFile cellLabels geneNames geneExpressionMat potRegs potRegsmRNA potRegMatmRNA targGenes targGeneMat tfaGenes tfaGeneMat + + end + end + +end diff --git a/src/00_Data/priorTFA.jl b/src/00_Data/priorTFA.jl new file mode 100755 index 0000000..221d0ee --- /dev/null +++ b/src/00_Data/priorTFA.jl @@ -0,0 +1,259 @@ +module PriorTFA + # Bring in the Data submodule so we can access GeneExpressionData, etc. + # Bring in MergeDegenerate submodule to access mergedTFsResult and TF-merging utilities + using ..Data + using ..MergeDegenerate + + # Standard libraries + using LinearAlgebra + using Statistics + using DelimitedFiles + using JLD2 + + export PriorTFAData, processPriorFile!, calculateTFA! + + mutable struct PriorTFAData + pRegs::Vector{String} + pTargs::Vector{String} + priorMatrix::Matrix{Float64} + pRegsNoTfa::Vector{String} + pTargsNoTfa::Vector{String} + priorMatrixNoTfa::Matrix{Float64} + noPriorRegs::Vector{String} + noPriorRegsMat::Matrix{Float64} + targExpression::Matrix{Float64} + medTfas::Matrix{Float64} + + function PriorTFAData() + return new( + [], # pRegs + [], # pTargs + Matrix{Float64}(undef, 0, 0),# priorMatrix + [], # pRegsNoTfa + [], # pTargsNoTfa + Matrix{Float64}(undef, 0, 0),# priorMatrixNoTfa + [], # noPriorRegs + Matrix{Float64}(undef, 0, 0),# noPriorRegsMat + Matrix{Float64}(undef, 0, 0),# targExpression + Matrix{Float64}(undef, 0, 0) # medTfas + ) + end + end + + """ + processPriorFile!(priorData::PriorTFAData, priorFile) + + Reads and processes a prior file, storing the extracted information in a `PriorTFAData` object. + + # Arguments + - `priorData::PriorTFAData`: The struct to be populated with data from the prior file. + - `priorFile::String`: The path to the prior file, formatted with tab-separated values. + + # Updates + - `priorData.pRegs`: Stores the list of transcription factors (TFs) from the prior file. + - `priorData.pTargs`: Stores the list of target genes from the prior file. + - `priorData.priorMatrix`: Stores the interactions matrix, indicating TF-gene relationships. + + # Raises + - An error if the prior file does not exist. + + # Notes + - Assumes the first row in the file contains TF names, and subsequent rows represent interactions with target genes. + - The gene list and matrix are sorted alphabetically by gene names. + """ + function processPriorFile!(priorData::PriorTFAData, + expressionData::GeneExpressionData, + priorFile; mergedTFsData::Union{mergedTFsResult, Nothing}=nothing, minTargets = 3) + + if isfile(priorFile) + + println("--- Case 1") + fid = open(priorFile) + C = readdlm(fid, '\t', '\n', skipstart=0) + close(fid) + + # Process and store genes and interactions matrix + pRegsTmp = convert(Vector{String}, filter(!isempty, C[1, :])) + C = C[2:end, :] + inds = sortperm(C[:, 1]) + C = C[inds, :] + pTargsTmp = convert(Vector{String}, C[:, 1]) + pMatrixTmp = convert(Matrix{Float64}, C[:, 2:end]) + + # Filter to only include those in potential regulator and target gene list + targInds = findall(in(expressionData.tfaGenes), pTargsTmp) + regInds = findall(in(expressionData.potRegs), pRegsTmp) + pRegsNoTfa = pRegsTmp[regInds] + pTargsNoTfa = pTargsTmp[targInds] + priorMatrixNoTfa = pMatrixTmp[targInds, regInds] + + # Find TFs that have expression data but arent in the prior + noPriorRegs = setdiff(expressionData.potRegsmRNA, pRegsNoTfa) + expInds = findall(in(noPriorRegs), expressionData.potRegsmRNA) + noPriorRegsMat = expressionData.potRegMatmRNA[expInds,:] + + println("--- Case 2") + ## Case 2: prior-based TFA is used + # Check whether there were degenerate TFs and outputs + if (mergedTFsData !== nothing) && + (mergedTFsData.mergedPrior !== nothing) && + (mergedTFsData.mergedTFMap !== nothing) + + println("-------- Using merge degenerate TFs prior file") + mergedTFs = mergedTFsData.mergedTFMap[:, 1] + individualTFs = mergedTFsData.mergedTFMap[:, 2] + + totMergedSets = length(individualTFs) + keepMergedIndices = Int[] + + for idx in 1:totMergedSets + currSet = split(individualTFs[idx], ", ") + usedTfs = intersect(currSet, expressionData.potRegs) + if !isempty(usedTfs) + push!(keepMergedIndices, idx) + end + end + + if !isempty(keepMergedIndices) # add merged potential regulators to our list + expressionData.potRegs = union(expressionData.potRegs, mergedTFs[keepMergedIndices]) + # Now load the merged prior matrix data (mergedPrior) + priorDF = mergedTFsData.mergedPrior + rowInd = sortperm(priorDF[:, 1]) + pRegsTmp = names(priorDF)[2:end] + totPRegs = length(pRegsTmp) + pTargsTmp = priorDF[rowInd, 1] + pMatrixTmp = Matrix(priorDF[rowInd, 2:end]) + end + end + + ### Filter TFs and Genes for TFA calculation + # Filter for potRegs and TFA genes + # pRegs = intersect(pRegsTmp,expressionData.potRegs) + # pTargs = intersect(pTargsTmp,expressionData.tfaGenes) + pTargInds = findall(in(expressionData.tfaGenes), pTargsTmp) + pRegInds = findall(in(expressionData.potRegs), pRegsTmp) + pTargs = pTargsTmp[pTargInds] + pRegs = pRegsTmp[pRegInds] + pInts = pMatrixTmp[pTargInds,pRegInds] + # Filter for minimum target genes in prior + interactionsPerTF = sum((abs.(sign.(pInts))), dims=1) + keepRegs = Tuple.(findall(x -> x > minTargets, interactionsPerTF)) + keepRegs = last.(keepRegs) + pInts = pInts[:,keepRegs] + pRegs = pRegs[keepRegs] + # Filter genes with no regulators + interactionsPerTarg = sum(abs.(sign.(pInts)), dims = 2) + keepTargs = Tuple.(findall(x -> x > 0, interactionsPerTarg)) + keepTargs = first.(keepTargs) + pInts = pInts[keepTargs,:] + pTargs = pTargs[keepTargs] + + ### Ensure expression and prior targets are in the same order + # pTargIndsTmp = first.(Tuple.(findall(in(expressionData.tfaGenes), pTargs))) + expTargInds = findall(in(pTargs), expressionData.tfaGenes) + # priorMatrix = pInts[pTargIndsTmp,:] + targExp = expressionData.tfaGeneMat[expTargInds,:] + if !(pTargs == expressionData.tfaGenes[expTargInds]) + println("Warnings, gene order in expression matrix and prior matrix do not match!!") + end + + # Add data to object + priorData.pRegs = pRegs + priorData.pTargs = pTargs + # priorData.priorMatrix = priorMatrix + priorData.priorMatrix = pInts + priorData.pRegsNoTfa = pRegsNoTfa + priorData.pTargsNoTfa = pTargsNoTfa + priorData.priorMatrixNoTfa = priorMatrixNoTfa + priorData.noPriorRegs = noPriorRegs + priorData.noPriorRegsMat = noPriorRegsMat + priorData.targExpression = targExp + + else + error("Prior file not found.") + end + end + + function calculateTFA!(priorData::PriorTFAData, expressionData::GeneExpressionData; + edgeSS = 0, zTarget::Bool = false, outputDir::Union{String, Nothing}=nothing) + priorMatrix = priorData.priorMatrix + targExp = priorData.targExpression + totTargs = size(priorMatrix, 1) + totPreds = size(priorMatrix, 2) + totConds = size(targExp, 2) + + if zTarget + targExp = (targExp .- mean(targExp, dims=2)) ./ std(targExp, dims=2) + println("Target expression normalized (z-score per gene).") + end + + if edgeSS > 0 + tfas = zeros(Float64, edgeSS, totPreds, totConds) + + for ss in 1:edgeSS + sPrior = zeros(Float64, totTargs, totPreds) + + for col in 1:totPreds + currTargs = priorMatrix[:, col] + targInds = findall(!iszero, currTargs) + totCurrTargs = length(targInds) + ssampleSize = Int(ceil(0.63 * totCurrTargs)) + ssample = rand(targInds, ssampleSize) + + sPrior[ssample, col] = priorMatrix[ssample, col] + end + tfas[ss, :, :] = sPrior \ targExp + end + + priorData.medTfas = median(tfas, dims=1) + println("Median from ", string(edgeSS), " subsamples used for prior-based TFA.") + else + # solves for X = Prior * TFA. + # TFA = argmin||priorMatrix * TFA - targExp||² + priorData.medTfas = priorMatrix \ targExp + println("No subsampling for prior-based TFA estimate.") + end + + if outputDir !== nothing && outputDir !== "" + + outputFile = joinpath(outputDir, "tfaMat.jld") + save_object(outputFile, priorData) + # @save outputFile priorData + + # pRegs = priorData.pRegs + # pTargs = priorData.pTargs + # priorMatrix = priorData.priorMatrix + # pRegsNoTfa = priorData.pRegsNoTfa + # pTargsNoTfa = priorData.pTargsNoTfa + # priorMatrixNoTfa = priorData.priorMatrixNoTfa + # noPriorRegs = priorData.noPriorRegs + # noPriorRegsMat = priorData.noPriorRegsMat + # targExpression = priorData.targExpression + # medTfas = priorData.medTfas + # @save outputFile pRegs pTargs priorMatrix pRegsNoTfa pTargsNoTfa priorMatrixNoTfa noPriorRegs noPriorRegsMat targExpression medTfas + + end + end + +end + + + # ## My implementaTION: reorder priorMatrix and prior targetGenes in the same order as expressionData.tfaGeneMat + + # expTargInds = findall(in(pTargs), expressionData.tfaGenes) + # targExp = expressionData.tfaGeneMat[expTargInds,:] + # tfaGenesTmp = expressionData.tfaGenes[expTargInds] + + # # (This works but O(N^2)) + # pTargInd2 = [findfirst(==(g), pTargs) for g in tfaGenesTmp if g in pTargs] + # pTargsTmp2 = pTargs[pTargInd2] + # pInt2 = pInts[pTargInd2, :] + + # # O(N) + # # Build a lookup from gene ⇒ row in pInts / pTargs + # posInPrior = Dict{String,Int}(gene => i for (i,gene) in enumerate(pTargs)) + # # For each gene in tfaGenesTmp grab its prior‐row + # rowOrder = [ posInPrior[g] for g in tfaGenesTmp ] + # pTargsTmp2 = pTargs[ rowOrder] + # pInt2 = pInts[ rowOrder, :] + diff --git a/src/01_Prior/.DS_Store b/src/01_Prior/.DS_Store new file mode 100755 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 length(s2) + s1, s2 = s2, s1 + end + c = 0 + for t in s1 + if t in s2 + c += 1 + end + end + return c + end + + function readNetwork(networkFile::String; fileFormat::Int) + """ + Parse a regulatory network file in either long or wide format. + + # Arguments + - `networkFile::String`: Path to the network file. + - `fileFormat::Int`: + - `1` = long format (TF, Target, Weight) + - `2` = wide format (Targets × TFs matrix) + + # Returns + - A dictionary mapping TF names to sets of (target, weight) tuples. + + # Notes + - Entries with weight zero are ignored. + - Weights are stored as strings (converted to Float64 later). + """ + # Initialize dictionary to store TF-target interactions + tfTargDic = Dict{String, Set{Tuple{String, String}}}() + + # Parse the network file based on format + if fileFormat == 1 + # Long format (direct TF-target-weight representation) + # Long format: each line has the form: TFTargetWeight. + open(networkFile, "r") do file + readline(file) # skip header + for line in eachline(file) + line = strip(line) + if isempty(line) + continue + end + parts = split(line, '\t') + if length(parts) < 3 + continue + end + tfName, target, weight = parts[1], parts[2], parts[3] + if weight != "0" + if !haskey(tfTargDic, tfName) + tfTargDic[tfName] = Set{Tuple{String, String}}() + end + push!(tfTargDic[tfName], (target, weight)) + end + end + println("Dict Created!!!") + end + + elseif fileFormat == 2 + # Wide format (TFs as columns, Targets as rows, and cells as interactions) + df = CSV.File(networkFile, header=1; delim='\t') |> DataFrame + melted = stack(df, Not(1)) + melted = rename!(melted, names(melted)[1] => :Target, names(melted)[2] => :TF, names(melted)[3] => :Weight) + melted = select!(melted, :TF, :Target, :Weight) + melted = filter(row -> row.Weight != 0, melted) + for row in eachrow(melted) + tfName = row.TF + target = row.Target + weight = row.Weight + if !(tfName in keys(tfTargDic)) + tfTargDic[tfName] = Set() + end + push!(tfTargDic[tfName], (target, string(weight))) + end + end + return tfTargDic + end + + + function groupRedundantTFs(tfTargDic::Dict{String, Set{Tuple{String, String}}}) + """ + Compute overlapping TFs with identical target–weight sets and group them into meta-TFs. + + # Arguments + - `tfTargDic::Dict{String, Set{Tuple{String, String}}}`: Mapping from TFs to their target–weight pairs. + + # Returns + - `tfMergers::Dict{String, Vector{String}}`: TFs grouped into merge sets. + - `overlaps::Dict{Tuple{String, String}, Int}`: Pairwise TF overlap counts. + - `tfTargNums::Dict{String, Int}`: Number of targets per TF. + - `tfNames::Vector{String}`: All TF names (sorted). + + # Notes + - Uses threading if multiple threads are available. + - TFs are merged if they have exactly the same set of target–weight pairs. + """ + # Check if more than 1 thread is available + nthreads = Threads.nthreads() + use_threads = nthreads > 1 + + # Step A: Determine TF overlaps + tfNames = sort(collect(keys(tfTargDic))) + tfMergers = Dict{String, Vector{String}}() + overlaps = Dict{Tuple{String, String}, Int}() + tfTargNums = Dict{String, Int}() + + # Fill tfTargNums and diagonal of overlap matrix + for (tf, targets) in tfTargDic + n = length(targets) + tfTargNums[tf] = n + overlaps[(tf,tf)] = n #seed the diagonal of the overlaps matrix + end + + if use_threads + localOverlapsArray = [Dict{Tuple{String, String}, Int}() for i in 1:nthreads] + localTFMergersArray = [Dict{String, Set{String}}() for i in 1:nthreads] + + println("Running in threaded mode ...") + Threads.@threads for i in 1:length(tfNames) + # Use the thread's id (an integer between 1 and nthreads) to access its local storage. + tid = threadid() + localOverlaps = localOverlapsArray[tid] + localMergers = localTFMergersArray[tid] + + tf1 = tfNames[i] + tf1targets = tfTargDic[tf1] + for j in (i+1):length(tfNames) + tf2 = tfNames[j] + tf2targets = tfTargDic[tf2] + overlapSize = countOverlap(tf1targets, tf2targets) + + # Save both orderings if needed. + localOverlaps[(tf1, tf2)] = overlapSize + localOverlaps[(tf2, tf1)] = overlapSize + + # If the overlap equals each TF's target count then they fully overlap. + # if tfTargNums[tf1] == overlapSize && tfTargNums[tf2] == overlapSize + if tfTargNums[tf1] == overlapSize && tfTargNums[tf2] == overlapSize + # Update local mergers for tf1. + localMergers[tf1] = get!(localMergers, tf1, Set([tf1])) + push!(localMergers[tf1], tf2) + + # Update local mergers for tf2. + localMergers[tf2] = get!(localMergers, tf2, Set([tf2])) + push!(localMergers[tf2], tf1) + end + end + end + + # Merge the thread-local results into global dictionaries. + for lo in localOverlapsArray + for (k,v) in lo + overlaps[k] = v + end + end + + for localx in localTFMergersArray + for (tf, mergerSet) in localx + if haskey(tfMergers, tf) + # Combine the sets, then convert back to vector making sure they're unique. + unionSet = union(Set(tfMergers[tf]), mergerSet) + tfMergers[tf] = collect(unionSet) + else + tfMergers[tf] = collect(mergerSet) + end + end + end + + else + println("Running in sequential mode ...") + for i in 1:length(tfNames) + + tf1 = tfNames[i] + tf1targets = tfTargDic[tf1] + + for j in i+1:length(tfNames) + tf2 = tfNames[j] + tf2targets = tfTargDic[tf2] + + # overlapSize = length(intersect(tf1targets, tf2targets)) # Slower + overlapSize = countOverlap(tf1targets, tf2targets) + overlaps[(tf1, tf2)] = overlapSize + overlaps[(tf2, tf1)] = overlapSize + + if tfTargNums[tf1] == tfTargNums[tf2] == overlapSize + # Use get! to set default values and append + push!(get!(tfMergers, tf1, [tf1]), tf2) + push!(get!(tfMergers, tf2, [tf2]), tf1) + end + end + end + end + println("TF Overlaps Determination Complete!!") + + return tfMergers, overlaps, tfTargNums, tfNames + # method1 = tfMergers + # all((Set(method1[k]) == Set(tfMergers[k])) for k in keys(method1)) + end + + + # Function to merge degenerate prior TFs + function mergeDegenerateTFs( + mergedTFsData::mergedTFsResult, + networkFile::String; + outFileBase::Union{String,Nothing}=nothing, + fileFormat::Int = 2, + connector::String = "_" + ) + """ + merge_degenerate_priors( + networkFile::String; + outFileBase::Union{String,Nothing}=nothing, + fileFormat::Int=1, + connector::String="_", + write_files::Bool=true + ) + -- NamedTuple{(:merged, :mergedTfs),Tuple{DataFrame,Vector{String}}} + + - networkFile – path to your priorFile + - outFileBase – optional “base path+stem” for writing the output files; + if `nothing` we auto‐derive it from `networkFile`. + - fileFormat – 1=long, 2=wide + - connector – string between merged TF names, default “_” + - write_files – if true (default) write the five output files; + if false, run purely in‐memory. + + Returns a NamedTuple + - merged = wide‐format DataFrame (targets×regulators) + - mergedTfs = Vector{String} of all the new meta‐TF names + """ + + # 1. ----- Read + compute mergers, overlaps, counts, names... + tfTargDic = readNetwork(networkFile; fileFormat) + tfMergers, overlaps, tfTargNums, tfNames = groupRedundantTFs(tfTargDic) + + # 2. ----- Write output files + + # If no outFileBase was supplied, derive it from networkFile: + stem = splitext(basename(networkFile))[1] # filename without extension + if outFileBase === nothing + dir = dirname(networkFile) + outFileBase = joinpath(dir, stem) + else + outFileBase = joinpath(outFileBase,stem) + end + netOutFile = outFileBase * "_merged_sp.tsv" + netMatOutFile = outFileBase * "_merged.tsv" + overlapsOutFile = outFileBase * "_overlaps.tsv" + totTargOutFile = outFileBase * "_targetTotals.tsv" + mergedTfsOutFile = outFileBase * "_mergedTFs.tsv" + + # Open all + netIO = open(netOutFile, "w") + overlapsIO = open(overlapsOutFile, "w") + totTargIO = open(totTargOutFile, "w") + mergedTfsIO = open(mergedTfsOutFile, "w") + + # In-memory collector + netDF = DataFrame(Regulator=String[], Target=String[], Weight=String[]) + tabMergedTFs = Vector{Vector{String}}() # will hold lines "metaTF\tmember1, member2,…" + allMergedTfs = collect(keys(tfMergers)) + usedMergedTfs = Set{String}() # keeps track of used TFs, so we don't output mergers twice + printedTfs = String[] + # overlapsMap = Dict{String, Vector{String}}() # key = printed TF name, value = list of overlap counts + + try + # Write header to network output file. + println(netIO, "Regulator\tTarget\tWeight") + for tf in tfNames + tfPrint = nothing + doPrint = false + if tf in allMergedTfs && !(tf in usedMergedTfs) + mergedTfs = sort(collect(tfMergers[tf])) + union!(usedMergedTfs, mergedTfs) + tfPrint = join(mergedTfs[1:2], connector) * (length(mergedTfs) > 2 ? "..." : "") + # 1) write merged‐TF mapping to disk + line = tfPrint * "\t" * join(mergedTfs, ", ") + println(mergedTfsIO, line) + # 2) store in memory + push!(tabMergedTFs, [tfPrint, join(mergedTfs, ", ")]) + doPrint = true + + elseif !(tf in allMergedTfs) + tfPrint = tf + doPrint = true + end + + if doPrint + # Write target totals. + println(totTargIO, "$(tfPrint)\t$(tfTargNums[tf])") + # Write each TF-target-weight record to a line. + for (targ, wgt) in tfTargDic[tf] + outline = "$(tfPrint)\t$(targ)\t$(wgt)" + println(netIO, outline) + # also push into netDF + push!(netDF, (Regulator=tfPrint, Target=targ, Weight=wgt)) + end + push!(printedTfs, tfPrint) + end + end + + # # Write the overlaps output file: + # # First line is header + println(overlapsIO, "\t" * join(printedTfs, "\t")) + for tf in printedTfs + row = [ string(overlaps[(first(split(tf,connector)), first(split(pt,connector)))]) + for pt in printedTfs ] + println(overlapsIO, tf * '\t' * join(row, '\t')) + end + println("Overlap Output File Successfully Written!!!") + finally + close(netIO); close(overlapsIO); close(totTargIO); close(mergedTfsIO) + end + + # # Read the network output file into a DataFrame + # mergedTab = CSV.read(netOutFile, DataFrame; delim='\t') + # Convert the DataFrame to wide format + netDF.Weight = parse.(Float64, netDF.Weight) + mergedPrior = convertToWide(netDF; indices=(2, 1, 3)) + mergedPrior .= coalesce.(mergedPrior, 0.0) + # write to file, while makig sure the first column is unnamed. + writeTSVWithEmptyFirstHeader(mergedPrior, netMatOutFile; delim ='\t') + + println("Output files:\n$mergedTfsIO\n$totTargOutFile\n$netOutFile\n$overlapsOutFile") + # return mergedPrior, mergedTfsList + # output = MergeDegeneratePriorOutput() + mergedTFsData.mergedPrior = mergedPrior + mergedTFsData.mergedTFMap = reduce(vcat, permutedims.(tabMergedTFs)) # Convert tabMergedTFs to a two columns matrix and then saves + # return output + + end +end + + +# # Usage +# outFileBase = nothing +# networkFile = "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/Priors/5kb/proximity_5kb_b.tsv" +# fileFormat = 2 +# res = mergedTFsResult() +# mergeDegenerateTFs(res, networkFile; fileFormat = 2); + +# # networkFile = "/data/miraldiNB/Michael/Scripts/Inferelator_JL/julia_fxns/proximity_10kb_b_sp.tsv" +# # outFileBase = "/data/miraldiNB/Michael/Scripts/Inferelator_JL/julia_fxns/" +# # y = mergeDegeneratePriorTfs(networkFile, outFileBase, 1); + +# # original_df = CSV.read(networkFile, DataFrame; delim='\t') + +# # merged_sp = "/data/miraldiNB/Michael/Scripts/Inferelator_JL/Tfh10_Example/inputs/priors/_merged_sp.tsv" +# # data = CSV.read(merged_sp, DataFrame; delim='\t') # For tab-separated values +# # Group by Regulator and Target, and filter for combinations that appear more than once + +# #= +# # Count the occurrences of each Regulator-Target combination +# counts = combine(groupby(data, [:Regulator, :Target]), nrow => :count) +# df_with_counts = innerjoin(data, counts, on = [:Regulator, :Target]) +# df_filtered = filter(:count => x -> x > 1, df_with_counts) +# df_sorted = sort(df_filtered, [:Regulator, :Target]) + +# df_check = filter(row -> row.TF == "Foxp4" && row.Target == "0610043K17Rik", original_df) + +# data1 = CSV.read("/data/miraldiNB/Michael/Scripts/Inferelator_JL/Tfh10_Example/inputs/priors/_merged_sp3.tsv", DataFrame; delim='\t') + +# for (i, row) in enumerate(eachrow(data)) +# # Convert row.Regulator and row.Target +# order_map[(String(row.Regulator), String(row.Target))] = i +# end + +# df1_order = [ get(order_map, (String(data1.Regulator[i]), String(data1.Target[i])), typemax(Int)) +# for i in 1:nrow(data1) ] +# row_indices = sortperm(df1_order) +# sorted_df1 = data1[row_indices, :] =# + diff --git a/src/02_GRN/.DS_Store b/src/02_GRN/.DS_Store new file mode 100755 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 x == globalMin[1], devs) # globalMin + minInd = minInds[end] + + lambdaRangeGene = grnData.lambdaRangeGene[targ] + push!(lambdaTrack, lambdaRangeGene[minInd]) + + networkStability[targ, :] = grnData.stabilityMat[minInd, targ, :] + betas[targ, :] = grnData.betas[targ, :, minInd] + + if minInd == 1 + totMins += 1 + elseif minInd == totLambdas + totMaxs += 1 + end + end + if totMins > 0 + println("Target instability reached at minimum lambda for ", string(totMins), " gene(s)") + end + if totMaxs > 0 + println("Target instability reached at maximum lambda for ", string(totMaxs), " gene(s)") + end + + elseif instabilityLevel == "Network" + println("Network instabilities detected.") + # find the single lambda corresponding to the cutoff + devs = abs.(grnData.netInstabilities .- targetInstability) + globalMin = findmin(devs) + minInds = first(Tuple(globalMin[2])) + globalMin = globalMin[1] + # take the largest lambda that is closest to targetInstability + minInd = minInds[end] + if minInd == 1 + println("Minimum lambda was used for maximum instability ", string(grnData.netInstabilities[minInd]), " to reach target cut = ", string(targetInstability), ".") + elseif minInd == totLambdas + println("Maximum lambda was used for minimum instability ", string(grnData.netInstabilities[minInd]), " to reach target cut = ", string(targetInstability), ".") + end + networkStability[:, :] = grnData.stabilityMat[minInd, :, :] + betas = grnData.betas[:, :, minInd] + lambdaTrack = lambdaRange[minInds] + + else + error("instabSource not recognized, should be either Gene or Network.") + end + + # ssMatrix has infinity entries to mark illegal TF-gene interactions + # (e.g., TF mRNA TFA cannot be used to predict TF gene expression) + networkStabilityVec = networkStability[:] + networkStabilityVec[isinf.(networkStabilityVec)] .= 0 + networkStability = reshape(networkStabilityVec, totNetGenes, totNetTfs) + + if length(lambdaTrack) == 1 + lambdaTrack = convert(Float64, lambdaTrack) + else + lambdaTrack = convert(Vector{Float64}, lambdaTrack) + end + + buildGrn.lambda = lambdaTrack + buildGrn.networkStability = networkStability + buildGrn.betas = betas +end + +# function firstNByGroup(vect::AbstractVector, N::Integer) +# """ +# firstNByGroupIndices(vec::AbstractVector, N::Integer) + +# Return the indices of the first `N` occurrences of each unique element in `vec`. + +# This is useful when you want to subset another array based on limited occurrences per group. + +# # Arguments +# - `vec`: A vector of values to group by (e.g., transcription factors). +# - `N`: The maximum number of elements to select per unique group. + +# # Returns +# - A vector of indices corresponding to the first `N` entries per group. +# """ +# seen = Dict{eltype(vect), Int}() +# idxs = Int[] +# for (i, v) in enumerate(vect) +# seen[v] = get(seen, v, 0) + 1 +# if seen[v] <= N +# push!(idxs, i) +# end +# end +# return idxs +# end + +function partialCorrelationMat(X::Matrix{Float64}; epsilon = 1e-7, first_vs_all = false) + # Mean centering of the columns (mean subtraction) + X_centered = X .- mean(X, dims=1) + + # Compute the covariance matrix + sigma = cov(X_centered) + # regularizing the covariance matrix to avoid ill-conditining and singularity + sigma = sigma + epsilon * I + + # Precision matrix (inverse of the covariance matrix) + theta = inv(sigma) + p = size(X, 2) # number of features/predictors/explanatory variables + + if first_vs_all + # Compute partial correlation for the first variable vs all others + P = ones(1, p) # Initialize the partial correlation matrix + for j in 2:p + P[j] = -theta[1, j] / sqrt(theta[1, 1] * theta[j, j]) + end + else + # Compute the full partial correlation matrix + P = ones(p, p) # Initialize the partial correlation matrix + for i in 1:(p-1) + for j in (i+1):p + P[i , j] = -theta[i, j] / sqrt(theta[i, i] * theta[j, j]) + P[j, i] = P[i, j] # Symmetry + end + end + end + + return P +end + +function rankEdges!(expressionData::GeneExpressionData, tfaData::PriorTFAData, grnData::GrnData, buildGrn::BuildGrn; + mergedTFsData::Union{mergedTFsResult, Nothing}=nothing, + useMeanEdgesPerGeneMode = true, + meanEdgesPerGene = 20, + correlationWeight = 1, + outputDir::Union{String, Nothing}=nothing) + + if isempty(outputDir) + error("You must provide a non-empty outputDir to save the results.") + end + + predictorMat = grnData.predictorMat + allPredictors = grnData.allPredictors + + totLambdas, totNetGenes, totNetTfs = size(grnData.stabilityMat) + totInts = totNetGenes * totNetTfs + targs = repeat(expressionData.targGenes, totNetTfs, 1) + regs = vec(repeat(permutedims(allPredictors), totNetGenes,1)) + rankTmp = buildGrn.networkStability[:] + keepInds = findall(x -> x != 0 && x != Inf, rankTmp) # keep nonzero, remove infinite values (e.g., corresponding to TF-TF edges when TF mRNA used for TFA) + rankTmp = rankTmp[keepInds] + inds = reverse(sortperm(rankTmp)) + rankings = sort(rankTmp,rev=true) + regs = regs[keepInds[inds]] + targs = targs[keepInds[inds]] + totInfInts = length(rankings) + + if useMeanEdgesPerGeneMode + totQuantEdges = length(unique(targs)) * meanEdgesPerGene + selectionIndices = 1:min(totQuantEdges, totInfInts) + else + selectionIndices = firstNByGroup(targs, meanEdgesPerGene) + end + + # Compute quantiles + F = ecdf(rankings[selectionIndices]) # uses the StatsBase package + quantiles = F.(rankings[selectionIndices]) + + ## take what's in the meanEdgesPerGene network and get partial correlations + allCoefs = zeros(totNetGenes,totNetTfs) + allQuants = zeros(totNetGenes,totNetTfs) + allStabsTest = buildGrn.networkStability + keptTargs = permutedims(targs[selectionIndices]) + uniTargs = unique(keptTargs) + totUniTargs = length(uniTargs) + tfsPerGene = zeros(totUniTargs,1) + + for targ = ProgressBar(1:totUniTargs) + currTarg = uniTargs[targ] + targRankInds = last.(Tuple.(findall(x -> x == currTarg, keptTargs))) + currRegs = regs[targRankInds] + + # --- Deduplicate currRegs while keeping aligned targRankInds --- + uniCurrRegsInds = firstNByGroup(currRegs, 1) # This reurns the index where each unique currRegs first appeared + uniCurrRegs = currRegs[uniCurrRegsInds] + targRankInds = targRankInds[uniCurrRegsInds] # Ensure the target corresponding to the duplicated Regulator is removed + # --- Get target index in full gene list + targInd = last.(Tuple.(findall(x -> x == currTarg, expressionData.targGenes))) + tfsPerGene[targ] = Int.(length(targRankInds)) + # tfsPerGene = Int.(tfsPerGene) + + # --- Match predictors --- + matchedRegs = intersect(allPredictors,currRegs) + # inds = findall(in(matchedRegs), allPredictors) + # regressIndsMat = first.(inds) + regressIndsMat = findall(in(matchedRegs), allPredictors) + rankVecInds = findall(in(matchedRegs),uniCurrRegs) + # --- Prepare input matrix: target + predictors --- + currTargVals = vec(transpose(grnData.responseMat[targInd,:])) + currPredVals = transpose(predictorMat[regressIndsMat,:]) + combTargPreds = vcat(currTargVals', currPredVals')' + combTargPreds = permutedims(combTargPreds') + prho = partialCorrelationMat(combTargPreds; first_vs_all = true) + prho = prho[2:end] + prho = vec(prho) + + if length(findall(x -> x == NaN, prho)) == 0 # make sure there weren't too many edges, + allCoefs[targInd,regressIndsMat] = prho + else + println(currTarg, " pcorr was singular, # TFs = ", string(length(regressIndsMat))) + end + + allQuants[targInd,regressIndsMat] = quantiles[targRankInds[rankVecInds]] + allStabsTest[targInd,regressIndsMat] = allStabsTest[targInd,regressIndsMat] + (correlationWeight .* transpose(round.(abs.(prho),digits = 4))) + end + + inPriorMat = sign.(abs.(grnData.priorMatProcessed)) + + mergeTfLocVec = zeros(totNetTfs) # for keeping track of merged TFs (needed for partial correlation calculation) + if (mergedTFsData !== nothing) && + (mergedTFsData.mergedPrior !== nothing) && + (mergedTFsData.mergedTFMap !== nothing) + + mergedTFs = mergedTFsData.mergedTFMap[:, 1] + individualTFs = mergedTFsData.mergedTFMap[:, 2] + totMerged = length(individualTFs) + + rmInds = [] # remove merged TFs from regulators + addRegs = [] + addInts = [] + addCoefs = [] + addPMat = [] + addPredMat = [] + addLoc = [] + addQuants = [] + for mind = 1:totMerged + mTf = mergedTFs[mind] + inputLocs = findall(x -> x == mTf,allPredictors) + totMInts = length(inputLocs) # number of interactions for merged TF + rmInds = [rmInds; inputLocs]; + if length(inputLocs) > 0 + indTfs = intersect(permutedims(split(individualTFs[mind],", ")),tfaData.pRegsNoTfa); # intersect ensures that TF was a potential regulator (e.g., based on gene expression) + totIndTfs = length(indTfs) + for indt = 1:totIndTfs + indTf = indTfs[indt] + append!(addRegs, fill(indTf, 1)) + append!(addInts, allStabsTest[:,inputLocs]) + append!(addPMat, inPriorMat[:,inputLocs]) + append!(addQuants, allQuants[:,inputLocs]) + append!(addCoefs, allQuants[:,inputLocs]) + append!(addPredMat, predictorMat[inputLocs,:]) + append!(addLoc, mind) + end + end + end + println("Total of ", string(length(rmInds)), " TFs expanded.") + keepInds = setdiff(1:totNetTfs,rmInds) + # remove merged TFs and add individual TFs + if length(addRegs) > 0 + allPredictors = collect(allPredictors[keepInds]) + append!(allPredictors, addRegs) + allStabsTest = hcat(allStabsTest[:,keepInds], reshape(addInts, size(allStabsTest)[1], Int(size(addInts)[1] / size(allStabsTest)[1]))) + allCoefs = hcat(allCoefs[:,keepInds], reshape(addCoefs, size(allStabsTest)[1], Int(size(addInts)[1] / size(allStabsTest)[1]))) + allQuants = hcat(allQuants[:,keepInds], reshape(addQuants, size(allStabsTest)[1], Int(size(addInts)[1] / size(allStabsTest)[1]))) + inPriorMat = hcat(inPriorMat[:,keepInds], reshape(addPMat, size(inPriorMat)[1], Int(size(addPMat)[1] / size(inPriorMat)[1]))) + predictorMat = vcat(predictorMat[keepInds, :], reshape(addPredMat, Int((size(addPredMat)[1]) / size(predictorMat)[2]), size(predictorMat)[2])) + append!(mergeTfLocVec, addLoc) + end + else + println("No merged TFs file found.") + end + + ## re-rank based on possibly de-merged TFs + rankings = allStabsTest[:] + coefVec = allCoefs[:] + + + inPriorVec = inPriorMat[:] + totNetTfs = length(allPredictors) + totInts = totNetGenes * totNetTfs + targs = repeat((expressionData.targGenes),totNetTfs,1) + regs1 = repeat(permutedims(allPredictors),totNetGenes,1) + regs = reshape(regs1,totInts,1) + + rankings = convert(Vector{Float64}, rankings) + coefVec = convert(Vector{Float64}, coefVec) + inPriorVec = convert(Vector{Float64}, inPriorVec) + predictorMat = convert(Matrix{Float64}, predictorMat) + allStabsTest = convert(Matrix{Float64}, allStabsTest) + allCoefs = convert(Matrix{Float64}, allCoefs) + allQuants = convert(Matrix{Float64}, allQuants) + inPriorMat = convert(Matrix{Float64}, inPriorMat) + + ## only keep nonzero rankings + keepRankings = findall(x -> x != 0 && x != Inf, rankings) + indsMerged = sortperm(rankings[keepRankings]) + indsMerged = reverse(indsMerged) + + # update info sources + rankings = rankings[keepRankings[indsMerged]] + coefVec = coefVec[keepRankings[indsMerged]] + inPriorVec = inPriorVec[keepRankings[indsMerged]] + regs = regs[keepRankings[indsMerged]] + targs = targs[keepRankings[indsMerged]] + totInfInts = length(rankings) + + # totQuantEdges = length(unique(targs))*meanEdgesPerGene + # Compute quantiles + F = ecdf(rankings) + quantiles = F.(rankings) + + # Compute signedQuantiles + signedQuantile = sign.(coefVec) .* quantiles + + ## ------- Color calculations for JP-Gene-Viz + # Compute strokeWidth and colors + minRank, maxRank = extrema(rankings) + rankRange = max(maxRank - minRank, eps()) # prevent division by zero + # Dash Styling + strokeDashArray = ifelse.(inPriorVec .!= 0, "Yes", "No") + networkMatrix = hcat(regs, targs, signedQuantile, rankings, coefVec, strokeDashArray) + if useMeanEdgesPerGeneMode + totQuantEdges = length(unique(targs)) * meanEdgesPerGene + # selectionIndices = 1:totQuantEdges + selectionIndices = 1:min(totQuantEdges, totInfInts) + else + selectionIndices = firstNByGroup(targs, meanEdgesPerGene) + + end + + networkMatrixSubset = hcat(regs[selectionIndices], targs[selectionIndices], signedQuantile[selectionIndices], + rankings[selectionIndices], coefVec[selectionIndices], strokeDashArray[selectionIndices] + ) + + buildGrn.regs = regs + buildGrn.targs = targs + buildGrn.signedQuantile = signedQuantile + buildGrn.rankings = rankings + buildGrn.partialCorrelation = coefVec + buildGrn.inPrior = strokeDashArray + buildGrn.networkMat = networkMatrix + buildGrn.networkMatSubset = networkMatrixSubset + buildGrn.inPriorVec = inPriorVec + buildGrn.mergeTfLocVec = mergeTfLocVec # just added + + + if outputDir !== nothing && outputDir !== "" + outputFile = joinpath(outputDir, "grnOutMat.jld") + save_object(outputFile, buildGrn) + end +end + +# function saveData(expressionData::GeneExpressionData, tfaData::PriorTFAData, grnData::GrnData, buildGrn::BuildGrn, outputDir::String, fileName::String) +# output = outputDir * "/" * fileName +# @save output expressionData tfaData grnData buildGrn +# end + +# function writeNetworkTable!(buildGrn::BuildGrn; outputDir::String, networkName::Union{String, Nothing}=nothing) +# local baseName = (networkName === nothing || isempty(networkName)) ? "edges" : networkName * "edges" + +# outputFile = joinpath(outputDir, baseName * ".tsv") +# colNames = "TF\tGene\tsignedQuantile\tStability\tCorrelation\tinPrior\n" +# open(outputFile, "w") do io +# write(io, colNames) +# writedlm(io, buildGrn.networkMat) +# end + +# outputFileSubset = joinpath(outputDir, baseName * "_subset.tsv") +# open(outputFileSubset, "w") do io +# write(io, colNames) +# writedlm(io, buildGrn.networkMatSubset) +# end +# end diff --git a/src/02_GRN/combineGRN.jl b/src/02_GRN/combineGRN.jl new file mode 100755 index 0000000..ccf537d --- /dev/null +++ b/src/02_GRN/combineGRN.jl @@ -0,0 +1,294 @@ +""" + GRN + +This module provides functionality for building and combining Gene Regulatory Networks (GRNs). + +The GRN files are assumed to have the following columns: + TF, Gene, signedQuantile, Stability, Correlation, strokeVals, strokeWidth, inPrior +Main function: +- `combineGRNs`: combine multiple GRN files with options for mean/max aggregation, + controlling edges per gene, and saving the combined network. + +Dependencies: +- Uses `PriorUtils` for prior-related helper functions. +- Includes internal utilities in `utilsGRN.jl`. +""" + +# using ..DataUtils +# using CSV, DataFrames, Statistics, StatsBase, Printf, Dates +# using ArgParse + +# export combineGRNs +# ──────────────────────────────────────────────────────────────── +# Helper: deterministic tie-breaker +# ──────────────────────────────────────────────────────────────── +function pickRow(g, primary::Union{String,Symbol}; order::Symbol = :max) + + """ + pickRow(g, primary; order = :max) + + Return a single row index from SubDataFrame `g` according to a deterministic + tie-breaking hierarchy. + + Arguments + ───────── + g - SubDataFrame produced by `groupby` + primary - "Stability"/:Stability or "Quantile"/:Quantile + order - :max (default) or :min (optimise up or down) + + Tie-breaking + ──────────── + If primary = Stability : Stability → Quantile → |Correlation| + If primary = Quantile : Quantile → |Correlation| + """ + + prim = Symbol(primary) # normalise to Symbol + + # 1) extreme value of the primary column + pVals = g[!, prim] + extreme = order === :max ? maximum(pVals) : minimum(pVals) + idxs = findall(==(extreme), pVals) + length(idxs) == 1 && return idxs[1] + + # 2) additional tie-breakers + if prim == :Stability && "signedQuantile" in names(g) + qVals = g.signedQuantile[idxs] + extQ = order === :max ? maximum(qVals) : minimum(qVals) + idxs = idxs[findall(==(extQ), qVals)] + length(idxs) == 1 && return idxs[1] + # if still tied we fall through to correlation + end + + if "Correlation" in names(g) + absCorr = abs.(g.Correlation[idxs]) + bestC = maximum(absCorr) # always take largest |ρ| + idxs = idxs[findall(==(bestC), absCorr)] + end + + return idxs[1] # deterministic fallback +end + + +# ──────────────────────────────────────────────────────────────── +# Main function +# ──────────────────────────────────────────────────────────────── +function combineGRNs(nets2combine::Vector{String}; combineOpt::Union{String,Symbol}, meanEdgesPerGene::Int, useMeanEdgesPerGeneMode::Bool, + saveDir::Union{String,Nothing}=nothing, saveName::String="") + + """ + # Arguments + - `nets2combine::Vector{String}`: A vector of GRN file names to combine. + - `combineOpt::Union{String,Symbol}`: Specifies the combination strategy (e.g., "max", "meanQuantile"). + - `meanEdgesPerGene::Int`: The mean number of edges per gene. + - `saveDir::Union{String,Nothing}`: Optional. Directory to save results. Defaults to `nothing` (do not save). + - `saveName::String`: Optional. Filename for saving combined GRN. Defaults to `""`. + useMeanEdgesPerGene = false # This option controls whether edge selection is done per-group or globally. If `true`, selects the top `meanEdgesPerGene` edges **per target gene**. + # If `false`, selects a flat total number of edges equal to `length(unique(targs)) * meanEdgesPerGene`. + """ + allDfs = DataFrame[] + + # ──── 1. Read each file and then combine; create a rank column if not present ─────────────────────────── + for file in nets2combine + # Adjust CSV reading as needed (TSV expected if tab sep) + df = CSV.read(file, DataFrame; delim='\t') + # println(size(df)) + + # # Ensure the required columns exist + # requiredCols = ["TF", "Gene", "signedQuantile", "Stability", "Correlation", "inPrior"] + # dfCols = names(df) + # for col in requiredCols + # if !(col in dfCols) + # error("Column $(col) not found in file $(file)") + # end + # end + # df = df[!, requiredCols] + + # ensure numeric columns are Float64 + for col in [:Stability, :Correlation] + if !(eltype(df[!, col]) <: AbstractFloat) + df[!, col] = parse.(Float64, string.(df[!, col])) + end + end + + # fill in ranks ONLY if these columns do not already exist + if !("signedQuantile" in names(df)) #|| !(:Ranks in names(df)) + st_ecdf = ecdf(df.Stability) + quantile = st_ecdf.(df.Stability) + df.signedQuantiles = quantile .* sign.(df.Correlation) + end + push!(allDfs, df) + end + + # ──── 2. CONCATENATE AND AGGREGATE ACCORDING TO combineOpt ─────────────────────────── + # Combine all dataframes vertically + combinedDf = vcat(allDfs...) + # Group by TF and Gene and aggregate according to the chosen method. + groupedDf = groupby(combinedDf, ["TF", "Gene"]) + aggRows = NamedTuple[] + + for g in groupedDf + # All rows in this group share the same TF, Gene and inPrior + tf, gene, dash = g.TF[1], g.Gene[1], g.inPrior[1] + # tf, gene, dash = g.TF[1], g.Gene[1], g.strokeDashArray[1] + + if combineOpt == "mean" + c = mean(g.Correlation) + push!(aggRows, (TF = tf, Gene = gene, Stability = mean(g.Stability), + Correlation = c, + inPrior = dash)) + + elseif combineOpt == "max" + # idx = combineOpt == "max" ? argmax(g.Stability) : argmin(g.Stability) + idx = pickRow(g, "Stability", order = :max) + row = g[idx, :] + push!(aggRows, (TF = tf, Gene = gene, signedQuantile = row.signedQuantile, Stability = row.Stability, + Correlation = row.Correlation, inPrior = row.inPrior)) + + elseif combineOpt == "min" + # idx = combineOpt == "max" ? argmax(g.Stability) : argmin(g.Stability) + idx = pickRow(g, "Stability", order = :min) + row = g[idx, :] + push!(aggRows, (TF = tf, Gene = gene, Stability = row.Stability, + Correlation = row.Correlation, inPrior = row.inPrior)) + + elseif combineOpt == "meanQuantile" + c = mean(g.Correlation) + push!(aggRows, (TF = tf, Gene = gene, signedQuantile = mean(g.signedQuantile), Stability = mean(g.Stability), + Correlation = c, + inPrior = dash)) + + elseif combineOpt == "maxQuantile" + # idx = argmax(g.signedQuantile) + idx = pickRow(g, "signedQuantile", order = :max) + row = g[idx, :] + push!(aggRows, (TF = tf, Gene = gene, signedQuantile = row.signedQuantile, Stability = row.Stability, Correlation = row.Correlation, + inPrior = row.inPrior)) + else + error("Invalid combineOpt: $(combineOpt).") + end + end + + aggregatedDf = DataFrame(aggRows) + + + # ────── 3. Filter out extrememly weak correlations and compute new signed quantiles using broadcasting ────────────────────── + pcut = 0.01 # Correlation cutoff + aggregatedDf = filter(:Correlation => x -> abs(x) > pcut, aggregatedDf) + # ── 3b. Compute signedQuantile ───────────────── + if combineOpt in ("max","min","mean") + stability = aggregatedDf.Stability # Vector + F = ecdf(stability) # F(x) = proportion ≤ x + signed_q = F.(stability) .* sign.(aggregatedDf.Correlation) + aggregatedDf[!,:signedQuantile] = signed_q + end + + # Checks: + # abs_q = abs.(aggregatedDf.signedQuantile) + # i_min = argmin(abs_q) + # row_min = aggregatedDf[i_min, :] + + # sort for reproducibility + sort!(aggregatedDf, :signedQuantile, by= abs, rev = true) + # sort!(aggregatedDf, combineOpt in ("meanQuantile", "maxQuantile") ? :signedQuantile : :Stability, + # by = combineOpt in ("meanQuantile", "maxQuantile") ? abs : identity, rev = true) + + # ────── 5. choose top meanEdgesPerGene * nGenes edges ────────────────────── + + if useMeanEdgesPerGeneMode + println("Selecting the top (meanEdgesPerGene * unique targets) edges") + # Select the top `topEdgeCount = meanEdgesPerGene * uniqueGenes` & compute quantiles for all rankings + uniqueGenes = length(unique(aggregatedDf.Gene)) # current number of unique targets + topEdgeCount = round(Int, meanEdgesPerGene * uniqueGenes) # Compute the number of edges to select + selectionIndices = 1:min(topEdgeCount, nrow(aggregatedDf)) + else + println("Selecting the top meanEdgesPerGene edges per target gene") + selectionIndices = firstNByGroup(aggregatedDf.Gene, meanEdgesPerGene) + end + selectedDf = aggregatedDf[selectionIndices, :] + println("Selected $(length(selectionIndices)) edges using meanEdgesPerGene = $meanEdgesPerGene.") + + # # ────── 6. Generate strokeWidth and colors + # # ────── Color calculations for JP-Gene-Viz + # minRank, maxRank = extrema(selectedDf.Stability) + # rankRange = max(maxRank - minRank, eps()) # prevent division by zero + # strokeWidth = 1 .+ (selectedDf.Stability .- minRank) ./ rankRange + # # Color mapping + # medRed = [228, 26, 28] + # lightGrey = [217, 217, 217] + # strokeVals = map(abs.(selectedDf.Correlation)) do corr + # color = corr * medRed .+ (1 - corr) * lightGrey + # "rgb($(floor(Int, round(color[1]))),$(floor(Int, round(color[2]))),$(floor(Int, round(color[3]))))" + # # "rgb(" * string(floor(Int, round(color[1]))) * "," * string(floor(Int, round(color[2]))) * "," * string(floor(Int, round(color[3]))) * ")" + # end + # selectedDf[!, :strokeVals] = strokeVals + # selectedDf[!, :strokeWidth] = strokeWidth + + + # ── 7. save to disk if saveDir is provided ────────────────────────────────────────── + if saveDir !== nothing + mkpath(saveDir) + tag = isnothing(saveName) || isempty(saveName) ? combineOpt : "$(saveName)_$(combineOpt)" + # CSV.write(joinpath(saveDir, "$(tag)_aggregated.tsv"), aggregatedDf; delim = '\t') + CSV.write(joinpath(saveDir, "combined_$(tag).tsv"), selectedDf; delim = '\t') + + wideDf = convertToWide(selectedDf[ :,["TF","Gene","signedQuantile"]]; indices=(2,1,3)) + wideDf = coalesce.(wideDf, 0) + wideFile = joinpath(saveDir, "combined_$(tag)_sp.tsv") + writeTSVWithEmptyFirstHeader(wideDf, wideFile; delim = '\t') + println("Files written under ", saveDir) + end + + return selectedDf +end + +# ----------------------- +# main procedure +# ----------------------- +# This section executes if the script is run directly. It will not execute if called/imported into another script. +if abspath(PROGRAM_FILE) == @__FILE__ + using ArgParse + + # Define argument parser + s = ArgParseSettings() + @add_arg_table s begin + "--combineOpt" + help = "Combination option: max, min, or mean (default: mean)" + arg_type = String + default = "mean" + "--meanEdgesPerGene" + help = "Mean number of edges per unique gene." + arg_type = Int + required = true + "--useMeanEdgesPerGeneMode" + help = "controls whether edge selection is done per-group or globally. If true, selects length(unique(targs)) * meanEdgesPerGene edges. + If `false`, selects selects the top meanEdgesPerGene edges per target gene." + arg_type = Bool + default = true + "--saveDir" + help = "Directory in which to save output files." + arg_type = String + default = "" + "--saveName" + help = "Base name for the saved file." + arg_type = String + default = "" + "files..." + help = "List of GRN files (TSV format) to combine." + end + + parsedArgs = parse_args(s) + fileList = parsedArgs["files"] + combineOpt = parsedArgs["combineOpt"] + meanEdgesPerGene = parsedArgs["meanEdgesPerGene"] + useMeanEdgesPerGeneMode = parsedArgs["useMeanEdgesPerGeneMode"] + saveDir = isempty(parsedArgs["saveDir"]) ? nothing : parsedArgs["saveDir"] + saveName = parsedArgs["saveName"] + + @printf("Combining %d files with combination option: %s\n", length(fileList), combineOpt) + + selectedDf = combineGRNs(fileList; combineOpt=combineOpt, meanEdgesPerGene=meanEdgesPerGene, saveDir=saveDir, saveName=saveName) + + # Optionally, print summary + @printf("Final network has %d edges spanning %d unique genes.\n", nrow(selectedDf), length(unique(selectedDf.Gene))) +end + diff --git a/src/02_GRN/combineGRN2.jl b/src/02_GRN/combineGRN2.jl new file mode 100755 index 0000000..8cbd8bc --- /dev/null +++ b/src/02_GRN/combineGRN2.jl @@ -0,0 +1,89 @@ +""" +GRN + +Main function: +- `combineGRNS2`: Combines GRNs using merged TFs, TFA data, and other metadata. + +Dependencies: +- `Data.GeneExpressionData` and `Data.PriorTFAData` +- `Prior.mergeDegenerateTFs` +""" + # Access dependencies from the package; no need to include files again + # using ..Data # For GeneExpressionData + # using ..PriorTFA # For PriorTFAData + # using ..MergeDegenerate + + # # Other standard packages + # using PyPlot + # using Statistics + # using CSV + # using DelimitedFiles + # using JLD2 + # using NamedArrays + + # Export only the function users need + # export combineGRNS2 + +function combineGRNS2(data::GeneExpressionData, mergedTFsData::mergedTFsResult, tfaGeneFile::Union{String, Nothing}, priorFile, edgeSS, minTargets, + geneExprFile::Union{String, Nothing}=nothing, + targetGeneFile::Union{String, Nothing}=nothing, + potRegFile::Union{String, Nothing}=nothing; + outputDir::Union{String, Nothing}=nothing) + if !isnothing(outputDir) + mkpath(outputDir) + end + + # Ensure required expression data are available and non-empty + requiredFields = [ + :tfaGenes, + :tfaGeneMat, + :potRegs, + :potRegsmRNA, + :potRegMatmRNA + ] + + missingFields = [field for field in requiredFields if isempty(getfield(data, field))] + + if !isempty(missingFields) + println("Missing or empty fields in data: ", missingFields) + println("Generating required data by loading from input files...") + + requiredFiles = [ + ("Gene Expression File", geneExprFile), + ("Target Gene File", targetGeneFile), + ("Potential Regulators File", potRegFile), + ] + + missingFiles = [name for (name, path) in requiredFiles if path === nothing || !isfile(path)] + if !isempty(missingFiles) + error("Cannot generate data. Missing input files: ", missingFiles) + end + + data = GeneExpressionData() + loadExpressionData!(data, geneExprFile) + loadAndFilterTargetGenes!(data, targetGeneFile; epsilon=0.01) + loadPotentialRegulators!(data, potRegFile) + processTFAGenes!(data, tfaGeneFile; outputDir = outputDir) + end + + + # 2. Integrate prior information for TFA estimation + tfaData = PriorTFAData() + processPriorFile!(tfaData, data, priorFile; mergedTFsData, minTargets = minTargets); + calculateTFA!(tfaData, data; edgeSS = edgeSS, outputDir = outputDir); + + # Save TFA as a text file# Save median TFA if outputDir is specified + if !isnothing(outputDir) + namedMedTFA = NamedArray(tfaData.medTfas) + setnames!(namedMedTFA, tfaData.pRegs, 1) + setnames!(namedMedTFA, data.cellLabels, 2) + + outputfile = joinpath(outputDir, "TFA.txt") + open(outputfile, "w") do io + writedlm(io, permutedims(data.cellLabels)) + writedlm(io, namedMedTFA) + end + end + + return tfaData +end diff --git a/src/02_GRN/prepareGRN.jl b/src/02_GRN/prepareGRN.jl new file mode 100755 index 0000000..9abf2a8 --- /dev/null +++ b/src/02_GRN/prepareGRN.jl @@ -0,0 +1,451 @@ +""" +GRN + +Functions: +- preparePredictorMat! +- preparePenaltyMatrix! +- constructSubsamples +- bstarsWarmStart +- bstartsEstimateInstability + +Dependencies: +""" + +# mutable struct GrnData +# predictorMat::Matrix{Float64} +# penaltyMat::Matrix{Float64} +# allPredictors::Vector{String} +# subsamps::Matrix{Int64} +# responseMat::Matrix{Float64} +# maxLambdaNet::Float64 +# minLambdaNet::Float64 +# minLambdas::Matrix{Float64} +# maxLambdas::Matrix{Float64} +# netInstabilitiesUb::Vector{Float64} +# netInstabilitiesLb::Vector{Float64} +# instabilitiesUb::Matrix{Float64} +# instabilitiesLb::Matrix{Float64} +# netInstabilities::Vector{Float64} +# geneInstabilities::Matrix{Float64} +# lambdaRange::Vector{Float64} +# lambdaRangeGene::Vector{Vector{Float64}} +# stabilityMat::Array{Float64} +# priorMatProcessed::Matrix{Float64} +# betas::Array{Float64,3} +# function GrnData() +# return new( +# Matrix{Float64}(undef, 0, 0), # predictorMat +# Matrix{Float64}(undef, 0, 0), # penaltyMat +# [], # allPredictors +# Matrix{Int64}(undef, 0, 0), # subsamps +# Matrix{Int64}(undef, 0, 0), # responseMat +# 0.0, # maxLambdasNet +# 0.0, # minLambdasNet +# Matrix{Int64}(undef, 0, 0), # minLambdas +# Matrix{Int64}(undef, 0, 0), # maxLambdas +# [], # netInstabilitiesUb +# [], # netInstabilitiesLb +# Matrix{Int64}(undef, 0, 0), # instabilitiesUb +# Matrix{Int64}(undef, 0, 0), # instabilitiesLb +# [], # netInstabilities +# Matrix{Int64}(undef, 0, 0), # geneInstabilities +# [], # lambdaRange +# Vector{Vector{Float64}}(undef, 0), # lambdaRangesGene +# Matrix{Int64}(undef, 0, 0), # stabilityMat +# Matrix{Float64}(undef, 0, 0), # priorMatProcessed +# Array{Float64,3}(undef, 0, 0, 0) # betas +# ) +# end +# end + +function preparePredictorMat!(grnData::GrnData, expressionData::GeneExpressionData, priorData::PriorTFAData, tfaOpt::String) + if tfaOpt != "" + println("noTfa option") + pRegs = priorData.pRegsNoTfa; + pTargs = priorData.pTargsNoTfa; + priorMatrix = priorData.priorMatrixNoTfa; + else + pRegs = priorData.pRegs; + pTargs = priorData.pTargs; + priorMatrix = priorData.priorMatrix; + end + # TFs in potRegs that have expression data that arent in prior. Get the index for these TFs + # in the TF expression matrix + uniNoPriorRegs = setdiff(expressionData.potRegsmRNA, pRegs) + uniNoPriorRegInds = findall(in(uniNoPriorRegs), expressionData.potRegsmRNA) + + # allPredictors include both the pRegs and the potRegs that wernt in prior + allPredictors = vcat(pRegs, uniNoPriorRegs) + totPreds = length(allPredictors) + + # Create new prior matrix that contains target genes in the same order as targGenes. If using + # TFA, missing TFs not in prior that we have expression data for + targGeneInds = findall(in(pTargs), expressionData.targGenes) + priorGeneInds = findall(in(expressionData.targGenes), pTargs) + totTargGenes = length(expressionData.targGenes) + totPRegs = length(pRegs) + priorMat = zeros(totTargGenes,totPreds) + priorMat[targGeneInds,1:totPRegs] = priorMatrix[priorGeneInds,:] + + # # predictorMat will be TFA (when available) and TFmRNA when TFA not available + # predictorMat = [priorData.medTfas; expressionData.potRegMatmRNA[uniNoPriorRegInds,:]] + # # If not using TFA, just set predictorMat to mRNA + # if tfaOpt != "" # use the mRNA levels of TFs + # currPredMat = zeros(totPreds,length(expressionData.cellLabels)) + # for prend = 1:totPreds + # prendInd = findall(x -> x==allPredictors[prend],expressionData.potRegsmRNA) + # currPredMat[prend,:] = expressionData.potRegMatmRNA[prendInd,:] + # end + # predictorMat = currPredMat + # println("TF mRNA used.") + # end + + # predictorMat will be TFA (when available) and TFmRNA when TFA not available + if tfaOpt == "" + # TFA: stack TFA estimates on top of mRNA for TFs not in prior + predictorMat = [priorData.medTfas; expressionData.potRegMatmRNA[uniNoPriorRegInds,:]] + else + # If not using TFA: use mRNA for all predictors + currPredMat = zeros(totPreds, length(expressionData.cellLabels)) + for prend = 1:totPreds + prendInd = findall(x -> x == allPredictors[prend], expressionData.potRegsmRNA) + currPredMat[prend, :] = expressionData.potRegMatmRNA[prendInd, :] + end + predictorMat = currPredMat + println("TF mRNA used.") + end + grnData.predictorMat = predictorMat + grnData.allPredictors = allPredictors + grnData.responseMat = expressionData.targGeneMat + grnData.priorMatProcessed = priorMat +end + + +function preparePenaltyMatrix!(expressionData::GeneExpressionData, grnData::GrnData, priorFilePenalties::Vector{String}, lambdaBias::Vector{Float64}, tfaOpt::String) + #1. Update Penalty Matrix + # Create a dictionary to store the minimum lambda for each interaction + minLambdaDict = Dict{Tuple{String,String}, Float64}() + penaltyMatrix = ones(length(expressionData.targGenes),length(grnData.allPredictors)) + + # Iterate through each prior file and its associated lambda + for (filePath, lambda) in zip(priorFilePenalties, lambdaBias) + # Read the prior file + priorData = readdlm(filePath) + # priorData = CSV.read(filePath, DataFrame; delim = "\t") + + # Extract gene names and TF names from the prior file + priorGenes = priorData[2:end, 1] + # Get TF names, filtering out any empty or missing entries + priorTFs = filter(tf -> !ismissing(tf) && !isempty(string(tf)), priorData[1, :]) + + # Create indices mapping for faster lookup + geneToIdx = Dict(gene => i for (i, gene) in enumerate(expressionData.targGenes)) + tfToIdx = Dict(tf => i for (i, tf) in enumerate(grnData.allPredictors)) + # Process the interactions + for (i, gene) in enumerate(priorGenes) + for (j, tf) in enumerate(priorTFs) + # println(" Trying ($gene, $tf): ") + if priorData[i+1, j+1] != 0 && haskey(geneToIdx, gene) && haskey(tfToIdx, tf) + interaction = (gene, tf) + if !haskey(minLambdaDict, interaction) || lambda < minLambdaDict[interaction] + minLambdaDict[interaction] = lambda + end + end + end + end + end + + # Apply the penalties using the minimum lambda values + for ((gene, tf), minLambda) in minLambdaDict + geneIdx = findfirst(==(gene), expressionData.targGenes) + tfIdx = findfirst(==(tf), grnData.allPredictors) + penaltyMatrix[geneIdx, tfIdx] = minLambda + end + + # 2. + totPreds = length(grnData.allPredictors) + if tfaOpt !== "" + ## set lambda penalty to infinity for positive feedback edges where TF + # mRNA levels serves both as gene expression and TFA estimate + for pr = 1:totPreds # Changed this from length(expressionData.potRegs) to length(grnData.allPredictors) + targInd = findall(x -> x==grnData.allPredictors[pr], expressionData.targGenes) + if length(targInd) > 0 # set lambda penalty to infinity, avoid predicting a TF's mRNA based on its own mRNA level + penaltyMatrix[targInd,pr] .= Inf # i.e., target gene is its own predictor + end + end + else # have to set prior inds to zero for TFs in TFA that don't have prior info + for pr = 1:totPreds # Changed this from expressionData.potRegs to grnData.allPredictors + if sum(abs.(grnData.priorMatProcessed[:,pr])) == 0 # we have no target edges to estimate TF's TFA + targInd = findall(x -> x==grnData.allPredictors[pr], expressionData.targGenes) + if length(targInd) > 0 # And TF is in the predictor set + penaltyMatrix[targInd,pr] .= Inf + end + end + end + end + + grnData.penaltyMat = penaltyMatrix +end + + +function constructSubsamples(expressionData::GeneExpressionData, grnData::GrnData; + leaveOutSampleList::Union{Vector{Vector{String}}, Nothing}=nothing,totSS = 200, subsampleFrac = 0.63) + totSamps = length(expressionData.cellLabels) + + if !(leaveOutSampleList in (nothing, "")) + println("Leave-out set detected: ", leaveOutSampleList) + # get leave-out set of samples + fin = open(leaveOutSampleList) + C = readdlm(fin,skipstart=0) + C = convert(Matrix{String}, C) + close(fin) + testInds = Tuple.(findall(in(C), expressionData.cellLabels)) + testInds = first.(testInds) + trainInds = setdiff(1:totSamps,testInds) + else + println("Full gene expression matrix used.") + trainInds = 1:totSamps # all training samples used + testInds = [] + end + + subsampleSize = floor(subsampleFrac*length(trainInds)) + subsampleSize = convert(Int64, subsampleSize) + # get subsamp indices + subsamps = zeros(totSS,subsampleSize) + for ss = 1:totSS + randSubs = randperm(totSamps) + randSubs = randSubs[1:subsampleSize] + subsamps[ss,:] = randSubs + end + subsamps = convert(Matrix{Int}, subsamps) + grnData.subsamps = subsamps +end + +function bstarsWarmStart(expressionData::GeneExpressionData, tfaData::PriorTFAData, grnData::GrnData; minLambda = 0.01, maxLambda = 0.5, totLambdasBstars = 20, totSS = 5, targetInstability = 0.05, zTarget = false) + # Determine the lambda levels to test + lambdaRange = collect(range(minLambda, stop = maxLambda, length = totLambdasBstars)) + #lamLog10step = 1/totLambdasBstars + #logLamRange = log10(minLambda):lamLog10step:log10(maxLambda) + #lambdaRange = 10 .^ (logLamRange) + lambdaRange = reverse(lambdaRange) + totResponses = size(grnData.responseMat)[1] + + instabilitiesLb = zeros(totResponses,totLambdasBstars) + instabilitiesUb = zeros(totResponses,totLambdasBstars) + minLambdas = zeros(totResponses,1) + maxLambdas = zeros(totResponses,1) + + responsePredInds = Vector{Vector{Int}}(undef,0) + for res = 1:totResponses + currWeights = grnData.penaltyMat[res,:] + push!(responsePredInds,findall(x -> x!=Inf, currWeights)) + end + + netInstabilitiesLb = zeros(totResponses, totLambdasBstars) + netInstabilitiesUb = zeros(totResponses, totLambdasBstars) + totEdges = zeros(totResponses) # denominator for network Instabilities + + theta2save = Vector{Matrix{Float64}}(undef, totResponses) # Debugging # + + Threads.@threads for res in ProgressBar(1:totResponses) # can be a parfor loop + predInds = responsePredInds[res] + currPredNum = length(predInds) + penaltyFactor = grnData.penaltyMat[res, predInds] + totEdges[res] = currPredNum + ssVals = zeros(totLambdasBstars,currPredNum) + for ss = 1:totSS + subsamp = grnData.subsamps[ss,:] + dt = fit(ZScoreTransform, grnData.predictorMat[predInds, subsamp], dims=2) + currPreds = transpose(StatsBase.transform(dt, grnData.predictorMat[predInds, subsamp])) + if zTarget + dt = fit(ZScoreTransform, grnData.responseMat[res, subsamp], dims=1) + currResponses = StatsBase.transform(dt, grnData.responseMat[res, subsamp]) + else + currResponses = grnData.responseMat[res, subsamp] + end + lsoln = glmnet(currPreds, currResponses, penalty_factor = penaltyFactor, lambda = lambdaRange, alpha = 1.0) + currBetas = lsoln.betas # flip so that the lambdas are increasing + # ssVals += abs.(sign.(currBetas))' + ssVals .+= abs.(sign.(currBetas))' + end + theta2 = (1/totSS)*ssVals # empirical edge probability + theta2save[res] = theta2 #For Debugging # + instabilitiesLb[res,:] = 2 * (1/currPredNum) .* sum(theta2 .* (1 .- theta2), dims=2) # bStARS lower bound + netInstabilitiesLb[res,:] = currPredNum*(instabilitiesLb[res,:]) + theta2mean = sum(theta2,dims=2)./currPredNum + instabilitiesUb[res,:] = 2 * theta2mean .* (1 .- theta2mean) # bStARS upper bound + netInstabilitiesUb[res,:] = currPredNum*instabilitiesUb[res,:] + end + + totEdges = sum(totEdges) + netInstabilitiesLb = sum(netInstabilitiesLb, dims=1)[:] + netInstabilitiesUb = sum(netInstabilitiesUb, dims=1)[:] + + for res = 1:totResponses + # take the supremum, find max Lambda, and set all smaller lambdas equal to that value + maxLb = findmax(instabilitiesLb[res,:]) + maxLbInd = findall(x -> x == maxLb[1], instabilitiesLb[res,:]) + maxLb = maxLb[1] + instabilitiesLb[res,maxLbInd[end]:end] .= maxLb + maxUb = findmax(instabilitiesUb[res,:]) + maxUbInd = findall(x -> x == maxUb[1], instabilitiesUb[res,:]) + maxUb = maxUb[1] + instabilitiesUb[res,maxUbInd[end]:end] .= maxUb + # find the minimum lambda for the gene, based on maximum for upper bound + # we are less interested in high instability lambdas, so okay to use + # upper bound + xx = findmin(abs.(instabilitiesLb[res,:] .- targetInstability)) + #xx = findall(x -> x == xx[1],abs.(instabilitiesLb[res,:] .- targetMaxInstability)) + xx = xx[2] + minLambdas[res] = (lambdaRange)[xx[end]] # to the right + # find the lambda nearest the min instability worth considering, use + # upper bound as that will be sure to find an lambda >= target instability lambda + xx = findmin(abs.(instabilitiesUb[res,:] .- targetInstability)) + xx = findall(x -> x == xx[1], abs.(instabilitiesUb[res,:] .- targetInstability)) + maxLambdas[res] = (lambdaRange)[xx[end]] # to the right + # note for typical bStARS, where you know what instability cutoff you + # want you'd use the upperbound to find the min lambda and the lb to + # find the max lambda + end + + netInstabilitiesUb = netInstabilitiesUb ./ totEdges + netInstabilitiesLb = netInstabilitiesLb ./ totEdges + maxLb = findmax(netInstabilitiesLb) + maxLbInd = findall(x -> x == maxLb[1], netInstabilitiesLb) + netInstabilitiesLb[maxLbInd[end]:end] .= maxLb[1] # take supremum for lambdas smaller than instability max + maxUb = findmax(netInstabilitiesUb) + maxUbInd = (findall(x -> x == maxUb[1], netInstabilitiesUb)) + netInstabilitiesUb[maxUbInd[end]:end] .= maxUb[1] + xx = findmin(abs.(netInstabilitiesLb .- targetInstability)) + maxInstInd = findall(x -> x == xx[1], abs.(netInstabilitiesLb .- targetInstability)) + minLambdaNet = (lambdaRange)[maxInstInd[end]] + xx = findmin(abs.(netInstabilitiesUb .- targetInstability)) + minInstInd = findall(x -> x == xx[1], abs.(netInstabilitiesUb .- targetInstability)) + maxLambdaNet = (lambdaRange)[minInstInd[end]] + + grnData.minLambdaNet = minLambdaNet + grnData.maxLambdaNet = maxLambdaNet + grnData.maxLambdas = maxLambdas + grnData.minLambdas = minLambdas + grnData.netInstabilitiesLb = netInstabilitiesLb + grnData.netInstabilitiesUb = netInstabilitiesUb + grnData.instabilitiesLb = instabilitiesLb + grnData.instabilitiesUb = instabilitiesUb +end + +function bstartsEstimateInstability(grnData::GrnData; totLambdas = 10, instabilityLevel = "Gene", zTarget = false, outputDir::Union{String, Nothing}=nothing) + + totResponses,totSamps = size(grnData.responseMat) # totResponses is same as length(grnData["targGenes"]) + totPreds = size(grnData.predictorMat,1) + + # if instabilityLevel == "Gene" + # minLambda = minimum(grnData.minLambdas) + # maxLambda = maximum(grnData.maxLambdas) + # elseif instabilityLevel == "Network" + # minLambda = grnData.minLambdaNet + # maxLambda = grnData.maxLambdaNet + # else + # println("Use either 'Gene' or 'Network' for instabilityLevel") + # end + + # Gene Level Instabilities + lambdaRangeGene = Vector{Vector{Float64}}(undef, totResponses) + for res in 1:totResponses + λmin = grnData.minLambdas[res] + λmax = grnData.maxLambdas[res] + lambdaRangeGene[res] = reverse(collect(range(λmin, stop=λmax, length=totLambdas))) + end + grnData.lambdaRangeGene = lambdaRangeGene + + # Network Level Instability + minLambda = grnData.minLambdaNet + maxLambda = grnData.maxLambdaNet + + lambdaRange = collect(range(minLambda, stop = maxLambda, length = totLambdas)) + lambdaRange = reverse(lambdaRange) + grnData.lambdaRange = lambdaRange + + geneInstabilities = zeros(totResponses,totLambdas) + netInstabilities = zeros(totLambdas,1) + totEdges = 0 # denominator for network Instabilities + # store number of subsamples for which an edge was nonzero, given that some + # prior weights can be set to infinity, track to make sure these edges are + # not counted + ssMatrix = Inf*ones(totLambdas,totResponses,totPreds) + subsamps = grnData.subsamps + totSS = size(subsamps)[1] + + # get (finite) predictor indices for each response + responsePredInds = Vector{Vector{Int}}(undef,0) + for res = 1:totResponses + currWeights = grnData.penaltyMat[res,:] + push!(responsePredInds,findall(x -> x!=Inf, currWeights)) + end + + totEdges = zeros(totResponses) + betas = Array{Float64,3}(undef, totResponses, totPreds, totLambdas) + Threads.@threads for res in ProgressBar(1:totResponses) + lambdaRange = instabilityLevel == "Network" ? + grnData.lambdaRange : # single shared vector + grnData.lambdaRangeGene[res] # per‑gene vector + predInds = responsePredInds[res] + currPredNum = length(predInds) + totEdges[res] = currPredNum + penaltyFactor = grnData.penaltyMat[res,predInds] + ssVals = zeros(totLambdas,currPredNum) + for ss = 1:totSS + subsamp = subsamps[ss,:] + dt = fit(ZScoreTransform, grnData.predictorMat[predInds, subsamp], dims=2) + currPreds = transpose(StatsBase.transform(dt, grnData.predictorMat[predInds, subsamp])) + if zTarget + dt = fit(ZScoreTransform, grnData.responseMat[res, subsamp], dims=1) + currResponses = StatsBase.transform(dt, grnData.responseMat[res, subsamp]) + else + currResponses = grnData.responseMat[res, subsamp] + end + lsoln = glmnet(currPreds, currResponses, penalty_factor = penaltyFactor, lambda = lambdaRange, alpha = 1.0) + currBetas = lsoln.betas + betas[res,predInds, :] = currBetas + ssVals = ssVals + abs.(sign.(currBetas))' + end + ssMatrix[:,res,predInds] = ssVals + end + + for res = 1:totResponses + currWeights = grnData.penaltyMat[res,:] + predInds = responsePredInds[res] + currPredNum = length(predInds) + ssVals = zeros(totLambdas,currPredNum) + ssVals[:,:] = ssMatrix[:,res,predInds] + theta2 = (1/totSS)*ssVals # empirical edge probability, lambdas X currPreds + instabilitiesPerEdge = 2*(theta2 .* (1 .- theta2)) + aveInstabilities = mean(instabilitiesPerEdge,dims=2) # lambdas X 1 + maxUb = (findmax(aveInstabilities)) + instabSUP = aveInstabilities + maxUbInd = first(Tuple(maxUb[2])) + instabSUP[maxUbInd:end,1] .= maxUb[1] + geneInstabilities[res,:] = instabSUP + end + + ## calculate instabilities network-wise + currSS = zeros(totResponses,totPreds) + instabRange = zeros(totLambdas,1) + for lind = 1:totLambdas # start at highest lambda (lowest instability and work down) + currSS[:,:] = ssMatrix[lind,:,:] + theta2 = (1/totSS)*currSS # empirical edge probability, responses X currPreds + instabilitiesPerEdge = 2*(theta2 .* (1 .- theta2)) + instabVec = instabilitiesPerEdge[:] + validEdges = findall(isfinite.(currSS[:])) # limit to finite edges + instabMax = findmax(instabRange)[1] + netInstabilities[lind] = max(mean(instabVec[validEdges]),max(instabMax)) + end + grnData.netInstabilities = vec(netInstabilities) + grnData.geneInstabilities = geneInstabilities + grnData.stabilityMat = ssMatrix + grnData.betas = betas + + if outputDir !== nothing && outputDir !== "" + outputFile = joinpath(outputDir, "instabOutMat.jld") + save_object(outputFile, grnData) + end +end diff --git a/src/02_GRN/utilsGRN.jl b/src/02_GRN/utilsGRN.jl new file mode 100755 index 0000000..548ef29 --- /dev/null +++ b/src/02_GRN/utilsGRN.jl @@ -0,0 +1,54 @@ +function firstNByGroup(vect::AbstractVector, N::Integer) + """ + firstNByGroupIndices(vec::AbstractVector, N::Integer) + + Return the indices of the first `N` occurrences of each unique element in `vec`. + + This is useful when you want to subset another array based on limited occurrences per group. + + # Arguments + - `vec`: A vector of values to group by (e.g., transcription factors). + - `N`: The maximum number of elements to select per unique group. + + # Returns + - A vector of indices corresponding to the first `N` entries per group. + """ + seen = Dict{eltype(vect), Int}() + idxs = Int[] + for (i, v) in enumerate(vect) + seen[v] = get(seen, v, 0) + 1 + if seen[v] <= N + push!(idxs, i) + end + end + return idxs +end + + + +# function firstNByGroup(vect::AbstractVector, N::Integer) +# """ +# firstNByGroup(vect::AbstractVector, N::Integer) + +# Return a boolean mask selecting the first `N` occurrences of each unique element in `vec`. + +# This is useful for logical indexing when subsetting arrays with the same length as `vec`. + +# # Arguments +# - `vect`: A vector of values to group by. +# - `N`: The maximum number of elements to select per group. + +# # Returns +# - A boolean mask vector with `true` at positions of the first `N` occurrences per group. +# """ +# seen = Dict{eltype(vect), Int}() +# mask = falses(length(vect)) +# for i in eachindex(vect) +# seen[vect[i]] = get(seen, vect[i], 0) + 1 +# mask[i] = seen[vect[i]] <= N +# end +# return mask +# end + + + diff --git a/src/03_Metrics/.DS_Store b/src/03_Metrics/.DS_Store new file mode 100755 index 0000000000000000000000000000000000000000..c347785244c62b7383202e06992ce1e65da2a73a GIT binary patch literal 6148 zcmeHK%}N6?5T3CW7QFQ6F;|a$gSD(r&=-*HLKRZ8V7=!}JPP^}-h3!8ev=uCOLGtp zA~LfiUowB$e6Yz55%KC_SQ0IWs6i8C8B|2nRnv(%p8~n$*wZcD(RR4s#7d&SsFHVY zXrxGq_IM8O-=`F@sbgc@F4vp18+A8Nk2RlN>bgFrUTp8Pi|66x?XvDSe9LdV`MTyS zQn@$-&VV!E3^)TnWk9YsNOPv>y))npI0FX;%gayJH3e-@x5`#4y`e1&oVOG>|Vk isfinite(x), allRecalls) + allFprs = reduce(vcat, filter(!isempty, gsFprsByTf)) + allFprs = filter(x -> isfinite(x), allFprs) + + maxRecall = isempty(allRecalls) ? 0.0 : maximum(allRecalls) + maxFpr = isempty(allFprs) ? 0.0 : maximum(allFprs) + + recStep = adaptiveStep(gsRecallsByTf; target_points=target_points, min_step=min_step, method=step_method) + fprStep = adaptiveStep(gsFprsByTf; target_points=target_points, min_step=min_step, method=step_method) + recInterpPts = 0.0:recStep:maxRecall # Interpolation points + fprInterpPts = 0.0:fprStep:maxFpr + + macroPrecisions = zeros(length(recInterpPts)) + macroTprs = zeros(length(fprInterpPts)) + nTFs = length(gsPrecisionsByTf) + validTFs = [i for i in 1:nTFs if !isempty(gsRecallsByTf[i]) && maximum(gsRecallsByTf[i]) > 0] + for i in validTFs + # PR Curve + interpPrec = dedupNInterpolate(gsRecallsByTf[i], gsPrecisionsByTf[i], recInterpPts) + macroPrecisions .+= interpPrec + # ROC Curve # This is needed for ROC curves. Here Tprs != Recall but interpolated Recall on Fprs + interpTpr = dedupNInterpolate(gsFprsByTf[i], gsRecallsByTf[i], fprInterpPts) + macroTprs .+= interpTpr + end + + macroPrecisions ./= length(validTFs) + macroTprs ./= length(validTFs) + macroAUPR_interpolated = sum(0.5 .* (macroPrecisions[2:end] + macroPrecisions[1:end-1]) .* diff(recInterpPts)) + macroAUROC_interpolated = sum(0.5 .* (macroTprs[2:end] + macroTprs[1:end-1]) .* diff(fprInterpPts)) + + macroResults = OrderedDict( + :macroPR => OrderedDict( + # :auprScalar => macroAUPR_scalar, + :auprInterpolated => macroAUPR_interpolated, + :precisions => macroPrecisions, + :recalls => collect(recInterpPts) + ), + :macroROC => OrderedDict( + # :aurocScalar => macroAUROC_scalar, + :aurocInterpolated => macroAUROC_interpolated, + :fprs => collect(fprInterpPts), + :tprs => macroTprs + ) +) + return macroResults +end + +""" + evaluatePerTF(regs, targs, rankings, infEdges, gsEdgesByTf, uniGsRegs, gsRandPRbyTf; + saveDir=nothing, breakTies=true, target_points=1000, min_step=1e-4, + step_method=:min_gap, xLimitRecall=1.0) + +Compute per-TF precision-recall and ROC metrics for an inferred gene regulatory network (GRN) +against a gold standard, and optionally plot per-TF PR/ROC curves. + +# Arguments +- `regs::Vector{<:AbstractString}` : Regulators for each inferred edge. +- `targs::Vector{<:AbstractString}` : Target genes for each inferred edge. +- `rankings::Vector{Float64}` : Confidence scores for inferred edges. +- `infEdges::Vector{String}` : Inferred edges as `"TF,Target"` strings. +- `gsEdgesByTf::Vector{Array{String}}` : Gold standard edges grouped by TF. +- `uniGsRegs::Vector{<:AbstractString}` : Unique TFs in the gold standard. +- `gsRandPRbyTf::Vector{Float64}` : Random baseline AUPR per TF. + +# Keyword Arguments +- `saveDir::Union{String,Nothing}=nothing` : Directory to save per-TF plots. If `nothing`, plots are skipped. +- `breakTies::Bool=true` : Average indicators over tied rankings to smooth PR curves. +- `target_points::Int=1000` : Interpolation points for macro curves. +- `min_step::Float64=1e-4` : Minimum step size for interpolation. +- `step_method::Symbol=:min_gap` : Step selection method for macro metrics. +- `xLimitRecall::Float64=1.0` : : Maximum recall shown on the x-axis of diagnostic per-TF PR plots. + +# Returns +An `OrderedDict` with: +- `:perTF` → `OrderedDict` with `:gsRegs`, `:tfEdges`, `:tfEdgesVec`, `:precisions`, + `:recalls`, `:fprs`, `:randPR`, `:auprs`, `:arocs` +- `:macroPR` → `OrderedDict` with `:auprInterpolated`, `:precisions`, `:recalls` +- `:macroROC` → `OrderedDict` with `:aurocInterpolated`, `:fprs`, `:tprs` + +# Example +```julia +results = evaluatePerTF(regs, targs, rankings, infEdges, totTargGenes, + gsEdgesByTf, uniGsRegs, gsRandPRbyTf) +results[:perTF][:auprs] # per-TF AUPR values +results[:macroPR][:auprInterpolated] # macro AUPR +``` +""" + +function evaluatePerTF( + regs::Vector{<:AbstractString}, targs::Vector{<:AbstractString}, rankings::Vector{Float64}, infEdges::Vector{String}, + gsEdgesByTf::Vector{Array{String}}, uniGsRegs::Vector{<:AbstractString}, gsRandPRbyTf::Vector{Float64}; + saveDir::Union{AbstractString, Nothing} = nothing, breakTies::Bool=true, target_points=1000, min_step=1e-4, step_method=:min_gap, xLimitRecall::Float64 = 1.0) + + + if saveDir !== nothing && !isempty(saveDir) + saveDir = joinpath(saveDir, "perTF") + mkpath(saveDir) + end + + totGsRegs = length(uniGsRegs) + totTargGenes = maximum(length.(gsEdgesByTf)) # approximate max targets + + println("---- Computing Per-TF Metrics") + gsAuprsByTf = zeros(totGsRegs,1) + gsArocsByTf = zeros(totGsRegs,1) + gsPrecisionsByTf = Array{Float64}[] + gsRecallsByTf = Array{Float64}[] + gsFprsByTf = Array{Float64}[] + + allTfEdgesVec = Array{Float64}[] # one vector per TF + allTfEdges = Vector{Vector{String}}() # raw edges + allTfRanks = Array{Float64}[] # one array per TF + for (iTF, tf) in enumerate(uniGsRegs) + # Filter inferred edges for this TF + inds = findall(x -> x == tf, regs) + tfEdges = infEdges[inds] + tfRanks = rankings[inds] + tfEdgesVec = [in(edge, gsEdgesByTf[iTF]) ? 1 : 0 for edge in tfEdges] + # Tie-break if needed + if breakTies + tfAbsRanks = abs.(tfRanks) + uniqueRanks = unique(tfAbsRanks) + meanIndicator = Dict{Float64, Float64}() + for r in uniqueRanks + idxs = findall(x -> x == r, tfAbsRanks) + meanIndicator[r] = mean(tfEdgesVec[idxs]) + end + tfEdgesVec = [meanIndicator[r] for r in tfAbsRanks] + end + # Save for later + push!(allTfEdgesVec, tfEdgesVec) + push!(allTfEdges, tfEdges) + push!(allTfRanks, tfRanks) # store adjusted ranks for this TF + + # Compute cumulative metrics + tfTP = 0.0 + tfPrec = Float64[] + tfRec = Float64[] + tfFpr = Float64[] + totNegativesTF = totTargGenes - length(gsEdgesByTf[iTF]) + for j in 1:length(tfEdgesVec) + tfTP += tfEdgesVec[j] + fp = j - tfTP + push!(tfPrec, tfTP / j) + push!(tfRec, tfTP / length(gsEdgesByTf[iTF])) + push!(tfFpr, fp / totNegativesTF) + end + # Prepend starting point + if !isempty(tfPrec) + tfRec = vcat(0.0, tfRec) + tfPrec = vcat(tfPrec[1], tfPrec) + tfFpr = vcat(0.0, tfFpr) + end + + # -- Plot perTF metric + currRandPR = gsRandPRbyTf[iTF] + plotPRCurve(tfRec, tfPrec, currRandPR, saveDir; xLimitRecall=xLimitRecall, baseName = tf) + plotROCCurve(tfFpr, tfRec, saveDir; baseName = tf) + + # Save per-TF metrics + push!(gsPrecisionsByTf, tfPrec) + push!(gsRecallsByTf, tfRec) + push!(gsFprsByTf, tfFpr) + # AUPR / AUROC per TF + heightsTF = (tfPrec[2:end] + tfPrec[1:end-1]) / 2 + widthsTF = tfRec[2:end] - tfRec[1:end-1] + gsAuprsByTf[iTF] = sum(heightsTF .* widthsTF) + + widthsRocTF = tfFpr[2:end] - tfFpr[1:end-1] + heightsRocTF = (tfRec[2:end] + tfRec[1:end-1]) / 2 + gsArocsByTf[iTF] = sum(widthsRocTF .* heightsRocTF) + end + + # Compute macro metrics + + # maxLen = maximum(length.(gsPrecisionsByTf)) + # macroPrecisions = zeros(maxLen) + # macroRecalls = zeros(maxLen) + # for i in 1:maxLen + # precVals = Float64[] + # recVals = Float64[] + # for (p, r) in zip(gsPrecisionsByTf, gsRecallsByTf) + # if i <= length(p) + # push!(precVals, p[i]) + # push!(recVals, r[i]) + # end + # end + # macroPrecisions[i] = mean(precVals) + # macroRecalls[i] = mean(recVals) + # end + # macroAUPR = mean(gsAuprsByTf) + # macroAUROC = mean(gsArocsByTf) + + # A global result but requires perTF to compute + macroResults = computeMacroMetrics(gsPrecisionsByTf, gsRecallsByTf, gsFprsByTf, gsAuprsByTf, gsArocsByTf; + target_points=target_points, min_step=min_step, step_method=step_method) + + perTFDict = OrderedDict( + :gsRegs => uniGsRegs, + :tfEdges => allTfEdges, # raw inferred edges per TF + :tfEdgesVec => allTfEdgesVec, # 0/1 indicator per TF + :precisions => gsPrecisionsByTf, + :recalls => gsRecallsByTf, + :fprs => gsFprsByTf, + :randPR => gsRandPRbyTf, + :auprs => gsAuprsByTf, + :arocs => gsArocsByTf + ) + + # merge!(perTF, macroResults) + results = OrderedDict( + :perTF => perTFDict, + :macroPR => macroResults[:macroPR], + :macroROC => macroResults[:macroROC] + ) + return results +end + +""" + computePR( + gsFile::String, infTrnFile::String; + gsRegsFile::Union{String, Nothing} = nothing, targGeneFile::Union{String, Nothing} = nothing, # filtering + breakTies::Bool = true, partialAUPRlimit::Float64 = 0.1, doPerTF::Bool = true, # computation + xLimitRecall::Float64 = 1.0, saveDir::Union{String, Nothing} = nothing, # plotting + target_points::Int = 1000, min_step::Float64 = 1e-4, step_method::Symbol = :min_gap) # interpolation + +Compute precision-recall (PR) and ROC metrics for an inferred gene regulatory network (GRN) +against a gold standard, including optional per-TF and macro-level metrics. +Results are saved to a `.jld` file and PR/ROC curves are plotted automatically. + +# Arguments +- `gsFile::String` : Gold standard TSV file with columns in order: TF (1), Target (2), Weight (3) — column names can be anything +- `infTrnFile::String` : Inferred GRN TSV file with columns in order: TF (1), Target (2), Weight (3) — column names can be anything + +# Keyword Arguments +- `gsRegsFile::Union{String,Nothing}=nothing` : File listing regulators to include. Default uses all GS regulators. +- `targGeneFile::Union{String,Nothing}=nothing` : File listing target genes to include. Default uses all GS targets. +- `rankColTrn::Int=3` : Column index for scores in the inferred GRN file. +- `breakTies::Bool=true` : Average indicators over tied rankings to smooth PR curves. +- `partialAUPRlimit::Float64=0.1` : Maximum recall cutoff for computing partial AUPR. +- `xLimitRecall::Float64=1.0` : Maximum recall shown on the x-axis of diagnostic PR plots. +- `doPerTF::Bool=true` : Whether to compute per-TF and macro metrics. +- `saveDir::Union{String,Nothing}=nothing` : Directory for saving plots and results. Defaults to a timestamped folder. +- `target_points::Int=1000` : Interpolation points for macro curves. +- `min_step::Float64=1e-4` : Minimum step size for interpolation. +- `step_method::Symbol=:min_gap` : Step selection method for macro metrics. + +# Returns +An `OrderedDict` with: +- `:gsRegs`, `:gsTargs`, `:gsEdges` → Gold standard regulators, targets, and edges. +- `:randPR` → Random baseline PR value. +- `:infRegs`, `:infEdges` → Inferred GRN regulators and edges. +- `:edgesVec` → Tie-adjusted or binary indicator vector. +- `:precisions`, `:recalls`, `:fprs` → Overall PR/ROC curve arrays. +- `:auprs` → Dict with `:full` AUPR and `:partial` AUPR (up to `partialAUPRlimit`). +- `:arocs` → Overall AUROC. +- `:f1scores` → F1-score array. +- `:perTF` → Per-TF metrics dict (see `evaluatePerTF`). `nothing` if `doPerTF=false`. +- `:macroPR` → Macro PR dict. `nothing` if `doPerTF=false`. +- `:macroROC` → Macro ROC dict. `nothing` if `doPerTF=false`. +- `:savedFile` → Path to saved `.jld` results file. + +# Example +```julia +results = computePR("gs.tsv", "inferredGRN.tsv"; + breakTies=true, partialAUPRlimit=0.1, saveDir="plots") + +results[:auprs][:full] # overall AUPR +results[:auprs][:partial][:value] # partial AUPR at recall limit +results[:perTF][:auprs] # per-TF AUPRs +results[:macroPR][:auprInterpolated] # macro AUPR +``` +""" +# function computePR( +# gsFile::String, infTrnFile::String; +# gsRegsFile::Union{String, Nothing} = nothing, targGeneFile::Union{String, Nothing} = nothing, +# # rankColTrn::Int = 3, +# breakTies::Bool = true, partialLimitRecall::Float64 = 0.1, doPerTF::Bool = true, +# saveDir::Union{String, Nothing} = nothing, target_points::Int = 1000, min_step::Float64 = 1e-4, +# step_method::Symbol = :min_gap) + +infTrnFile = "/data/miraldiNB/anthony/Inferelator_Julia/outputs/251125_HAE_ISGF3_GAS_combined/combined/combined_sp.tsv" +gsFile = "/data/miraldiNB/giulia/GS_10/IFNB_A549.tsv" +targGeneFile = "/data/miraldiNB/giulia/intersection/IFNB_A549_target_genes.txt" +gsRegsFile = "/data/miraldiNB/Katko/Projects/Julia/AnthonyData/tfs.txt" + +function computePR( + gsFile::String, infTrnFile::String; + gsRegsFile::Union{String, Nothing} = nothing, targGeneFile::Union{String, Nothing} = nothing, # filtering + breakTies::Bool = true, partialAUPRlimit::Float64 = 0.1, doPerTF::Bool = true, # computation + xLimitRecall::Float64 = 1.0, saveDir::Union{String, Nothing} = nothing, # plotting + target_points::Int = 1000, min_step::Float64 = 1e-4, step_method::Symbol = :min_gap) # interpolation + + # Helper function for empty/fallback PR result + function emptyPRResult(; + gsRegs::Vector{String} = String[], + gsTargs::Vector{String} = String[], + gsEdges::Vector{String} = String[], + infRegs::Vector{String} = String[], + infEdges::Vector{String} = String[]) + return OrderedDict( + :gsRegs => gsRegs, + :gsTargs => gsTargs, + :gsEdges => gsEdges, + :infRegs => infRegs, + :infEdges => infEdges, + :precisions => Float64[], + :recalls => Float64[], + :fprs => Float64[], + :auprs => Dict( + :full => 0.0, + :partial => Dict( + :value => 0.0, + :recallLimit => partialAUPRlimit # captured from outer scope + ) + ), + :arocs => 0.0, + :f1scores => Float64[], + :perTF => nothing, + :macroPR => nothing, + :macroROC => nothing, + :savedFile => nothing + ) + end + # ----- Part 1. Load and Validate Input Files + gsData = CSV.File(gsFile; delim="\t", header=true) + trnData = CSV.File(infTrnFile; delim="\t", header=true) + + validateColumnCount(gsData, gsFile) + validateColumnCount(trnData, infTrnFile) + + # Reload selecting only first 3 columns by position + gsData = CSV.File(gsFile; delim="\t", select=[1, 2, 3]) + trnData = CSV.File(infTrnFile; delim="\t", select=[1, 2, 3]) + + + # Initialize defaults BEFORE any conditionals + potTargGenes = String[] + potTargGenesSet = Set{String}() + gsPotRegs = String[] + gsPotRegsSet = Set{String}() + + # ----- Part 2. Load Optional Filter Files (Potential Targets and Regulators) + if targGeneFile !== nothing && !isempty(targGeneFile) + potTargGenes = readlines(targGeneFile) + potTargGenesSet = Set(potTargGenes) + totTargGenes = length(unique(potTargGenes)) + @info "Target gene file loaded: $(length(potTargGenes)) genes from $targGeneFile" + else + @info "No target gene file provided — will use gold standard targets after GS is loaded" + end + + # Load regulator list if provided + if gsRegsFile !== nothing && !isempty(gsRegsFile) + gsPotRegs = readlines(gsRegsFile) + gsPotRegsSet = Set(gsPotRegs) + @info "Regulator file loaded: $(length(gsPotRegs)) regulators from $gsRegsFile" + else + @info "No regulator file provided — will use all regulators in gold standard" + end + + # ----- Part 3. Filter and Process Gold Standard + + # Filter gold standard by regulators and target genes if specified: limit to TF-gene interactions considered by the model + gsFilteredData = filter(row -> row[3] > 0, gsData) + # if gsRegsFile !== nothing && !isempty(gsRegsFile) + # # gsFilteredData = filter(row -> (row.TF in gsPotRegs) && (row.Target in potTargGenes), gsFilteredData) + # gsFilteredData = filter(row ->(row[1] in gsPotRegsSet) && (row[2] in potTargGenesSet), gsFilteredData) + # size(gsFilteredData) + # end + + # Filter by regulators if provided + if !isempty(gsPotRegsSet) + gsFilteredData = filter(row -> row[1] in gsPotRegsSet, gsFilteredData) + end + + # Filter by targets if provided + if !isempty(potTargGenesSet) + gsFilteredData = filter(row -> row[2] in potTargGenesSet, gsFilteredData) + end + + # Edge-case check: skip if no gold standard edges + if isempty(gsFilteredData) + @warn "No overlapping gold standard edges after filtering. Skipping PR/ROC evaluation." + return emptyPRResult() + end + + # Define gold standard edges (each as "TF,Target") + gsRegs = collect(row[1] for row in gsFilteredData) + gsTargs = collect(row[2] for row in gsFilteredData) + totGsInts = size(gsFilteredData)[1] + uniGsRegs = unique(gsRegs) # unique regulators or TFs in GS + totGsRegs = length(uniGsRegs) + uniGsTargs = unique(gsTargs) # unique targets in GS + totGsTargs = length(uniGsTargs) + # Create new edges vector. (Each edge is a tuple (TF, gene)) + gsEdges = [string(gsRegs[i], ",", gsTargs[i]) for i in 1:length(gsRegs)] + + # If targGeneFile wasn’t provided, use GS targets. + if targGeneFile === nothing || isempty(targGeneFile) + potTargGenes = uniGsTargs + totTargGenes = length(potTargGenes) + end + + # Compute evaluation universe and random PR baseline + gsTotPotInts = totTargGenes*totGsRegs # complete universe size + # gsTotPotInts = totGsRegs * totGsTargs + gsRandPR = totGsInts/gsTotPotInts + + # group gold standard edges by regulator + gsEdgesByTf = Array{String}[] + gsRandPRbyTf = zeros(totGsRegs) + for gind = 1:totGsRegs + currInds = findall(x -> x == uniGsRegs[gind], gsRegs) + push!(gsEdgesByTf, vec(permutedims(gsEdges[currInds]))) + gsRandPRbyTf[gind] = length(gsEdgesByTf[gind])/totGsTargs + end + + # ----- Part 4. Load and Filter Inferred GRN + @info "Loading and Processing Inferred GRN" + # Filter "inferred GRN" to include only 'TFs' in GS and 'TargetGenes' in 'potTargGenes' + grnData = filter(row -> (row[1] in uniGsRegs) && (row[2] in potTargGenes), trnData) + grnData = collect(grnData) + + if isempty(grnData) + @warn "No inferred edges after filtering. Skipping evaluation." + return emptyPRResult(gsRegs=uniGsRegs, gsTargs=uniGsTargs, gsEdges=gsEdges) + end + + # Order by absolute value of weights/confidences + grnData = sort(grnData, by = row -> abs(row[3]), rev = true) + regs = collect(row[1] for row in grnData) + targs = collect(row[2] for row in grnData) + rankings = collect(row[3] for row in grnData) + absRankings = abs.(rankings) + # Create inferred edges vector. (Each edge is a tuple (TF, gene)) + infEdges = [string(regs[i], ",", targs[i]) for i in 1:length(regs)] + totTrnInts = length(infEdges) + + # ----- Part 5. Compute Edge Indicators + @info "Computing Edge Indicators and Labels" + # create binary labels. 1 if infEgde in gsEdge and 0 otherwise + commonEdgesBinaryVec = [in(edge, gsEdges) ? 1 : 0 for edge in infEdges] + @info "Total Interactions w/ GS TFs ($(length(unique(regs)))): $totTrnInts" + + if breakTies # If breaking ties is desired, compute tie-adjusted (mean) vector. + # Break ties in weights/confidences to smooth out abrupt jumps caused by abitrary ordering of tied predictions + # Create a dictionary mapping each score to the mean value of commonEdgesVec for tied predictions + @info "Tie breaker enabled" + uniqueRankings = unique(absRankings) + meanIndicator = Dict{Float64, Float64}() + for currRank in uniqueRankings + inds = findall(x -> x == currRank, absRankings) + meanIndicator[currRank] = mean(commonEdgesBinaryVec[inds]) + end + meanEdgesVec = [meanIndicator[ix] for ix in absRankings] + edgesVec = meanEdgesVec + else # If not breaking ties, use binary vector + edgesVec = commonEdgesBinaryVec + end + + # ----- Part 6. Compute Overall PR/ROC Performance Metrics + @info "Computing Performance Metrics" + totalNegatives = gsTotPotInts - totGsInts # Total posisble interactions - length(gsEdges) = TN + FP + gsPrecisions = zeros(totTrnInts) + gsRecalls = zeros(totTrnInts) + gsFprs = zeros(totTrnInts) + + cummulativeTP = 0.0 # can be fractional in tie-adjusted mode + for idx in 1:totTrnInts + cummulativeTP += edgesVec[idx] # Add the effective contirbution for this prediction. This is also True Positive + # False positive (FP). This is a weighted FP in the case of tie breaking + falsePositives = idx - cummulativeTP + + # Compute precision: TP divided by total prediction so far + # idx is always the total predicted positive at any point j + # such that j = TP + FP + gsPrecisions[idx] = cummulativeTP / idx # + gsRecalls[idx] = cummulativeTP / totGsInts # truePositives/length(gsEdges) == tp/tp+fn + gsFprs[idx] = falsePositives / totalNegatives + end + + # Prepend starting point for plotting. + if !isempty(gsPrecisions) + gsRecalls = vcat(0.0, gsRecalls) + gsPrecisions = vcat(gsPrecisions[1], gsPrecisions) + gsFprs = vcat(0.0, gsFprs) + end + # Compute F1-scores + # gsF1scores = 2 * (gsPrecisions .* gsRecalls) ./ (gsPrecisions + gsRecalls); + gsF1scores = [p + r > 0 ? 2p*r/(p+r) : 0.0 for (p, r) in zip(gsPrecisions, gsRecalls)]; + + # ----- Part 7. Compute AUPR and AUROC + @info "Computing AUPR and AUROC" + # Here, AUPR is computed using trapezoidal rule. Other methods available is a step-function approximation + heights = (gsPrecisions[2:end] + gsPrecisions[1:end-1])/2 + widths = gsRecalls[2:end] - gsRecalls[1:end-1] + gsAuprs = sum(heights .* widths) + #= + # Step-function approximation. This works but is less robust + gsAuprs = 0.0 + prev_recall = 0.0 + for (r, p) in zip(gsRecalls, gsPrecisions) + delta = r - prev_recall + gsAuprs += delta * p + prev_recall = r + end + =# + + # PARTIAL AUPR @ partialAUPRlimit + indx = findall(r -> r <= partialAUPRlimit, gsRecalls) # Find indices of recalls <= partialUpperLimRecall + recalls_sub = gsRecalls[indx] + precisions_sub = gsPrecisions[indx] + heights_sub = (precisions_sub[2:end] + precisions_sub[1:end-1]) ./ 2 + widths_sub = recalls_sub[2:end] - recalls_sub[1:end-1] + partialAUPR = sum(heights_sub .* widths_sub) + + # AROC : Trapezoidal Rule + widthsRoc = gsFprs[2:end] - gsFprs[1:end-1] # Change in FPR (the x-axis) between successive points. + heightsRoc = (gsRecalls[2:end] + gsRecalls[1:end-1]) / 2 # Average TPR (recall) for each segment. + gsArocs = sum(widthsRoc .* heightsRoc) + #= + gsArocs = 0.0 + prev_fpr = 0.0 + for (f, r) in zip(fprs, recalls) + delta = f - prev_fpr + gsArocs += delta * r + prev_fpr = f + end + =# + + # ----- Part 8. Save Directory + baseName = splitext(basename(infTrnFile))[1] + if saveDir === nothing || isempty(saveDir) + dateStr = Dates.format(now(), "yyyymmdd_HHMMSS") + saveDir = joinpath(pwd(), baseName * "_" * dateStr) + end + mkpath(saveDir) + + # ----- Part 9. Per-TF and Macro Metrics + resultsTF = nothing + perTFDict = nothing + macroPRDict = nothing + macroROCDict = nothing + if doPerTF + if !isempty(regs) && !isempty(targs) && !isempty(gsEdgesByTf) + resultsTF = evaluatePerTF( + regs, targs, rankings, infEdges, gsEdgesByTf, uniGsRegs, gsRandPRbyTf; + saveDir, + breakTies=breakTies, target_points=target_points, min_step=min_step, + step_method=step_method, xLimitRecall=xLimitRecall + ) + # Extract Per-TF Results + perTFDict = resultsTF[:perTF] + macroPRDict = resultsTF[:macroPR] + macroROCDict = resultsTF[:macroROC] + else + @warn "Skipping per-TF evaluation: no valid edges after filtering" + end + end + + + # ----- Part 10. Plot and Save Results + suffix = breakTies ? "_tiesBroken" : "" + baseName = baseName * suffix + plotPRCurve(gsRecalls, gsPrecisions, gsRandPR, saveDir; xLimitRecall=xLimitRecall, baseName = baseName) + plotROCCurve(gsFprs, gsRecalls, saveDir; baseName = baseName) + + + results = OrderedDict( + :gsRegs => uniGsRegs, + :gsTargs => uniGsTargs, + :gsEdges => gsEdges, + :randPR => gsRandPR, + :infRegs => regs, + :infEdges => infEdges, + :breakTies => breakTies, + :stepVals => rankings, + :rankings => rankings, + # :commonEdgesBinaryVec => commonEdgesBinaryVec, + # :meanEdgesVec => breakTies ? meanEdgesVec : nothing, + :edgesVec => edgesVec, + :precisions => gsPrecisions, + :recalls => gsRecalls, + :fprs => gsFprs, + # :auprs => gsAuprs, + :auprs => Dict( + :full => gsAuprs, + :partial => Dict( + :value => partialAUPR, + :recallLimit => partialAUPRlimit + ) + ), + :arocs => gsArocs, + :f1scores => gsF1scores, + # --- Include results from evaluatePerTF --- + :perTF => perTFDict, + :macroPR => macroPRDict, + :macroROC => macroROCDict + ) + + # Define a standard filename for saving the performance metrics + savedFile = joinpath(saveDir, baseName * "_PerformanceMetric.jld") + @save savedFile results + + # Add the saved file path to the results, so the caller immediately knows it + results[:savedFile] = savedFile + + return results +end \ No newline at end of file diff --git a/src/03_Metrics/constants.jl b/src/03_Metrics/constants.jl new file mode 100755 index 0000000..8314a21 --- /dev/null +++ b/src/03_Metrics/constants.jl @@ -0,0 +1,9 @@ +# const GS_REQUIRED_COLS = [:TF, :Target, :Weight] +# const TRN_REQUIRED_COLS = [:TF, :Target, :Score] +const MIN_REQUIRED_COLS = 3 + +const DEFAULT_COLORS = [ + "#377eb8", "#ff7f00", "#4daf4a", "#e41a1c", "#984ea3", "#a65628", + "#f781bf", "#00ced1", "#000000", "#5A9D5A", "#D96D3B", "#FFAD12", + "#66628D", "#91569A", "#B6742A", "#DD87B4", "#D26D7A", "#DAA520", "#dede00" +] \ No newline at end of file diff --git a/src/03_Metrics/plotting/plotBatchMetrics.jl b/src/03_Metrics/plotting/plotBatchMetrics.jl new file mode 100755 index 0000000..4d1946f --- /dev/null +++ b/src/03_Metrics/plotting/plotBatchMetrics.jl @@ -0,0 +1,928 @@ +# ---------------------------------------------------------------------- +# Color Handling +# ---------------------------------------------------------------------- + +""" + padColors(listFiles) + +Ensures enough colors for plotting by extending a predefined palette. +Returns a vector of hex color codes, generating random colors if needed. + +# Arguments +- `listFiles`: Vector of items needing unique colors. +""" +function padColors(listFiles) + lineColors = copy(DEFAULT_COLORS) + lenFile = length(listFiles) + lenColor = length(lineColors) + excessCT = lenFile - lenColor + + if excessCT > 0 + println("Warning: There are more entries in listFilePR than available colors. Generating random colors for the excesses") + colorGen = [hex(RGB(rand(), rand(), rand())) for _ in 1:(excessCT + 12)] # Generate random colors for plots + uniqueCols = setdiff(colorGen, lineColors) + # Pad lineColors with the unique additional colors + lineColors = vcat(lineColors, uniqueCols[1:excessCT]) + end + return lineColors +end + +# ---------------------------------------------------------------------- +# Helper: Data Loading +# ---------------------------------------------------------------------- +# function loadPRData(source) +# if isa(source, String) +# try +# data = load(source) +# return Dict( +# :precisions => data["results"][:precisions], +# :recalls => data["results"][:recalls], +# :randPR => get(data["results"], :randPR, nothing) +# ) +# catch e +# @warn "Could not load $source" exception=(e, catch_backtrace()) +# return nothing +# end +# elseif isa(source, Dict) || isa(source, OrderedDict) +# if haskey(source, :precisions) && haskey(source, :recalls) +# return Dict(:precisions=>source[:precisions], :recalls=>source[:recalls], +# :randPR=>get(source, :randPR, nothing)) +# elseif haskey(source, "results") +# r = source["results"] +# return Dict(:precisions=>r[:precisions], :recalls=>r[:recalls], +# :randPR=>get(r, :randPR, nothing)) +# end +# end +# return nothing +# end + +function loadPRData(source; mode::Symbol=:global) + # Load file safely if source is a path + r = if isa(source, String) + try + load(source)["results"] + catch e + @warn "Could not load $source" exception=(e, catch_backtrace()) + return nothing + end + else + source + end + + if r === nothing + return nothing + end + + # Determine mode + if mode == :macro + if !haskey(r, :macroPR) + @warn "No macroPR found, falling back to global" + r_macro = r + else + r_macro = r[:macroPR] + end + return Dict( + :precisions => r_macro[:precisions], + :recalls => r_macro[:recalls], + :randPR => get(r_macro, :randPR, get(r, :randPR, nothing)) # fallback to global randPR + ) + elseif mode == :global + return Dict( + :precisions => r[:precisions], + :recalls => r[:recalls], + :randPR => get(r, :randPR, nothing) + ) + elseif mode == :perTF + if !haskey(r, :perTF) + @warn "No perTF data found" + return nothing + end + return r[:perTF] + else + error("Unknown mode: $mode") + end +end + +# ---------------------------------------------------------------------- +# Transformation Helper +# ---------------------------------------------------------------------- +""" + function makeTransform(yScale::String) +Creates forward and inverse transformation functions for plot scaling. + +# Arguments +- `yScale::String`: Transformation type ("sqrt", "cubert", or "linear") + +Returns a tuple of (forward, inverse) functions. Defaults to identity functions if scale type not recognized. + """ +function makeTransform(yScale::String) + if yScale == "linear" + forwardFunc = x -> x + inverseFunc = x -> x + elseif yScale == "sqrt" + forwardFunc = x -> sqrt.(x) + inverseFunc = x -> x .^ 2 + elseif yScale == "cubert" + forwardFunc = x -> x .^ (1/3) + inverseFunc = x -> x .^ 3 + else + # For "linear" or any unknown yScale, return identity functions + @warn "Unknown scale type: $yScale. Using linear scale." + forwardFunc = x -> x + inverseFunc = x -> x + end + return forwardFunc, inverseFunc +end + +# ---------------------------------------------------------------------- +# Plotting Helpers +# ---------------------------------------------------------------------- + +# # Helper function: Plot data on one or more axes. +# function plotFileData!(axes, legendLabel::String, dataSource, color; lineType=nothing) +# """ +# Plots precision-recall curves on specified axes from data file or direct data. + +# # Arguments +# - `axes`: Array of plot axes to draw on +# - `legendLabel::String`: Label for plot legend +# - `dataSource`: Either a file path (String) or a Dict containing PR data +# - `color`: Color for plotting +# - `lineType=nothing`: Optional line style specification + +# # Returns +# Loaded/processed data or nothing if processing fails. +# """ + +# # Initialize variables to hold the data +# precisions = nothing +# recalls = nothing +# randPR = nothing +# fileData = nothing + +# # Process the data source based on its type +# if isa(dataSource, String) +# # It's a file path - try to load it +# try +# fileData = load(dataSource) +# # Extract data from the loaded file +# precisions = fileData["results"][:precisions] +# recalls = fileData["results"][:recalls] +# randPR = get(fileData["results"], :randPR, nothing) +# catch e +# println("Error loading file: $dataSource - $e") +# return nothing +# end +# elseif isa(dataSource, Dict) || isa(dataSource, OrderedDict) +# # It's already a data dictionary +# fileData = dataSource + +# # Check if it's a results dictionary or a direct data dictionary +# if haskey(dataSource, :precisions) && haskey(dataSource, :recalls) +# # Direct data format +# precisions = dataSource[:precisions] +# recalls = dataSource[:recalls] +# randPR = get(dataSource, :randPR, nothing) +# elseif haskey(dataSource, "results") +# # Nested results format (like from a loaded JLD file) +# precisions = dataSource["results"][:precisions] +# recalls = dataSource["results"][:recalls] +# randPR = get(dataSource["results"], :randPR, nothing) +# else +# println("Error: Data dictionary does not contain required precision/recall data") +# return nothing +# end +# else +# println("Error: Unsupported data source type: $(typeof(dataSource))") +# return nothing +# end + +# # Ensure we have valid data before plotting +# if isnothing(precisions) || isnothing(recalls) +# println("Error: Could not extract precision/recall data from source") +# return nothing +# end + +# # Plot the data on each provided axis +# for ax in axes +# if isnothing(lineType) || lineType == "" +# # Use default linestyle +# ax.plot(recalls, precisions, label=legendLabel, color=color, linewidth=0.8) +# else +# # Specify the provided line type (linestyle) +# ax.plot(recalls, precisions, label=legendLabel, color=color, linewidth=0.5, linestyle=lineType) +# end +# end + +# return Dict( +# # "data" => fileData, +# "precisions" => precisions, +# "recalls" => recalls, +# "randPR" => randPR +# ) +# end + +function plotFileData!(axes::AbstractVector, source, label::String, color::String; lineType=nothing, lineWidth=nothing, mode::Symbol=:global) + data = loadPRData(source; mode) + if isnothing(data) + @warn "Skipping $label ($source) — no valid PR data found" + return false + end + + for ax in axes + ax.plot(data[:recalls], data[:precisions], label=label, color=color,linewidth=(lineWidth === nothing ? 0.7 : lineWidth), + linestyle=(lineType === nothing ? "-" : lineType)) + end + return data +end + + +# Function to combine legend handles and labels from every axis. +function combineLegends(fig) + allHandles = Any[] + allLabels = String[] + # Loop over each axis in the figure. + for ax in fig.axes + handles, labels = ax.get_legend_handles_labels() + for (h, l) in zip(handles, labels) + if !(l in allLabels) + push!(allHandles, h) + push!(allLabels, l) + end + end + end + return allHandles, allLabels +end + +function styleAxis!(ax; xZoom=0.1, yRange::Union{Nothing,Tuple}=nothing, + xStep=0.05, yStep=0.1, yScale="linear", setXticks=true, tickLabelSize::Int = 7) +# function styleAxis!(ax; xZoom, yRange, xStep, yStep, yScale, setXticks, tickLabelSize) + # X-axis + ax.set_xlim(0, xZoom) + if setXticks + xMax = mod(xZoom, xStep) == 0 ? xZoom : ceil(xZoom/xStep) * xStep + ax.set_xticks(0:xStep:xMax) + end + + # Y-axis + forwardFunc, inverseFunc = makeTransform(yScale) + yMin, yMax = isnothing(yRange) ? (0.0, 1.0) : yRange + ax.set_ylim(yMin, yMax) + + # Y ticks + yTicks = yMin:yStep:yMax + ax.set_yticks(yTicks) + ax.set_yticklabels(string.(round.(yTicks; digits=2))) + ax.set_yscale("function", functions=(forwardFunc, inverseFunc)) + + # Labels + # ax.set_xlabel(xlabel, fontsize=axisTitleSize, family="Nimbus Sans") + # ax.set_ylabel(ylabel, fontsize=axisTitleSize, family="Nimbus Sans") + + # Grid & ticks + ax.grid(true, which="major", linestyle="-", linewidth=0.5, color="lightgray") + ax.minorticks_on() + ax.grid(true, which="minor", linestyle=":", linewidth=0.25, color="lightgray") + ax.tick_params(axis="both", which="both", labelsize=tickLabelSize, direction="out") +end + +""" + plotPRCurves(listFilePR, dirOut, saveName; kwargs...) + +Plot and save precision-recall curves for one or more networks against a gold standard. +Supports single-axis and broken y-axis modes. Saves both legend and no-legend versions. + +# Arguments +- `listFilePR` : `OrderedDict` mapping legend labels to either file paths or + `OrderedDict`s with `:precisions`, `:recalls`, and `:randPR` keys. +- `dirOut::String` : Output directory for saved plots. +- `saveName::String`: Base name for output files. + +# Keyword Arguments +- `xLimitRecall::Float64=0.1` : Maximum recall shown on x-axis. +- `yZoomPR::Vector=[]` : Y-axis display range. + `[]` → full range 0–1. + `[0.8]` → limit y-axis to 0–0.8. + `[0.4, 0.9]` → broken y-axis with gap between 0.4 and 0.9. +- `xStepSize::Union{Nothing,Real}=nothing` : X-axis tick step size. Default auto-set to 0.05. +- `yStepSize::Union{Nothing,Real}=nothing` : Y-axis tick step size. Default auto-set to 0.2. +- `yScale::String="linear"` : Y-axis scale — `"linear"`, `"sqrt"`, or `"cubert"`. +- `isInside::Bool=true` : Place legend inside (`true`) or outside (`false`) the plot. +- `lineColors::Vector=[]` : Custom line colors. Empty uses default palette. +- `lineTypes::Vector=[]` : Custom line styles. Empty uses solid lines. +- `lineWidths::Vector=[]` : Custom line widths. Empty uses default width. +- `heightRatios=nothing` : Height ratios for broken y-axis panels `[topHeight, bottomHeight]`. + `nothing` --> auto-computed proportionally from `yZoomPR` values. + Pass explicit values e.g. `[3.0, 0.5]` to control panel sizes manually — + larger value = taller panel. +- `mode::Symbol=:global` : PR data to use: + `:global` → overall PR curve. + `:macro` → macro-averaged PR (falls back to global if missing). + `:perTF` → per-TF PR curves. + +# Returns +Path to the saved plot file. + +# Example +```julia +plotPRCurves(listFilePR, "plots", "MyNetwork"; + xLimitRecall=0.1, yZoomPR=[0.4, 0.9], + isInside=false, mode=:macro) +``` +""" +function plotPRCurves(listFilePR, dirOut::String, saveName::String; + xLimitRecall = 0.1, yZoomPR = [], xStepSize::Union{Nothing,Real}=nothing, + yStepSize::Union{Nothing,Real}=nothing, + yScale::String="linear", isInside::Bool=true, + lineColors=[], # empty vector means "use default" + lineTypes=[], # empty vector means "use default", + lineWidths = [], + heightRatios=[0.5, 3.0], + mode::Symbol = :global + ) + + # Defaults + xStep = isnothing(xStepSize) ? 0.05 : xStepSize + yStep = isnothing(yStepSize) ? 0.2 : yStepSize + dirOut = isempty(dirOut) ? pwd() : mkpath(dirOut) + # Style parameters + axisTitleSize = 9 + tickLabelSize = 7 + # plotTitleSize = 16 + legendSize = 9 + + # Color assignment + if isempty(lineColors) + lineColors = padColors(listFilePR) + end + + # Auto height ratios if broken axis + if length(yZoomPR) == 2 && heightRatios === nothing + lowerSpan = max(yZoomPR[1] - 0.0, 0.0) # bottom panel span (0 -> yZoomPR[1]) + upperSpan = max(1.0 - yZoomPR[2], 0.0) # top panel span (yZoomPR[2] -> 1) + # protect against zero spans + if upperSpan == 0.0 + upperSpan = 1e-6 + end + if lowerSpan == 0.0 + lowerSpan = 1e-6 + end + # gridspec expects [top_height, bottom_height] + heightRatios = [upperSpan, lowerSpan] + end + # lastPlotData = nothing # will store last loaded file data (for randPR) + # println(">>> Inside plotPRCurves: yZoomPR = ", yZoomPR, " length=", length(yZoomPR)) + + ## Making Plots + if isempty(yZoomPR) || length(yZoomPR) == 1 + yZoom = isempty(yZoomPR) ? 1.0 : yZoomPR[1] + + # Create a Single PyPlot figure + fig, ax = subplots(figsize= isInside ? (2, 2) : (2, 2), layout="constrained") #5,4 + styleAxis!(ax; xZoom=xLimitRecall, yRange=(0,yZoom), xStep=xStep, yStep=yStep, yScale=yScale) + + @info "Plotting PR curves" + lastPlotData = nothing + for (idx, (legendLabel, currFilePR)) in enumerate(listFilePR) + @info "Plot $idx: $legendLabel" file=currFilePR + # If lineType is provided and nonempty, then pick its element if available. + currentLineType = (length(lineTypes) ≥ idx && lineTypes[idx] != "") ? lineTypes[idx] : nothing + currentLineWidth = (length(lineWidths) ≥ idx &&lineWidths[idx] != "") ? lineWidths[idx] : nothing + lastPlotData = plotFileData!([ax], currFilePR, legendLabel, lineColors[idx]; + lineType=currentLineType, lineWidth = currentLineWidth, mode) + end + + # Plot random PR line from the last file if available + if lastPlotData !== nothing && lastPlotData[:randPR] !== nothing + randPR = lastPlotData[:randPR] + ax.axhline(randPR, linestyle="-.", linewidth=2, color=[0.6, 0.6, 0.6], label="Random") + end + + # # Set labels and legend + ax.set_xlabel("Recall", fontsize=axisTitleSize, family="Nimbus Sans") + ax.set_ylabel("Precision", fontsize=axisTitleSize, family="Nimbus Sans") + # save figure without legend + if saveName !== nothing && !isempty(saveName) + savePath = joinpath(dirOut, string(saveName, "_PR_noLegend.pdf")) + else + savePath = joinpath(dirOut, string(Dates.format(now(), "yyyymmdd_HHMMSS"), "_PR_noLegend.pdf")) + end + PyPlot.savefig(savePath, dpi=600) + + # Add Figure Legends + if isInside + ax.legend(borderaxespad=0.2, frameon=true) + else + ax.legend(loc="center", bbox_to_anchor=(1.25, 0.5), borderaxespad=0.2, frameon=false) + fig.set_size_inches(3.5, 2.5) # wider only for outside legend + end + savePathLegend = replace(savePath, "PR_noLegend.pdf" => "PR.pdf") + PyPlot.savefig(savePathLegend, dpi=600) + + elseif length(yZoomPR) == 2 + # # ---- Two-subplot (broken y-axis) mode ---- + fig, (ax1, ax2) = subplots(2, 1, sharex=true, figsize=(2,2), gridspec_kw=Dict("heightRatios" => heightRatios, + "hspace" => 0.0), layout="constrained") #6,5 + + # Upper axis + styleAxis!(ax1; xZoom=xLimitRecall, yRange=(yZoomPR[2],1.0), xStep=xStep, yStep=yStep, yScale=yScale, setXticks=false) + # Lower axis + styleAxis!(ax2; xZoom=xLimitRecall, yRange=(0,yZoomPR[1]), xStep=xStep, yStep=yStep, yScale=yScale) + ax2.set_xlabel("Recall", fontsize=9) + + # Plot curves + lastPlotData = nothing + @info "Plotting PR curves" + for (idx, (legendLabel, currFilePR)) in enumerate(listFilePR) + @info "Plot $idx: $legendLabel; File: $currFilePR" + # If lineType is provided and nonempty, then pick its element if available. + currentLineType = (length(lineTypes) ≥ idx && lineTypes[idx] != "") ? lineTypes[idx] : nothing + currentLineWidth = (length(lineWidths) ≥ idx &&lineWidths[idx] != "") ? lineWidths[idx] : nothing + lastPlotData = plotFileData!([ax1, ax2], currFilePR, legendLabel, lineColors[idx]; + lineType=currentLineType, lineWidth = currentLineWidth, mode) + + end + + # Extract randPR from the last file + # Plot the random PR line on both axes if available. + if lastPlotData !== nothing && lastPlotData[:randPR] !== nothing + randPR = lastPlotData[:randPR] + # println(randPR) + for ax in (ax1, ax2) + ax.axhline(randPR, linestyle="-.", linewidth=2, color=[0.6, 0.6, 0.6], label="Random") + end + end + + # Hide the spines between ax1 and ax2 + ax1.spines["bottom"].set_visible(false) + ax2.spines["top"].set_visible(false) + ax1.tick_params(axis="x", which="both", bottom=false, top=false, labelbottom=false, labeltop=false) + ax2.xaxis.tick_bottom() + + # Add break indicators + d = 0.006 # Size of the diagonal lines in axes coordinates + # Top-left and top-right diagonals for ax1 + ax1.plot([-d, +d], [-d, +d]; transform=ax1.transAxes, color="k", clip_on=false) # Top-left diagonal + ax1.plot([1 - d, 1 + d], [-d, +d]; transform=ax1.transAxes, color="k", clip_on=false) # Top-right diagonal + # Bottom-left and bottom-right diagonals for ax2 + ax2.plot([-d, +d], [1 - d, 1 + d]; transform=ax2.transAxes, color="k", clip_on=false) # Bottom-left diagonal + ax2.plot([1 - d, 1 + d], [1 - d, 1 + d]; transform=ax2.transAxes, color="k", clip_on=false) # Bottom-right diagonal + + # Common labels: a shared y-axis label and x-axis label on the lower plot. + # fig.text(0.01, 0.5, "Precision", va="center", rotation="vertical", fontsize=axisTitleSize) + fig.supylabel("Precision", fontsize=axisTitleSize) + ax2.set_xlabel("Recall", fontsize=axisTitleSize) + + + # save figure without legend + if saveName !== nothing && !isempty(saveName) + savePath = joinpath(dirOut, string(saveName, "_PR_noLegend.pdf")) + else + savePath = joinpath(dirOut, string(Dates.format(now(), "yyyymmdd_HHMMSS"), "_PR_noLegend.pdf")) + end + PyPlot.savefig(savePath, dpi=600) + + # Add Figure Legends + if isInside + ax2.legend(fontsize= legendSize, borderaxespad=0.2, frameon=true) + else + ax2.legend(fontsize= legendSize, loc="center", bbox_to_anchor=(1.25, 0.5), borderaxespad=0.2, frameon=false) + fig.set_size_inches(5, 4) # wider only for outside legend + end + + savePathLegend = replace(savePath, "PR_noLegend.pdf" => "PR.pdf") + PyPlot.savefig(savePathLegend, dpi=600) + + end + PyPlot.close("all") +end + + +# ------------------------------------------------------------------------------------- +# Part 2: Making a DotPlot or BarPlot of AUPR using PyPlot +# ------------------------------------------------------------------------------------- +function plotAUPR(gsParam::OrderedDict{String,<:AbstractDict}, dirOut::String; + saveName::Union{Nothing, String} = nothing, + metricType::String="full", + figSize::Tuple{Real,Real}=(5,5), + axisTitleSize::Int=9, tickLabelSize::Int=7, + legendFontSize::Int=9, tickRotation::Int=45, + plotType::String="", + saveLegend::Bool=true) + + # ---------------------- + # Configure matplotlib + # rc = PyPlot.matplotlib["rcParams"] + # rc["font.family"] = "Nimbus Sans" + # rc["axes.labelsize"] = axisTitleSize + # rc["xtick.labelsize"] = tickLabelSize + # rc["ytick.labelsize"] = tickLabelSize + # rc["legend.fontsize"] = legendFontSize + + + # Ensure output directory exists + dirOut = joinpath(dirOut, "AUPR") + mkpath(dirOut) + + # Load AUPR values + function loadAupr(filePath::String, metricType::String) + try + data = load(filePath) + auprs = data["results"][:auprs] + return lowercase(metricType) == "partial" ? auprs[:partial][:value] : auprs[:full] + catch e + @warn "Could not load AUPR from $filePath" exception=(e, catch_backtrace()) + return nothing + end + end + + lineColors = copy(DEFAULT_COLORS) + # Build dataframe + gsNames = collect(keys(gsParam)) + netNames = unique(vcat([collect(keys(files)) for files in values(gsParam)]...)) + + dfRows = Vector{NamedTuple{(:xGroups,:Network,:AUPR),Tuple{String,String,Float64}}}() + for gs in gsNames, net in netNames + filePath = haskey(gsParam[gs], net) ? gsParam[gs][net] : nothing + # And update the caller to skip nothing values + auprVal = filePath === nothing ? nothing : loadAupr(filePath, metricType) + push!(dfRows, (xGroups=gs, Network=net, AUPR=something(auprVal, NaN))) # NaN is visible in plot + # push!(dfRows, (xGroups=gs, Network=net, AUPR=auprVal)) + end + + df = DataFrame(dfRows) + numGS = length(unique(df.xGroups)) + numNet = length(unique(df.Network)) + + # Decide plot type automatically if user hasn't overridden + autoPlotType = "" + if lowercase(plotType) in ["bar","dot"] + autoPlotType = lowercase(plotType) + else + autoPlotType = (numGS == 1 || numNet == 1) ? "dot" : "bar" + end + + xLabelVal = (numGS == 1 ? "Network" : "Gold Standards") + yLabelVal = lowercase(metricType) == "partial" ? "Partial AUPR" : "AUPR" + + # Function to generate a plot + function make_plot(includeLegend::Bool) + fig, ax = plt.subplots(figsize=figSize, layout="constrained") + colors = lineColors[1:numNet] + + if autoPlotType == "bar" + if numGS == 1 + for (i, net) in enumerate(netNames) + idxs = findall(df.Network .== net) + ax.bar.(i, df.AUPR[idxs], color=colors[i], label=(includeLegend ? net : nothing)) + end + ax.set_xticks(1:numNet) + ax.set_xticklabels(netNames, rotation=tickRotation) + elseif numNet == 1 + ax.bar.(1:numGS, df.AUPR, color=colors[1], label=(includeLegend ? netNames[1] : nothing)) + ax.set_xticks(1:numGS) + ax.set_xticklabels(gsNames, rotation=tickRotation) + else + barWidth = 0.15 + groupWidth = numNet * barWidth + groupSpacing = 0.4 + groupPositions = [i*(groupWidth + groupSpacing) for i in 0:(numGS-1)] + for j in 1:numNet + offset = (j-(numNet+1)/2)*barWidth + xPos = [gp + groupWidth/2 + offset for gp in groupPositions] + auprs = [df.AUPR[(i-1)*numNet + j] for i in 1:numGS] + ax.bar(xPos, auprs, barWidth, color=colors[j], label=(includeLegend ? netNames[j] : nothing)) + end + ax.set_xticks([gp + groupWidth/2 for gp in groupPositions]) + ax.set_xticklabels(gsNames, rotation=tickRotation) + end + elseif autoPlotType == "dot" + if numGS == 1 + for (i, net) in enumerate(netNames) + idxs = findall(df.Network .== net) + ax.scatter.(i, df.AUPR[idxs], color=colors[i], s=40, label=(includeLegend ? net : nothing)) + end + ax.set_xticks(1:numNet) + ax.set_xticklabels(netNames, rotation=tickRotation) + elseif numNet == 1 + ax.scatter.(1:numGS, df.AUPR, color=colors[1], s=40, label=(includeLegend ? netNames[1] : nothing)) + ax.set_xticks(1:numGS) + + ax.set_xticklabels(gsNames, rotation=tickRotation) + else + for j in 1:numNet + xvals = 1:numGS + yvals = [df.AUPR[(i-1)*numNet + j] for i in 1:numGS] + ax.scatter.(xvals, yvals, color=colors[j], s=40, label=(includeLegend ? netNames[j] : nothing)) + end + ax.set_xticks(1:numGS) + ax.set_xticklabels(gsNames, rotation=tickRotation) + end + end + + ax.set_xlabel(xLabelVal, fontsize=axisTitleSize) + ax.set_ylabel(yLabelVal, fontsize=axisTitleSize) + ax.tick_params(axis="both", which="both") + # ax.tick_params(axis="both", which="both", labelsize=tickLabelSize, direction="in") + ax.grid(true, which="major", linestyle="-", linewidth=0.5, color="lightgray") + if includeLegend + # ax.legend(fontsize=legendFontSize, loc="best") + ax.legend(fontsize=legendFontSize, loc="center", bbox_to_anchor=(1.25, 0.5), borderaxespad=0.2, frameon=false) + else + # Remove x-axis labels and ticks when no legend + ax.set_xticks([]) + ax.set_xticklabels([]) + ax.set_xlabel("") + end + + return fig, ax + end + + saveName = (saveName === nothing || isempty(saveName)) ? "AUPR_$(Dates.format(now(), "yyyymmdd_HHMMSS"))" : saveName + # Save plot according to saveLegend + savePath = joinpath(dirOut, saveName * "_$(autoPlotType)_" * (saveLegend ? "withLegend" : "noLegend") * ".pdf") + fig, _ = make_plot(saveLegend) + fig.savefig(savePath, dpi=600) + plt.close(fig) + println("Saved plot to:\n $savePath") +end + + +# function plotAUPR(gsParam::OrderedDict{String,<:AbstractDict}, dirOut::String, saveName::String; figSize::Tuple{Real, Real}) + +# """ +# Generate a visualization of Area Under the Precision-Recall Curve (AUPR) values using either a bar plot or a dot plot, depending on the input data structure. +# - If there are multiple gold standards and networks, a bar plot is created, with each group of bars representing a gold standard and each bar within a group representing a network. +# - If there is only one gold standard or one network, a dot plot is created, with each dot representing a network or gold standard. +# - The plot is saved as a PDF file in the specified `dirOut` directory, with a resolution of 600 DPI. + + +# # Arguments +# - `gsParam::OrderedDict{String,Dict{String,String}}`: Mapping of gold standards to network file paths. +# - `dirOut::String`: Directory to save the plot. +# - `saveName::String`: Base name for the output file. + +# # Keywords +# - `figSize::Tuple{Real, Real}=(5, 5)`: Size of the figure. + +# # Returns +# Nothing, but saves the plot to a file. +# """ + +# function loadAupr(filePath) +# try +# data = load(filePath) +# return data["results"][:auprs] +# catch e +# println("Error loading $filePath: $e") +# return nothing +# end +# end + +# # Check if dirOut is either nothing or an empty string +# dirOut = if dirOut === nothing || isempty(dirOut) +# pwd() +# else +# mkpath(dirOut) +# end + +# if isempty(figSize) || length(figSize) == 1 +# println("Warning: figSize must be a tuple of 2 element") +# figSize = (5,5) +# end + +# axisTitleSize = 16 +# tickLabelSize = 14 +# plotTitleSize = 16 +# legendSize = 10 + +# lineColors = [ #Initial Color palette +# "#377eb8", "#ff7f00", "#4daf4a", "#f781bf", "#a65628", +# "#984ea3", "#e41a1c", "#00ced1", "#000000", +# "#5A9D5A", "#D96D3B", "#FFAD12", "#66628D", "#91569A", +# "#B6742A", "#DD87B4", "#D26D7A", "#dede00"] + +# # Load aupr values +# gsNames = collect(keys(gsParam)) +# netNames = unique(vcat([collect(keys(files)) for files in values(gsParam)]...)) +# # auprValues = [loadAupr(gsParam[gs][netName]) !== nothing ? loadAupr(gsParam[gs][netName]) : 0.0 for gs in gsNames, netName in netNames] + +# auprValues = [] +# # Load AUPR values for each GS and file +# for gs in gsNames +# for netName in netNames +# # Load the AUPR value +# filePath = gsParam[gs][netName] +# aupr = loadAupr(filePath) + +# # Use 0.0 if the AUPR value is missing +# push!(auprValues, aupr === nothing ? 0.0 : aupr) +# end +# end +# # Reshape auprValues into a matrix +# auprMatrix = transpose(reshape(auprValues, length(netNames), length(gsNames))) + +# boolNet = any(length(netNames) > 1 for netNames in values(gsParam)) +# numNet = length(netNames) +# numGS = length(gsNames) +# # = +# # ---- Load AUPR values (Works but not using) +# # auprData = Dict(gs => Dict(name => loadAupr(path) for (name, path) in files) for (gs, files) in gsParam) +# # netNames = unique(vcat([collect(keys(files)) for files in values(gsParam)]...)) +# # auprValues = [get(auprData[gs], netName, 0.0) for gs in gsNames, netName in netNames] +# = # +# if boolNet && numGS > 1 +# # Create a figure +# fig, ax = plt.subplots(figsize=figSize, layout="constrained") +# # Define parameters for grouping: +# barWidth = 0.15 # Width of each bar. +# groupSpacing = 0.4 # Extra space between groups. +# groupWidth = numNet * barWidth # Total width occupied by bars in one group. + +# # Compute positions for each group. +# # Compute a vector where each element is the left edge (or starting point) of each group. +# # Use 0-based indexing for groups. +# groupPositions = [i * (groupWidth + groupSpacing) for i in 0:(numGS-1)] + +# # Now plot each bar (each network) in every group. +# # Use a centered offset for each bar in the group. +# for idx in 1:numNet +# # Calculate the offset to center the bar within the group. +# # (idx - (numNet+1)/2)*barWidth shifts bars so they cluster around the center. +# offset = (idx - (numNet+1)/2)*barWidth +# # The x-positions for the bars are: +# # groupPositions + offset + groupWidth/2 +# # Adding groupWidth/2 centers the bars in each group. +# x_positions = [gp + groupWidth/2 + offset for gp in groupPositions] +# # Plot the bar for curr network across all groups. +# ax.bar(x_positions, auprMatrix[:, idx], barWidth, label=netNames[idx], +# color = lineColors[mod1(idx, length(lineColors))]) +# end + +# # Set the xticks to the center positions of each group. +# groupCenters = [gp + groupWidth/2 for gp in groupPositions] +# ax.set_xticks(groupCenters) +# ax.set_xticklabels(gsNames) +# ax.set_xlabel("Gold Standards", fontsize=axisTitleSize) +# ax.set_ylabel("AUPR", fontsize=axisTitleSize) +# ax.tick_params(axis="both", which="both", labelsize=tickLabelSize) +# ax.legend(fontsize=legendSize, loc="center left", bbox_to_anchor=(1, 0.5)) + +# elseif numGS == 1 || numNet == 1 +# # Dot Plot +# fig, ax = subplots(figsize= figSize, layout="constrained") +# if numGS == 1 +# # One GS, multiple files +# for idx in 1:numNet +# ax.scatter(idx, auprMatrix[idx], color=lineColors[idx], label=netNames[idx]) +# ax.text(idx, auprMatrix[idx], netNames[idx], fontsize=9, ha="right") +# end +# ax.set_xlabel("Network", fontsize=axisTitleSize) +# ax.set_xticks([]) +# ax.set_xticklabels([]) +# # ax.grid(true, which="major", linestyle="-", linewidth=0.5, color="lightgray") +# else +# for idx in 1:numGS +# ax.scatter(idx, auprMatrix[idx], color=lineColors[idx], label=gsNames[idx]) +# ax.text(idx, auprMatrix[idx], gsNames[idx], fontsize=9, ha="right") +# end +# ax.set_xlabel("Gold Standard", fontsize=axisTitleSize) +# ax.set_xticks([]) +# ax.set_xticklabels([]) +# # ax.grid(true, which="major", linestyle="-", linewidth=0.5, color="lightgray") +# end +# end + +# # Common settings for both plots. +# ax.grid(true, which="major", linestyle="-", linewidth=0.5, color="lightgray") +# ax.tick_params(axis="both", which="both", labelsize=tickLabelSize) +# # plt.tight_layout() + + +# if !isempty(saveName) +# plt.savefig(joinpath(dirOut, saveName * "_AUPR.pdf"), dpi=600) +# else +# dateStr = Dates.format(now(), "yyyymmdd_HHMMSS") +# plt..savefig(joinpath(dirOut, dateStr * "_AUPR.pdf"), dpi=600) +# end + +# plt.close("all") +# end + + + + + + +# --------------------------------------------------------------------------------------------------------------------------------------------------------------------- +# ------ Using StatsPlots +# --------------------------------------------------------------------------------------------------------------------------------------------------------------------- +# function plotAUPR(gsParam::OrderedDict{String,<:AbstractDict}, dirOut::String, +# saveName::String, plotType::String = "bar"; +# figSize::Tuple{Real,Real} = (8,5), axisTitleSize::Int = 13, +# tickLabelSize::Int = 10) + +# legendFontSize = 9 + +# # Check if dirOut is either nothing or an empty string +# dirOut = if dirOut === nothing || isempty(dirOut) +# pwd() +# else +# mkpath(dirOut) +# end + +# # Dummy load function – replace with your actual data-loading mechanism. +# function loadAupr(filePath) +# try +# data = load(filePath) +# return data["results"][:auprs] +# catch e +# println("Error loading $filePath: $e") +# return nothing +# end +# end + +# # Initial color palette +# lineColors = ["#377eb8", "#ff7f00", "#4daf4a", "#f781bf", "#a65628", +# "#984ea3", "#e41a1c", "#00ced1", "#000000", +# "#5A9D5A", "#D96D3B", "#FFAD12", "#66628D", "#91569A", +# "#B6742A", "#DD87B4", "#D26D7A", "#dede00"] + +# # Build a DataFrame with columns: xGroups, Network, AUPR +# gsNames = collect(keys(gsParam)) +# netNames = unique(vcat([collect(keys(files)) for files in values(gsParam)]...)) +# if length(netNames) > length(lineColors) +# lineColors = padColors(netNames) # Ensure padColors is defined! +# end + +# dfRows = Vector{NamedTuple{(:xGroups, :Network, :AUPR), Tuple{String,String,Float64}}}() +# for gs in gsNames +# for net in netNames +# filePath = haskey(gsParam[gs], net) ? gsParam[gs][net] : nothing +# temp = filePath === nothing ? nothing : loadAupr(filePath) +# val = (temp === nothing || temp === missing) ? 0.0 : temp +# push!(dfRows, (xGroups = gs, Network = net, AUPR = val)) +# end +# end +# df = DataFrame(dfRows) + +# # Number of unique groups. +# numGS = length(unique(df.xGroups)) +# numNet = length(unique(df.Network)) + +# # Set common labels and legend; use camelCase. +# xLabelVal = (numGS == 1 ? "Network" : "Gold Standards") +# yLabelVal = "AUPR" +# legendPos = :outerright + +# # Common arguments (for scatter and bar, without color arguments). +# commonArgs = (xlabel = xLabelVal, ylabel = yLabelVal, legend = legendPos, +# guidefontsize = axisTitleSize, tickfontsize = tickLabelSize, legendfontsize = legendFontSize, +# framestyle = :box) + +# colors = lineColors[1:numNet] + +# p = nothing +# if lowercase(plotType) == "scatter" +# if numGS == 1 +# p = @df df sp.scatter(:Network, :AUPR; markersize = 6, +# color = colors, xrotation = 45, commonArgs...) +# elseif numNet == 1 +# p = @df df sp.scatter(:xGroups, :AUPR; markersize = 6, +# color = lineColors[1], xrotation = 45, commonArgs...) +# else +# p = @df df sp.scatter(:xGroups, :AUPR; group = :Network, markersize = 6, +# palette = colors, commonArgs...) +# end + +# elseif lowercase(plotType) == "bar" +# if numGS == 1 +# p = @df df sp.bar(:Network, :AUPR; +# color = colors, xrotation = 45, commonArgs...) +# elseif numNet == 1 +# p = @df df sp.bar(:xGroups, :AUPR; +# color = lineColors[1], xrotation = 45, commonArgs...) +# else +# p = @df df sp.groupedbar(:xGroups, :AUPR; group = :Network, +# palette = colors, commonArgs...) +# end +# else +# error("Invalid plot type. Please choose 'scatter' or 'bar'") +# end + +# # Adjust figure size (e.g. figSize = (6,3.5) converts to (600,350) pixels) +# p = sp.plot(p, size = (Int(figSize[1]*100), Int(figSize[2]*100)), titlefontsize = axisTitleSize) + + +# if !isempty(saveName) +# savePath = joinpath(dirOut, saveName * "_AUPR_$(plotType).pdf") +# sp.savefig(p, savePath) +# else +# dateStr = Dates.format(now(), "yyyymmdd_HHMMSS") +# savePath = joinpath(dirOut, dateStr * "_AUPR_$(plotType).pdf") +# sp.savefig(p, savePath) +# end +# end + + + diff --git a/src/03_Metrics/plotting/plotSingleUtils.jl b/src/03_Metrics/plotting/plotSingleUtils.jl new file mode 100755 index 0000000..a202236 --- /dev/null +++ b/src/03_Metrics/plotting/plotSingleUtils.jl @@ -0,0 +1,94 @@ +""" + plotPRCurve(rec, prec, randPR, saveDir; kwargs...) + +Plot and save a Precision-Recall (PR) curve using PyPlot. + +# Arguments +- `rec::AbstractVector` : Recall values. +- `prec::AbstractVector` : Precision values. +- `randPR::Float64` : Random baseline precision (horizontal reference line). +- `saveDir::String` : Directory to save the plot. + +# Keyword Arguments +- `xLimitRecall::Float64=1.0` : Maximum recall shown on the x-axis of diagnostic PR plots. +- `axisTitleSize::Int=16` : Font size for axis titles. +- `tickLabelSize::Int=14` : Font size for tick labels. +- `plotTitleSize::Int=18` : Font size for plot title. +- `baseName=nothing` : Base filename. If `nothing`, a timestamp is used. +""" +function plotPRCurve( + rec::AbstractVector, prec::AbstractVector, randPR::Float64, saveDir::String; + xLimitRecall::Float64 = 1.0, axisTitleSize::Int = 16, + tickLabelSize::Int = 14, plotTitleSize::Int = 18, baseName = nothing) + + if baseName === nothing + baseName = Dates.format(now(), "yyyymmdd_HHMMSS") + end + + if !isempty(rec) && !isempty(prec) && any(prec .> 0) + PyPlot.figure() + PyPlot.axhline(y=randPR, linestyle="-.", color="k") + PyPlot.plot(rec, prec, color="b") + PyPlot.xlabel("Recall", fontsize=axisTitleSize) + PyPlot.ylabel("Precision", fontsize=axisTitleSize) + PyPlot.title("PR Curve: $baseName", fontsize=plotTitleSize) + PyPlot.xlim(0, xLimitRecall) + PyPlot.ylim(0, 1) + PyPlot.grid(true, which="major", linestyle="--", linewidth=0.75, color="gray") + PyPlot.minorticks_on() + PyPlot.grid(true, which="minor", linestyle=":", linewidth=0.5, color="lightgray") + PyPlot.tick_params(axis="both", which="major", labelsize=tickLabelSize) + PyPlot.tick_params(axis="both", which="minor", labelsize=tickLabelSize - 2) + PyPlot.savefig(joinpath(saveDir, baseName * "_PR.png"), dpi=600) + PyPlot.close() + else + @warn "Skipping PR plot for $baseName: empty or all-zero precision/recall vectors" + end +end + + +""" + plotROCCurve(fpr, tpr, saveDir; kwargs...) + +Plot and save a ROC curve using PyPlot. + +# Arguments +- `fpr::AbstractVector` : False positive rate values (x-axis). +- `tpr::AbstractVector` : True positive rate / recall values (y-axis). +- `saveDir::String` : Directory to save the plot. + +# Keyword Arguments +- `axisTitleSize::Int=16` : Font size for axis titles. +- `tickLabelSize::Int=14` : Font size for tick labels. +- `plotTitleSize::Int=18` : Font size for plot title. +- `baseName=nothing` : Base filename. If `nothing`, a timestamp is used. +""" +function plotROCCurve( + fpr::AbstractVector, tpr::AbstractVector, saveDir::String; + axisTitleSize::Int = 16, tickLabelSize::Int = 14, + plotTitleSize::Int = 18, baseName = nothing) + + if baseName === nothing + baseName = Dates.format(now(), "yyyymmdd_HHMMSS") + end + + if !isempty(fpr) && !isempty(tpr) && (maximum(tpr) > 0 || maximum(fpr) > 0) + PyPlot.figure() + PyPlot.plot([0, 1], [0, 1], linestyle="--", color="k") + PyPlot.plot(fpr, tpr, color="b") + PyPlot.xlabel("FPR", fontsize=axisTitleSize) + PyPlot.ylabel("TPR", fontsize=axisTitleSize) + PyPlot.title("ROC Curve: $baseName", fontsize=plotTitleSize) + PyPlot.xlim(0, 1) + PyPlot.ylim(0, 1) + PyPlot.grid(true, which="major", linestyle="--", linewidth=0.75, color="gray") + PyPlot.minorticks_on() + PyPlot.grid(true, which="minor", linestyle=":", linewidth=0.5, color="lightgray") + PyPlot.tick_params(axis="both", which="major", labelsize=tickLabelSize) + PyPlot.tick_params(axis="both", which="minor", labelsize=tickLabelSize - 2) + PyPlot.savefig(joinpath(saveDir, baseName * "_ROC.png"), dpi=600) + PyPlot.close() + else + @warn "Skipping ROC plot for $baseName: empty or all-zero TPR/FPR vectors" + end +end \ No newline at end of file diff --git a/src/03_Metrics/utils.jl b/src/03_Metrics/utils.jl new file mode 100755 index 0000000..cf53659 --- /dev/null +++ b/src/03_Metrics/utils.jl @@ -0,0 +1,96 @@ +""" + adaptiveStep(xs; target_points=1000, min_step=1e-4, method=:min_gap) + +Select a step size for interpolation based on actual values in `xs`. + +# Arguments +- `xs`: Array of arrays (per TF) or single array of values. +- `target_points`: Desired number of points if `method=:target_points`. +- `min_step`: Minimum allowable step size. +- `method`: `:min_gap` uses smallest nonzero gap; `:target_points` uses range/target_points. +""" +function adaptiveStep(xs; target_points=1000, min_step=1e-4, method=:min_gap) + vals = isa(xs[1], AbstractArray) ? reduce(vcat, xs) : xs + vals = sort(unique(vals)) + + if method == :min_gap + gaps = diff(vals) + nonzero_gaps = filter(g -> g > 0, gaps) + step = isempty(nonzero_gaps) ? min_step : max(minimum(nonzero_gaps), min_step) + elseif method == :target_points + range_span = maximum(vals) - minimum(vals) + step = max(range_span / target_points, min_step) + else + error("Unknown method: $method") + end + + return step +end + + +""" + dedupNInterpolate(xs, ys, interpPts) + +Linearly interpolate `ys` onto `interpPts`, handling duplicates and invalid values. + +Filters `NaN`/`Inf`, sorts by `xs`, collapses duplicate `xs` by keeping max `y`, +then interpolates using `Flat()` extrapolation. Returns zeros if fewer than 2 valid points. +""" +function dedupNInterpolate(xs::Vector{Float64}, ys::Vector{Float64}, interpPts::AbstractVector{Float64}) + # Filter out invalid values + valid_inds = isfinite.(xs) .& isfinite.(ys) + xs = xs[valid_inds] + ys = ys[valid_inds] + + # Check if there are enough points to interpolate + if length(xs) < 2 + return zeros(length(interpPts)) + end + + # Sort xs and corresponding ys + perm = sortperm(xs) + xsSorted = xs[perm] + ysSorted = ys[perm] + + # Collapse duplicates: keep max y for each unique x + dict = Dict{Float64, Float64}() + for (x, y) in zip(xsSorted, ysSorted) + dict[x] = haskey(dict, x) ? max(dict[x], y) : y + end + + xsUnique = sort(collect(keys(dict))) + ysUnique = [dict[x] for x in xsUnique] + + # Handle edge case: all ys are identical (LinearInterpolation still works) + itp = LinearInterpolation(xsUnique, ysUnique, extrapolation_bc=Flat()) + return itp.(interpPts) +end + +""" + validateColumns(cols, required, filename) + +Check that all `required` column names are present in `cols`. +Throws an informative error naming the missing columns if any are absent. + +usage: +validateColumns(Symbol.(propertynames(gsData)), GS_REQUIRED_COLS, gsFile) +""" +# function validateColumns(cols, required, filename) +# missing_cols = setdiff(required, cols) +# if !isempty(missing_cols) +# error(""" +# Missing columns in $filename: $(join(missing_cols, ", ")) +# Required columns: $(join(required, ", ")) +# Found columns: $(join(cols, ", ")) +# """) +# end +# end + +function validateColumnCount(data, filename) + if length(propertynames(data)) < MIN_REQUIRED_COLS + error(""" + $filename must have at least 3 columns in order: TF, Target, Score + Found: $(length(propertynames(data))) column(s) + """) + end +end \ No newline at end of file diff --git a/src/Inferelator.jl b/src/Inferelator.jl new file mode 100755 index 0000000..08c8253 --- /dev/null +++ b/src/Inferelator.jl @@ -0,0 +1,94 @@ +module InferelatorJL + +# ========================= +# Include source files +# ========================= +include("00_Data/geneExpression.jl") +include("Utils/dataUtils.jl") +include("01_Prior/mergeDegenerateTFs.jl") +include("00_Data/priorTFA.jl") +include("Utils/networkIO.jl") +include("02_GRN/GRN.jl") + +# include("Utils/partialCorrelation.jl") + +# ========================= +# Helper function to include all files in a folder +# ========================= +# function include_all(dir::String) +# for file in sort(filter(f -> endswith(f, ".jl"), readdir(dir, join=true))) +# include(file) +# end +# end + +# # ---------------------------------- +# # Include submodules +# # ---------------------------------- + +# # Base data and utils +# include_all(joinpath(@__DIR__, "00_Data")) +# include_all(joinpath(@__DIR__, "Utils")) +# include_all(joinpath(@__DIR__, "01_Prior")) +# include_all(joinpath(@__DIR__, "02_GRN")) + +# --------------------------------------------------------------------------------- +# Bring submodules into module namespace +# --------------------------------------------------------------------------------- +using .Data +using .DataUtils +using .MergeDegenerate +using .PriorTFA +using .NetworkIO +using .GRN + +# --------------------------- +# Public API +# --------------------------- +export + # Core structs + GeneExpressionData, + mergedTFsResult, + PriorTFAData, + GrnData, + BuildGrn, + + # Data loading + loadExpressionData!, + loadAndFilterTargetGenes!, + loadPotentialRegulators!, + processTFAGenes!, + + # Prior / TFA + # MergeDegenerate, + processPriorFile!, + mergeDegenerateTFs, + calculateTFA!, + + # Data utilities + convertToLong, + convertToWide, + frobeniusNormalize, + completeDF, + mergeDFs, + check_column_norms, + writeTSVWithEmptyFirstHeader, + binarizeNumeric!, + + # GRN building + preparePredictorMat!, + preparePenaltyMatrix!, + constructSubsamples, + bstarsWarmStart, + bstartsEstimateInstability, + chooseLambda!, + rankEdges!, + + # Output + writeNetworkTable!, + combineGRNs, + combineGRNS2, + + #I/O + saveData, writeNetworkTable! + +end \ No newline at end of file diff --git a/src/Utils/dataUtils.jl b/src/Utils/dataUtils.jl new file mode 100755 index 0000000..956a7f9 --- /dev/null +++ b/src/Utils/dataUtils.jl @@ -0,0 +1,338 @@ +module DataUtils + + using DataFrames + using CSV + using LinearAlgebra + using TickTock + using FileIO + + export convertToLong, convertToWide, frobeniusNormalize, completeDF, + mergeDFs, check_column_norms, writeTSVWithEmptyFirstHeader, binarizeNumeric! + + # Convert Wide/long data to long/wide + function convertToLong(data) + #dfs = [convertToLong(df) for df in dfs] + if ncol(data) > 3 + return stack(data, Not(1) ) + else + return data + end + end + + function convertToWide(Data; indices::Union{Nothing, NTuple{3, Int}}=nothing) + """ + Converts a 3‑column long-format DataFrame to wide‑format using the unstack function. + If the input DataFrame has exactly 3 columns, the conversion is performed. + + - Data columns is a wide matrix with columns as TF and rows as target genes or + a long data with columns in the order TF, Gene, Weights. + indices::Union{Nothing, NTuple{3, Int}} = nothing: + A tuple specifying the column indices in the order (pivot, key, value). + - pivot: The column that provides the row identifier. + - key: The column whose unique values will become new column names. + - value: The column from which the cell values are taken. + + If no indices are provided, the function defaults to (1, 2, 3). + If the DataFrame has more than 3 columns, the original DataFrame is returned. + If the DataFrame has less than 3 columns, an error is thrown. + + # USAGE + dfs = [convertToWide(df) for df in dfs] + dfs = [convertToWide(df; indices = (1,2,3)) for df in dfs] + """ + + ncols = ncol(Data) + + if ncols < 3 + error("DataFrame has less than 3 columns. A 3‑column DataFrame or a wide-matrix is required.") + elseif ncols > 3 + # More than 3 columns: return data unchanged. + return Data + else # Exactly 3 columns + # Use provided indices, or default to (2, 1, 3) + inds = isnothing(indices) ? (2,1,3) : indices + # Convert the specified columns to Symbols for unstack. + idSym = Symbol(names(Data)[inds[1]]) + keySym = Symbol(names(Data)[inds[2]]) + valueSym = Symbol(names(Data)[inds[3]]) + + return unstack(Data, idSym, keySym, valueSym) + end + end + + + + function frobeniusNormalize(df::DataFrame, dims::Symbol =:column) + + """ + frobeniusNormalize(df::DataFrame; dims::Symbol = :row) + + Normalize a DataFrame based on the Frobenius (L2) norm. + + • If dims = :row (default), it normalizes each row (ignoring the first column). + • If dims = :column, it normalizes each column (ignoring the first column). + + The first column is assumed to contain non‐numeric data (e.g., row identifiers) + and is left unchanged. + """ + # Create a copy so that the original DataFrame remains unchanged. + dfNorm = deepcopy(df) + dfMat = Matrix(dfNorm[!, 2:end]) # Extract numerical part + dfMat = convert(Matrix{Float64}, dfMat) # Ensure it's Float64 + + if dims == :row + # Normalize each row. + for i in 1:size(dfMat, 1) + nrm = norm(dfMat[i, :], 2) # Compute the L2 norm for the row. + if nrm ≠ 0 + dfMat[i, :] ./= nrm + end + end + elseif dims == :column + # Normalize each column. + for j in 1:size(dfMat, 2) + nrm = norm(dfMat[:, j], 2) # Compute the L2 norm for the column. + if nrm ≠ 0 + dfMat[:, j] ./= nrm + end + end + else + throw(ArgumentError("dims must be :row or :column")) + end + + # Update the DataFrame with the normalized numeric values. + dfNorm[!, 2:end] .= dfMat + return dfNorm + end + + + + function completeDF(df::DataFrame, id::Symbol, idsALL, allCols) + """ + completeDF(df::DataFrame, id::Symbol, idsALL, allCols) + + Aligns a DataFrame to a common set of row identifiers and columns. + + ### Arguments: + - `df::DataFrame`: The input DataFrame to align. + - `id::Symbol`: The identifier column (e.g., gene or sample ID). + - `idsALL`: A vector of all unique row identifiers across multiple DataFrames. + - `allCols`: A vector of all unique column names across multiple DataFrames. + + ### Returns: + - A new DataFrame with: + - Rows corresponding to `idsALL` (missing rows filled with `missing`). + - Columns matching `allCols` (missing columns filled with `missing`). + - The original values retained where available. + + """ + dfNew = DataFrame() + dfNew[!, id] = idsALL # Ensure all IDs are included + + for col in allCols + if col in Symbol.(names(df)) + mapping = Dict(row[id] => row[col] for row in eachrow(df)) # Store existing values + dfNew[!, col] = [get(mapping, rid, missing) for rid in idsALL] # Align data + else + dfNew[!, col] = fill(missing, length(idsALL)) # Fill missing columns + end + end + return dfNew + end + + + function mergeDFs(dfs::Vector{DataFrame}, id::Symbol = :Gene, option::String = "sum") + """ + "Merges a vector of DataFrames by summing or averaging them cell-wise. + + Arguments: + - dfs::Vector{DataFrame}: A vector of DataFrames to merge. + - id::Symbol: The identifier column (common to every DataFrame). + - option::String: The operation to perform: either "sum" or "avg". + + Returns: + A DataFrame where the first column is the identifier and the remaining columns are the cell-wise + sum or average (depending on the option) of the numeric values from all data frames (with columns sorted in alphabetical order). + """ + + # 1. Compute the full set of row identifiers + idsALL = reduce(union, [unique(df[!, id]) for df in dfs]) + sort!(idsALL) + + # 2. Compute the full set of numeric columns + allCols = reduce(union, [setdiff(names(df), [string(id)]) for df in dfs]) + allCols = sort(Symbol.(allCols)) + + # 3. Complete all DataFrames to have the same structure + completed_dfs = [completeDF(df, id, idsALL, allCols) for df in dfs] + + # 4. Initialize matrices for sum and count tracking + nRows, nCols = length(idsALL), length(allCols) + sumMat = zeros(nRows, nCols) + countMat = zeros(Int, nRows, nCols) + + # 5. Compute sum and count matrices + for df in completed_dfs + mat = Matrix(df[!, allCols]) + mat = coalesce.(mat, 0.0) + sumMat .+= mat + countMat .+= (mat .!= 0) # Count nonzero interactions + end + + # 6. Compute sum or average + resultMat = option == "avg" ? sumMat ./ max.(countMat, 1) : sumMat + + # 7. Create the merged DataFrame + merged = DataFrame(id => idsALL) + for (j, col) in enumerate(allCols) + merged[!, col] = resultMat[:, j] + end + + return merged + end + + + function check_column_norms(df::DataFrame; atol=1e-8) + + """ + Checks if the columns are of length 1 (L2 Norm). Returns the number of true and false cases + Checks if each numeric column (except the first) is L2 normalized (norm approx 1). + Returns a Dict of column name => Bool, and counts of true and false cases. + """ + details = Dict{String,Bool}() + count_true = 0 + count_false = 0 + + for col in names(df)[2:end] + # Check if the column is numeric by testing its element type. + if !(eltype(df[!, col]) <: Number) + println("Column: ", col, " is non-numeric, skipping...") + continue + end + # Convert the column values to Float64. + values = Float64.(df[!, col]) + # Compute the L₂ norm. + col_norm = norm(values, 2) + # Check whether the norm is approximately 1. + is_norm_one = isapprox(col_norm, 1.0; atol=atol) + + details[string(col)] = is_norm_one + if is_norm_one + count_true += 1 + else + count_false += 1 + end + # println("Column: ", col, " Norm: ", col_norm, " ~ 1? ", is_norm_one) + end + + println("Total numeric columns normalized (True): ", count_true) + println("Total numeric columns not normalized (False): ", count_false) + return details, count_true, count_false + end + + function writeTSVWithEmptyFirstHeader(df::DataFrame, filepath::String; delim::Char='\t') + open(filepath, "w") do io + # Create custom header: + # Make the first header cell an empty string. + # The remaining header cells are the remaining column names. + hdr = [""; names(df)[2:end]] + # Write the header row using the delimiter. + println(io, join(hdr, delim)) + + # Write each row. Each row is converted into strings. + for row in eachrow(df) + # Convert every element of the row to a string. + row_values = [string(x) for x in collect(row)] + println(io, join(row_values, delim)) + end + end + end + + function binarizeNumeric!(data) + if isa(data, DataFrame) + for col in names(data) + if eltype(data[!, col]) <: Number + data[!, col] .= Int.(data[!, col] .!= 0) + end + end + return data + elseif isa(data, AbstractMatrix) + data .= Int.(data .!= 0) + return data + else + throw(ArgumentError("Input must be DataFrame or matrix")) + end + end + +end + +# function binarizeNonZero!(data) +# if isa(data, DataFrame) +# for c in names(data) +# if eltype(data[!, c]) <: Number +# data[!, c] .= ifelse.(data[!, c] .!= 0, 1, 0) +# end +# end +# elseif isa(data, AbstractMatrix) +# data .= ifelse.(data .!= 0, 1, 0) +# else +# throw(ArgumentError("Input must be a DataFrame or a Matrix")) +# end +# return data +# end + + +#= +# Test workflow with small data + +Example DataFrames with row names (represented as the first column in the DataFrame) +df1 = DataFrame(RowName = ["W", "X"], A = [1, 2], B = [3, 4], C = [5, 6]) +df2 = DataFrame(Gene = ["Y", "X"], B = [7, 8], C = [9, 10], D = [11, 12]) +df3 = DataFrame(Blue = ["P", "Q"], A = [13, 14], D = [15, 16], E = [17,18]) + +# # # # List of all DataFrames you want to combine +dfs = [df1, df2, df3] +dfs = [convertToLong(df) for df in dfs] +dfs = [convertToWide(df; indices = (1,2,3)) for df in dfs] + +tick() +dfNorm = [frobeniusNormalize1(df, :row) for df in dfs] +tock() + +commonID = :Gene +dfNorm = [rename!(df, names(df)[1] => commonID) for df in dfNorm] + +tick() +mergeDFs(dfNorm, :Gene, "sum") +tock() +=# + + +#Compute Frobenius norm for each row (excluding the first column) +# norm_dfs = [DataFrame(A = df[!, 1], FrobeniusNorm = [norm(row[2:end], 2) for row in eachrow(df)]) for df in dfs] # row +# norm_dfs = [DataFrame(Column = names(df)[2:end], FrobeniusNorm = [ norm(col, 2) for col in eachcol(df[!, 2:end]) ]) for df in dfs] # col + + +# df1 = "/data/miraldiNB/Michael/projects/GRN/hCD4T_Katko/dataBank/Priors/SCENICp/majorCellType/tf2gene_only_TF2G_wide_binary.tsv" +# df1 = CSV.read(df1, DataFrame; delim = "\t") +# df1Norm = frobeniusNormalize(df1, :column) +# check_column_norms(df1Norm; atol=1e-3) + +# df2 = "/data/miraldiNB/Michael/projects/GRN/hCD4T_Katko/dataBank/Priors/MotifScan5kbTSS_b.tsv" +# df2 = CSV.read(df2, DataFrame; delim = "\t") +# check_column_norms(df2; atol=1e-8) + +# dfs = [df1, df2] +# dfs = [CSV.read(df1, DataFrame; delim = "\t") for df in dfpath] +# merged = mergeDFs(dfs, :Column1, "sum") +# mergedNorm = frobeniusNormalize(merged, :column) +# check_column_norms(mergedNorm; atol=1e-8) +# maximum(Matrix(merged_b[:, 2:end])) + +# merged_b = binarizeNonZero!(merged) +# # order = sortperm(merged_b[:, "Column1"], rev = false) +# # merged_b = merged_b[order, :] +# writeTSVWithEmptyFirstHeader(merged_b, "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/Priors/tf2gene_5kbTSS_b.tsv"; delim='\t') + + + diff --git a/src/Utils/installPackages.jl b/src/Utils/installPackages.jl new file mode 100755 index 0000000..75e7849 --- /dev/null +++ b/src/Utils/installPackages.jl @@ -0,0 +1,35 @@ +# Pkg module for managing packages +using Pkg +# Define the path to the file containing required package names +package_file = "/data/miraldiNB/Michael/Scripts/GRN/InferelatorJL/Packages.txt" + +# Read package names from the file +required_packages = [] +if isfile(package_file) + # Open the file and read package names + open(package_file) do file + for line in eachline(file) + line = strip(line) + if !isempty(line) + push!(required_packages, line) + end + end + end +else + println("Error: Package requirements file '$package_file' not found.") + exit(1) +end + +# deps = keys(Pkg.project().dependencies) + +# Check and install required packages +for pkg in required_packages + # if !(pkg in deps) + try + eval(Meta.parse("using $pkg")) # check if package already exist by loading + catch + println("Installing $pkg...") #install package if not installed + Pkg.add(pkg) + eval(Meta.parse("using $pkg")) # Load the package after installation + end +end diff --git a/src/Utils/networkIO.jl b/src/Utils/networkIO.jl new file mode 100755 index 0000000..d4ed020 --- /dev/null +++ b/src/Utils/networkIO.jl @@ -0,0 +1,34 @@ +module NetworkIO + + using JLD2 + using DelimitedFiles + using Printf + using Dates + + export saveData, writeNetworkTable! + + # Save all core structs + function saveData(expressionData, tfaData, grnData, buildGrn, outputDir::String, fileName::String) + output = joinpath(outputDir, fileName) + @save output expressionData tfaData grnData buildGrn + end + + # Write network tables + function writeNetworkTable!(buildGrn; outputDir::String, networkName::Union{String, Nothing}=nothing) + baseName = (networkName === nothing || isempty(networkName)) ? "edges" : networkName * "edges" + + outputFile = joinpath(outputDir, baseName * ".tsv") + colNames = "TF\tGene\tsignedQuantile\tStability\tCorrelation\tinPrior\n" + open(outputFile, "w") do io + write(io, colNames) + writedlm(io, buildGrn.networkMat) + end + + outputFileSubset = joinpath(outputDir, baseName * "_subset.tsv") + open(outputFileSubset, "w") do io + write(io, colNames) + writedlm(io, buildGrn.networkMatSubset) + end + end + +end \ No newline at end of file diff --git a/src/Utils/partialCorrelation.jl b/src/Utils/partialCorrelation.jl new file mode 100755 index 0000000..be8c2a6 --- /dev/null +++ b/src/Utils/partialCorrelation.jl @@ -0,0 +1,96 @@ + +# Method 1: Matrix Inversion Method: +# this methods computes partial correlation from precision-matrix (the inverse of variance-covariance matrix) +using LinearAlgebra +function partialCorrelationMat(X::Matrix{Float64}; epsilon = 1e-7, first_vs_all = false) + # Mean centering of the columns (mean subtraction) + X_centered = X .- mean(X, dims=1) + + # Compute the covariance matrix + sigma = cov(X_centered) + # regularizing the covariance matrix to avoid ill-conditining and singularity + sigma = sigma + epsilon * I + + # Precision matrix (inverse of the covariance matrix) + theta = inv(sigma) + p = size(X, 2) # number of features/predictors/explanatory variables + + if first_vs_all + # Compute partial correlation for the first variable vs all others + P = ones(1, p) # Initialize the partial correlation matrix + for j in 2:p + P[j] = -theta[1, j] / sqrt(theta[1, 1] * theta[j, j]) + end + else + # Compute the full partial correlation matrix + P = ones(p, p) # Initialize the partial correlation matrix + for i in 1:(p-1) + for j in (i+1):p + P[i , j] = -theta[i, j] / sqrt(theta[i, i] * theta[j, j]) + P[j, i] = P[i, j] # Symmetry + end + end + end + + return P +end + + +# Method 2: Using Regression +using GLM, Statistics, DataFrames + +function partialCorrReg(X::Matrix{Float64}; first_vs_all = false) + p = size(X, 2) + P = ones(p, p) + + # Convert the matrix to a DataFrame for easier variable handling + df = DataFrame(X, :auto); + col_names = names(df) + + if first_vs_all + P = ones(1, p) + for j in 2:p + keep_indices = setdiff(2:p, j) + covariates = col_names[keep_indices] # All columns except i and j + + # First, regress the first covariate on all other features except + model_1 = lm(term.(col_names[1]) ~ sum(term.(covariates)), df) + res_1 = residuals(model_1) # Get residuals for 1 + + # Then, regress j on all others except i + model_j = lm(term.(col_names[j]) ~ sum(term.(covariates)), df) + res_j = residuals(model_j) # Get residuals for j + + # Compute c1rrelation between the residuals + r = cor(res_1, res_j) + P[j] = r + end + else + P = ones(p, p) + for i in 1:p + for j in (i+1):p + + keep_indices = setdiff(1:p, [i ,j]) + covariates = col_names[keep_indices] # All columns except i and j + + # reference: https://discourse.julialang.org/t/using-all-independent-variables-with-formula-in-a-multiple-linear-model/43691/4 + + # First, regress i on all other features except j (i is the response) + model_i = lm(term.(col_names[i]) ~ sum(term.(covariates)), df) + res_i = residuals(model_i) # Get residuals for i + + # Then, regress j on all others except i + model_j = lm(term.(col_names[j]) ~ sum(term.(covariates)), df) + res_j = residuals(model_j) # Get residuals for j + + # Compute correlation between the residuals + r = cor(res_i, res_j) + P[i, j] = r + P[j, i] = r # Symmetric matrix + end + end + end + return P +end + + From dc56d7596ae5dd50e61ce0ac2381020488d18fce Mon Sep 17 00:00:00 2001 From: Seyifunmi Owoeye Date: Sat, 28 Mar 2026 22:54:51 -0400 Subject: [PATCH 2/6] Add evaluation pipeline (R-based GRN benchmarking tools) --- .DS_Store | Bin 10244 -> 0 bytes evaluation/R/ComparePrior.R | 33 ++ evaluation/R/DeltaTFA.R | 376 +++++++++++++ evaluation/R/GRN_Tfh10_PR_heatmap.R | 444 ++++++++++++++++ evaluation/R/TFA_Viz.R | 245 +++++++++ evaluation/R/evaluateNetUtils.R | 644 ++++++++++++++++++++++ evaluation/R/evaluateNetworks.R | 311 +++++++++++ evaluation/R/evaluateNetworks1.R | 708 +++++++++++++++++++++++++ evaluation/R/histogramConfidences.R | 194 +++++++ evaluation/R/saveNormCountsArrowFIle.R | 35 ++ src/.DS_Store | Bin 14340 -> 0 bytes src/03_Metrics/.DS_Store | Bin 6148 -> 0 bytes 12 files changed, 2990 insertions(+) delete mode 100755 .DS_Store create mode 100755 evaluation/R/ComparePrior.R create mode 100755 evaluation/R/DeltaTFA.R create mode 100755 evaluation/R/GRN_Tfh10_PR_heatmap.R create mode 100755 evaluation/R/TFA_Viz.R create mode 100755 evaluation/R/evaluateNetUtils.R create mode 100755 evaluation/R/evaluateNetworks.R create mode 100755 evaluation/R/evaluateNetworks1.R create mode 100755 evaluation/R/histogramConfidences.R create mode 100755 evaluation/R/saveNormCountsArrowFIle.R delete mode 100755 src/.DS_Store delete mode 100755 src/03_Metrics/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100755 index 49f7ee53e44778925011b721ec4b5baa7ba9a653..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10244 zcmeHMTWl0n7(V~Bv@>*+=?z$}14V9DXek$q0^9APKnpEx=@n>acSkxhJG1W0Zi}_h z7mZ+4;)B;m@P0`M0#S(()PN7ZG!1HaFvd$l0>=1Y;)BNjoHJW@TbuBLQ8Fi)^Pl<8 z|DW@pZ@zzK&sjnUgi^6;LR3PCh!>TLjMYCB+E1@ZMesB;Q3CoCVvsb^%>&h~QG1fL zukaD@5%3Z45%3Z45%@14fZuFd#3C;J+DE`gz(-&l0s4MW@uD*5$q_Eus{<>x1wfgP zYF4m~`vB&ndNSzA5iXf4j>&fq;JJdg#Q^S({VFv_8T8}`m)xBIcPHS@4Bid}*zD-1 zGIIh!F8$g^z(-&*0(9?QNHmfpQPQ8ie~%}!j3-#c?Ql3^+7Yx6um2Ig-gf>{-Q;oN zcI+ms68-eFV8U!$$sooQCkbK_J?2Esh;5rR^9xB%7e8vqIM)?Sw#W|k-IJ&KM^Ag9 z=8Tga>eN=bp*G`WL?$|hn!q}V;Kfl|#Kq9f+{mcu)VA3%F8J>GYaX0aj-4It&DiJ0 z&0@(T4t?a20Vm--$a9f|X*h0G)fEu(@~0IPilSH~?rZJ02U=atO*tVg*+cKz@KHoE zZ7tGn=mWhSTTDbWT&i{!+cwLhbT!>uQF?BsJg1QQmw8U zu2hF=*Hl-k8#b&N9+t%Niq-eE^d2I9)2qyI5Q^H?O54yCs)Abs3 z%p9xEZpTi#^=#Zu1E-?qy-Eq4t|>vsaoY8ygHn>-n=yO-;-z<2t=qUIysfc$OeroY zEt6-;-3Ykl8V?zIvP*NKx)n351A7LHnA_ixHXI{j>TQ-5*CpkwG*((ttOVu+=7~y$ z*qMqLkE9Iu&{%14i6RHoMUtqT*&D-U?X+Wbz!>WZ1k}4kv4`sGlRDO1EeBStLAEaa@kW z)qzmGC@Pot)43Zp$8AdJ)_7E*2Su^l>DQ8a(*UwE(VS!LB)Cg;Zb;5dKi?B`DEwl^0 zd<8-#759#>h&2TI!N}yDllI;#!i6caOWnR>M?>SzUH^yd`ep{pr}CMzW-p>iR=c@= zoQs^j8Ny~K0^@#8#xU+Pa!e#`V9wlm^W}x=9!w5>aH>>Q3TYaMW7K-FyhK%|Vfygf zW%3GDDZtd>x0UKDRS_^_`E9jYBP)5Bj}r4;udb365c$nU`F>f+$IO(*=FOQKkmb4% z=IL35Fw?O5fU3|myM=_mLcS$Gk{jfABzzH4y&RUnJx~LipdK1vCsKSjv>?Iv!G0vU zh9ozUIzZ44#6gk?t?ROGx%tknFF)>+mML1*hSCI0NV41GosE!RK%Z zF2ffv3RmF=xCXyYpmoy(TIc;u>+BEp;p0t5Ti#|rCOpz-f0ifr=6r;w_0dT@zjSNT zGHvSsYBH%sd;Q`g;3ME8;3ME8aBC2tUp6c%J^$Z2_5c6O$gg|^d<1S!1Ry`$8V=#w zruz9w&)QYI`tYKK+Kq6@T(Dxt@hI7GJl@dbc(yG@owpBWenHez{}C>+h2>xWGk~58 R`1^l<|9_LGYq#V6|1UH=MKu5b diff --git a/evaluation/R/ComparePrior.R b/evaluation/R/ComparePrior.R new file mode 100755 index 0000000..e0163aa --- /dev/null +++ b/evaluation/R/ComparePrior.R @@ -0,0 +1,33 @@ +library(ggplot2) + +prior1 <- read.table("/data/miraldiNB/Katko/Projects/Barski_CD4_Multiome/Outs/TRAC_loop/Prior/Prior_sum.tsv") +prior2 <- read.table("/data/miraldiNB/Katko/Projects/Barski_CD4_Multiome/Outs/Seurat/Prior/MEMT_050723_FIMOp5_b.tsv") + +index <- intersect(rownames(prior1), rownames(prior2)) +prior1 <- prior1[index,] +prior2 <- prior2[index,] + +prior_df <- as.matrix(rep(0, length(colnames(prior1)))) +prior_df <- cbind(prior_df, rep(0, length(colnames(prior2)))) +rownames(prior_df) <- colnames(prior1) +colnames(prior_df) <- c("TRAC","Body") + +for(i in 1:length(colnames(prior1))){ + prior1_targets <- length(which(prior1[,i] > 0)) + prior2_targets <- length(which(prior2[,i] > 0)) + prior_df[i,1] <- prior1_targets + prior_df[i,2] <- prior2_targets +} + +prior_df <- as.data.frame(prior_df) +pdf("Prior_Scatter.pdf", width = 8, height = 8) +ggplot(prior_df, aes(x = TRAC, y = Body)) + geom_point() + geom_abline(slope=1, intercept=0) +dev.off() + +prior_df_hist <- data.frame(Freq = c(prior_df[,1], prior_df[,2])) +prior_df_hist$TF <- c(rownames(prior_df), rownames(prior_df)) +prior_df_hist$Prior <- c(rep("Prior1", length(colnames(prior1))), rep("Prior2", length(colnames(prior2)))) + +pdf("Prior_hist.pdf", width = 8, height = 8) +ggplot(prior_df_hist, aes(x = Freq, color = Prior)) + geom_histogram(fill = "white", position = "identity", alpha = 0.7) +dev.off() \ No newline at end of file diff --git a/evaluation/R/DeltaTFA.R b/evaluation/R/DeltaTFA.R new file mode 100755 index 0000000..50ac71d --- /dev/null +++ b/evaluation/R/DeltaTFA.R @@ -0,0 +1,376 @@ +library(biomaRt) +library(stringr) +library(DESeq2) + + +counts <- read.table("GSE271788_dedup_counts.txt", header = T) +rownames(counts) <- counts[,1] +counts <- counts[,7:length(counts)] + +new_names <- str_extract(colnames(counts), "Donor_\\d+_[A-Za-z0-9]+") +new_names <- sapply(strsplit(new_names, "_"), function(x) paste(x[3], x[1], x[2], sep = "_")) +colnames(counts) <- new_names + +ensembl_ids_clean <- sub("\\..*", "", rownames(counts)) +gtf <- import("gencode.v48.chr_patch_hapl_scaff.annotation.gtf.gz") +# Keep only gene entries +genes <- gtf[gtf$type == "gene"] + +# Build mapping table +mapping <- data.frame( + ensembl_gene_id = gsub("\\..*", "", genes$gene_id), # remove version + gene_name = genes$gene_name +) %>% + distinct() +symbol_map <- mapping$gene_name[match(gene_ids, mapping$ensembl_gene_id)] +symbol_map[is.na(symbol_map)] <- gene_ids[is.na(symbol_map)] + + +gene_ids <- gsub("\\..*", "", rownames(counts)) # remove versions +symbol_map <- mapping$gene_name[match(gene_ids, mapping$ensembl_gene_id)] +rownames(counts) <- ifelse(is.na(symbol_map), gene_ids, symbol_map) +counts <- counts[!grepl("^ENSG", rownames(counts)), ] + +# Build metadata dataframe +colnames(counts) <- make.unique(colnames(counts)) + +clean_names <- sub("\\.\\d+$", "", colnames(counts)) + +# Split by underscore +split_info <- do.call(rbind, strsplit(clean_names, "_")) + +# Build metadata dataframe +metadata <- data.frame( + TF = split_info[, 1], + Donor = paste(split_info[, 2], split_info[, 3], sep = "_"), + row.names = colnames(counts), + stringsAsFactors = FALSE +) + +dds <- DESeqDataSetFromMatrix( + countData = counts, + colData = metadata, + design = ~ Donor + TF +) +dds <- DESeq(dds) + +#cor_matrix <- dcast(network, Gene ~ TF, value.var = "signedQuantile", fill = 0) + + +### Plot TFA changes +library(tidyverse) +bulk_cor <- read.table("/data/miraldiNB/Katko/Projects/Julia/Inferelator_Julia/outputs/subNetworks/FullPseudobulk/FullPseudobulk/lambda0p5_200totSS_20tfsPerGene_subsamplePCT63/Combined/TFA_cor.txt") +bulk_quant <- read.table("/data/miraldiNB/Katko/Projects/Julia/Inferelator_Julia/outputs/subNetworks/FullPseudobulk/FullPseudobulk/lambda0p5_200totSS_20tfsPerGene_subsamplePCT63/Combined/TFA_quant.txt") +bulk_sign <- read.table("/data/miraldiNB/Katko/Projects/Julia/Inferelator_Julia/outputs/subNetworks/FullPseudobulk/FullPseudobulk/lambda0p5_200totSS_20tfsPerGene_subsamplePCT63/Combined/TFA_sign.txt") +sc_cor <- read.table("/data/miraldiNB/Katko/Projects/Julia/Inferelator_Julia/outputs/subNetworks/scSubsampleFraction/lambda0p5_200totSS_20tfsPerGene_subsamplePCT10/Combined/TFA_cor.txt") +sc_quant <- read.table("/data/miraldiNB/Katko/Projects/Julia/Inferelator_Julia/outputs/subNetworks/scSubsampleFraction/lambda0p5_200totSS_20tfsPerGene_subsamplePCT10/Combined/TFA_quant.txt") +sc_sign <- read.table("/data/miraldiNB/Katko/Projects/Julia/Inferelator_Julia/outputs/subNetworks/scSubsampleFraction/lambda0p5_200totSS_20tfsPerGene_subsamplePCT10/Combined/TFA_binary.txt") +tfa_matrix <- list(bulk_cor, bulk_quant, bulk_sign, sc_cor, sc_quant, sc_sign) +names(tfa_matrix) <- c("bulk_cor","bulk_quant","bulk_sign","sc_cor","sc_quant","sc_sign") + +tf_list <- c( + "AIRE", "BACH2", "BCL11B", "BPTF", "CLOCK", "EPAS1", "ETS1", "FOXK1", "FOXP1", "FOXP3", + "GATA3", "GFI1", "HIVEP2", "IKZF1", "IRF1", "IRF2", "IRF4", "IRF7", "IRF9", "KLF2", + "KMT2A", "MBD2", "MYB", "NFE2L2", "NFAT5", "NFKB1", "NFKB2", "RELA", + "RELB", "REL", "RFX5", "RORC", "SETDB1", "SREBF1", "STAT1", "STAT2", "STAT3", "STAT5A", + "STAT5B", "TBX21", "TCF3", "TP53", "YBX1", "YBX3", "YY1", "ZBTB14", "ZFP3", "ZKSCAN1", + "ZNF329", "ZNF341", "ZNF791" +) +tf_list <- tf_list[which(tf_list %in% rownames(tfa_matrix[[1]]))] +library(dplyr) +library(tidyr) +library(ggplot2) +library(purrr) + + +analyse_one_matrix <- function(tfa_matrix, + tf_list, + pdf_name, + sig_cutoffs = c(`***` = 0.001, + `**` = 0.01, + `*` = 0.05)) { + + p_to_symbol <- function(p) { + stars <- names(sig_cutoffs)[which(p < sig_cutoffs)] + if (length(stars) == 0) "ns" else stars[1] + } + + res <- purrr::map_dfr(tf_list, function(tf) { + + #--- pull the row safely --------------------------------------------------- + if (!tf %in% rownames(tfa_matrix)) { + warning("TF ", tf, " not present in matrix; skipping.") + return(tibble::tibble(TF = tf, p.value = NA_real_, + signif = "NA", direction = "NA", plot = list(NULL))) + } + + tf_activity <- tfa_matrix[tf, ] # numeric vector + tf_df <- tibble::tibble(Sample = names(tf_activity), + Activity = as.numeric(tf_activity)) %>% + dplyr::mutate( + Knockout_TF = sub("_Donor_.*", "", Sample), + Donor = sub(".*_Donor_", "", Sample) %>% sub("\\..*", "", .) + ) + + #--- average per donor ----------------------------------------------------- + controls <- tf_df %>% dplyr::filter(Knockout_TF == "AAVS1") %>% + dplyr::group_by(Donor) %>% + dplyr::summarise(Control = mean(Activity), .groups = "drop") + + knockouts <- tf_df %>% dplyr::filter(Knockout_TF == tf) %>% + dplyr::group_by(Donor) %>% + dplyr::summarise(Knockout = mean(Activity), .groups = "drop") + + paired <- dplyr::inner_join(controls, knockouts, by = "Donor") + + if (nrow(paired) < 2) { + warning("TF ", tf, ": fewer than 2 paired donors – skipped") + return(tibble::tibble(TF = tf, p.value = NA_real_, + signif = "NA", direction = "NA", plot = list(NULL))) + } + + #--- stats ----------------------------------------------------------------- + t_res <- stats::t.test(paired$Knockout, paired$Control, paired = TRUE) + p_val <- t_res$p.value + direction <- ifelse(mean(paired$Knockout - paired$Control) > 0, "Up", "Down") + signif_sym <- p_to_symbol(p_val) + + #--- plot ------------------------------------------------------------------ + long <- paired %>% + tidyr::pivot_longer(Control:Knockout, + names_to = "Condition", + values_to = "Activity") + + g <- ggplot2::ggplot(long, ggplot2::aes(Condition, Activity, group = Donor)) + + ggplot2::geom_line(ggplot2::aes(color = Donor), linewidth = 1.1, alpha = 0.8) + + ggplot2::geom_point(ggplot2::aes(color = Donor), size = 3) + + ggplot2::theme_minimal(base_size = 14) + + ggplot2::labs( + title = paste(tf, "Activity Before and After Knockout"), + subtitle = paste0("p = ", signif(p_val, 3), + " (", signif_sym, ", ", direction, ")"), + x = "Condition", y = "Estimated TF Activity", color = "Donor") + + ggplot2::theme(plot.title = ggplot2::element_text(hjust = 0.5, face = "bold"), + plot.subtitle = ggplot2::element_text(hjust = 0.5)) + + tibble::tibble(TF = tf, + p.value = p_val, + signif = signif_sym, + direction = direction, + plot = list(g)) + }) + + #--- write one PDF with all TFs --------------------------------------------- + grDevices::pdf(pdf_name, width = 8, height = 10) + purrr::walk(res$plot[!purrr::map_lgl(res$plot, is.null)], print) + grDevices::dev.off() + + res # keep the plot column this time +} + +################################################################ +## 2. MANY-MATRIX WRAPPER (robust, no imap_dfr required) ## +################################################################ +analyse_many_matrices <- function(tfa_list, # named list or named file vector + tf_list, + out_dir = ".") { + + # read .rds files if character vector of paths was supplied + if (is.character(tfa_list) && !is.matrix(tfa_list[[1]])) { + tfa_list <- lapply(tfa_list, readRDS) + names(tfa_list) <- basename(names(tfa_list)) # keep vector names + } + + if (is.null(names(tfa_list)) || any(names(tfa_list) == "")) + stop("tfa_list must be a *named* list or vector so datasets can be labelled.") + + dir.create(out_dir, showWarnings = FALSE, recursive = TRUE) + + results <- vector("list", length(tfa_list)) + out_i <- 1L + + for (mat_name in names(tfa_list)) { + + mat <- tfa_list[[mat_name]] + if (!(is.matrix(mat) || is.data.frame(mat))) { + warning("Skipping '", mat_name, "': not a matrix/data.frame") + next + } + + pdf_file <- file.path(out_dir, + paste0("TF_activity_changes_", mat_name, ".pdf")) + + message("Processing ", mat_name, " …") + stats_one <- tryCatch( + analyse_one_matrix(mat, tf_list, pdf_name = pdf_file), + error = function(e) { + warning("Failed on ", mat_name, ": ", conditionMessage(e)) + NULL + } + ) + + if (is.null(stats_one)) next # skip failures + + stats_one <- tibble::as_tibble(stats_one) # force tibble + stats_one <- dplyr::mutate(stats_one, dataset = mat_name, .before = 1) + + results[[out_i]] <- stats_one + out_i <- out_i + 1L + } + + dplyr::bind_rows(results[seq_len(out_i - 1L)]) +} + + +all_stats <- analyse_many_matrices(tfa_matrix, + tf_list, + out_dir = "plots") # PDFs in ./plots + +## View or export the combined statistics: +print(all_stats) +library(ComplexHeatmap) +library(circlize) +library(grid) +library(dplyr) +library(tidyr) + +pct_expressing <- function(obj, + genes, + assay = "RNA", + slot = "data") { + + expr <- Seurat::GetAssayData(obj[[assay]], slot = slot) # genes × cells + vec <- vapply(genes, function(g) { + if (!g %in% rownames(expr)) return(NA_real_) + mean(expr[g, ] > 0) * 100 # % of cells + }, numeric(1)) + names(vec) <- genes + vec +} + +DefaultAssay(obj) <- "RNA" # if not already set +pct_vec <- pct_expressing(obj, tf_list) + +score_mat <- all_stats %>% + mutate(score = ifelse( + is.na(p.value), + NA_real_, + -log10(p.value) * ifelse(direction == "Up", 1, -1)) + ) %>% + select(TF, dataset, score) %>% + pivot_wider(names_from = dataset, values_from = score) %>% + as.data.frame() + +rownames(score_mat) <- score_mat$TF +score_mat <- as.matrix(score_mat[, -1, drop = FALSE]) + +star_mat <- all_stats %>% + mutate(stars = case_when( + is.na(p.value) ~ "", + p.value < 0.001 ~ "***", + p.value < 0.01 ~ "**", + p.value < 0.10 ~ "*", + TRUE ~ "" + )) %>% + select(TF, dataset, stars) %>% + pivot_wider(names_from = dataset, values_from = stars) %>% + as.data.frame() + +rownames(star_mat) <- star_mat$TF +star_mat <- as.matrix(star_mat[, -1, drop = FALSE]) + +deg_counts <- setNames(rep(0, nrow(score_mat)), rownames(score_mat)) +deg_counts[names(target_number)] <- target_number # overwrite where present + +# ---- LEFT annotation: % of cells expressing ---------------------------------- +row_anno_left <- rowAnnotation( + `% Cells\nExpressing` = anno_barplot( + pct_vec[rownames(score_mat)], # ensure correct order + gp = gpar(fill = "steelblue", col = NA), + bar_width = 0.85, + border = FALSE, + axis_param = list(at = c(0, 50, 100), gp = gpar(fontsize = 7)) + ), + annotation_name_side = "top", + annotation_name_gp = gpar(fontsize = 9, fontface = "bold"), + width = unit(2.4, "cm") +) + +# ---- RIGHT annotation: # of DE genes ----------------------------------------- +row_anno_right <- rowAnnotation( + `# DE genes` = anno_barplot( + deg_counts, + gp = gpar(fill = "grey40", col = NA), + bar_width = 0.85, + border = FALSE, + axis_param = list(at = c(0, max(deg_counts)), gp = gpar(fontsize = 7)) + ), + annotation_name_side = "top", + annotation_name_gp = gpar(fontsize = 9, fontface = "bold"), + width = unit(2.3, "cm") +) + +max_abs <- max(abs(score_mat), na.rm = TRUE) +col_fun <- circlize::colorRamp2(c(-max_abs, 0, max_abs), + c("navy", "white", "firebrick")) +text_col <- function(fill) { + rgb <- col2rgb(fill) / 255 + ifelse(0.299*rgb[1] + 0.587*rgb[2] + 0.114*rgb[3] < 0.5, "white", "black") +} + +row_labels <- rowAnnotation( + TF = anno_text( + rownames(score_mat), + gp = gpar(fontsize = 9), + just = "left", + location = 0.5 # centred vertically in each cell + ), + width = unit(2.2, "cm"), # reserve space for the longest name + show_annotation_name = FALSE +) + +############################################################################### +## 2. Core heat-map (row names turned OFF) ################################## +############################################################################### +ht_core <- Heatmap( + score_mat, + name = "-log10(p)", + col = col_fun, + na_col = "grey90", + border = TRUE, + row_km = 3, + + show_row_names = FALSE, # <── row names handled by row_labels + column_names_gp = gpar(fontsize = 9), + + heatmap_legend_param = list( + title_gp = gpar(fontsize = 10, fontface = "bold"), + labels_gp = gpar(fontsize = 9) + ), + cell_fun = function(j, i, x, y, width, height, fill) { + lab <- star_mat[i, j] + if (nzchar(lab)) { + grid.text( + lab, x, y, + gp = gpar(fontsize = 8, + fontface = "bold", + col = text_col(fill)) + ) + } + } +) + +############################################################################### +## 3. Assemble LEFT + CORE + LABELS + RIGHT ########################## +############################################################################### +ht_full <- row_anno_left + ht_core + row_labels + row_anno_right +# %-expressing matrix TF names #DE genes + +############################################################################### +## 4. Draw / save ########################################################### +############################################################################### +pdf("/data/miraldiNB/Katko/Projects/Julia/Inferelator_Julia/outputs/subNetworks/scSubsampleFraction/lambda0p5_200totSS_20tfsPerGene_subsamplePCT10/Combined/plots//TF_heatmap_with_two_bars_and_labels.pdf", width = 7.5, height = 9) +draw(ht_full, + heatmap_legend_side = "right", + annotation_legend_side = "right") +dev.off() \ No newline at end of file diff --git a/evaluation/R/GRN_Tfh10_PR_heatmap.R b/evaluation/R/GRN_Tfh10_PR_heatmap.R new file mode 100755 index 0000000..83f420e --- /dev/null +++ b/evaluation/R/GRN_Tfh10_PR_heatmap.R @@ -0,0 +1,444 @@ +# GRN_Tfh10_PR_heatmap_final2.R +# Heatmap of PR results +rm(list=ls()) +options(stringsAsFactors=FALSE) +set.seed(42) +suppressPackageStartupMessages({ +library(ggplot2) +library(scales) +}) +source('/data/miraldiNB/Michael/Scripts/GRN/utils_grn_analysis.R') +source('/data/miraldiNB/wayman/scripts/net_utils.R') +source('/data/miraldiNB/wayman/scripts/clustering_utils.R') + +####### INPUTS ####### + +# Output directory +dir_out <- '/data/miraldiNB/Michael/mCD4T_Wayman/Figures/Tfh10_S4B/2p5PCT' +dir.create(dir_out, showWarnings=FALSE, recursive=TRUE) + +# File save name +file_save <- 'Tfh10_S4' + +# Gene target file +file_targ <- "/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/targRegLists/targetGenes_names.txt" + +# TF list +file_tf <- "/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/targRegLists/potRegs_names.txt" + +# list of networks (in order) +file_grn <- c( + +# No Prior : lambdaBias=1 + tfaOpt='_TFmRNA' + ATAC_KOprior +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/Bulk/lambda1p0_80totSS_20tfsPerGene_subsamplePCT63/TFmRNA/edges_subset.txt", # NoPrior (TFmRNA + ATAC) + +# ATAC Prior +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/Bulk/lambda0p5_80totSS_20tfsPerGene_subsamplePCT63/TFmRNA/edges_subset.txt", # +mRNA (ATAC) +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/Bulk/lambda0p25_80totSS_20tfsPerGene_subsamplePCT63/TFmRNA/edges_subset.txt", # ++mRNA (ATAC) + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/Bulk/lambda1p0_80totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.txt", # TFA +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/Bulk/lambda0p5_80totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.txt", # +TFA (ATAC) +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/Bulk/lambda0p25_80totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.txt", # ++TFA (ATAC) +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/Bulk/lambda0p5_80totSS_20tfsPerGene_subsamplePCT63/Combined/combined.tsv", # +Combine (ATAC) + + +# ATAC Prior (sc-Inferelator) +# + +# ATAC+ChIP Prior +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_ChIPprior/Bulk/lambda0p5_80totSS_20tfsPerGene_subsamplePCT63/TFmRNA/edges_subset.txt", # +mRNA (ATAC + ChIP) +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_ChIPprior/Bulk/lambda0p25_80totSS_20tfsPerGene_subsamplePCT63/TFmRNA/edges_subset.txt", # ++mRNA (ATAC + ChIP) +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_ChIPprior/Bulk/lambda1p0_80totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.txt", # TFA +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_ChIPprior/Bulk/lambda0p5_80totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.txt", # +TFA (ATAC + ChIP) +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_ChIPprior/Bulk/lambda0p25_80totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.txt", # ++TFA (ATAC + ChIP) +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_ChIPprior/Bulk/lambda0p5_80totSS_20tfsPerGene_subsamplePCT63/Combined/combined.tsv", # +Combine (ATAC + ChIP) + +# ATAC + ChIP Prior (sc- Inferelator) +# + +# ATAC+KO Prior +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_KOprior/Bulk/lambda0p5_80totSS_20tfsPerGene_subsamplePCT63/TFmRNA/edges_subset.txt", # +mRNA (ATAC + KO) +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_KOprior/Bulk/lambda0p25_80totSS_20tfsPerGene_subsamplePCT63/TFmRNA/edges_subset.txt", # ++mRNA (ATAC + KO) +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_KOprior/Bulk/lambda1p0_80totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.txt", # TFA +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_KOprior/Bulk/lambda0p5_80totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.txt", # +TFA (ATAC + KO) +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_KOprior/Bulk/lambda0p25_80totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.txt", # ++TFA (ATAC + KO) +"/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_KOprior/Bulk/lambda0p5_80totSS_20tfsPerGene_subsamplePCT63/Combined/combined.tsv", # +Combine (ATAC + KO) + +# ATAC + KO Prior (sc- Inferelator) +# + +#Cell Oracle +# "/data/miraldiNB/Michael/mCD4T_Wayman/CellOracle/GRNs/Unprocessed/Combined/Old/max_pCutoff_0.1pct_combined_GRN_3cols.tsv", +"/data/miraldiNB/Michael/mCD4T_Wayman/CellOracle/GRNs/Unprocessed/CombinedGRNs/max/max_pCut0.1_meanEdgesPerGene20_combined_GRN.tsv", + +# Scenic Plus +"/data/miraldiNB/Michael/mCD4T_Wayman/scenicPlus/eRegulons_direct_filtered.tsv", # Direct +"/data/miraldiNB/Michael/mCD4T_Wayman/scenicPlus/eRegulons_extended_filtered.tsv", # Extended + +#SupirFactor +"/data/miraldiNB/Michael/mCD4T_Wayman/SupirFactor/Hierarchical_20250212_2043/GRN_Hierarchical_long.tsv", # Hierarchical +"/data/miraldiNB/Michael/mCD4T_Wayman/SupirFactor/Shallow_20250330_0212/GRN_Shallow_long.tsv" # Shallow +) + +# network names +name_grn <- c( + +'No Prior', + +'ATAC mRNA +', +'ATAC mRNA ++', +'ATAC TFA', +'ATAC TFA +', +'ATAC TFA ++', +'ATAC Combined +', + +# 'sc ATAC mRNA +', +# 'sc ATAC TFA +', +# 'sc ATAC Combined +', + +'ChIP mRNA +', +'ChIP mRNA ++', +'ChIP TFA', +'ChIP TFA +', +'ChIP TFA ++', +'ChIP Combined +', + +# 'sc ChIP mRNA +', +# 'sc ChIP TFA +', +# 'sc ChIP Combined +', + +'KO mRNA +', +'KO mRNA ++', +'KO TFA', +'KO TFA +', +'KO TFA ++', +'KO Combined +', + +# 'sc KO mRNA +', +# 'sc KO TFA +', +# 'sc KO Combined +', + +'Cell Oracle', +# 'Cell Oracle Mean', + +'SCENIC+ Dir', +'SCENIC+ Ext', + +'SupirFactor H', +'SupirFactor Sh' + +) + +# network type +type_grn <- c( + +'No Prior', + +'ATAC', +'ATAC', +'ATAC', +'ATAC', +'ATAC', +'ATAC', + +# 'ATAC', +# 'ATAC', +# 'ATAC', + +'ChIP', +'ChIP', +'ChIP', +'ChIP', +'ChIP', +'ChIP', + +# 'ChIP', +# 'ChIP', +# 'ChIP', + +'KO', +'KO', +'KO', +'KO', +'KO', +'KO', + +# 'KO', +# 'KO', +# 'KO', + +'Cell Oracle', +# 'Cell Oracle', + +'SCENIC+', +'SCENIC+', + +'SupirFactor', +'SupirFactor' +) + +# Gold standard names +name_gs <- c( +'ChIP', +'KO', +'KC' +) + +# List of gold standards +file_gs <- c( + # '/data/miraldiNB/wayman/projects/Tfh10/outs/202204/GRN/GS/TF_binding/priors/FDR5_Rank50/prior_ChIP_Thelper_Miraldi2019Th17_combine_FDR5_Rank50_sp.tsv', + # '/data/miraldiNB/wayman/projects/Tfh10/outs/202204/GRN/GS/RNA/priors/prior_RNA_Thelper_Miraldi2019Th17_combine_Log2FC0p5_FDR20_Rank50_sp.tsv', + # '/data/miraldiNB/wayman/projects/Tfh10/outs/202204/GRN/GS/KC/prior_KC_Thelper_Miraldi2019Th17_Rank100_sp.tsv' + "/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/goldStandards/prior_ChIP_Thelper_Miraldi2019Th17_combine_FDR5_Rank50_sp.tsv", + "/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/goldStandards/prior_TF_KO_RNA_Thelper_Miraldi2019Th17_combine_Log2FC0p5_FDR20_Rank50_sp.tsv", + "/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/goldStandards/prior_KC_Thelper_Miraldi2019Th17_Rank100_sp.tsv" +) + +# gold standard save file name +save_gs <- '' + +# params +min_targ_gs <- 20 # gold standard min target cutoff +cutoff_rec <- 0.025 # recall cutoff +range_fc <- c(-2,2) # color scale range for log2fc heatmap +breaks_fc <- c(-2,-1,0,1,2) +heat_h <- 8 # heatmap height +# heat_w <- 15 # heatmap width +heat_w <- 21 # heatmap width +box_w <- 6 # boxplot width + +###################### + +# Output file label +label_pcut <- 100*cutoff_rec +file_save <- if (nzchar(save_gs)) paste0(file_save, '_', save_gs) else file_save + + +# Load tf list +tf <- readLines(file_tf) + +# Calc precision-recall +df_pr <- NULL +for (ix in 1:length(file_grn)){ + + print(paste0('Process GRN: ',ix,'/',length(file_grn))) + for (jx in 1:length(file_gs)){ + + # # Filter gold standard TFs + gs <- read.delim(file_gs[jx], header=TRUE, sep='\t') + gs <- subset(gs, TF %in% tf) + keep_tf <- names(which(table(gs$TF) >= min_targ_gs)) + gs <- subset(gs, TF %in% keep_tf) + + # All PR + curr_tf <- sort(unique(gs$TF)) + pr <- grn_pr(grn=file_grn[ix], gs=gs, gene_tar=file_targ, tf=curr_tf) + prec <- pr$P; prec <- c(prec, pr$Rand) + recall <- pr$R; recall <- c(recall, 1) + + # Precision at cutoff + slope_pr <- (prec[which(recall >= cutoff_rec)[1]] - prec[which(recall >= cutoff_rec)[1]-1])/ + (recall[which(recall >= cutoff_rec)[1]] - recall[which(recall >= cutoff_rec)[1]-1]) + pcut <- prec[which(recall >= cutoff_rec)[1]-1] + + slope_pr*(cutoff_rec-recall[which(recall >= cutoff_rec)[1]-1]) + curr_df <- data.frame(GRN=name_grn[ix], Type=type_grn[ix], GS=name_gs[jx], + TF='All TF', Pcut=pcut, Rand=pr$Rand) + df_pr <- rbind(df_pr, curr_df) + + for (kx in curr_tf){ + + # pr <- grn_pr(grn=file_grn[ix], gs=file_gs[jx], gene_tar=file_targ, tf=kx) + pr <- grn_pr(grn=file_grn[ix], gs=gs, gene_tar=file_targ, tf=kx) + if (!is.null(pr)){ + prec <- pr$P; prec <- c(prec, pr$Rand) + recall <- pr$R; recall <- c(recall, 1) + + # precision at cutoff + slope_pr <- (prec[which(recall >= cutoff_rec)[1]] - prec[which(recall >= cutoff_rec)[1]-1])/ + (recall[which(recall >= cutoff_rec)[1]] - recall[which(recall >= cutoff_rec)[1]-1]) + pcut <- prec[which(recall >= cutoff_rec)[1]-1] + + slope_pr*(cutoff_rec-recall[which(recall >= cutoff_rec)[1]-1]) + curr_df <- data.frame(GRN=name_grn[ix], Type=type_grn[ix], GS=name_gs[jx], + TF=kx, Pcut=pcut, Rand=pr$Rand) + df_pr <- rbind(df_pr, curr_df) + } + } + } +} +df_pr$PcutLog2FC <- log2(df_pr$Pcut/df_pr$Rand) +df_pr$GS_TF <- paste(df_pr$GS, df_pr$TF, sep='_') + +# order gold standards +order_gs <- NULL +for (ix in name_gs){ + order_gs <- c(order_gs, paste(ix,'All TF',sep='_')) + curr_pr <- subset(df_pr, GS==ix & TF != 'All TF') + mat <- net_sparse_2_full(curr_pr[,c('GS_TF','GRN','PcutLog2FC')]) + curr_order <- colnames(mat)[cluster_hierarchical(mat)] + order_gs <- c(order_gs, curr_order) +} +df_pr$GS_TF <- factor(df_pr$GS_TF, levels=order_gs) + +# order networks +df_pr$GRN <- factor(df_pr$GRN, levels=rev(name_grn)) + + +# Axis label +label_gs <- order_gs +for(ix in name_gs){ + label_gs <- gsub(paste0(ix,'_'),'',label_gs) +} +face_gs <- rep('plain',length(label_gs)) +face_gs[which(label_gs=='All TF')] <- 'bold' + +tf_ko <- unique(subset(df_pr, GS=='KO' & TF != 'All TF')$TF) +tf_chip <- unique(subset(df_pr, GS=='ChIP' & TF != 'All TF')$TF) +tf_int <- intersect(tf_ko, tf_chip) +for (ix in tf_int){ + face_gs[which(label_gs==ix)[1]] <- 'italic' +} + +# Lines separating gold standards +# vline_sep <- ceiling(cumsum(table(df_pr$GS)[name_gs]/length(name_grn)))+0.5 +tf_counts <- tapply(df_pr$TF, df_pr$GS, function(x) length(unique(x))) +vline_sep <- cumsum(tf_counts[name_gs]) + 0.5 +vline_sep <- vline_sep[1:(length(vline_sep)-1)] + +# lines separating network types +order_type <- rev(unique(type_grn)) +hline_sep <- cumsum(table(type_grn)[order_type])+0.5 +hline_sep <- hline_sep[1:(length(hline_sep)-1)] + +color_pal <- c(low='dodgerblue3', 'white', high='firebrick3') # c('#2c7bb6','#abd9e9','white','#d7191c','#b10026') +# Heatmap precision at cutoff +print('Heatmap precision at cutoff') +ggplot(df_pr, aes(x=GS_TF, y=GRN, fill=PcutLog2FC)) + + geom_tile() + + # geom_tile(data = subset(df_pr, !is.na(PcutLog2FC))) + # Removes NA tiles + # geom_tile(data = subset(df_pr, is.na(PcutLog2FC)), fill = "lightgrey") + # Adds NA tiles without borders + geom_vline(xintercept=vline_sep, lwd=0.8, colour='gray30') + + geom_hline(yintercept=hline_sep, lwd=0.8, colour='gray30') + + labs(x=' ', y=' ', fill='Log2 FC') + + theme(axis.text=element_text(size=12,face='plain')) + + theme(axis.title=element_text(size=14,face='plain')) + + theme(axis.text.x=element_text(angle=45, hjust=1)) + + theme(axis.text.x=element_text(face=face_gs)) + + # scale_fill_gradient2(low='dodgerblue3', high='firebrick3', limits=range_fc, na.value="lightgrey",oob=squish) + + scale_fill_gradientn( + # colors=c(low='dodgerblue3', high='firebrick3'), + limits=range_fc, + colors= color_pal, + breaks=breaks_fc, + na.value="lightgrey",oob=squish + )+ + scale_x_discrete(labels=label_gs) +file_out <- file.path(dir_out, paste0('heat_',file_save,'_PRcut',label_pcut,'.pdf')) +ggsave(file_out, height=heat_h, width=heat_w) + +ggplot(df_pr, aes(x=GS_TF, y=GRN, fill=PcutLog2FC)) + + geom_tile() + + # geom_tile(data = subset(df_pr, !is.na(PcutLog2FC))) + # Removes NA tiles + # geom_tile(data = subset(df_pr, is.na(PcutLog2FC)), fill = "lightgrey") + # Adds NA tiles without borders + geom_vline(xintercept=vline_sep, lwd=0.8, colour='gray30') + + geom_hline(yintercept=hline_sep, lwd=0.8, colour='gray30') + + labs(x=' ', y=' ', fill=' ') + + theme(axis.text=element_text(size=12,face='plain')) + + theme(axis.title=element_text(size=14,face='plain')) + + theme(axis.text.x=element_text(angle=45, hjust=1)) + + theme(axis.text.x=element_text(face=face_gs)) + + # scale_fill_gradient2(low='dodgerblue3', high='firebrick3', limits=range_fc, na.value="lightgrey",oob=squish) + + scale_fill_gradientn( + # colors=c(low='dodgerblue3', high='firebrick3'), + limits=range_fc, + colors= color_pal, + breaks=breaks_fc, + na.value="lightgrey",oob=squish + )+ + scale_x_discrete(labels=label_gs) + + theme(legend.position='bottom', legend.text=element_text(size=16), legend.key.width=unit(1.5,'cm')) +file_out <- file.path(dir_out, paste0('heat_',file_save,'_PRcut',label_pcut,'_2.pdf')) +ggsave(file_out, height=heat_h, width=heat_w) + +# Boxplot log2fc +print('boxplot log2fc') +for (ix in name_gs){ + df_pr_sub <- subset(df_pr, GS==ix & TF != 'All TF') + ggplot(df_pr_sub, aes(x=GRN, y=PcutLog2FC)) + geom_boxplot() + coord_flip() + + theme_minimal() + + labs(x=' ', y='Log2 Fold-Change') + + theme(axis.text=element_text(size=12,face='plain')) + + theme(axis.title=element_text(size=14,face='plain')) + + theme(axis.line=element_line(size=0.6, colour='grey30', linetype='solid')) + file_out <- file.path(dir_out, paste0('box_',file_save,'_PRcut',label_pcut,'_',ix,'.pdf')) + ggsave(file_out, height=heat_h, width=box_w) +} + + + +# set regions where GS is tested on itself to NA +df_pr_grey <- df_pr +df_pr_grey$PcutLog2FC <- ifelse((df_pr$Type == "ChIP" & df_pr$GS != "KO") | + (df_pr$Type == "KO" & df_pr$GS != "ChIP"), + NA, df_pr$PcutLog2FC) + +# Heatmap precision at cutoff +print('Heatmap precision at cutoff - greyed out GS tested regions') +ggplot(df_pr_grey, aes(x=GS_TF, y=GRN, fill=PcutLog2FC)) + + geom_tile() + + # geom_tile(data = subset(df_pr, !is.na(PcutLog2FC))) + # Removes NA tiles + # geom_tile(data = subset(df_pr, is.na(PcutLog2FC)), fill = "lightgrey") + # Adds NA tiles without borders + geom_vline(xintercept=vline_sep, lwd=0.8, colour='gray30') + + geom_hline(yintercept=hline_sep, lwd=0.8, colour='gray30') + + labs(x=' ', y=' ', fill='Log2 FC') + + theme(axis.text=element_text(size=12,face='plain')) + + theme(axis.title=element_text(size=14,face='plain')) + + theme(axis.text.x=element_text(angle=45, hjust=1)) + + theme(axis.text.x=element_text(face=face_gs)) + + # scale_fill_gradient2(low='dodgerblue3', high='firebrick3', limits=range_fc, na.value="lightgrey",oob=squish) + + scale_fill_gradientn( + limits=range_fc, + colors= color_pal, + breaks=breaks_fc, + na.value="lightgrey",oob=squish + )+ + scale_x_discrete(labels=label_gs) +file_out <- file.path(dir_out, paste0('heat_',file_save,'_PRcut',label_pcut,'_greyed_out.pdf')) +ggsave(file_out, height=heat_h, width=heat_w) + +ggplot(df_pr_grey, aes(x=GS_TF, y=GRN, fill=PcutLog2FC)) + + geom_tile() + + # geom_tile(data = subset(df_pr, !is.na(PcutLog2FC))) + # Removes NA tiles + # geom_tile(data = subset(df_pr, is.na(PcutLog2FC)), fill = "lightgrey") + # Adds NA tiles without borders + geom_vline(xintercept=vline_sep, lwd=0.8, colour='gray30') + + geom_hline(yintercept=hline_sep, lwd=0.8, colour='gray30') + + labs(x=' ', y=' ', fill=' ') + + theme(axis.text=element_text(size=12,face='plain')) + + theme(axis.title=element_text(size=14,face='plain')) + + theme(axis.text.x=element_text(angle=45, hjust=1)) + + theme(axis.text.x=element_text(face=face_gs)) + + # scale_fill_gradient2(low='dodgerblue3', high='firebrick3', limits=range_fc, na.value="lightgrey",oob=squish) + + scale_fill_gradientn( + # colors=c(low='dodgerblue3', high='firebrick3'), + limits=range_fc, + colors= color_pal, + breaks=breaks_fc, + na.value="lightgrey",oob=squish + )+ + scale_x_discrete(labels=label_gs) + + theme(legend.position='bottom', legend.text=element_text(size=16), legend.key.width=unit(1.5,'cm')) +file_out <- file.path(dir_out, paste0('heat_',file_save,'_PRcut',label_pcut,'_greyed_out_2.pdf')) +ggsave(file_out, height=heat_h, width=heat_w) + +# Boxplot log2fc +print('boxplot log2fc') +for (ix in name_gs){ + df_pr_sub <- subset(df_pr_grey, GS==ix & TF != 'All TF') + ggplot(df_pr_sub, aes(x=GRN, y=PcutLog2FC)) + geom_boxplot() + coord_flip() + + theme_minimal() + + labs(x=' ', y='Log2 Fold-Change') + + theme(axis.text=element_text(size=12,face='plain')) + + theme(axis.title=element_text(size=14,face='plain')) + + theme(axis.line=element_line(size=0.6, colour='grey30', linetype='solid')) + file_out <- file.path(dir_out, paste0('box_',file_save,'_PRcut',label_pcut,'_',ix,'_greyed_out.pdf')) + ggsave(file_out, height=heat_h, width=box_w) +} +print('DONE') diff --git a/evaluation/R/TFA_Viz.R b/evaluation/R/TFA_Viz.R new file mode 100755 index 0000000..8a89c1b --- /dev/null +++ b/evaluation/R/TFA_Viz.R @@ -0,0 +1,245 @@ +rm(list = ls()) +options(stringsAsFactors=FALSE) +suppressPackageStartupMessages({ + library(ComplexHeatmap) + library(dplyr) + library(tidyverse) + library(circlize) + library(RColorBrewer) + library(gridtext) + library(readxl) + library(reshape2) + library(Seurat) + library(ggplot2) +}) + +source("/data/miraldiNB/Michael/Scripts/standardize_normalize_pseudobulk.R") +source("/data/miraldiNB/Michael/Scripts/GSEA/GSEA_utils.R") + +dirOut <- "/data/miraldiNB/Michael/mCD4T_Wayman/Figures/Fig4/Test" +dir.create(dirOut, showWarnings = F, recursive = T) + + +# network <- "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/Bulk/ATAC/newPipe/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.tsv" +tfa_file <- "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/Bulk/ATAC/newPipe/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/Combined/TFA.txt" +file_k_clust_across <- NULL #file.path(dir_out, 'k_clust_across.rds') +file_k_clust_within <- NULL #file.path(dir_out, 'k_clust_within.rds') + +order_celltype <- c('Tfh10','Tfh_Int','Tfh', + 'Tfr','cTreg','eTreg','rTreg','Treg_Rorc', + 'Th17','Th1','CTL_Prdm1','CTL_Bcl6', + #'TM_Act','TM_ISG', + 'TEM','TCM') +order_age <- c('Young','Old') +order_rep <- c('R1','R2','R3','R4') + +tfa <- read.table(tfa_file) +meta_data <- NULL +for (ix in order_celltype){ + for (jx in order_age){ + curr_sample <- paste(order_rep, jx, ix, sep='_') + curr_meta <- data.frame(row.names=curr_sample, CellType=ix, Age=jx, Rep=order_rep) + meta_data <- rbind(meta_data, curr_meta) + } +} + +# diff <- setdiff(rownames(meta_data), colnames(tfa)) +meta_data <- meta_data[intersect(colnames(tfa), rownames(meta_data)), ] + +# rearrange tfa matrix +tfa <- tfa[ ,rownames(meta_data)] +# Z-scoring or Standardization +z_tfa <- standardizeAndNormalizeCounts(tfa, meta_data = meta_data, celltype_var = "CellType", epsilon = NULL) +tfa_z_across = z_tfa$z_across +tfa_z_within = z_tfa$z_within + +# Create Annotation Color +getPalette = colorRampPalette(brewer.pal(13, "Set1")) +celltype_colors = getPalette(length(unique(meta_data[,1]))) +celltype_colors <- setNames(celltype_colors, c(unique(meta_data[,1]))) +treatment_colors <- c('Young'='grey66', 'Old'='grey33') + +meta_data[,2] <- factor() +#Create annotation column +colAnn_top <- HeatmapAnnotation( + `Cell Type` = meta_data[,1], + `Age` = meta_data[,2], + col = list('Cell Type' = celltype_colors, + 'Age' = treatment_colors), + show_annotation_name = FALSE + ) + +across_center <- 6 +# within_center <- 4 +gc() +if (is.null(file_k_clust_across)){ + k_clust_across <- kmeans(tfa_z_across, centers=across_center,nstart=20, iter.max = 50) +}else{ + k_clust_across <- readRDS(file_k_clust_across) +} + +# gc() +# if (is.null(file_k_clust_within)){ +# k_clust_within <- kmeans(tfa_z_within, centers=within_center, nstart=20, iter.max = 50) +# }else{ +# k_clust_within <- readRDS(file_k_clust_within) +# } + +# # Cluster +k_clust_across <- kmeans(tfa_z_across, centers=6,nstart=20, iter.max = 50) +split_by <- factor(k_clust_across$cluster, levels = c(1:6)) + +heat_col <- colorRamp2(c(-2, 0, 2), c('dodgerblue3','white','red')) ## Colors for heatmap +pdf(file.path(dirOut, "htmap_zscore_across.pdf"), width = 4, height = 6, compress = T) +ht1 <- Heatmap(tfa_z_across, + name='Z-score', + show_row_names = F, + row_split=split_by, + row_gap = unit(0, "mm"), + column_gap = unit(0, "mm"), + border = TRUE, + row_title = NULL, + row_dend_reorder = F, + use_raster = F, + col = heat_col, + # clustering settings + cluster_rows = TRUE, # allow hierarchical clustering + cluster_row_slices = TRUE, # reorder within each k-means cluster + cluster_columns = FALSE, + cluster_column_slices = F, + # column_order = colnames(tfa_z_across), + + # row/column dendrograms + show_row_dend = FALSE, # show dendrogram within slices + show_column_dend = FALSE, + + show_column_names = FALSE, + column_names_side = 'top', + # column_names_rot = 45, + column_title = NULL, + column_split = meta_data[,1], + top_annotation = colAnn_top + #bottom_annotation = colAnn_bottom + ) +draw(ht1) +dev.off() + + +# cluster_annot <- data.frame(across=k_clust_across$cluster,within=k_clust_within$cluster) +# cluster_annot$cluster <- paste0(cluster_annot$across,cluster_annot$within) +# cluster_annot <- cluster_annot[order(cluster_annot$cluster), ] +# within_clust_order <- c(1,2,3,4) +# across_clust_order <- c(2,3,5,4,6,1) +# cluster_annot$within <- factor(cluster_annot$within, levels=within_clust_order, ordered=T) +# cluster_annot$across <- factor(cluster_annot$across, levels=across_clust_order, ordered=T) + + +# annot_cols_across <- c('1'='#cc0001','2'='#fb940b','3'='#ffff01','4'='#01cc00','5'='#2085ec','6'='#fe98bf','7'='#762ca7','8'='#ad7a5b', +# '9'='grey50', '10'='turquoise4') + +# annot_cols_within <- c('1'='#badf55','2'='#35b1c9','3'='#b06dad','4'="#14A76C", '5' = 'grey90', '6' = '#e96060') + +# #reorder clusters +# split <- factor(cluster_annot$cluster, levels=c( +# 21,22,23,24, +# 31,32,33,34, +# 51,52,53,54, +# 41,42,43,44, +# 61,62,63,64, +# 11,12,13,14 +# )) + +# cluster_annot$cluster <- factor(cluster_annot$cluster, levels=levels(split)) +# cluster_annot <- cluster_annot[order(cluster_annot$cluster),] +# names(split) <- rownames(cluster_annot) + +# rowAnn_across_left1 <- HeatmapAnnotation(`Across` = cluster_annot[,'across'], #df = cluster_annot[,'across'], +# col = list('Across'= annot_cols_across), +# which = 'row', +# simple_anno_size = unit(3, 'mm'), +# #annotation_width = unit(c(1, 4), 'cm'), +# #gap = unit(0, 'mm'), +# show_annotation_name = F) + + +# pdf(file.path(dirOut, "htmap_zscore_across1.pdf"), width = 4, height = 6, compress = T) +# ht1 <- Heatmap(tfa_z_across[rownames(cluster_annot),], +# name='Z-score', +# show_row_names = F, +# row_split=cluster_annot$across, +# show_column_dend =F, +# row_gap = unit(0, "mm"), +# column_gap = unit(0, "mm"), +# border = TRUE, +# row_title = NULL, +# row_dend_reorder = F, +# use_raster = F, +# col = heat_col, +# column_names_gp = gpar(fontsize = 8, fontface = 2), +# show_row_dend = FALSE, +# cluster_rows = TRUE, +# cluster_columns = F, +# cluster_row_slices = F, +# cluster_column_slices = F, +# column_order = colnames(tfa_z_across), +# show_column_names = FALSE, +# column_names_side = 'top', +# # column_labels = gt_render(column_label), +# column_names_rot = 45, +# column_title = NULL, +# column_split = meta_data[,1], +# left_annotation = rowAnn_across_left1, +# # right_annotation = rowAnn_across_right1, +# row_order = rownames(cluster_annot), +# top_annotation = colAnn_top +# #bottom_annotation = colAnn_bottom +# ) + + + +# PART C: TFA Visualization between data representation +tfaFiles <- list( + "PB" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/Bulk/ATAC/newPipe/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/Combined/TFA.txt", + "SC" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/SC/ATAC/geneLambda0p5_220totSS_20tfsPerGene_subsamplePCT5/Combined/TFA.txt", + "MC2" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/metaCells/MC2/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63_logNorm/Combined/TFA.txt", + "SEA" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/metaCells/SEACells/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/Combined/TFA.txt" +) + +tfaMatList <- list() +for (typeName in names(tfaFiles)) { + filePath <- tfaFiles[[typeName]] + tfaMatList[[typeName]] <- read.table(filePath, header = TRUE, sep = "\t", stringsAsFactors = FALSE) +} + +numTFA <- length(tfaMatList) +tfaNames <- names(tfaMatList) + + + +# ----------------------------- +# Feature Plot +# ----------------------------- +objPath <- "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/annotation_scrna_final/obj_Tfh10_RNA_annotated.rds" +tfa_file1 <- "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/SC/ATAC/geneLambda0p5_220totSS_20tfsPerGene_subsamplePCT5/Combined/TFA.txt" +obj <- readRDS(objPath) +tfa_mat1 <- read.table(tfa_file1, header = T) +lineage_TFs <- c("Bcl6","Maf","Batf","Tox","Ascl2", + "Foxp3","Ikzf2","Rorc", + "Rorc","Stat3", + "Tbx21","Stat4","Eomes","Prdm1", + "Tcf7","Klf2","Lef1") + +# Filter lineage TFs present in your TFA matrix +lineage_TFs_present <- intersect(lineage_TFs, rownames(tfa_mat)) + +# Add each TFA as metadata +for(tf in lineage_TFs_present){ + obj[[tf]] <- tfa_mat[tf, colnames(obj)] +} + +# Example: FeaturePlot for all lineage TFs +FeaturePlot(seurat_obj, + features = lineage_TFs_present, + cols = c("lightgrey","red"), + reduction = "umap") # or "tsne" if you use tSNE + diff --git a/evaluation/R/evaluateNetUtils.R b/evaluation/R/evaluateNetUtils.R new file mode 100755 index 0000000..bea8551 --- /dev/null +++ b/evaluation/R/evaluateNetUtils.R @@ -0,0 +1,644 @@ +suppressPackageStartupMessages({ + library(dplyr) + library(tidyr) + library(ggplot2) + library(ComplexHeatmap) + library(pheatmap) + # library(VennDiagram) + # library(ggVennDiagram) + library(grid) + library(reshape2) + library(RColorBrewer) + +}) + +# ========================== +# PART A: EDGE SET CONSTRUCTION +# ========================== + +# -------- 1. Build Edge Sets ----------- +getEdgeSets <- function(netDataList, tfCol="TF", targetCol="Gene", rankCol="signedQuantile", + mode = c("topNpercent", "topN"), N = NULL) { + #' getEdgeSets + #' + #' Extract edge sets from a list of networks, optionally selecting top-ranked edges. + #' + #' @param netDataList A named list of data.frames. Each data.frame represents a network with at least + #' the columns for TF, target gene, and ranking. + #' @param tfCol Character. Name of the column containing transcription factors (default "TF"). + #' @param targetCol Character. Name of the column containing target genes (default "Gene"). + #' @param rankCol Character. Name of the column containing ranking scores (default "signedQuantile"). + #' @param mode Character. Method to select edges when N is specified. Choices are: + #' "topNpercent" (default) - select top N percent of edges based on rankCol, + #' "topN" - select top N edges. + #' Ignored if N is NULL. + #' @param N Numeric. Number of edges (for "topN") or percent (for "topNpercent") to select. + #' If NULL (default), all edges are returned and mode is ignored. + #' + #' @return A named list of character vectors. Each element contains edges in the form "TF~Gene". + #' + mode <- match.arg(mode) + + edgeSets <- lapply(seq_along(netDataList), function(i) { + df <- netDataList[[i]] %>% + filter(.data[[rankCol]] != 0) %>% + arrange(desc(.data[[rankCol]])) + + if(!rankCol %in% colnames(df)) stop(paste("Column", rankCol, "not found in network", names(netDataList)[i])) + + df <- df %>% + mutate(edge = paste(.data[[tfCol]], .data[[targetCol]], sep="~")) + # If N is NULL, return all edges + if (is.null(N)) { + return(df$edge) + } + + if(mode == "topN") { + df <- df %>% slice_head(n = N) + } else { # topNpercent + cutoff <- quantile(df[[rankCol]], 1 - N/100, na.rm = TRUE) + df <- df %>% filter(abs(.data[[rankCol]]) >= cutoff) + } + + df$edge + }) + + names(edgeSets) <- names(netDataList) + return(edgeSets) +} + +# ====================================================== +# PART B: Pairwise Overlap / Set Metrics & Correlation +# ====================================================== + +# - 1. Global Network Jaccard ----------- +computeJaccardMatrix <- function(edgeSets) { + networkNames <- names(edgeSets) + numNetworks <- length(edgeSets) + + pairInt <- matrix(NA, nrow=numNetworks, ncol=numNetworks, dimnames=list(networkNames, networkNames)) + pairJac <- matrix(NA, nrow=numNetworks, ncol=numNetworks, dimnames=list(networkNames, networkNames)) + + if (numNetworks > 1) { + for (i in 1:(numNetworks-1)) { + for (j in (i+1):numNetworks) { + shared <- intersect(edgeSets[[i]], edgeSets[[j]]) + un <- union(edgeSets[[i]], edgeSets[[j]]) + pairInt[i,j] <- length(shared) + pairJac[i,j] <- pairJac[j,i] <- if (length(un)) length(shared)/length(un) else NA + } + } + } + + diag(pairInt) <- vapply(edgeSets, length, integer(1)) + diag(pairJac) <- 1 + + return(list(pairJac = pairJac, pairInt = pairInt)) +} + + +# ============================================================================== +# - 2. Compute Spearman Correlation between pairs of ranks or weights +# ============================================================================== +#' +#' @param netDataList Named list of data.frames. Each data.frame must contain TF, target gene, and score columns. +#' @param edgeSets Optional named list of character vectors (from getEdgeSets). If provided, only these edges are used. +#' @param tfCol Character. Name of TF column (default "TF"). +#' @param targetCol Character. Name of target gene column (default "Gene"). +#' @param rankCol Character. Name of score column (default "signedQuantile"). +#' +#' @return A tibble with columns: Net1, Net2, Spearman correlation. +#' +#' @examples +#' # Spearman on all edges +#' computeSpearman(netDataList) +#' +#' # Spearman on top edges only +#' edges_top10pct <- getEdgeSets(netDataList, N=10) +#' computeSpearman(netDataList, edgeSets = edges_top10pct) +computeSpearman <- function(netDataList, + edgeSets = NULL, + ref = NULL, + tfCol="TF", + targetCol="Gene", + rankCol="signedQuantile") { + + prepForCor <- function(df, edges = NULL) { + df <- df %>% + filter(.data[[rankCol]] != 0) %>% + mutate(edge = paste(.data[[tfCol]], .data[[targetCol]], sep="~")) %>% + dplyr::select(edge, score = .data[[rankCol]]) + + if(!is.null(edges)) { + df <- df %>% filter(edge %in% edges) + } + + return(df) + } + + # Prepare each network + nets <- lapply(seq_along(netDataList), function(i) { + edges_to_use <- if(!is.null(edgeSets)) edgeSets[[names(netDataList)[i]]] else NULL + prepForCor(netDataList[[i]], edges = edges_to_use) + }) + + netNames <- names(netDataList) + names(nets) <- netNames + + # --- Compute pairwise Spearman correlations + # Build comparison pairs + if(!is.null(ref)){ + if(!ref %in% netNames){ + stop("Reference not found in edgeSets") + } + combs <- lapply(setdiff(netNames, ref), function(x) + c(ref, x)) + } else { + combs <- combn(netNames, 2, simplify = FALSE) + } + + map_dfr(combs, function(p) { + a <- nets[[p[1]]] + b <- nets[[p[2]]] + + joined <- inner_join(a, b, by = "edge", suffix = c(".1", ".2")) + rho <- if(nrow(joined) > 1) { + cor(joined$score.1, joined$score.2, method = "spearman", use = "complete.obs") + } else {NA} + + tibble( + Net1 = p[1], + Net2 = p[2], + Spearman = rho + ) + }) +} + + +# ==== 3.Compute Edge overlaps ==== +computeOverlapStats <- function(edgeSets, ref = NULL){ + nets <- names(edgeSets) + # Build comparison pairs + if(!is.null(ref)){ + if(!ref %in% nets){ + stop("Reference not found in edgeSets") + } + combs <- lapply(setdiff(nets, ref), function(x) + c(ref, x)) + } else { + combs <- combn(nets, 2, simplify = FALSE) + } + # combs <- combn(names(edgeSets), 2, simplify=FALSE) + + map_dfr(combs, function(p){ + A <- edgeSets[[p[1]]] + B <- edgeSets[[p[2]]] + + nA <- length(A) + nB <- length(B) + + shared <- length(intersect(A,B)) + un <- length(union(A,B)) + + # Dice Coefficient (or Sorensen-Dice Index) + dice <- ifelse( + nA + nB > 0, + 2 * shared / (nA + nB), + NA + ) + + # What fraction of the smaller set is shared with the larger set? + # Szymkiewicz–Simpson coefficient + fracOverlap = + ifelse(min(nA,nB)>0, + shared / min(nA,nB), + NA) + tibble( + Net1 = p[1], + Net2 = p[2], + pairID = paste0(p[1], "~", p[2]), + size1 = nA, + size2 = nB, + Intersection = shared, + frac1in2 = ifelse(nA > 0, shared / nA, NA), + frac2in1 = ifelse(nB > 0, shared / nB, NA), + Jaccard = ifelse(un> 0, shared/un, NA), + Dice = dice, + FractionOverlap = fracOverlap + + ) + }) +} + +# ==== 3.Compute TF overlaps ==== +computeTFoverlap <- function(netDataList, ref = NULL, tfCol = "TF") { + # Build TF sets + tfSets <- lapply(netDataList, function(df) unique(df[[tfCol]])) + + nets <- names(tfSets) + + # Build comparison pairs + if(!is.null(ref)){ + if(!ref %in% nets){ + stop("Reference not found in netDataList") + } + combs <- lapply(setdiff(nets, ref), function(x) c(ref, x)) + } else { + combs <- combn(nets, 2, simplify = FALSE) + } + + # Compute pairwise overlaps + map_dfr(combs, function(p){ + A <- tfSets[[p[1]]] + B <- tfSets[[p[2]]] + nA <- length(A) + nB <- length(B) + ov <- intersect(A,B) + shared <- length(ov) + un <- length(union(A,B)) + + # dice <- ifelse(nA + nB > 0, 2 * shared / (nA + nB), NA) + # fracOverlap <- ifelse(min(nA,nB) > 0, shared / min(nA,nB), NA) + + tibble( + Net1 = p[1], + Net2 = p[2], + pairID = paste0(p[1], "~", p[2]), + size1 = nA, + size2 = nB, + Overlaps = paste(ov, collapse = ","), + nIntersection = shared, + frac1in2 = ifelse(nA > 0, shared / nA, NA), + frac2in1 = ifelse(nB > 0, shared / nB, NA) + # Jaccard = ifelse(un > 0, shared/un, NA), + # Dice = dice, + # FractionOverlap = fracOverlap + ) + }) +} + +# ==================================================== +# PART C: TF-Level / Aggregated Metrics +# ==================================================== + +# ===== 1. TF-centric Jaccard ==== +#' For each TF, how consistent are its predicted targets across networks? +#' Biological networks are TF-driven, so checking target consistency per TF +#' Jaccard penalizes sets with different sizes. It measures proportion of shared elements out of all unique elements +#' +computeTFJaccard <- function(netDataList, tfList=NULL){ + networkNames <- names(netDataList) + numNetworks <- length(netDataList) + + allTFs <- unique(unlist(lapply(netDataList, function(df) df$TF))) + tfs <- if(!is.null(tfList)) intersect(allTFs, tfList) else allTFs + + pairNames <- combn(networkNames,2,function(x) paste(x, collapse="~")) + tfMatrix <- matrix(NA, nrow=length(tfs), ncol=length(pairNames), dimnames=list(tfs, pairNames)) + + for(tf in tfs){ + tfTargets <- lapply(netDataList, function(df) df$Gene[df$TF == tf]) + for(k in seq_along(pairNames)){ + pair <- combn(seq_len(numNetworks),2)[,k] + shared <- intersect(tfTargets[[pair[1]]], tfTargets[[pair[2]]]) + un <- union(tfTargets[[pair[1]]], tfTargets[[pair[2]]]) + tfMatrix[tf,k] <- if(length(un)) length(shared) /length(un) else NA + } + } + return(tfMatrix) +} + +# plotTFHeatmap <- function(tfMatrix, fileOut, fontsize_number=7, fontsize=9, fig_width = 6, fig_height = 6){ +# heatCols <- colorRampPalette(RColorBrewer::brewer.pal(9,"YlOrRd"))(100) +# pdf(fileOut, width = fig_width, height = fig_height) +# pheatmap(tfMatrix, +# display_numbers=TRUE, +# number_color="black", +# number_format="%.2f", +# fontsize_number=fontsize_number, +# fontsize=fontsize, +# cluster_rows=FALSE, +# cluster_cols=FALSE, +# show_rownames = TRUE, +# show_colnames = TRUE, +# legend = FALSE , # removes grid lines +# color=heatCols, +# main="" +# ) +# dev.off() +# } + +# ==== 2. Aggregated TF Jaccard per Network Pair ===== +computeAggregatedPairJaccard <- function(tfMatrix, fun=median){ + # tfMatrix: rows = TFs, cols = network pairs + aggVec <- apply(tfMatrix, 2, fun, na.rm=TRUE) + + # Convert to symmetric matrix + pairNames <- colnames(tfMatrix) + nets <- unique(unlist(strsplit(pairNames, "~"))) + numNetworks <- length(nets) + aggMat <- matrix(NA, nrow=numNetworks, ncol=numNetworks, dimnames=list(nets,nets)) + + for(k in seq_along(pairNames)){ + pair <- strsplit(pairNames[k], "~")[[1]] + aggMat[pair[1], pair[2]] <- aggVec[k] + aggMat[pair[2], pair[1]] <- aggVec[k] + } + diag(aggMat) <- 1 + return(aggMat) +} + + +#' ==== 3./ Compute and plot hub TF set overlap across networks ==== +#' +#' This function identifies the top N hub transcription factors (TFs) in each +#' network, computes pairwise similarity between hub sets across networks using +#' either the Jaccard index or the overlap coefficient, and plots a heatmap of +#' the similarity matrix. +#' +#' @param netDataList List of data frames, one per network, each containing edges. +#' @param tfCol Column name (string) indicating which column contains TF identifiers. +#' @param networkNames Character vector of network names, matching the order of netDataList. +#' @param dirOut Output directory for saving the heatmap PDF. +#' @param topN Integer. Number of top hubs (by edge count) to include per network. Default = 50. +#' @param metric Similarity metric. One of "jaccard" or "overlap". Default = "jaccard". +#' @param heatCols Color palette for the heatmap. Default = 100-color YlOrRd. +#' @param fontsize Base font size for heatmap text. Default = 9. +#' @param fontsize_number Font size for numbers displayed in cells. Default = 7. +#' #' @param fig_width Width of the PDF figure in inches. Default = 6. +#' @param fig_height Height of the PDF figure in inches. Default = 6. + +#' +#' @return Invisibly returns the similarity matrix used to generate the heatmap. +#' +#' @examples +#' hubSim <- computeHubOverlapHeatmap( +#' netDataList = netDataList, +#' tfCol = "TF", +#' networkNames = c("Net1", "Net2", "Net3"), +#' dirOut = "results/", +#' topN = 50, +#' metric = "jaccard", +#' fig_width = 8, +#' fig_height = 8 +#' ) +#' +#' @export +computeHubOverlapHeatmap <- function(netDataList, tfCol, networkNames, + dirOut, topN = 50, + metric = c("jaccard", "overlap"), + heatCols = colorRampPalette(RColorBrewer::brewer.pal(9, "YlOrRd"))(100), + fontsize = 9, fontsize_number = 7, fig_width = 6, fig_height = 6) { + # --- Argument check + metric <- match.arg(metric) + numNetworks <- length(netDataList) + + # --- 1. Count edges per TF and rank hubs + hubRankList <- lapply(netDataList, function(df) { + tfCounts <- table(df[[tfCol]]) + sort(tfCounts, decreasing = TRUE) + }) + + # --- 2. Take top N hubs + topHubs <- lapply(hubRankList, function(x) head(names(x), topN)) + + # --- 3. Initialize similarity matrix + hubSim <- matrix(NA, numNetworks, numNetworks, + dimnames = list(networkNames, networkNames)) + + # --- 4. Compute similarity + for (i in 1:(numNetworks-1)) { + for (j in (i+1):numNetworks) { + inter <- intersect(topHubs[[i]], topHubs[[j]]) + + if(metric == "jaccard") { + union <- union(topHubs[[i]], topHubs[[j]]) + hubSim[i,j] <- hubSim[j,i] <- length(inter) / length(union) + } else if(metric == "overlap") { + minSize <- min(length(topHubs[[i]]), length(topHubs[[j]])) + hubSim[i,j] <- hubSim[j,i] <- length(inter) / minSize + } + } + } + diag(hubSim) <- 1 + + # --- 5. Plot heatmap + pdf(file.path(dirOut, paste0("HubTF_Overlap_", metric, "_top", topN, ".pdf")), width = fig_width, height = fig_height) + pheatmap(hubSim, + display_numbers = TRUE, + number_color = "black", + number_format = "%.2f", + fontsize_number = fontsize_number, + fontsize = fontsize, + cluster_rows = FALSE, + cluster_cols = FALSE, + show_rownames = TRUE, + show_colnames = TRUE, + legend = FALSE, + color = heatCols, + main = "") + dev.off() + + invisible(hubSim) +} + +# ================================================== +# - Plotting Ulis +# ================================================== + + +# ==== Edge Sharing ==== +plotEdgeSharing <- function(edgeSets, fileSuffix, outDir=".") { + library(ggVennDiagram) + library(ComplexUpset) + library(ComplexHeatmap) + + numNetworks <- length(edgeSets) + dir.create(outDir, showWarnings = FALSE, recursive = TRUE) + + if(numNetworks <= 3){ + # --- Venn diagram + pdf(file.path(outDir, paste0("Venn2_", fileSuffix, ".pdf")), width=6, height=6) + ggVennDiagram(edgeSets, label_alpha = 0.6) + + scale_fill_gradient(low="grey90", high="red") + + theme(legend.position="none") + dev.off() + + } else { + # --- UpSet plots for >2 networks + # Convert edgeSets to a presence/absence matrix + allEdges <- unique(unlist(lapply(edgeSets, function(df) paste(df$TF, df$Gene, sep="~")))) + incMat <- sapply(edgeSets, function(df) as.integer(allEdges %in% paste(df$TF, df$Gene, sep="~"))) + colnames(incMat) <- names(edgeSets) + rownames(incMat) <- allEdges + + # Intersect mode + mObjIntersect <- make_comb_mat(incMat, mode="intersect") + pdf(file.path(outDir, "UpSet_Intersect.pdf"), width=8.5, height=4) + draw(UpSet(mObjIntersect, + set_order = names(edgeSets), + top_annotation = upset_top_annotation(mObjIntersect, annotation_name_rot=90, axis=FALSE, + add_numbers=TRUE, numbers_rot=90, + gp=gpar(col=comb_degree(mObjIntersect), fontsize=6), height=unit(4,"cm")), + right_annotation = upset_right_annotation(mObjIntersect, gp=gpar(fill="black", fontsize=6), + width=unit(4,"cm"), show_annotation_name=FALSE, add_numbers=TRUE))) + dev.off() + + # Distinct mode + mObjDistinct <- make_comb_mat(incMat, mode="distinct") + pdf(file.path(outDir, paste0("UpSet_distinct_", fileSuffix, ".pdf")), width=8.5, height=4) + draw(UpSet(mObjDistinct, + set_order = names(edgeSets), + top_annotation = upset_top_annotation(mObjDistinct, annotation_name_rot=90, axis=FALSE, + add_numbers=TRUE, numbers_rot=90, + gp=gpar(col=comb_degree(mObjDistinct), fontsize=6), height=unit(4,"cm")), + right_annotation = upset_right_annotation(mObjDistinct, gp=gpar(fill="black", fontsize=6), + width=unit(4,"cm"), show_annotation_name=FALSE, add_numbers=TRUE))) + dev.off() + } + + message("Edge sharing plots generated.") +} + + +plotSimilarityHeatmap <- function(simMat, fileOut="sim_heatmap.pdf", + fontsize_number=7, fontsize=9, fig_width=6, fig_height=6) { + simMat[lower.tri(simMat)] <- t(simMat)[lower.tri(simMat)] + heatCols <- colorRampPalette(RColorBrewer::brewer.pal(9,"YlOrRd"))(100) + pdf(fileOut, height = fig_height, width = fig_width) + pheatmap::pheatmap(simMat, + display_numbers=TRUE, + number_color="black", + number_format="%.2f", + fontsize_number=fontsize_number, + fontsize=fontsize, + cluster_rows=FALSE, + cluster_cols=FALSE, + show_rownames = TRUE, + show_colnames = TRUE, + legend = FALSE , + color=heatCols, + main="" + ) + dev.off() +} + +plotAggregatedJaccard <- function(aggMat, fileOut, fontsize_number=7, fontsize=9, fig_width = 6, fig_height = 6){ + heatCols <- colorRampPalette(RColorBrewer::brewer.pal(9,"YlOrRd"))(100) + pdf(fileOut, width = fig_width, height = fig_height) + pheatmap(aggMat, + display_numbers=TRUE, + number_color="black", + number_format="%.2f", + fontsize_number=fontsize_number, + fontsize=fontsize, + cluster_rows=FALSE, + cluster_cols=FALSE, + show_rownames = TRUE, + show_colnames = TRUE, + legend = FALSE , # removes grid lines + color=heatCols, + main="Aggregated TF Jaccard per Network Pair") + dev.off() +} + +plotMetricTrend <- function(df, xCol, yCol, groupCol = "pairID", colorMap = NULL, + xLabel = NULL, yLabel = NULL, yLimits = NULL, xBreaks = NULL, + outFile = NULL, width = 6, height = 3, dpi = 600){ + p <- ggplot(df, + aes_string(x = xCol, y = yCol, color = groupCol, group = groupCol) + ) + + geom_line(linewidth = 0.5) + + geom_point(size = 1) + + theme_bw(base_family = "Helvetica") + + theme( + axis.text.x = element_text(size = 7, color = "black"), + axis.text.y = element_text(size = 7, color = "black"), + axis.title = element_text(size = 9, color = "black"), + legend.title = element_blank(), + legend.text = element_text(size = 9, color = "black"), + legend.position = "right", + legend.box.just = "left", + panel.grid.major = element_line(color = "grey80", linewidth = 0.3), + panel.grid.minor = element_line(color = "grey90", linewidth = 0.1) + ) + + # Optional scales + if(!is.null(colorMap)){ + p <- p + scale_color_manual(values = colorMap) + } + + if(!is.null(xBreaks)){ + p <- p + scale_x_continuous(breaks = xBreaks) + } + + if(!is.null(yLimits)){ + p <- p + scale_y_continuous(limits = yLimits) + } else{ + dataMin <- min(df[[yCol]], na.rm = TRUE) + dataMax <- max(df[[yCol]], na.rm = TRUE) + yMin <- if (dataMin >= 0) 0 else dataMin + yMax <- max(dataMax, 1) + yLimits <- c(yMin, yMax) + p <- p + scale_y_continuous(limits = yLimits) + } + # Labels + p <- p + labs( x = xLabel, y = yLabel ) + # Save if requested + if(!is.null(outFile)) ggsave(outFile, plot = p, width = width, height = height, dpi = dpi) + + return(p) +} + +# ========================================== +# High-Level Pipeline / API (Top Level) +# ========================================== +computeTopNMetrics <- function(netDataList, + ref = NULL, + topNs = NULL, + tfCol="TF", + targetCol="Target", + rankCol="Weight", + mode = c("topNpercent", "topN")) { + #' Compute Top-N overlap and correlation metrics for multiple networks + #' + #' @param netDataList Named list of network data.frames + #' @param topNs Numeric vector of percentages (Top N%) or number to evaluate + #' @param tfCol Character, column name for transcription factor + #' @param targetCol Character, column name for target gene + #' @param rankCol Character, column name for ranking score + #' + #' @return data.frame with columns: pairID, Jaccard, FractionOverlap, Spearman, topNs + mode = match.arg(mode) + if (is.null(topNs)) topNs <- "all" + topNResults <- lapply(topNs, function(k) { + cat("Top-N:", k, "\n") + + if (k == "all"){ + edges <- getEdgeSets(netDataList, tfCol = tfCol, + targetCol = targetCol, rankCol = rankCol) + } else{ + # Extract Top-N% edges + edges <- getEdgeSets(netDataList, tfCol = tfCol, + targetCol = targetCol, rankCol = rankCol, + mode = mode, N = k) + } + # Overlap statistics + stats <- computeOverlapStats(edges, ref) + stats$Npct <- if(k == "all") NA else k + + # Spearman correlation + corRes <- computeSpearman(netDataList, edgeSets = edges, ref = ref, tfCol = tfCol, targetCol = targetCol, rankCol = rankCol) + # Merge Spearman with stats + stats <- stats %>% + left_join(corRes %>% + mutate(pairID = paste(Net1, Net2, sep="~")) %>% + dplyr::select(pairID, Spearman), + by = "pairID") + + stats + }) + + bind_rows(topNResults) +} + diff --git a/evaluation/R/evaluateNetworks.R b/evaluation/R/evaluateNetworks.R new file mode 100755 index 0000000..ee2671f --- /dev/null +++ b/evaluation/R/evaluateNetworks.R @@ -0,0 +1,311 @@ +rm(list = ls()) +suppressPackageStartupMessages({ + library(dplyr) + library(tidyr) + library(ggplot2) + library(ComplexHeatmap) + library(pheatmap) + # library(VennDiagram) + library(ggVennDiagram) + library(grid) + library(reshape2) +}) + +source("/data/miraldiNB/Michael/Scripts/VennDiagram.R") + +# dirOut <- "/data/miraldiNB/Michael/hCD4T_Katko/Inferelator/noMergedTF/Bulk/5kbTSS/newPipe/spikeIgnored/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63" +dirOut <- "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/networkEval/" +dir.create(dirOut, recursive = T, showWarnings = F) + +# Combined/combined_max.tsv +# TFmRNA/edges_subset.txt +# netFiles <- list( +# "TFA" = "/data/miraldiNB/Michael/hCD4T_Katko/Inferelator/noMergedTF/SC/SCENIC/geneLambda0p5_220totSS_20tfsPerGene_subsamplePCT5_eRegulon/TFA/edges_subset.tsv", +# "TFmRNA" = "/data/miraldiNB/Michael/hCD4T_Katko/Inferelator/noMergedTF/SC/SCENIC/geneLambda0p5_220totSS_20tfsPerGene_subsamplePCT5_eRegulon/TFmRNA/edges_subset.tsv", +# "Combined" = "/data/miraldiNB/Michael/hCD4T_Katko/Inferelator/noMergedTF/SC/SCENIC/geneLambda0p5_220totSS_20tfsPerGene_subsamplePCT5_eRegulon/Combined/combined_max.tsv" +# ) + +netFiles <- list( + "TFA" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/Bulk/ATAC/newPipe/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.tsv", + "TFmRNA" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/Bulk/ATAC/newPipe/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/TFmRNA/edges_subset.tsv", + "Combined" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/Bulk/ATAC/newPipe/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/Combined/combined_max.tsv" + ) + +# priorFile = "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/Priors/MotifScan5kbTSS_b_sp.tsv" +priorFile = "/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/priors/ATAC/ATAC_Tfh10_sp.tsv" +# k <- read.table("/data/miraldiNB/Michael/hCD4T_Katko/dataBank/Priors/SCENICp/SCENICp_RE2Glinks_FIMO_5Kb_derived_b.tsv") +# k$Target <- rownames(k) +# b <- melt(k, id.vars = "Target", variable.names = "TF", value.name = "Weigths") +# colnames(b)[2:3] <- c("TF", "Weights") +# b <- b[, c("TF", "Target", "Weights")] +# bb <- b[b$Weights != 0, ] +# write.table(bb, "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/Priors/SCENICp/SCENICp_RE2Glinks_FIMO_5Kb_derived_sp.tsv", row.names = F, sep ="\t", quote = F) + +tfCol <- "TF" # specify TF column name here +targetCol <- "Gene" # specify Target column name here +priorTfCol <- "TF" # TF column name in prior file +priorTargetCol <- "Target" # Target column name in prior file +priorWgtCol <-"Weight" +nSelect <- 10 +stabCol <- "Stability" +compareNets <- TRUE +# ======================================================================== +# ------- Read Input files +# ======================================================================== + +# ----- Read Prior +priorData <- read.table(priorFile, header = TRUE, sep = "\t", stringsAsFactors = FALSE) +# priorData <- priorData[priorData[[priorWgtCol]] != 0, ] +priorPairs <- unique(paste(priorData[[priorTfCol]], priorData[[priorTargetCol]], sep = "_")) + +# --- Preload all network data into a list --- +netDataList <- list() +for (typeName in names(netFiles)) { + filePath <- netFiles[[typeName]] + netDataList[[typeName]] <- read.table(filePath, header = TRUE, sep = "\t", stringsAsFactors = FALSE) +} + +numNetworks <- length(netDataList) +networkNames <- names(netDataList) +# ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +# ---- PART ONE: Make a table of number of network ppts - uniqueTFs, uniqueTargets, Total Interactions, and % supported by prior +# ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +# process each network +summaryList <- list() +for (typeName in networkNames) { + data <- netDataList[[typeName]] + + uniqueTf <- length(unique(data[[tfCol]])) + uniqueTarget <- length(unique(data[[targetCol]])) + + pairStrings <- unique(paste(data[[tfCol]], data[[targetCol]], sep = "_")) + totalInteractions <- length(pairStrings) + + supportedCount <- length(intersect(pairStrings, priorPairs)) + percentSupportedByPrior <- 100 * supportedCount / totalInteractions + + summaryList[[typeName]] <- data.frame(type = typeName, uniqueTf = uniqueTf, + uniqueTarget = uniqueTarget, totalInteractions = totalInteractions, + percentSupportedByPrior = percentSupportedByPrior) +} + +summaryTable <- do.call(rbind, summaryList) +print(summaryTable) + +write.table(as.data.frame(summaryTable), file.path(dirOut, "summaryStatistics.tsv"), col.names = NA, row.names = TRUE, quote = FALSE, sep = "\t") + + +# ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +# ---- PART TWO: Compare Networks (can handle more than 2) +# ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +if (compareNets){ + + #--- Build edge sets + edgeSets <- lapply(netDataList, function(df) paste(df[[tfCol]], df[[targetCol]], sep="~")) + names(edgeSets) <- names(netDataList) + networkNames <- names(netDataList) + numNetworks <- length(netDataList) + + #--- Edges shared between all networks + allShared <- Reduce(intersect, edgeSets) + + #1. --- Pairwise Jaccard, Intersections, and Heatmap + + if (numNetworks > 1) { + pairInt <- matrix(NA, numNetworks, numNetworks, dimnames=list(networkNames,networkNames)) + pairJac <- matrix(NA, numNetworks, numNetworks, dimnames=list(networkNames,networkNames)) + + for (i in 1:(numNetworks-1)) { + for (j in (i+1):numNetworks) { + shared <- intersect(edgeSets[[i]], edgeSets[[j]]) + un <- union(edgeSets[[i]], edgeSets[[j]]) + pairInt[i,j] <- length(shared) + pairJac[i,j] <- pairJac[j,i] <- if (length(un)) length(shared)/length(un) else NA + } + } + diag(pairInt) <- vapply(edgeSets, length, integer(1)) + diag(pairJac) <- 1 + + # heat-map of Jaccard ---------------------------------------- + heatCols <- colorRampPalette(RColorBrewer::brewer.pal(9,"YlOrRd"))(100) + pdf(file.path(dirOut, "Jaccard.pdf")) + pheatmap(pairJac, display_numbers = TRUE, main = "Pairwise Jaccard Index", color = heatCols) + dev.off() + + # -------- unique-edge counts ----------------------------------------- ## + uniqCountsDf <- data.frame( + network = names(edgeSets), + numUniqueEdges = sapply(seq_along(edgeSets), function(k){ + curr <- edgeSets[[k]] + others <- unique(unlist(edgeSets[-k])) + sum(!curr %in% others) + }), + row.names = NULL, + stringsAsFactors = FALSE + ) + + # 3. ------- Save Pairwise and uniqueDF counts + pairDf <- melt(pairInt, varnames = c("network1", "network2"), value.name = "numIntersection", na.rm = TRUE) + write.table(pairDf, file.path(dirOut, "num_Pairwise_Intersection.tsv"), quote = F, col.names = NA, row.names=TRUE, sep = "\t") + + } + + # 2.------ Venn or UpSet Plot + if (numNetworks >= 2) { + vennInput <- edgeSets + ggVennDiagram(vennInput, label_alpha = 0.6) + scale_fill_gradient(low="grey90", high = "red") + ggsave(file.path(dirOut, "Venn2.pdf")) + # venn.plot <- venn.diagram( + # vennInput, + # category.names = names(vennInput), + # filename = NULL, + # output = TRUE, + # main = "Shared TF~Gene Pairs" + # ) + # pdf(file.path(dirOut, "Venn2.pdf")) + # grid::grid.draw(venn.plot) + # dev.off() + + } else if (numNetworks > 3) { + # allEdges <- unique(unlist(edgeSets)) + # incMat <- sapply(edgeSets, function(edgeSet) as.integer(allEdges %in% edgeSet)) + # colnames(incMat) <- names(edgeSets) + # rownames(incMat) <- allEdges + upSetData <- edgeSets + + plotUpSet <- function(setsData, Mode){ + mObj <- make_comb_mat(setsData, mode = Mode) + ht <- UpSet(mObj, set_order = names(setsData), + top_annotation = upset_top_annotation(mObj, annotation_name_rot = 90, axis = FALSE, + add_numbers = TRUE, numbers_rot = 90, + gp = gpar(col = comb_degree(mObj), fontsize = 6), height = unit(4, "cm"), + axis_param = list(side = "left")), + right_annotation = upset_right_annotation(mObj, axis_param = list(side = "bottom"), #labels = FALSE,labels_rot = 0 + gp = gpar(fill = "black", fontsize = 6), width = unit(4, "cm"), show_annotation_name = FALSE, + add_numbers = TRUE, axis = FALSE + ) + ) + return(ht) + } + + pdf(file.path(dirOut, "UpSet_distinct.pdf"), width = 8.5, height = 4) + htDistinct <- plotUpSet(upSetData, Mode = "distinct") + draw(htDistinct) + dev.off() + + pdf(file.path(dirOut, "UpSet_Intersect.pdf"), width = 8.5, height = 4) + htIntersect <- plotUpSet(upSetData, Mode = "intersect") + draw(htIntersect) + dev.off() + } +} + + +# ──────────────────────────────────────────────────────────────────────────────────────────── +# PART THREE: Histogram distributions of: + # A: Targets per TF: # times each gene is targeted (number of TFs regulating it). + # B: TFs per Target: # times each TF is a regulator (number of genes it controls) + # C: Box-Plot of Stability of the top N low and high in degree genes + + #NOTE: Each Figure is saved in the same path as the network being evaluated +# ──────────────────────────────────────────────────────────────────────────────────────────── +# Generate both plots for each network +for (typeName in names(netDataList)) { + data <- netDataList[[typeName]] + basePath <- dirname(netFiles[[typeName]]) + + # Plot 1: # Targets per TF + tfTargetCounts <- table(data[[tfCol]]) + dfTF <- data.frame(TF = names(tfTargetCounts), targetCount = as.integer(tfTargetCounts)) + + p1 <- ggplot(dfTF, aes(x = targetCount)) + + geom_histogram(binwidth = 1, boundary = 0.5, fill = "#0072B2", color = "black", alpha = 0.8) + + # scale_x_continuous(breaks = seq(0, max(dfTF$targetCount), 1)) + + labs(title = paste("Distribution of # Targets per TF -", typeName), + x = "# Targets", y = "# TFs") + + theme_bw(base_family = "Helvetica") + + theme( + panel.grid.major.y = element_line(color = "grey80", linewidth = 0.3), + panel.grid.minor = element_blank(), + # axis.line = element_line(color = "black", linewidth = 0.4), + axis.text.x = element_text(size = 7), + axis.text.y = element_text(size = 7), + axis.title = element_text(size = 9), + plot.margin = margin(5, 5, 5, 5), + # panel.background = element_rect("black", fill = NA) + ) + + ggsave(file.path(basePath, paste0("TargetCountPerTF_", typeName, ".pdf")), + plot = p1, width = 3, height = 3) + + # Plot 2: # TFs per Target + targetTFCounts <- table(data[[targetCol]]) + dfTarget<- data.frame(Target = names(targetTFCounts), tfCount = as.integer(targetTFCounts)) + dfTargetSorted <- dfTarget[order(dfTarget$tfCount), ] + + p2 <- ggplot(dfTarget, aes(x = tfCount)) + + geom_histogram(binwidth = 1, boundary = 0.5, fill = "blue", color = "black", alpha = 0.7) + + # scale_x_continuous(breaks = seq(0, max(dfTarget$tfCount), 1)) + + labs(title = paste("Distribution of # TFs per Target -", typeName), + x = "# TFs", y = "# Targets") + + theme_bw(base_family = "Helvetica") + + theme( + panel.grid.major.y = element_line(color = "grey80", linewidth = 0.3), + panel.grid.minor = element_blank(), + # axis.line = element_line(color = "black", linewidth = 0.4), + axis.text.x = element_text(size = 7), + axis.text.y = element_text(size = 7), + axis.title = element_text(size = 9), + plot.margin = margin(5, 5, 5, 5), + # panel.background = element_rect("black", fill = NA) + ) + + ggsave(file.path(basePath, paste0("TFCountPerTarget_", typeName, ".pdf")), + plot = p2, width = 3, height = 3) + + # Select top N low and high TF targets + lowTargs <- dfTargetSorted$Target[1:nSelect] + highTargs <- tail(dfTargetSorted, nSelect)$Target + + stabilityDF <- data[data[[targetCol]] %in% c(lowTargs, highTargs), c(targetCol, stabCol)] + # Add Group column based on whether the target is in lowTargs or highTargs + stabilityDF$Group <- ifelse(stabilityDF[[targetCol]] %in% lowTargs, + "Low TFs per Target", + "High TFs per Target") + stabilityDF <- merge(stabilityDF, dfTargetSorted, by.x = targetCol, by.y = "Target") + stabilityDF$targetLabel <- paste0(stabilityDF[[targetCol]], "(", stabilityDF$tfCount, ")") + # Keep plotting order +# stabilityDF$targetLabel <- factor(stabilityDF$targetLabel, +# levels = unique(stabilityDF$targetLabel)) + # Order based on tfCount directly + stabilityDF$targetLabel <- factor( + stabilityDF$targetLabel, + levels = unique(stabilityDF$targetLabel[order(stabilityDF$tfCount)]) + ) + + # Plot: boxplot per target + p3 <- ggplot(stabilityDF, aes(x = targetLabel, y = Stability, fill = Group)) + + geom_boxplot(outlier.size = 0.5, alpha = 0.8) + + labs(title = paste("Per-Target Stability Distribution -", typeName), + x = "Number of TFs per Target (TF count)", y = "Stability") + + theme_minimal() + + theme_bw(base_family = "Helvetica") + + theme( + panel.grid.major.y = element_line(color = "grey80", linewidth = 0.3), + panel.grid.minor = element_blank(), + # axis.line = element_line(color = "black", linewidth = 0.4), + axis.text.x = element_text(size = 7, angle = 90, vjust = 0.5), + axis.text.y = element_text(size = 7), + axis.title = element_text(size = 9), + plot.margin = margin(5, 5, 5, 5), + legend.position = "top" + # panel.background = element_rect("black", fill = NA) + ) + + # Save + ggsave(file.path(basePath, paste0("Top", nSelect, "HighorLow_inDegreeGenes_Boxplot", typeName, ".pdf")), + plot = p3, width = 12, height = 5) + +} + diff --git a/evaluation/R/evaluateNetworks1.R b/evaluation/R/evaluateNetworks1.R new file mode 100755 index 0000000..f25e504 --- /dev/null +++ b/evaluation/R/evaluateNetworks1.R @@ -0,0 +1,708 @@ +rm(list = ls()) +suppressPackageStartupMessages({ + library(dplyr) + library(tidyr) + library(ggplot2) + library(ComplexHeatmap) + library(pheatmap) + library(circlize) + # library(VennDiagram) +# library(ggVennDiagram) + library(grid) + library(reshape2) + library(RColorBrewer) + library(purrr) + +}) + +source("/data/miraldiNB/Michael/Scripts/VennDiagram.R") +source("/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/RprogUtils/evaluateNetUtils.R") + +# dirOut <- "/data/miraldiNB/Michael/hCD4T_Katko/Inferelator/noMergedTF/Bulk/5kbTSS/newPipe/spikeIgnored/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63" +dirOut <- "/data/miraldiNB/Michael/mCD4T_Wayman/Figures/Fig4/networkComparison1" +dir.create(dirOut, recursive = T, showWarnings = F) + +# Combined/combined_max.tsv +# TFmRNA/edges_subset.txt +# netFiles <- list( +# "TFA" = "/data/miraldiNB/Michael/hCD4T_Katko/Inferelator/noMergedTF/SC/SCENIC/geneLambda0p5_220totSS_20tfsPerGene_subsamplePCT5_eRegulon/TFA/edges_subset.tsv", +# "TFmRNA" = "/data/miraldiNB/Michael/hCD4T_Katko/Inferelator/noMergedTF/SC/SCENIC/geneLambda0p5_220totSS_20tfsPerGene_subsamplePCT5_eRegulon/TFmRNA/edges_subset.tsv", +# "Combined" = "/data/miraldiNB/Michael/hCD4T_Katko/Inferelator/noMergedTF/SC/SCENIC/geneLambda0p5_220totSS_20tfsPerGene_subsamplePCT5_eRegulon/Combined/combined_max.tsv" +# ) + +# netFiles <- list( +# "TFA" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/Bulk/ATAC/newPipe/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/TFA/edges_subset.tsv", +# "TFmRNA" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/Bulk/ATAC/newPipe/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/TFmRNA/edges_subset.tsv", +# "Combined" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/Bulk/ATAC/newPipe/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/Combined/combined_max.tsv" +# ) + +netFiles <- list( + "PB" = "/data/miraldiNB/Michael/projects/GRN/mCD4T_Wayman/Inferelator/noMergedTF/Bulk/ATAC/newPipe/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/Combined/combined_max.tsv", + "SC" = "/data/miraldiNB/Michael/projects/GRN/mCD4T_Wayman/Inferelator/noMergedTF/SC/ATAC/geneLambda0p5_220totSS_20tfsPerGene_subsamplePCT5/Combined/combined_max.tsv" + # "MC2" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/metaCells/MC2/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63_logNorm/Combined/combined_max.tsv", + # "SEA" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/noMergedTF/metaCells/SEACells/networkLambda0p5_100totSS_20tfsPerGene_subsamplePCT63/Combined/combined_max.tsv" +) + +# priorFile = "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/Priors/MotifScan5kbTSS_b_sp.tsv" +priorFile = "/data/miraldiNB/Michael/Scripts/GRN/Inferelator_JL/Tfh10_Example/inputs/priors/ATAC/ATAC_Tfh10_sp.tsv" +# k <- read.table("/data/miraldiNB/Michael/hCD4T_Katko/dataBank/Priors/SCENICp/SCENICp_RE2Glinks_FIMO_5Kb_derived_b.tsv") +# k$Target <- rownames(k) +# b <- melt(k, id.vars = "Target", variable.names = "TF", value.name = "Weigths") +# colnames(b)[2:3] <- c("TF", "Weights") +# b <- b[, c("TF", "Target", "Weights")] +# bb <- b[b$Weights != 0, ] +# write.table(bb, "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/Priors/SCENICp/SCENICp_RE2Glinks_FIMO_5Kb_derived_sp.tsv", row.names = F, sep ="\t", quote = F) + +tfCol <- "TF" # specify TF column name here +targetCol <- "Gene" # specify Target column name here +priorTfCol <- "TF" # TF column name in prior file +priorTargetCol <- "Target" # Target column name in prior file +priorWgtCol <-"Weight" +nSelect <- 10 +stabCol <- "Stability" +rankCol <- "signedQuantile" +compareNets <- TRUE +tfList <- NULL +lineageTFs <-c("Bcl6","Maf","Batf","Tox","Ascl2", "Foxp3","Ikzf2","Rorc", "Rorc","Stat3", + "Tbx21","Stat4","Eomes","Prdm1","Tcf7","Klf2","Lef1") + +# lineageTFs <- c("TCF7","LEF1","ID3","KLF2","PRDM1","ZEB2","RUNX3","EOMES", +# "TBX21","STAT1","STAT4","HIF1A","RORC","RORA","STAT3","IRF4", +# "BATF","FOXP3","IKZF2","IKZF4","STAT5","FOXO1","GATA3","STAT6", +# "CIITA","RFX5","RFXAP","RFXANK","NLRC5") +N <- NULL +# file_k_clust_across <- "/Users/owop7y/Desktop/MiraldiLab/PROJECTS/GRN/Benchmark/mCD4T/Figures/Fig4/networkComparison/Kmeans_Jaccard_Edges_perTF.rds" +file_k_clust_across <- NULL + +# ======================================================================== +# ------- Read Input files +# ======================================================================== + +# ----- Read Prior +priorData <- read.table(priorFile, header = TRUE, sep = "\t", stringsAsFactors = FALSE) +head(priorData, 3) +priorData <- priorData[priorData[[priorWgtCol]] != 0, ] +priorPairs <- unique(paste(priorData[[priorTfCol]], priorData[[priorTargetCol]], sep = "_")) + +# --- Preload all network data into a list --- +netDataList <- list() +for (typeName in names(netFiles)) { + filePath <- netFiles[[typeName]] + netDataList[[typeName]] <- read.table(filePath, header = TRUE, sep = "\t", stringsAsFactors = FALSE) +} + +numNetworks <- length(netDataList) +networkNames <- names(netDataList) + +# ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +# ---- PART ONE: Make a table of number of network ppts - uniqueTFs, uniqueTargets, Total Interactions, and % supported by prior +# ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +# process each network +summaryList <- list() +for (typeName in networkNames) { + data <- netDataList[[typeName]] + + uniqueTf <- length(unique(data[[tfCol]])) + uniqueTarget <- length(unique(data[[targetCol]])) + + pairStrings <- unique(paste(data[[tfCol]], data[[targetCol]], sep = "_")) + totalInteractions <- length(pairStrings) + + supportedCount <- length(intersect(pairStrings, priorPairs)) + percentSupportedByPrior <- 100 * supportedCount / totalInteractions + + summaryList[[typeName]] <- data.frame(type = typeName, uniqueTf = uniqueTf, + uniqueTarget = uniqueTarget, totalInteractions = totalInteractions, + percentSupportedByPrior = percentSupportedByPrior) +} + +summaryTable <- do.call(rbind, summaryList) +print(summaryTable) + +write.table(as.data.frame(summaryTable), file.path(dirOut, "summaryStatistics.tsv"), col.names = NA, row.names = TRUE, quote = FALSE, sep = "\t") + +# ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +# ---- PART TWO: Compare Networks (can handle more than 2) +# ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +# ──── Compare TF-Targets across methods +# Initialize a data frame to store results +tfTargetCounts <- data.frame(TF = unique(unlist(lapply(netDataList, function(x) unique(x$TF))))) + +# Count targets for each TF in each network +for(netName in names(netDataList)) { + netData <- netDataList[[netName]] + + # Count number of unique target genes per TF + targetCounts <- netData %>% + group_by(TF) %>% + summarise(targetCount = n()) + + colname <- netName + tfTargetCounts <- tfTargetCounts %>% + left_join(targetCounts, by = "TF") %>% + rename(!!colname := targetCount) +} +# Replace NA with 0 for TFs not present in a network +tfTargetCounts[is.na(tfTargetCounts)] <- 0 + +# Build file suffix +fileSuffix <- if(!is.null(topNpercent)) { + paste0(rankCol, "_top", topNpercent, "pct") +} else { + paste0(rankCol, "_all") +} +networkNames <- names(netDataList) +numNetworks <- length(netDataList) + +edgeSets <- getEdgeSets(netDataList, tfCol, targetCol, rankCol) + +# -------- unique-edge counts ----------------------------------------- ## +uniqCountsDf <- data.frame( + network = names(edgeSets), + numUniqueEdges = sapply(seq_along(edgeSets), function(k){ + curr <- edgeSets[[k]] + others <- unique(unlist(edgeSets[-k])) + sum(!curr %in% others) + }), + row.names = NULL, + stringsAsFactors = FALSE + ) + +write.table(uniqCountsDf, + file.path(dirOut, paste0("num_UniqueEdges_", fileSuffix, ".tsv")), + quote=F, col.names=NA, row.names=TRUE, sep="\t") + +# ------- Plot global Jaccard heatmap +# pairs <- plotGlobalJaccard(edgeSets, fileOut = file.path(dirOut, paste0("Global_Jaccard_",fileSuffix, ".pdf")), fig_width = 2.5, fig_height = 2.5) +# pairInt <- pairs[["pairInt"]] + +plotSimilarityHeatmap(pairs[["pairJac"]], fileOut=file.path(dirOut, paste0("Global_Jaccard_",fileSuffix, ".pdf")), fontsize_number=7, fontsize=9, fig_width=2.5, fig_height=2.5) +pairDf <- melt(pairs[["pairInt"]], varnames = c("network1", "network2"), value.name = "numIntersection", na.rm = TRUE) +write.table(pairDf,file.path(dirOut, paste0("num_Pairwise_Intersection_", fileSuffix, ".tsv")), quote=F, col.names=NA, row.names=TRUE, sep="\t") + + +# --------- Compute Jaccard for multiple topN% ------------- +topNpercentList <- seq(10, 100, by = 10) +jaccardResults <- list() +for (topN in topNpercentList) { + currEdgeSets <- getEdgeSets(netDataList, tfCol, targetCol, rankCol, "topNpercent", topN) + # Compute pairwise Jaccard + fileSuffix <- paste0(rankCol, "_top", topN, "pct") + pairs <- computeJaccardMatrix(currEdgeSets) + pairJac <- pairs[["pairJac"]] + # Convert pairwise matrix to long format + pairJacDf <- as.data.frame(as.table(pairJac)) + colnames(pairJacDf) <- c("Network1", "Network2", "Jaccard") + pairJacDf$TopNpercent <- topN + jaccardResults[[as.character(topN)]] <- pairJacDf +} +# Combine all percentages +jaccardDf <- do.call(rbind, jaccardResults) +# Filter out self-comparisons +jaccardDf <- jaccardDf[jaccardDf$Network1 != jaccardDf$Network2, ] +jaccardDf<- jaccardDf %>% + dplyr::mutate( + Network1 = as.character(Network1), + Network2 = as.character(Network2), + pairID = ifelse(Network1 < Network2, + paste(Network1, Network2, sep = "-"), + paste(Network2, Network1, sep = "-")) + ) %>% + dplyr::group_by(TopNpercent, pairID) %>% + dplyr::slice(1) %>% + dplyr::ungroup() +write.table(jaccardDf,file.path(dirOut, "Jaccard_Sim.tsv"), quote=F, col.names=NA, row.names=TRUE, sep="\t") + +# Create color mapping based on Set1 palette +uniquePairs <- sort(unique(jaccardDf$pairID)) +colors <- c("#449B75" ,"#FFE528", "#999999", "#E41A1C", "#AC5782", "#C66764") +comparisonColor = setNames(colors, uniquePairs) + +# Plot line graph with ggplot2 +p_jac <- ggplot(jaccardDf, aes(x = TopNpercent, y = Jaccard,color = pairID,group = pairID)) + + geom_line(linewidth = 0.5) + + geom_point(size = 1) + + scale_color_manual(values = comparisonColor) + + scale_x_continuous(breaks = seq(10, 100, by = 10)) + + scale_y_continuous(limits = c(0,1)) + + labs( + x = "Top N% edges", + y = "Jaccard similarity", + color = "Network pair" + ) + + theme_bw(base_family = "Helvetica") + + theme( + axis.text.x = element_text(size = 7, color = "black"), + axis.text.y = element_text(size = 7, color = "black"), + axis.title = element_text(size = 9, color = "black"), + legend.title = element_blank(), + legend.text = element_text(size = 9, color = "black"), + legend.position = "right", + legend.box.just = "left", # centers the legend + panel.grid.major = element_line(color = "grey80", linewidth = 0.3), + panel.grid.minor = element_line(color = "grey90", linewidth = 0.1) + ) +ggsave(file.path(dirOut, "linePlot_JaccardSim.pdf"),plot = p_jac, width = 4, height = 3, dpi = 600) + +#plotEdgeSharing(edgeSets, fileSuffix, outDir) + +# --- 3. TF-centric analysis +# For each TF, how consistent are its predicted targets across networks? +# Biological networks are TF-driven, so checking target consistency per TF +tfMatrix <- computeTFJaccard(netDataList, tfList) +# MAke Heatmap +getPalette = colorRampPalette(brewer.pal(13, "Set1")) +comparisonColor = getPalette(length(colnames(tfMatrix))) +comparisonColor = setNames(comparisonColor, colnames(tfMatrix)) +colAnn <- HeatmapAnnotation( + `Comparison` = colnames(tfMatrix), + col = list('Comparison' = comparisonColor), + annotation_legend_param = list( + Comparison = list( + title_gp = gpar(fontsize = 8, fontface = "plain"), # title size + labels_gp = gpar(fontsize = 6) # category label size + )), + simple_anno_size = unit(3, "mm"), + show_annotation_name = FALSE + ) + + +tfMatrix[is.na(tfMatrix)] <- 0 +tfRobustness <- rowMeans(tfMatrix, na.rm = TRUE) # average Jaccard per TF + +k_center = 6 +if (is.null(file_k_clust_across)){ + k_clust_across <- kmeans(tfMatrix, centers=k_center,nstart=20, iter.max = 50) +}else{ + k_clust_across <- readRDS(file_k_clust_across) +} +split_by <- factor(k_clust_across$cluster, levels = c(1:6)) + +# Add row markers (labels with lines) +row_mark <- rowAnnotation( + mark = anno_mark( + at = which(rownames(tfMatrix) %in% lineageTFs), + labels = lineageTFs, + labels_gp = gpar(fontsize = 6, fontface = "plain"), + padding = unit(0.3, "mm"), + side = "left", # line starts from left of heatmap + ) +) + +annot_cols <- c('1'='#fb940b','2'='#01cc00','3'='#2085ec','4'='#fe98bf','5'='#ad7a5b', '6'='turquoise4') +df_clust <- data.frame(across=k_clust_across$cluster) +df_clust$across <- factor(df_clust$across, levels=1:k_center, ordered=T) +rowAnn_left <- HeatmapAnnotation(`Cluster`= df_clust$across, + col = list('Cluster'= annot_cols), + which = 'row', + simple_anno_size = unit(2, 'mm'), + show_legend = FALSE, + show_annotation_name = F) + +# robustCols <- colorRamp2(c(min(tfRobustness), max(tfRobustness)), c("lightgrey","blue")) +# robustCols <- colorRamp2( seq(from = min(tfRobustness), to = max(tfRobustness), length.out = 200),inferno(200)) +robustCols <- colorRamp2(seq(min(tfRobustness), max(tfRobustness), length.out = 100), colorRampPalette(brewer.pal(9, "Blues"))(100)) +rowRobustness <- rowAnnotation( + Robustness = anno_simple( + tfRobustness, + col = robustCols,, + border = TRUE + ), + show_annotation_name = F, + width = unit(1.5, "mm") +) + +# Create a separate legend for robustness +robustLegend <- Legend( + title = "TF Robustness", + col_fun = robustCols, + title_gp = gpar(fontsize = 8, fontface = "plain"), # unbold + labels_gp = gpar(fontsize = 6) # optional: label size +) + +# dirOut <- "/data/MiraldiLab/team/Michael/mCD4T_WaymanFigures/Fig4/networkComparison/" +# dir.create(dirOut, recursive = T, showWarnings = F) + +heatCols <- colorRampPalette(RColorBrewer::brewer.pal(9,"YlOrRd"))(100) +pdf(file.path(dirOut, "Jaccard_Edges_perTF1.pdf"), width = 2.3, height = 4.5, compress = T) +ht1 <- Heatmap(tfMatrix, + name='Jaccard Similarity', + show_row_names = F, + row_split=split_by, + row_gap = unit(0, "mm"), + column_gap = unit(0, "mm"), + border = TRUE, + row_title = NULL, + row_dend_reorder = F, + use_raster = F, + col = heatCols, + # clustering settings + cluster_rows = TRUE, # allow hierarchical clustering + cluster_row_slices = TRUE, # reorder within each k-means cluster + cluster_columns = FALSE, + cluster_column_slices = F, + # column_order = colnames(tfa_z_across), + show_row_dend = FALSE, # show dendrogram within slices + show_column_dend = FALSE, + show_column_names = FALSE, + column_names_side = 'top', + # column_names_rot = 45, + column_title = NULL, + column_split = colnames(tfMatrix), + top_annotation = colAnn, + left_annotation = rowAnn_left, + heatmap_legend_param = list( + direction = "horizontal", + title = "Jaccard", + title_position = "topcenter", + title_gp = gpar(fontsize = 8, fontface = "plain"), # legend title size + labels_gp = gpar(fontsize = 6), # numbers/tick labels size + legend_width = unit(2.5, "cm"), + legend_height = unit(0.5, "cm") + ) + ) +draw(row_mark+ht1+rowRobustness , heatmap_legend_side = "bottom") +dev.off() + +ht2 <- draw(ht1) +row_order_list <- row_order(ht2) +saveRDS(row_order_list, file.path(dirOut, "final_row_order.rds") ) +# Save Legend as PDF +pdf(file.path(dirOut, "TF_Robustness_Legend.pdf"), width = 2, height = 2) +draw(robustLegend) +dev.off() +saveRDS(k_clust_across, file.path(dirOut, "Kmeans_Jaccard_Edges_perTF.rds")) + + +# ---------- Check +clust_df <- df_clust +clust_df$TF <- rownames(clust_df) +colnames(clust_df)[1] <- "cluster" +tfTargetCounts <- tfTargetCounts %>% + left_join(clust_df, by = "TF") + +# Calculate average targets across all methods +tfTargetCounts$avgTargets <- rowMeans(tfTargetCounts[, names(netDataList)]) +tfTargetCounts$medianTargets <- apply(tfTargetCounts[, names(netDataList)],1,median, na.rm = T) +# # Summary statistics by cluster +# clusterSummary <- tfTargetCounts %>% +# group_by(cluster) %>% +# summarise( +# meanTargets = mean(avgTargets), +# medianTargets = median(avgTargets), +# sdTargets = sd(avgTargets), +# nTFs = n() +# ) + +write.table(tfTargetCounts, file.path(dirOut, "TF_avgTargetCounts_byRobustnessCluster.tsv"), quote=F, col.names=NA, row.names=TRUE, sep="\t") + +pAvg <- ggplot(tfTargetCounts, aes(x = cluster, y = avgTargets, fill = cluster)) + + geom_boxplot(outlier.color = "red", outlier.size = 1,outlier.shape = 19, outlier.alpha = 0.7) + + geom_jitter(width = 0.2, size = 0.1) + + labs(title = "", + y = "Average # of Targets", + x = "") + + scale_fill_manual(values = annot_cols) + + theme_minimal() + + theme_bw(base_family = "Helvetica") + + theme( + axis.text.y = element_text(size = 7, color = "black"), + axis.text.x = element_blank(), + axis.ticks.x = element_blank() , + axis.title = element_text(size = 9, color = "black"), + legend.position = "none", + panel.grid.major = element_line(color = "grey80", linewidth = 0.3), + panel.grid.minor = element_line(color = "grey90", linewidth = 0.1) + ) +ggsave(file.path(dirOut, "TF_avgTargetCounts_Distribution.pdf"), pAvg, width = 2.5, height = 2, dpi = 600) + + +pMedian <- ggplot(tfTargetCounts, aes(x = cluster, y = medianTargets, fill = cluster)) + + geom_boxplot(outlier.color = "red", outlier.size = 1,outlier.shape = 19, outlier.alpha = 0.7) + + geom_jitter(width = 0.2, size = 0.1) + + labs(title = "", + y = "Median # of Targets", + x = "") + + scale_fill_manual(values = annot_cols) + + theme_minimal() + + theme_bw(base_family = "Helvetica") + + theme( + axis.text.y = element_text(size = 7, color = "black"), + axis.text.x = element_blank(), + axis.ticks.x = element_blank() , + axis.title = element_text(size = 9, color = "black"), + legend.position = "none", + panel.grid.major = element_line(color = "grey80", linewidth = 0.3), + panel.grid.minor = element_line(color = "grey90", linewidth = 0.1) + ) +ggsave(file.path(dirOut, "TF_medianTargetCounts_Distribution.pdf"), pMedian, width = 2.5, height = 2, dpi = 600) + + +# tf_stats <- lapply(names(netDataList), function(nm){ +# df <- netDataList[[nm]] +# df %>% +# count(!!sym(tfCol)) %>% +# rename(TF = !!sym(tfCol), !!nm := n) +# }) %>% +# reduce(full_join, by = "TF") %>% +# mutate(cluster = k_clust_across$cluster[TF]) +# +# # Which cluster have higher mean degree and which has lower mean degree +# cluster_summary <- tf_stats %>% +# group_by(cluster) %>% +# summarise(across(starts_with("PB") | starts_with("SC") | starts_with("MC") | starts_with("SEA"), +# list(mean = ~mean(.x, na.rm=TRUE), +# sd = ~sd(.x, na.rm=TRUE))), +# n_TFs = n()) +# +# ## Only Lineage TFs +# tfMatrix_lineage <- tfMatrix[intersect(lineageTFs, rownames(tfMatrix)), ] +# k_clust_lineage<- kmeans(tfMatrix_lineage, centers=4,nstart=20, iter.max = 50) +# split_by_lineage <- factor(k_clust_lineage$cluster, levels = c(1:4)) +# +# pdf(file.path(dirOut, "Jaccard_Edges_perTF_lineage.pdf"), width = 2, height = 3, compress = T) +# ht <- Heatmap(tfMatrix_lineage, +# name = 'Jaccard Similarity', +# show_row_names = TRUE, +# row_names_gp = gpar(fontsize = 6), +# row_names_side = "left", # rownames on the left +# row_split = split_by_lineage, +# row_gap = unit(0, "mm"), +# column_gap = unit(0, "mm"), +# border = TRUE, +# row_title = NULL, +# row_dend_reorder = FALSE, +# use_raster = FALSE, +# col = heatCols, +# cluster_rows = TRUE, +# cluster_row_slices = TRUE, +# cluster_columns = FALSE, +# cluster_column_slices = FALSE, +# show_row_dend = FALSE, +# show_column_dend = FALSE, +# show_column_names = FALSE, +# column_names_side = 'top', +# column_title = NULL, +# column_split = colnames(tfMatrix_lineage), +# top_annotation = colAnn, +# heatmap_legend_param = list( +# direction = "horizontal", +# title = "Jaccard Similarity", +# title_position = "topcenter", +# title_gp = gpar(fontsize = 8, fontface = "plain"), # legend title size +# labels_gp = gpar(fontsize = 6), # numbers/tick labels size +# legend_width = unit(2.5, "cm"), +# legend_height = unit(0.5, "cm") +# ) +# ) +# draw(ht, heatmap_legend_side = "bottom") +# dev.off() +# saveRDS(k_clust_lineage, file.path(dirOut, "Kmeans_Jaccard_Edges_perTF_lineage.rds")) + +# --- 4. Aggregated network-pair Jaccard +fun = median +fun_name = "median" # returns "mean" +aggMat <- computeAggregatedPairJaccard(tfMatrix, fun = fun) +plotAggregatedJaccard(aggMat,file.path(dirOut, paste0("AggregatedTF_Jaccard_", fun_name, ".pdf")), fig_width = 2.5, fig_height = 2.5) + +fun = mean +fun_name = "mean" # returns "mean" +aggMat <- computeAggregatedPairJaccard(tfMatrix, fun = fun) +plotAggregatedJaccard(aggMat,file.path(dirOut, paste0("AggregatedTF_Jaccard_", fun_name, ".pdf")), fig_width = 2.5, fig_height = 2.5) + + +# ----- Are top regulators (high-degree TFs) stable across pseudobulk / single-cell / metacell networks. +hubSim <- computeHubOverlapHeatmap( + netDataList = netDataList, tfCol = tfCol, networkNames = networkNames, + dirOut = dirOut, topN = 50, + metric = "jaccard", # or "overlap" + fontsize = 9, fontsize_number = 7, fig_width = 2.5, fig_height = 2.5 +) + +hubSim <- computeHubOverlapHeatmap( + netDataList = netDataList, tfCol = tfCol, networkNames = networkNames, + dirOut = dirOut, topN = 50, + metric = "overlap", # or "jaccard" + fontsize = 9, fontsize_number = 7, fig_width = 2.5, fig_height = 2.5 +) + +# ──────────────────────────────────────────────────────────────────────────────────────────── +# PART THREE: Histogram distributions of: + # A: Targets per TF: # times each gene is targeted (number of TFs regulating it). + # B: TFs per Target: # times each TF is a regulator (number of genes it controls) + # C: Box-Plot of Stability of the top N low and high in degree genes + + #NOTE: Each Figure is saved in the same path as the network being evaluated +# ──────────────────────────────────────────────────────────────────────────────────────────── +# Generate both plots for each network +for (typeName in names(netDataList)) { + data <- netDataList[[typeName]] + basePath <- dirname(netFiles[[typeName]]) + + # Plot 1: # Targets per TF + # tfTargetCounts <- table(data[[tfCol]]) + # dfTF <- data.frame(TF = names(tfTargetCounts), targetCount = as.integer(tfTargetCounts)) + dfTF <- tfTargetCounts[, c("TF", tfCol), drop = F] + colnames(dfTF)[2] <- "targetCount" + + p1 <- ggplot(dfTF, aes(x = targetCount)) + + geom_histogram(binwidth = 1, boundary = 0.5, fill = "#0072B2", color = "black", alpha = 0.8) + + # scale_x_continuous(breaks = seq(0, max(dfTF$targetCount), 1)) + + labs(title = paste("Distribution of # Targets per TF -", typeName), + x = "# Targets", y = "# TFs") + + theme_bw(base_family = "Helvetica") + + theme( + panel.grid.major.y = element_line(color = "grey80", linewidth = 0.3), + panel.grid.minor = element_blank(), + # axis.line = element_line(color = "black", linewidth = 0.4), + axis.text.x = element_text(size = 7), + axis.text.y = element_text(size = 7), + axis.title = element_text(size = 9), + plot.margin = margin(5, 5, 5, 5), + # panel.background = element_rect("black", fill = NA) + ) + + ggsave(file.path(basePath, paste0("TargetCountPerTF_", typeName, ".pdf")), + plot = p1, width = 3, height = 3) + + # Plot 2: # TFs per Target + targetTFCounts <- table(data[[targetCol]]) + dfTarget<- data.frame(Target = names(targetTFCounts), tfCount = as.integer(targetTFCounts)) + dfTargetSorted <- dfTarget[order(dfTarget$tfCount), ] + + p2 <- ggplot(dfTarget, aes(x = tfCount)) + + geom_histogram(binwidth = 1, boundary = 0.5, fill = "blue", color = "black", alpha = 0.7) + + # scale_x_continuous(breaks = seq(0, max(dfTarget$tfCount), 1)) + + labs(title = paste("Distribution of # TFs per Target -", typeName), + x = "# TFs", y = "# Targets") + + theme_bw(base_family = "Helvetica") + + theme( + panel.grid.major.y = element_line(color = "grey80", linewidth = 0.3), + panel.grid.minor = element_blank(), + # axis.line = element_line(color = "black", linewidth = 0.4), + axis.text.x = element_text(size = 7), + axis.text.y = element_text(size = 7), + axis.title = element_text(size = 9), + plot.margin = margin(5, 5, 5, 5), + # panel.background = element_rect("black", fill = NA) + ) + + ggsave(file.path(basePath, paste0("TFCountPerTarget_", typeName, ".pdf")), + plot = p2, width = 3, height = 3) + + # Select top N low and high TF targets + lowTargs <- dfTargetSorted$Target[1:nSelect] + highTargs <- tail(dfTargetSorted, nSelect)$Target + + stabilityDF <- data[data[[targetCol]] %in% c(lowTargs, highTargs), c(targetCol, stabCol)] + # Add Group column based on whether the target is in lowTargs or highTargs + stabilityDF$Group <- ifelse(stabilityDF[[targetCol]] %in% lowTargs, + "Low TFs per Target", + "High TFs per Target") + stabilityDF <- merge(stabilityDF, dfTargetSorted, by.x = targetCol, by.y = "Target") + stabilityDF$targetLabel <- paste0(stabilityDF[[targetCol]], "(", stabilityDF$tfCount, ")") + # Keep plotting order +# stabilityDF$targetLabel <- factor(stabilityDF$targetLabel, +# levels = unique(stabilityDF$targetLabel)) + # Order based on tfCount directly + stabilityDF$targetLabel <- factor( + stabilityDF$targetLabel, + levels = unique(stabilityDF$targetLabel[order(stabilityDF$tfCount)]) + ) + + # Plot: boxplot per target + p3 <- ggplot(stabilityDF, aes(x = targetLabel, y = Stability, fill = Group)) + + geom_boxplot(outlier.size = 0.5, alpha = 0.8) + + labs(title = paste("Per-Target Stability Distribution -", typeName), + x = "Number of TFs per Target (TF count)", y = "Stability") + + theme_minimal() + + theme_bw(base_family = "Helvetica") + + theme( + panel.grid.major.y = element_line(color = "grey80", linewidth = 0.3), + panel.grid.minor = element_blank(), + # axis.line = element_line(color = "black", linewidth = 0.4), + axis.text.x = element_text(size = 7, angle = 90, vjust = 0.5), + axis.text.y = element_text(size = 7), + axis.title = element_text(size = 9), + plot.margin = margin(5, 5, 5, 5), + legend.position = "top" + # panel.background = element_rect("black", fill = NA) + ) + + # Save + ggsave(file.path(basePath, paste0("Top", nSelect, "HighorLow_inDegreeGenes_Boxplot", typeName, ".pdf")), + plot = p3, width = 12, height = 5) + +} + + + + +# + + + +# +# library(reshape2) +# library(ggplot2) +# +# # Convert to long format for ggplot +# df_long <- melt(tfa_by_celltype) +# colnames(df_long) <- c("TF", "CellType", "TFA") +# +# # Add a flag for lineage TFs +# df_long$Lineage <- mapply(function(tf, ct) { +# if(tf %in% lineage_TFs[[ct]]) "Lineage" else "Other" +# }, df_long$TF, df_long$CellType) +# +# # Boxplot or violin plot +# ggplot(df_long, aes(x = CellType, y = TFA, fill = Lineage)) + +# geom_violin(alpha = 0.6) + +# geom_boxplot(width=0.1, outlier.shape=NA) + +# scale_fill_manual(values = c("Lineage"="red", "Other"="grey")) + +# theme_minimal() + +# theme(axis.text.x = element_text(angle=45, hjust=1)) + +# labs(title="Lineage TF enrichment in TFA", y="Mean TFA") +# +# +# lineage_TFs <- list( +# Tfh10 = c("Bcl6","Maf","Batf"), +# Tfh_Int = c("Bcl6","Maf","Batf"), +# Tfh = c("Bcl6","Maf"), +# Tfr = c("Foxp3","Bcl6"), +# cTreg = c("Foxp3","Ikzf2"), +# eTreg = c("Foxp3","Ikzf2"), +# rTreg = c("Foxp3","Ikzf2"), +# Treg_Rorc = c("Foxp3","Rorc"), +# Th17 = c("Rorc","Batf","Stat3"), +# Th1 = c("Tbx21","Stat4"), +# CTL_Prdm1 = c("Tbx21","Eomes","Prdm1"), +# CTL_Bcl6 = c("Tbx21","Eomes","Prdm1"), +# TEM = c("Eomes","Tcf7","Klf2"), +# TCM = c("Eomes","Tcf7","Klf2") +# ) +# + +# lineage_tfs_list <- list( +# TCM = c("TCF7", "LEF1", "ID3", "KLF2"), +# TEM = c("PRDM1", "ZEB2", "RUNX3", "EOMES"), +# Th1 = c("TBX21", "STAT1", "STAT4", "RUNX3", "HIF1A"), +# Th17 = c("RORC", "RORA", "STAT3", "IRF4", "BATF"), +# Treg = c("FOXP3", "IKZF2", "IKZF4", "STAT5"), +# Naive = c("TCF7", "LEF1", "KLF2", "FOXO1"), +# Th2 = c("GATA3", "STAT5", "STAT6", "IRF4"), +# CTL = c("EOMES", "TBX21", "RUNX3", "ZEB2", "PRDM1"), +# MHCII = c("CIITA", "RFX5", "RFXAP", "RFXANK", "NLRC5") +# ) + +# +# +# # Distribution per F1. +# # Also show F1 score based on heatmap +# ggplot(pr_df, aes(x = network, y = F1)) + +# geom_boxplot(outlier.size = 0.8) + +# geom_jitter(width = 0.15, alpha = 0.6, size = 1) + +# theme_minimal() + +# theme(axis.text.x = element_text(angle=45, hjust=1)) + +# ylab("F1 score") + xlab("Representation") diff --git a/evaluation/R/histogramConfidences.R b/evaluation/R/histogramConfidences.R new file mode 100755 index 0000000..c233cf0 --- /dev/null +++ b/evaluation/R/histogramConfidences.R @@ -0,0 +1,194 @@ +# Plot weights/confidences +rm(list=ls()) +library(ggplot2) + +# Custom function to format y-axis labels +custom_labels <- function(x) { + # Keep 0 as is, scale other values by 1000 and add "K" + labels <- ifelse(x == 0, "0", paste0(scales::number(x / 1000, accuracy = 0.1), "K")) + return(labels) +} + +histogramConfidencesDir <- function(currNetDirs, breaks) { + # Ensure breaks length matches currNetDirs length + if (length(currNetDirs) != length(breaks)) { + stop("Length of breaks must match length of currNetDirs.") + } + + for (ix in seq_along(currNetDirs)) { + currNetDir <- currNetDirs[ix] + subfolders <- list.dirs(currNetDir, recursive = FALSE) + target_folders <- subfolders[basename(subfolders) %in% c("TFA", "TFmRNA")] + + # Load and plot individually + for (subfolder in target_folders) { + # subfolder = target_folders[jx] + file_path <- file.path(subfolder, "edges_subset.txt") + if (file.exists(file_path)) { + data <- read.table(file_path, header = TRUE, sep = "\t") + + # Create individual histogram + conf <- data.frame(confidence = floor(data$Stability) / max(floor(data$Stability))) + p <- ggplot((conf), aes(x = confidence)) + + geom_histogram(bins = breaks[ix], fill = "blue", color = "black", alpha = 0.9) + + labs(title = basename(subfolder), x = "Confidence", y = "Frequency") + + theme_bw() + + theme( + axis.title = element_text(size = 16, color = "black"), + axis.text = element_text(size = 14, color = "black"), + plot.title = element_text(size = 20,, color = "black", hjust = 0.5) + ) + scale_y_continuous(labels = custom_labels)#+ scale_y_continuous(labels = label_number(scale = 1e-3, suffix = "K")) + + # Save individual plot + hist_file <- file.path(subfolder, paste0("confidence_distribution_", breaks[ix], ".png")) + ggsave(hist_file, plot = p, width = 6, height = 5, dpi = 600) + } else { + message("File not found: ", file_path) + } + } + + } + +} + + +histogramConfidencesData <- function(currNetFiles, breaks) { + # Ensure breaks length matches currNetFiles length + if (length(currNetFiles) > length(breaks)) { + stop("Length of breaks must match or be greater length of currNetFiles.") + } + + # Loop over the directories in currNetFiles + for (ix in seq_along(currNetFiles)) { + currNetFile <- currNetFiles[ix] # Full path to edges_subset.txt + + # Check if the file exists + if (file.exists(currNetFile)) { + data <- read.table(currNetFile, header = TRUE, sep = "\t") + + # Create individual histogram + conf <- data.frame(confidence = floor(data$Stability) / max(floor(data$Stability))) + p <- ggplot(conf, aes(x = confidence)) + + geom_histogram(bins = breaks[ix], fill = "blue", color = "black", alpha = 0.9) + + labs(title = basename(dirname(currNetFile)), x = "Confidence", y = "Frequency") + + theme_bw() + + theme( + axis.title = element_text(size = 16, color = "black"), + axis.text = element_text(size = 14, color = "black"), + plot.title = element_text(size = 20, color = "black", hjust = 0.5) + ) + scale_y_continuous(labels = custom_labels) # Custom y-axis labels + + # Save individual plot in the same directory + hist_file <- file.path(dirname(currNetFile), paste0("confidence_distribution_", breaks[ix], ".png")) + ggsave(hist_file, plot = p, width = 6, height = 5, dpi = 600) + } else { + message("File not found: ", currNetFile) + } + } +} + +currNetFile <- "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/SC/1KCells/lambda0p5_220totSS_20tfsPerGene_subsamplePCT20/TFmRNA/edges_subset.txt" + +conf <- data.frame(confidence = floor(data$Stability)) +breaks <- max(conf) +p <- ggplot(conf, aes(x = confidence)) + + geom_histogram(bins = breaks, fill = "blue", color = "black", alpha = 0.9) + + labs(title = basename(dirname(currNetFile)), x = "Confidence", y = "Frequency") + + theme_bw() + + theme( + axis.title = element_text(size = 16, color = "black"), + axis.text = element_text(size = 14, color = "black"), + plot.title = element_text(size = 20, color = "black", hjust = 0.5) + ) + + hist_file <- file.path(dirname(currNetFile), paste0("testConf_", breaks, ".png")) + ggsave(hist_file, plot = p, width = 6, height = 5, dpi = 600) + +# USAGE + + +currNetDirs <- c( + # ---Pseudobulk Inferelator + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/Bulk/lambda0p25_80totSS_20tfsPerGene_subsamplePCT63", + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/Bulk/lambda0p5_80totSS_20tfsPerGene_subsamplePCT63", + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/Bulk/lambda1p0_80totSS_20tfsPerGene_subsamplePCT63", + + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/Inferelator/ATAC_ChIPprior/Bulk/lambda0p25_80totSS_20tfsPerGene_subsamplePCT63", + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_ChIPprior/Bulk/lambda0p5_80totSS_20tfsPerGene_subsamplePCT63", + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_ChIPprior/Bulk/lambda1p0_80totSS_20tfsPerGene_subsamplePCT63", + + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/Inferelator/ATAC_KOprior/Bulk/lambda0p25_80totSS_20tfsPerGene_subsamplePCT63", + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_KOprior/Bulk/lambda0p5_80totSS_20tfsPerGene_subsamplePCT63", + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_KOprior/Bulk/lambda1p0_80totSS_20tfsPerGene_subsamplePCT63", + + # --- single cell inferelator + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/SC/lambda0p25_220totSS_20tfsPerGene_subsamplePCT10", + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/SC/lambda0p5_220totSS_20tfsPerGene_subsamplePCT10", + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/SC/lambda1p0_220totSS_20tfsPerGene_subsamplePCT10", + + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_ChIPprior/SC/lambda0p25_220totSS_20tfsPerGene_subsamplePCT10", + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_ChIPprior/SC/lambda0p5_220totSS_20tfsPerGene_subsamplePCT10", + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_ChIPprior/SC/lambda1p0_220totSS_20tfsPerGene_subsamplePCT10", + + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_KOprior/SC/lambda0p25_220totSS_20tfsPerGene_subsamplePCT10", + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_KOprior/SC/lambda0p5_220totSS_20tfsPerGene_subsamplePCT10", + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATAC_KOprior/SC/lambda1p0_220totSS_20tfsPerGene_subsamplePCT10", + + # --- single cell downsampled + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/SC/1KCells/lambda0p5_220totSS_20tfsPerGene_subsamplePCT27", + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/SC/10KCells/lambda0p5_220totSS_20tfsPerGene_subsamplePCT27", + "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/SC/30KCells/lambda0p5_220totSS_20tfsPerGene_subsamplePCT13" +) + + +# Extract subsample fractions +# subsample_fracs <- sapply(currNetDirs, function(x) sub(".*subsampleFrac_([0-9p]+).*", "\\1", x)) +# subsample_fracs <- gsub("p", ".", subsample_fracs) # Convert 'p' to '.' for numeric comparison +# unique_fracs <- unique(subsample_fracs) + +breaks = c(rep(150, 9), rep(500, 12)) + + + +netFiles = c( + "1K" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/SC/1KCells/lambda0p5_220totSS_20tfsPerGene_subsamplePCT27/TFA/edges_subset.txt", + "10K" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/SC/10KCells/lambda0p5_220totSS_20tfsPerGene_subsamplePCT27/TFA/edges_subset.txt", + "30K" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/SC/30KCells/lambda0p5_220totSS_20tfsPerGene_subsamplePCT13/TFA/edges_subset.txt", + "77K" = "/data/miraldiNB/Michael/mCD4T_Wayman/Inferelator/ATACprior/SC/lambda0p5_220totSS_20tfsPerGene_subsamplePCT10/TFA/edges_subset.txt" + ) + +histogramConfidencesStacked <- function(currNetFiles, breaks, dirOut, saveName) { + + allData <- data.frame(confidence = numeric(), network = character()) + + for (ix in seq_along(currNetFiles)) { + currNetFile <- currNetFiles[ix] # Full path to edges_subset.txt + currNetName <- names(currNetFile) + if (file.exists(currNetFile)) { + data <- read.table(currNetFile, header = TRUE, sep = "\t") + # Compute absolute value of signedQuantile and create a temporary data frame. + conf <- data.frame(confidence = floor(data$Stability) / max(floor(data$Stability))) + tempDf <- data.frame(confidence = conf, + network = currNetName) + allData <- rbind(allData, tempDf) + } else { + message("File not found: ", currNetFile) + } + } + + p <- ggplot(allData, aes(x = confidence, color = network)) + + # geom_freqpoly(bins = breaks[1], size = 1) + + geom_histogram(bins = breaks[1], alpha = 0.6, position = "identity", color = "black") + + labs(x = "Confidence", y = "Frequency") + + theme_bw() + + theme( + axis.title = element_text(size = 16, color = "black"), + axis.text = element_text(size = 14, color = "black") + ) + histFile <- file.path(dirOut, paste0(saveName, ".pdf")) + ggsave(histFile, plot = p, width = 6, height = 5, dpi = 600) + +} + + +histogramConfidencesStacked(netFiles, breaks = 300, dirOut = "/data/miraldiNB/Michael/mCD4T_Wayman/Figures", saveName = "confidencesTest") \ No newline at end of file diff --git a/evaluation/R/saveNormCountsArrowFIle.R b/evaluation/R/saveNormCountsArrowFIle.R new file mode 100755 index 0000000..ddf647a --- /dev/null +++ b/evaluation/R/saveNormCountsArrowFIle.R @@ -0,0 +1,35 @@ +rm(list=ls()) +options(stringsAsFactors=FALSE) +set.seed(42) +suppressPackageStartupMessages({ + library(Signac) + library(Seurat) + library(arrow) +}) + +# make sure you load or have the latest version of curl installed. + +# make sure you load or have the latest version of curl installed. + +rds_path <- "/data/miraldiNB/wayman/projects/Tfh10/outs/202404/annotation_scrna_final/obj_Tfh10_RNA_annotated.rds" +obj <- readRDS(rds_path) + +# Process File for scGRN using Inferelator. +obj <- NormalizeData(obj) +norm_counts <- as.matrix(GetAssayData(obj, layer='data')) +# write_parquet(as.data.frame(norm_counts), "/data/miraldiNB/Michael/GRN_Benchmark/Data/Tfh10_scRNA_logNorm_Counts.parquet") + +norm_counts <- as.data.frame(norm_counts) +norm_counts$Genes <- rownames(norm_counts) +norm_counts <- norm_counts[, c("Genes", setdiff(colnames(norm_counts), "Genes"))] # Reorder the columns to make 'Genes' the first column + +# Save as an arrow file +write_feather(norm_counts, "/data/miraldiNB/Michael/GRN_Benchmark/Data/Tfh10_scRNA_logNorm_Counts.arrow") + +# write_feather(norm_counts, "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/scNormCounts.arrow") + +# feather_file <- read_feather("/data/miraldiNB/Katko/Projects/Barski_CD4_Multiome/Outs/Pseudobulk/RNA2/SC_counts.feather") +# write_ipc_file(feather_file, "/data/miraldiNB/Katko/Projects/Barski_CD4_Multiome/Outs/Pseudobulk/RNA2/SC_counts.arrow") + + +rds_path <- "/data/miraldiNB/Michael/hCD4T_Katko/dataBank/ObjFiltered.rds" \ No newline at end of file diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100755 index 28efbebbf62f0ce3d97c588199f008fa66a2a744..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14340 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 diff --git a/src/03_Metrics/.DS_Store b/src/03_Metrics/.DS_Store deleted file mode 100755 index c347785244c62b7383202e06992ce1e65da2a73a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}N6?5T3CW7QFQ6F;|a$gSD(r&=-*HLKRZ8V7=!}JPP^}-h3!8ev=uCOLGtp zA~LfiUowB$e6Yz55%KC_SQ0IWs6i8C8B|2nRnv(%p8~n$*wZcD(RR4s#7d&SsFHVY zXrxGq_IM8O-=`F@sbgc@F4vp18+A8Nk2RlN>bgFrUTp8Pi|66x?XvDSe9LdV`MTyS zQn@$-&VV!E3^)TnWk9YsNOPv>y))npI0FX;%gayJH3e-@x5`#4y`e1&oVOG>|Vk Date: Sun, 29 Mar 2026 02:02:42 -0400 Subject: [PATCH 3/6] =?UTF-8?q?Refactor=20GRN=20module:=20rename=20combine?= =?UTF-8?q?GRN=20=E2=86=92=20aggregateNetworks,=20combineGRN2=20=E2=86=92?= =?UTF-8?q?=20refineTFA=20for=20clarity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/02_GRN/{combineGRN.jl => aggregateNetworks.jl} | 0 src/02_GRN/{combineGRN2.jl => refineTFA.jl} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/02_GRN/{combineGRN.jl => aggregateNetworks.jl} (100%) rename src/02_GRN/{combineGRN2.jl => refineTFA.jl} (100%) diff --git a/src/02_GRN/combineGRN.jl b/src/02_GRN/aggregateNetworks.jl similarity index 100% rename from src/02_GRN/combineGRN.jl rename to src/02_GRN/aggregateNetworks.jl diff --git a/src/02_GRN/combineGRN2.jl b/src/02_GRN/refineTFA.jl similarity index 100% rename from src/02_GRN/combineGRN2.jl rename to src/02_GRN/refineTFA.jl From 97bf7cca668653c77869f599597e16d9508e137b Mon Sep 17 00:00:00 2001 From: Seyifunmi Owoeye Date: Sun, 29 Mar 2026 02:05:11 -0400 Subject: [PATCH 4/6] =?UTF-8?q?Refactor=20GRN=20module:=20rename=20combine?= =?UTF-8?q?GRN=20=E2=86=92=20aggregateNetworks,=20combineGRN2=20=E2=86=92?= =?UTF-8?q?=20refineTFA=20for=20clarity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_diagram.png | Bin 0 -> 571805 bytes src/02_GRN/GRN.jl | 4 +- src/02_GRN/aggregateNetworks.jl | 4 +- src/02_GRN/refineTFA.jl | 4 +- src/API.jl | 441 ++++++++++++++++++++++++++++++++ src/Inferelator.jl | 13 +- 6 files changed, 459 insertions(+), 7 deletions(-) create mode 100644 pipeline_diagram.png create mode 100644 src/API.jl diff --git a/pipeline_diagram.png b/pipeline_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..fd9c761e9570cbd48a566b8b84c0f57ea9f00aa1 GIT binary patch literal 571805 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

li*G@X98J#VQsA~vlDfonyVx3HW`Fl#4)Am5^ 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

@9YO?kjN%gY@>M$P016O<~x7-OPU%!F*c)k|5Da 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

EmFez~UU@G-1Kb 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

aL|SN|-!kT^Y0qpyZEr$+URTSv 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