### A Pluto.jl notebook ###
# v0.17.3

using Markdown
using InteractiveUtils

# ╔═╡ b70ee5f2-3c05-11ec-37db-1704f9c112b5
begin
	using ValidatedNumerics
	using StaticArrays
	using DelimitedFiles
	using IntervalArithmetic
	using IntervalRootFinding
	using LinearAlgebra
	using PyCall
	np = pyimport("numpy")
	cel = pyimport("celluloid")
	using PyPlot
end

# ╔═╡ f7a8506d-2e53-4e72-aaa8-b4133a6520cf
md"# Interval arithmetic"

# ╔═╡ 5397e0bd-378c-42fe-a23e-d5eca0b5a010
md"## Self positive feedback loop"

# ╔═╡ 17757d95-5205-4620-b0ad-c6dd844c13cc
begin
	function fself(x,c1,c2)
	    fself1 = c1*x^2/(c2+x^2) - x
	    return fself1
	end

	Sself = [];
	Sselfx = [];
	Sselfy = [];

	for c1 in 0.1:1:20.0
	    for c2 in 0.1:1:20.0
	        X0 = (0.0..c1)
	        rts = roots(xx -> fself(xx, c1, c2), X0, Krawczyk, 0.000001)
	        nsol = 0
	        for rt in rts
	            if rt.status == :unique
	                nsol += 1
	            end
	        end
	        if nsol >= 2
	            append!(Sself,[[c1,c2]])
	            append!(Sselfx,c1)
	            append!(Sselfy,c2)
	        end

	    end
	end
end

# ╔═╡ c92b4d43-33a1-4ca1-8403-e2900ca6d2be
begin
	tself = np.arange(0.0, 10.0, 0.2)
	uself = tself.^2/4
end;

# ╔═╡ 957a1119-3ffa-4f29-aae6-8d4991e0ebfc
begin
	plt.figure(figsize=(7,7))
	plt.scatter(Sselfx,Sselfy,label = "Bistable configuration",color="#D24D3E",s=25)
	plt.plot(tself,uself,label = "Critical curve",color="#781C81")
	plt.legend()
	plt.gca().set_aspect("equal")
	plt.xlabel("c (a.u.)")
	plt.ylabel("K (a.u.)")
	plt.xlim(0,20)
	plt.ylim(0,20)
	plt.show()
end

# ╔═╡ 64334315-5d05-4032-9ad3-8130d66fe008
md"## Double positive feedback loop"

# ╔═╡ 0b88f3f9-8e5f-479a-9c06-ee0aebfd200e
begin
	function fpos(x,c1,c2)
	    fpos1 = c1*x[2]^2/(1+x[2]^2) - x[1]
	    fpos2 = c2*x[1]^2/(1+x[1]^2) - x[2]
	    return SVector(fpos1, fpos2)
	end

	Spos = [];
	Sposx = [];
	Sposy = [];

	for c1 in 0.0:0.4:10.0
	    for c2 in 0.0:0.4:10.0

	        X0 = IntervalBox(0.0..c1, 0.0..c2)
	        rts = roots(xx -> fpos(xx, c1, c2), X0, Krawczyk, 0.000001)
	        nsol = 0
	        for rt in rts
	            if rt.status == :unique
	                nsol += 1
	            end
	        end
	        if nsol >= 2
	            append!(Spos,[[c1,c2]])
	            append!(Sposx,c1)
	            append!(Sposy,c2)
	        end

	    end
	end
end

# ╔═╡ 20a1adeb-9286-45d7-8f27-b5a41d6052d9
begin
	tpos = np.arange(0.0, 1.6, 0.01)
	apos = (4 .*tpos) ./(3 .- tpos.^2)
	bpos = (((4 ./(1 .+tpos.^2)).-1).+0im).^ (1/2) .* (1 .+ 1 ./tpos.^2)
end;

# ╔═╡ c0d8791e-190e-4799-9ec9-224fcd7bfcfb
begin
	plt.figure(figsize=(7,7))
	plt.scatter(Sposx,Sposy,label = "Bistable configuration",color="#D24D3E",s=20)
	plt.scatter(0,0,color="#3D52A1",s=50)
	plt.plot(bpos,apos,label = "Critical curve",color="#781C81")
	plt.legend()
	plt.gca().set_aspect("equal")
	plt.xlabel("c (a.u.)")
	plt.ylabel("K (a.u.)")
	plt.xlim(-0.5,10)
	plt.ylim(-0.5,10)
	plt.show()
end

# ╔═╡ 6ee9dd70-2e76-4fb3-8972-14edbe3f4103
md"## Double negative feedback loop"

# ╔═╡ 6218e2ed-902b-4a3c-9f19-8fced206df1c
begin
	function fneg(x,c1,c2)
	    fneg1 = c1/(1+x[2]^2) - x[1]
	    fneg2 = c2/(1+x[1]^2) - x[2]
	    return SVector(fneg1, fneg2)
	end

	Sneg = [];
	Snegx = [];
	Snegy = [];

	for c1 in 0.1:0.4:10.0
	    for c2 in 0.1:0.4:10.0

	        X0 = IntervalBox(0.0..c1, 0.0..c2)
	        #rts = roots(xx -> f(xx, c1, c2), X0,0.000001)
	        rts = roots(xx -> fneg(xx, c1, c2), X0, Krawczyk, 0.000001)
	        nsol = 0
	        for rt in rts
	            if rt.status == :unique
	                nsol += 1
	            end
	        end
	        if nsol >= 2
	            append!(Sneg,[[c1,c2]])
	            append!(Snegx,c1)
	            append!(Snegy,c2)
	        end

	    end
	end
end

# ╔═╡ f5f2d5b5-effb-46ac-ac23-a10f88f00b46
begin
	tneg = np.arange(0.6, 5.0, 0.01)
	aneg = (4 .*tneg.^3) ./(3 .*tneg.^2 .- 1)
	bneg = ((1 .+ tneg.^2)./(3 .*tneg.^2 .- 1).+0im).^ (1/2) .* (1 .+ tneg.^2)
end;

# ╔═╡ 954f9963-1249-43b4-800e-6d46604a27b0
begin
	plt.figure(figsize=(7,7))
	plt.scatter(Snegx,Snegy,label = "Bistable configuration",color="#D24D3E",s=20)
	plt.scatter(0,0,color="#3D52A1",s=50)
	plt.plot(aneg,bneg,label = "Critical curve",color="#781C81")
	plt.legend()
	plt.gca().set_aspect("equal")
	plt.xlabel("c (a.u.)")
	plt.ylabel("K (a.u.)")
	plt.xlim(-0.5,10)
	plt.ylim(-0.5,10)
	plt.show()
end

# ╔═╡ 00000000-0000-0000-0000-000000000001
PLUTO_PROJECT_TOML_CONTENTS = """
[deps]
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
IntervalArithmetic = "d1acc4aa-44c8-5952-acd4-ba5d80a2a253"
IntervalRootFinding = "d2bf35a9-74e0-55ec-b149-d360ff49b807"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
PyPlot = "d330b81b-6aea-500a-939a-2ce795aea3ee"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
ValidatedNumerics = "d621b6e3-7715-5857-9c6f-c67000ef6083"

[compat]
IntervalArithmetic = "~0.17.8"
IntervalRootFinding = "~0.5.7"
PyCall = "~1.92.5"
PyPlot = "~2.10.0"
StaticArrays = "~1.2.13"
ValidatedNumerics = "~0.11.0"
"""

# ╔═╡ 00000000-0000-0000-0000-000000000002
PLUTO_MANIFEST_TOML_CONTENTS = """
# This file is machine-generated - editing it directly is not advised

[[ArgTools]]
uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"

[[Artifacts]]
uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"

[[Base64]]
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

[[CRlibm]]
deps = ["Libdl"]
git-tree-sha1 = "9d1c22cff9c04207f336b8e64840d0bd40d86e0e"
uuid = "96374032-68de-5a5b-8d9e-752f78720389"
version = "0.8.0"

[[ChainRulesCore]]
deps = ["Compat", "LinearAlgebra", "SparseArrays"]
git-tree-sha1 = "3533f5a691e60601fe60c90d8bc47a27aa2907ec"
uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
version = "1.11.0"

[[ColorTypes]]
deps = ["FixedPointNumbers", "Random"]
git-tree-sha1 = "024fe24d83e4a5bf5fc80501a314ce0d1aa35597"
uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f"
version = "0.11.0"

[[Colors]]
deps = ["ColorTypes", "FixedPointNumbers", "Reexport"]
git-tree-sha1 = "417b0ed7b8b838aa6ca0a87aadf1bb9eb111ce40"
uuid = "5ae59095-9a9b-59fe-a467-6f913c188581"
version = "0.12.8"

[[CommonSubexpressions]]
deps = ["MacroTools", "Test"]
git-tree-sha1 = "7b8a93dba8af7e3b42fecabf646260105ac373f7"
uuid = "bbf7d656-a473-5ed7-a52c-81e309532950"
version = "0.3.0"

[[Compat]]
deps = ["Base64", "Dates", "DelimitedFiles", "Distributed", "InteractiveUtils", "LibGit2", "Libdl", "LinearAlgebra", "Markdown", "Mmap", "Pkg", "Printf", "REPL", "Random", "SHA", "Serialization", "SharedArrays", "Sockets", "SparseArrays", "Statistics", "Test", "UUIDs", "Unicode"]
git-tree-sha1 = "dce3e3fea680869eaa0b774b2e8343e9ff442313"
uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
version = "3.40.0"

[[CompilerSupportLibraries_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"

[[Conda]]
deps = ["Downloads", "JSON", "VersionParsing"]
git-tree-sha1 = "6cdc8832ba11c7695f494c9d9a1c31e90959ce0f"
uuid = "8f4d0f93-b110-5947-807f-2305c1781a2d"
version = "1.6.0"

[[Dates]]
deps = ["Printf"]
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"

[[DelimitedFiles]]
deps = ["Mmap"]
uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab"

[[DiffResults]]
deps = ["StaticArrays"]
git-tree-sha1 = "c18e98cba888c6c25d1c3b048e4b3380ca956805"
uuid = "163ba53b-c6d8-5494-b064-1a9d43ac40c5"
version = "1.0.3"

[[DiffRules]]
deps = ["NaNMath", "Random", "SpecialFunctions"]
git-tree-sha1 = "7220bc21c33e990c14f4a9a319b1d242ebc5b269"
uuid = "b552c78f-8df3-52c6-915a-8e097449b14b"
version = "1.3.1"

[[Distributed]]
deps = ["Random", "Serialization", "Sockets"]
uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b"

[[DocStringExtensions]]
deps = ["LibGit2"]
git-tree-sha1 = "b19534d1895d702889b219c382a6e18010797f0b"
uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
version = "0.8.6"

[[Downloads]]
deps = ["ArgTools", "LibCURL", "NetworkOptions"]
uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6"

[[ErrorfreeArithmetic]]
git-tree-sha1 = "d6863c556f1142a061532e79f611aa46be201686"
uuid = "90fa49ef-747e-5e6f-a989-263ba693cf1a"
version = "0.5.2"

[[ExprTools]]
git-tree-sha1 = "b7e3d17636b348f005f11040025ae8c6f645fe92"
uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
version = "0.1.6"

[[FastRounding]]
deps = ["ErrorfreeArithmetic", "Test"]
git-tree-sha1 = "224175e213fd4fe112db3eea05d66b308dc2bf6b"
uuid = "fa42c844-2597-5d31-933b-ebd51ab2693f"
version = "0.2.0"

[[FixedPointNumbers]]
deps = ["Statistics"]
git-tree-sha1 = "335bfdceacc84c5cdf16aadc768aa5ddfc5383cc"
uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93"
version = "0.8.4"

[[ForwardDiff]]
deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "LinearAlgebra", "NaNMath", "Preferences", "Printf", "Random", "SpecialFunctions", "StaticArrays"]
git-tree-sha1 = "63777916efbcb0ab6173d09a658fb7f2783de485"
uuid = "f6369f11-7733-5829-9624-2563aa707210"
version = "0.10.21"

[[InlineStrings]]
deps = ["Parsers"]
git-tree-sha1 = "19cb49649f8c41de7fea32d089d37de917b553da"
uuid = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48"
version = "1.0.1"

[[InteractiveUtils]]
deps = ["Markdown"]
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"

[[IntervalArithmetic]]
deps = ["CRlibm", "FastRounding", "LinearAlgebra", "Markdown", "Random", "RecipesBase", "RoundingEmulator", "SetRounding", "StaticArrays"]
git-tree-sha1 = "00cce14aeb4b256f2f57caf3f3b9354c27d93259"
uuid = "d1acc4aa-44c8-5952-acd4-ba5d80a2a253"
version = "0.17.8"

[[IntervalConstraintProgramming]]
deps = ["IntervalArithmetic", "IntervalContractors", "IntervalRootFinding", "MacroTools", "Test"]
git-tree-sha1 = "e5234b7d91eaaa80305cd8e74a5f2231d4d0e05e"
uuid = "138f1668-1576-5ad7-91b9-7425abbf3153"
version = "0.9.1"

[[IntervalContractors]]
deps = ["IntervalArithmetic"]
git-tree-sha1 = "02e049c761e7fd911566c6b0eff9deffe7de876b"
uuid = "15111844-de3b-5229-b4ba-526f2f385dc9"
version = "0.4.5"

[[IntervalOptimisation]]
deps = ["IntervalArithmetic"]
git-tree-sha1 = "7928f5018b5f54adb6780a43b975880633ead850"
uuid = "c7c68f13-a4a2-5b9a-b424-07d005f8d9d2"
version = "0.4.5"

[[IntervalRootFinding]]
deps = ["ForwardDiff", "IntervalArithmetic", "LinearAlgebra", "Polynomials", "StaticArrays"]
git-tree-sha1 = "2b3c1cbe892ceb2f3936fd734c1c98ea97dfb18e"
uuid = "d2bf35a9-74e0-55ec-b149-d360ff49b807"
version = "0.5.7"

[[Intervals]]
deps = ["Dates", "Printf", "RecipesBase", "Serialization", "TimeZones"]
git-tree-sha1 = "323a38ed1952d30586d0fe03412cde9399d3618b"
uuid = "d8418881-c3e1-53bb-8760-2df7ec849ed5"
version = "1.5.0"

[[InverseFunctions]]
deps = ["Test"]
git-tree-sha1 = "f0c6489b12d28fb4c2103073ec7452f3423bd308"
uuid = "3587e190-3f89-42d0-90ee-14403ec27112"
version = "0.1.1"

[[IrrationalConstants]]
git-tree-sha1 = "7fd44fd4ff43fc60815f8e764c0f352b83c49151"
uuid = "92d709cd-6900-40b7-9082-c6be49f344b6"
version = "0.1.1"

[[JLLWrappers]]
deps = ["Preferences"]
git-tree-sha1 = "642a199af8b68253517b80bd3bfd17eb4e84df6e"
uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
version = "1.3.0"

[[JSON]]
deps = ["Dates", "Mmap", "Parsers", "Unicode"]
git-tree-sha1 = "8076680b162ada2a031f707ac7b4953e30667a37"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "0.21.2"

[[LaTeXStrings]]
git-tree-sha1 = "f2355693d6778a178ade15952b7ac47a4ff97996"
uuid = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f"
version = "1.3.0"

[[LazyArtifacts]]
deps = ["Artifacts", "Pkg"]
uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3"

[[LibCURL]]
deps = ["LibCURL_jll", "MozillaCACerts_jll"]
uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"

[[LibCURL_jll]]
deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"]
uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"

[[LibGit2]]
deps = ["Base64", "NetworkOptions", "Printf", "SHA"]
uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"

[[LibSSH2_jll]]
deps = ["Artifacts", "Libdl", "MbedTLS_jll"]
uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"

[[Libdl]]
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"

[[LinearAlgebra]]
deps = ["Libdl", "libblastrampoline_jll"]
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"

[[LogExpFunctions]]
deps = ["ChainRulesCore", "DocStringExtensions", "InverseFunctions", "IrrationalConstants", "LinearAlgebra"]
git-tree-sha1 = "6193c3815f13ba1b78a51ce391db8be016ae9214"
uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688"
version = "0.3.4"

[[Logging]]
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"

[[MacroTools]]
deps = ["Markdown", "Random"]
git-tree-sha1 = "3d3e902b31198a27340d0bf00d6ac452866021cf"
uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
version = "0.5.9"

[[Markdown]]
deps = ["Base64"]
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"

[[MbedTLS_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"

[[Mmap]]
uuid = "a63ad114-7e13-5084-954f-fe012c677804"

[[Mocking]]
deps = ["Compat", "ExprTools"]
git-tree-sha1 = "29714d0a7a8083bba8427a4fbfb00a540c681ce7"
uuid = "78c3b35d-d492-501b-9361-3d52fe80e533"
version = "0.7.3"

[[MozillaCACerts_jll]]
uuid = "14a3606d-f60d-562e-9121-12d972cd8159"

[[MutableArithmetics]]
deps = ["LinearAlgebra", "SparseArrays", "Test"]
git-tree-sha1 = "8d9496b2339095901106961f44718920732616bb"
uuid = "d8a4904e-b15c-11e9-3269-09a3773c0cb0"
version = "0.2.22"

[[NaNMath]]
git-tree-sha1 = "bfe47e760d60b82b66b61d2d44128b62e3a369fb"
uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3"
version = "0.3.5"

[[NetworkOptions]]
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"

[[OpenBLAS_jll]]
deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"]
uuid = "4536629a-c528-5b80-bd46-f80d51c5b363"

[[OpenLibm_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "05823500-19ac-5b8b-9628-191a04bc5112"

[[OpenSpecFun_jll]]
deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"]
git-tree-sha1 = "13652491f6856acfd2db29360e1bbcd4565d04f1"
uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e"
version = "0.5.5+0"

[[Parsers]]
deps = ["Dates"]
git-tree-sha1 = "d911b6a12ba974dabe2291c6d450094a7226b372"
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
version = "2.1.1"

[[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"

[[Polynomials]]
deps = ["Intervals", "LinearAlgebra", "MutableArithmetics", "RecipesBase"]
git-tree-sha1 = "7499556d31417baeabaa55d266a449ffe4ec5a3e"
uuid = "f27b6e38-b328-58d1-80ce-0feddd5e7a45"
version = "2.0.17"

[[Preferences]]
deps = ["TOML"]
git-tree-sha1 = "00cfd92944ca9c760982747e9a1d0d5d86ab1e5a"
uuid = "21216c6a-2e73-6563-6e65-726566657250"
version = "1.2.2"

[[Printf]]
deps = ["Unicode"]
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"

[[PyCall]]
deps = ["Conda", "Dates", "Libdl", "LinearAlgebra", "MacroTools", "Serialization", "VersionParsing"]
git-tree-sha1 = "4ba3651d33ef76e24fef6a598b63ffd1c5e1cd17"
uuid = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
version = "1.92.5"

[[PyPlot]]
deps = ["Colors", "LaTeXStrings", "PyCall", "Sockets", "Test", "VersionParsing"]
git-tree-sha1 = "14c1b795b9d764e1784713941e787e1384268103"
uuid = "d330b81b-6aea-500a-939a-2ce795aea3ee"
version = "2.10.0"

[[REPL]]
deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"

[[Random]]
deps = ["SHA", "Serialization"]
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[[RecipesBase]]
git-tree-sha1 = "44a75aa7a527910ee3d1751d1f0e4148698add9e"
uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
version = "1.1.2"

[[Reexport]]
deps = ["Pkg"]
git-tree-sha1 = "7b1d07f411bc8ddb7977ec7f377b97b158514fe0"
uuid = "189a3867-3050-52da-a836-e630ba90ab69"
version = "0.2.0"

[[RoundingEmulator]]
git-tree-sha1 = "40b9edad2e5287e05bd413a38f61a8ff55b9557b"
uuid = "5eaf0fd0-dfba-4ccb-bf02-d820a40db705"
version = "0.2.1"

[[SHA]]
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"

[[Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"

[[SetRounding]]
git-tree-sha1 = "d7a25e439d07a17b7cdf97eecee504c50fedf5f6"
uuid = "3cc68bcd-71a2-5612-b932-767ffbe40ab0"
version = "0.2.1"

[[SharedArrays]]
deps = ["Distributed", "Mmap", "Random", "Serialization"]
uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383"

[[Sockets]]
uuid = "6462fe0b-24de-5631-8697-dd941f90decc"

[[SparseArrays]]
deps = ["LinearAlgebra", "Random"]
uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"

[[SpecialFunctions]]
deps = ["ChainRulesCore", "IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"]
git-tree-sha1 = "f0bccf98e16759818ffc5d97ac3ebf87eb950150"
uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
version = "1.8.1"

[[StaticArrays]]
deps = ["LinearAlgebra", "Random", "Statistics"]
git-tree-sha1 = "3c76dde64d03699e074ac02eb2e8ba8254d428da"
uuid = "90137ffa-7385-5640-81b9-e52037218182"
version = "1.2.13"

[[Statistics]]
deps = ["LinearAlgebra", "SparseArrays"]
uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"

[[TOML]]
deps = ["Dates"]
uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"

[[Tar]]
deps = ["ArgTools", "SHA"]
uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"

[[Test]]
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[[TimeZones]]
deps = ["Dates", "Downloads", "InlineStrings", "LazyArtifacts", "Mocking", "Pkg", "Printf", "RecipesBase", "Serialization", "Unicode"]
git-tree-sha1 = "8de32288505b7db196f36d27d7236464ef50dba1"
uuid = "f269a46b-ccf7-5d73-abea-4c690281aa53"
version = "1.6.2"

[[UUIDs]]
deps = ["Random", "SHA"]
uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[[Unicode]]
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"

[[ValidatedNumerics]]
deps = ["IntervalArithmetic", "IntervalConstraintProgramming", "IntervalContractors", "IntervalOptimisation", "IntervalRootFinding", "Reexport", "Test"]
git-tree-sha1 = "3e913fa00b3943675ac8ddd4121adeb1754e99f9"
uuid = "d621b6e3-7715-5857-9c6f-c67000ef6083"
version = "0.11.0"

[[VersionParsing]]
git-tree-sha1 = "e575cf85535c7c3292b4d89d89cc29e8c3098e47"
uuid = "81def892-9a0e-5fdd-b105-ffc91e053289"
version = "1.2.1"

[[Zlib_jll]]
deps = ["Libdl"]
uuid = "83775a58-1f1d-513f-b197-d71354ab007a"

[[libblastrampoline_jll]]
deps = ["Artifacts", "Libdl", "OpenBLAS_jll"]
uuid = "8e850b90-86db-534c-a0d3-1478176c7d93"

[[nghttp2_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"

[[p7zip_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
"""

# ╔═╡ Cell order:
# ╟─f7a8506d-2e53-4e72-aaa8-b4133a6520cf
# ╠═b70ee5f2-3c05-11ec-37db-1704f9c112b5
# ╟─5397e0bd-378c-42fe-a23e-d5eca0b5a010
# ╠═17757d95-5205-4620-b0ad-c6dd844c13cc
# ╠═c92b4d43-33a1-4ca1-8403-e2900ca6d2be
# ╠═957a1119-3ffa-4f29-aae6-8d4991e0ebfc
# ╟─64334315-5d05-4032-9ad3-8130d66fe008
# ╠═0b88f3f9-8e5f-479a-9c06-ee0aebfd200e
# ╠═20a1adeb-9286-45d7-8f27-b5a41d6052d9
# ╠═c0d8791e-190e-4799-9ec9-224fcd7bfcfb
# ╟─6ee9dd70-2e76-4fb3-8972-14edbe3f4103
# ╠═6218e2ed-902b-4a3c-9f19-8fced206df1c
# ╠═f5f2d5b5-effb-46ac-ac23-a10f88f00b46
# ╠═954f9963-1249-43b4-800e-6d46604a27b0
# ╟─00000000-0000-0000-0000-000000000001
# ╟─00000000-0000-0000-0000-000000000002


using DynamicPolynomials
const  MPoly = DynamicPolynomials.MultivariatePolynomials
using GroupsCore
using PermutationGroups

struct VariablePermutation <: SymbolicWedderburn.ByPermutations end

function SymbolicWedderburn.action(a::VariablePermutation, g::PermutationGroups.AbstractPerm, m::Monomial)
    v = variables(m)
    return m(v => SymbolicWedderburn.action(a, g, v))
end

function SymbolicWedderburn.action(::VariablePermutation, g::PermutationGroups.AbstractPerm, v::AbstractVector)
    return map(i -> v[i^g], eachindex(v))
end

abstract type OnMonomials <: SymbolicWedderburn.ByLinearTransformation end

function SymbolicWedderburn.action(
    a::Union{VariablePermutation,OnMonomials},
    el::GroupElement,
    term::MPoly.AbstractTerm,
)
    return MPoly.coefficient(term) * SymbolicWedderburn.action(a, el, MPoly.monomial(term))
end
function SymbolicWedderburn.action(
    a::Union{VariablePermutation,OnMonomials},
    el::GroupElement,
    poly::MPoly.AbstractPolynomial,
)
    return sum([SymbolicWedderburn.action(a, el, term) for term in MPoly.terms(poly)])
end

function SymbolicWedderburn.decompose(
    k::MPoly.AbstractPolynomialLike,
    hom::SymbolicWedderburn.InducedActionHomomorphism,
)
    # correct only if basis(hom) == monomials
    I = SymbolicWedderburn._int_type(hom)
    indcs = I[hom[mono] for mono in MPoly.monomials(k)]
    coeffs = MPoly.coefficients(k)

    return indcs, coeffs
end


for N in [Float64, Rational{Int}, Float32]
    # random zero set
    rand(ZeroSet)

    Z = ZeroSet{N}(2)
    B = BallInf(ones(N, 2), N(1))

    # support vector
    d = N[1, 0]
    @test σ(d, Z) == zeros(N, 2)

    # testing that the zero set is neutral element for the Minkowski sum
    @test B ⊕ Z == B && Z ⊕ B == B

    cpa = MinkowskiSumArray([B, N(2) * B, N(3) * B])
    @test cpa ⊕ Z == cpa && Z ⊕ cpa == cpa

    # test M-sum of zero set with itself
    @test ZeroSet{N}(2) ⊕ ZeroSet{N}(2) == ZeroSet{N}(2)

    # boundedness
    @test isbounded(Z)

    # element & an_element function
    @test element(Z) ∈ Z
    @test element(Z, 1) == 0
    @test an_element(Z) ∈ Z

    # high and low
    @test high(Z) == zeros(N, 2)
    @test low(Z) == zeros(N, 2)

    # isempty
    @test !isempty(Z)

    # isuniversal
    answer, w = isuniversal(Z, true)
    @test !isuniversal(Z) && !answer && w ∉ Z

    # subset
    z = ZeroSet{N}(1)
    s1 = Singleton(N[0])
    s2 = Singleton(N[2])
    @test z ⊆ s1 && ⊆(z, s1, true)[1]
    subset, point = ⊆(z, s2, true)
    @test z ⊈ s2 && !subset && point ∈ z && point ∉ s2
    @test z ⊆ z && z ⊈ ZeroSet{N}(2)

    # linear map (concrete)
    M = hcat(N[1])
    Mz = linear_map(M, z)
    @test Mz isa ZeroSet && dim(Mz) == 1

    M = N[-1 -2;]
    MZ = linear_map(M, Z)
    @test MZ isa ZeroSet && dim(MZ) == 1

    # translation
    @test translate(Z, N[1, 2]) == Singleton(N[1, 2])

    # base type and element type
    @test basetype(Z) == basetype(typeof(Z)) == ZeroSet
    @test eltype(Z) == eltype(typeof(Z)) == N
end


# Written in Julia by André Ferrari
# April 2015
#
# Translation in julia of algorithm in section 2.2:
# "Generating Stationary Processes via Circulant embedding"

# Kroese, D.P. and Botev, Z.I. (2013).
# "Spatial Process Generation."
# Lectures on Stochastic Geometry, Spatial Statistics and Random Fields,
# Volume II: Analysis, Modeling and Simulation of Complex Structures,
# V. Schmidt (Ed.).  Springer-Verlag, Berlin.
#
# http://arxiv.org/abs/1308.0399
#

module GHF

export genghf

function genghf(tx::Vector, ty::Vector, rho::Function)
    m = length(tx)
    n = length(ty)
    rows = zeros(m,n)
    cols = zeros(m,n)

    for i = 1:n, j = 1:m
        rows[j,i] = rho(tx[i]-tx[1],ty[j]-ty[1])
        cols[j,i] = rho(tx[1]-tx[i],ty[j]-ty[1])
    end

    blkcirc_row = [[rows cols[:,end:-1:2]] ;
        [cols[end:-1:2,:] rows[end:-1:2,end:-1:2]]]

    lam = real(fft(blkcirc_row))/(2*m-1)/(2*n-1)

    ind0 = lam .< 0.0
    if (isempty(ind0)) && (abs(minimum(lam[ind0])) > 1e-15)
        error("Could not find positive definite embedding!")
    else
        lam[ind0] = 0.0
        lam = sqrt.(lam)
    end

    z = complex.(randn(2*m-1, 2*n-1), randn(2*m-1, 2*n-1))
    f = fft(lam .* z)
    f = f[1:m,1:n]
    field1 = real(f)
#   field2 = imag(f)  # if needed

end
end


"""
    GCNConv([fg,] in => out, σ=identity; bias=true, init=glorot_uniform)

Graph convolutional layer.

# Arguments

- `fg`: Optionally pass a [`FeaturedGraph`](@ref).
- `in`: The dimension of input features.
- `out`: The dimension of output features.
- `σ`: Activation function.
- `bias`: Add learnable bias.
- `init`: Weights' initializer.

The input to the layer is a node feature array `X`
of size `(num_features, num_nodes)`.
"""
struct GCNConv{A<:AbstractMatrix, B, F, S<:AbstractFeaturedGraph} <: AbstractGraphLayer
    weight::A
    bias::B
    σ::F
    fg::S
end

function GCNConv(fg::AbstractFeaturedGraph, ch::Pair{Int,Int}, σ=identity;
                 init=glorot_uniform, bias::Bool=true)
    in, out = ch
    W = init(out, in)
    b = Flux.create_bias(W, bias, out)
    GCNConv(W, b, σ, fg)
end

GCNConv(ch::Pair{Int,Int}, σ = identity; kwargs...) =
    GCNConv(NullGraph(), ch, σ; kwargs...)

@functor GCNConv

function (l::GCNConv)(fg::FeaturedGraph, x::AbstractMatrix)
    L̃ = normalized_laplacian(fg, eltype(x); selfloop=true)
    l.σ.(l.weight * x * L̃ .+ l.bias)
end

(l::GCNConv)(fg::FeaturedGraph) = FeaturedGraph(fg, nf = l(fg, node_feature(fg)))

function Base.show(io::IO, l::GCNConv)
    out, in = size(l.weight)
    print(io, "GCNConv($in => $out")
    l.σ == identity || print(io, ", ", l.σ)
    print(io, ")")
end

"""
    ChebConv([fg,] in=>out, k; bias=true, init=glorot_uniform)

Chebyshev spectral graph convolutional layer.

# Arguments

- `fg`: Optionally pass a [`FeaturedGraph`](@ref).
- `in`: The dimension of input features.
- `out`: The dimension of output features.
- `k`: The order of Chebyshev polynomial.
- `bias`: Add learnable bias.
- `init`: Weights' initializer.
"""
struct ChebConv{A<:AbstractArray{<:Number,3}, B, S<:AbstractFeaturedGraph} <: AbstractGraphLayer
    weight::A
    bias::B
    fg::S
    k::Int
end

function ChebConv(fg::AbstractFeaturedGraph, ch::Pair{Int,Int}, k::Int;
                  init=glorot_uniform, bias::Bool=true)
    in, out = ch
    W = init(out, in, k)
    b = Flux.create_bias(W, bias, out)
    ChebConv(W, b, fg, k)
end

ChebConv(ch::Pair{Int,Int}, k::Int; kwargs...) =
    ChebConv(NullGraph(), ch, k; kwargs...)

@functor ChebConv

function (c::ChebConv)(fg::FeaturedGraph, X::AbstractMatrix{T}) where T
    check_num_nodes(fg, X)
    @assert size(X, 1) == size(c.weight, 2) "Input feature size must match input channel size."

    L̃ = scaled_laplacian(fg, eltype(X))

    Z_prev = X
    Z = X * L̃
    Y = view(c.weight,:,:,1) * Z_prev
    Y += view(c.weight,:,:,2) * Z
    for k = 3:c.k
        Z, Z_prev = 2*Z*L̃ - Z_prev, Z
        Y += view(c.weight,:,:,k) * Z
    end
    return Y .+ c.bias
end

(l::ChebConv)(fg::FeaturedGraph) = FeaturedGraph(fg, nf = l(fg, node_feature(fg)))

function Base.show(io::IO, l::ChebConv)
    out, in, k = size(l.weight)
    print(io, "ChebConv(", in, " => ", out)
    print(io, ", k=", k)
    print(io, ")")
end

"""
    GraphConv([fg,] in => out, σ=identity, aggr=+; bias=true, init=glorot_uniform)

Graph neural network layer.

# Arguments

- `fg`: Optionally pass a [`FeaturedGraph`](@ref).
- `in`: The dimension of input features.
- `out`: The dimension of output features.
- `σ`: Activation function.
- `aggr`: An aggregate function applied to the result of message function. `+`, `-`,
`*`, `/`, `max`, `min` and `mean` are available.
- `bias`: Add learnable bias.
- `init`: Weights' initializer.
"""
struct GraphConv{V<:AbstractFeaturedGraph, A<:AbstractMatrix, B} <: MessagePassing
    fg::V
    weight1::A
    weight2::A
    bias::B
    σ
    aggr
end

function GraphConv(fg::AbstractFeaturedGraph, ch::Pair{Int,Int}, σ=identity, aggr=+;
                   init=glorot_uniform, bias::Bool=true)
    in, out = ch
    W1 = init(out, in)
    W2 = init(out, in)
    b = Flux.create_bias(W1, bias, out)
    GraphConv(fg, W1, W2, b, σ, aggr)
end

GraphConv(ch::Pair{Int,Int}, σ=identity, aggr=+; kwargs...) =
    GraphConv(NullGraph(), ch, σ, aggr; kwargs...)

@functor GraphConv

message(gc::GraphConv, x_i, x_j::AbstractVector, e_ij) = gc.weight2 * x_j

update(gc::GraphConv, m::AbstractVector, x::AbstractVector) = gc.σ.(gc.weight1*x .+ m .+ gc.bias)

function (gc::GraphConv)(fg::FeaturedGraph, x::AbstractMatrix)
    check_num_nodes(fg, x)
    _, x = propagate(gc, adjacency_list(fg), Fill(0.f0, 0, ne(fg)), x, +)
    x
end

(l::GraphConv)(fg::FeaturedGraph) = FeaturedGraph(fg, nf = l(fg, node_feature(fg)))

function Base.show(io::IO, l::GraphConv)
    in_channel = size(l.weight1, ndims(l.weight1))
    out_channel = size(l.weight1, ndims(l.weight1)-1)
    print(io, "GraphConv(", in_channel, " => ", out_channel)
    l.σ == identity || print(io, ", ", l.σ)
    print(io, ", aggr=", l.aggr)
    print(io, ")")
end

"""
    GATConv([fg,] in => out;
            heads=1,
            concat=true,
            init=glorot_uniform
            bias=true,
            negative_slope=0.2)

Graph attentional layer.

# Arguments

- `fg`: Optionally pass a [`FeaturedGraph`](@ref).
- `in`: The dimension of input features.
- `out`: The dimension of output features.
- `bias::Bool`: Keyword argument, whether to learn the additive bias.
- `heads`: Number attention heads
- `concat`: Concatenate layer output or not. If not, layer output is averaged.
- `negative_slope::Real`: Keyword argument, the parameter of LeakyReLU.
"""
struct GATConv{V<:AbstractFeaturedGraph, T, A<:AbstractMatrix{T}, B} <: MessagePassing
    fg::V
    weight::A
    bias::B
    a::A
    negative_slope::T
    channel::Pair{Int, Int}
    heads::Int
    concat::Bool
end

function GATConv(fg::AbstractFeaturedGraph, ch::Pair{Int,Int};
                 heads::Int=1, concat::Bool=true, negative_slope=0.2f0,
                 init=glorot_uniform, bias::Bool=true)
    in, out = ch
    W = init(out*heads, in)
    b = Flux.create_bias(W, bias, out*heads)
    a = init(2*out, heads)
    GATConv(fg, W, b, a, negative_slope, ch, heads, concat)
end

GATConv(ch::Pair{Int,Int}; kwargs...) = GATConv(NullGraph(), ch; kwargs...)

@functor GATConv

# Here the α that has not been softmaxed is the first number of the output message
function message(gat::GATConv, x_i::AbstractVector, x_j::AbstractVector)
    x_i = reshape(gat.weight*x_i, :, gat.heads)
    x_j = reshape(gat.weight*x_j, :, gat.heads)
    x_ij = vcat(x_i, x_j+zero(x_j))
    e = sum(x_ij .* gat.a, dims=1)  # inner product for each head, output shape: (1, gat.heads)
    e_ij = leakyrelu.(e, gat.negative_slope)
    vcat(e_ij, x_j)  # shape: (n+1, gat.heads)
end

# After some reshaping due to the multihead, we get the α from each message,
# then get the softmax over every α, and eventually multiply the message by α
function apply_batch_message(gat::GATConv, i, js, X::AbstractMatrix)
    e_ij = mapreduce(j -> GeometricFlux.message(gat, _view(X, i), _view(X, j)), hcat, js)
    n = size(e_ij, 1)
    αs = Flux.softmax(reshape(view(e_ij, 1, :), gat.heads, :), dims=2)
    msgs = view(e_ij, 2:n, :) .* reshape(αs, 1, :)
    reshape(msgs, (n-1)*gat.heads, :)
end

update_batch_edge(gat::GATConv, adj, E::AbstractMatrix, X::AbstractMatrix, u) = update_batch_edge(gat, adj, X)

function update_batch_edge(gat::GATConv, adj, X::AbstractMatrix)
    n = size(adj, 1)
    # a vertex must always receive a message from itself
    Zygote.ignore() do
        GraphLaplacians.add_self_loop!(adj, n)
    end
    mapreduce(i -> apply_batch_message(gat, i, adj[i], X), hcat, 1:n)
end

# The same as update function in batch manner
update_batch_vertex(gat::GATConv, M::AbstractMatrix, X::AbstractMatrix, u) = update_batch_vertex(gat, M)

function update_batch_vertex(gat::GATConv, M::AbstractMatrix)
    M = M .+ gat.bias
    if !gat.concat
        N = size(M, 2)
        M = reshape(mean(reshape(M, :, gat.heads, N), dims=2), :, N)
    end
    return M
end

function (gat::GATConv)(fg::FeaturedGraph, X::AbstractMatrix)
    check_num_nodes(fg, X)
    _, X = propagate(gat, adjacency_list(fg), Fill(0.f0, 0, ne(fg)), X, +)
    X
end

(l::GATConv)(fg::FeaturedGraph) = FeaturedGraph(fg, nf = l(fg, node_feature(fg)))

function Base.show(io::IO, l::GATConv)
    in_channel = size(l.weight, ndims(l.weight))
    out_channel = size(l.weight, ndims(l.weight)-1)
    print(io, "GATConv(", in_channel, "=>", out_channel)
    print(io, ", LeakyReLU(λ=", l.negative_slope)
    print(io, "))")
end

"""
    GatedGraphConv([fg,] out, num_layers; aggr=+, init=glorot_uniform)

Gated graph convolution layer.

# Arguments

- `fg`: Optionally pass a [`FeaturedGraph`](@ref).
- `out`: The dimension of output features.
- `num_layers`: The number of gated recurrent unit.
- `aggr`: An aggregate function applied to the result of message function. `+`, `-`,
`*`, `/`, `max`, `min` and `mean` are available.
"""
struct GatedGraphConv{V<:AbstractFeaturedGraph, A<:AbstractArray{<:Number,3}, R} <: MessagePassing
    fg::V
    weight::A
    gru::R
    out_ch::Int
    num_layers::Int
    aggr
end

function GatedGraphConv(fg::AbstractFeaturedGraph, out_ch::Int, num_layers::Int;
                        aggr=+, init=glorot_uniform)
    w = init(out_ch, out_ch, num_layers)
    gru = GRUCell(out_ch, out_ch)
    GatedGraphConv(fg, w, gru, out_ch, num_layers, aggr)
end

GatedGraphConv(out_ch::Int, num_layers::Int; kwargs...) =
    GatedGraphConv(NullGraph(), out_ch, num_layers; kwargs...)

@functor GatedGraphConv

message(ggc::GatedGraphConv, x_i, x_j::AbstractVector, e_ij) = x_j

update(ggc::GatedGraphConv, m::AbstractVector, x) = m

function (ggc::GatedGraphConv)(fg::FeaturedGraph, H::AbstractMatrix{S}) where {T<:AbstractVector,S<:Real}
    check_num_nodes(fg, H)
    m, n = size(H)
    @assert (m <= ggc.out_ch) "number of input features must less or equals to output features."
    adj = adjacency_list(fg)
    if m < ggc.out_ch
        Hpad = similar(H, S, ggc.out_ch - m, n)
        H = vcat(H, fill!(Hpad, 0))
    end
    for i = 1:ggc.num_layers
        M = view(ggc.weight, :, :, i) * H
        _, M = propagate(ggc, adj, Fill(0.f0, 0, ne(fg)), M, +)
        H, _ = ggc.gru(H, M)  # BUG: FluxML/Flux.jl#1381
    end
    H
end

(l::GatedGraphConv)(fg::FeaturedGraph) = FeaturedGraph(fg, nf = l(fg, node_feature(fg)))

function Base.show(io::IO, l::GatedGraphConv)
    print(io, "GatedGraphConv(($(l.out_ch) => $(l.out_ch))^$(l.num_layers)")
    print(io, ", aggr=", l.aggr)
    print(io, ")")
end

"""
    EdgeConv([fg,] nn; aggr=max)

Edge convolutional layer.

# Arguments

- `fg`: Optionally pass a [`FeaturedGraph`](@ref).
- `nn`: A neural network (e.g. a Dense layer or a MLP).
- `aggr`: An aggregate function applied to the result of message function. `+`, `max` and `mean` are available.
"""
struct EdgeConv{V<:AbstractFeaturedGraph} <: MessagePassing
    fg::V
    nn
    aggr
end

EdgeConv(fg::AbstractFeaturedGraph, nn; aggr=max) = EdgeConv(fg, nn, aggr)
EdgeConv(nn; kwargs...) = EdgeConv(NullGraph(), nn; kwargs...)

@functor EdgeConv

message(ec::EdgeConv, x_i::AbstractVector, x_j::AbstractVector, e_ij) = ec.nn(vcat(x_i, x_j .- x_i))
update(ec::EdgeConv, m::AbstractVector, x) = m

function (ec::EdgeConv)(fg::FeaturedGraph, X::AbstractMatrix)
    check_num_nodes(fg, X)
    _, X = propagate(ec, adjacency_list(fg), Fill(0.f0, 0, ne(fg)), X, ec.aggr)
    X
end

(l::EdgeConv)(fg::FeaturedGraph) = FeaturedGraph(fg, nf = l(fg, node_feature(fg)))

function Base.show(io::IO, l::EdgeConv)
    print(io, "EdgeConv(", l.nn)
    print(io, ", aggr=", l.aggr)
    print(io, ")")
end

"""
    GINConv([fg,] nn, [eps=0])

    Graph Isomorphism Network.

# Arguments

- `fg`: Optionally pass in a FeaturedGraph as input.
- `nn`: A neural network/layer.
- `eps`: Weighting factor.

The definition of this is as defined in the original paper,
Xu et. al. (2018) https://arxiv.org/abs/1810.00826.
"""
struct GINConv{G,R} <: MessagePassing
    fg::G
    nn
    eps::R

    function GINConv(fg::G, nn, eps::R=0f0) where {G<:AbstractFeaturedGraph,R<:Real}
        new{G,R}(fg, nn, eps)
    end
end

function GINConv(nn, eps::Real=0f0)
    GINConv(NullGraph(), nn, eps)
end

Flux.trainable(g::GINConv) = (fg=g.fg, nn=g.nn)

message(g::GINConv, x_i::AbstractVector, x_j::AbstractVector) = x_j
update(g::GINConv, m::AbstractVector, x) = g.nn((1 + g.eps) * x + m)

@functor GINConv

function (g::GINConv)(fg::FeaturedGraph, X::AbstractMatrix)
    gf = graph(fg)
    GraphSignals.check_num_node(gf, X)
    _, X = propagate(g, adjacency_list(gf), Fill(0.f0, 0, ne(gf)), X, +)
    X
end

(l::GINConv)(fg::FeaturedGraph) = FeaturedGraph(fg.graph, nf = l(fg, node_feature(fg)))

"""
    CGConv([fg,] (node_dim, edge_dim), out, init, bias=true, as_edge=false)

Crystal Graph Convolutional network. Uses both node and edge features.

# Arguments

- `fg`: Optional [`FeaturedGraph`] argument(@ref)
- `node_dim`: Dimensionality of the input node features. Also is necessarily the output dimensionality.
- `edge_dim`: Dimensionality of the input edge features.
- `out`: Dimensionality of the output features.
- `init`: Initialization algorithm for each of the weight matrices
- `bias`: Whether or not to learn an additive bias parameter.
- `as_edge`: When call to layer `CGConv(M)`, accept input feature as node features or edge features.

# Usage

You can call `CGConv` in several different ways:

- Pass a FeaturedGraph: `CGConv(fg)`, returns `FeaturedGraph`
- Pass both node and edge features: `CGConv(X, E)`
- Pass one matrix, which is determined as node features or edge features by `as_edge` keyword argument.
"""
struct CGConv{E, V<:AbstractFeaturedGraph, A<:AbstractMatrix, B} <: MessagePassing
    fg::V
    Wf::A
    Ws::A
    bf::B
    bs::B
end

@functor CGConv

function CGConv(fg::G, dims::NTuple{2,Int};
                init=glorot_uniform, bias=true, as_edge=false) where {G<:AbstractFeaturedGraph}
    node_dim, edge_dim = dims
    Wf = init(node_dim, 2*node_dim + edge_dim)
    Ws = init(node_dim, 2*node_dim + edge_dim)
    bf = Flux.create_bias(Wf, bias, node_dim)
    bs = Flux.create_bias(Ws, bias, node_dim)
    T, S = typeof(Wf), typeof(bf)

    CGConv{as_edge,G,T,S}(fg, Wf, Ws, bf, bs)
end

function CGConv(dims::NTuple{2,Int}; init=glorot_uniform, bias=true, as_edge=false)
    CGConv(NullGraph(), dims; init=init, bias=bias, as_edge=as_edge)
end

message(c::CGConv,
        x_i::AbstractVector, x_j::AbstractVector, e::AbstractVector) = begin
    z = vcat(x_i, x_j, e)
    σ.(c.Wf * z + c.bf) .* softplus.(c.Ws * z + c.bs)
end
update(c::CGConv, m::AbstractVector, x) = x + m

function (c::CGConv)(fg::FeaturedGraph, X::AbstractMatrix, E::AbstractMatrix)
    check_num_nodes(fg, X)
    check_num_edges(fg, E)
    _, Y = propagate(c, adjacency_list(fg), E, X, +)
    Y
end

(l::CGConv)(fg::FeaturedGraph) = FeaturedGraph(fg, nf=l(fg, node_feature(fg),
                                                        edge_feature(fg)),
                                               ef=edge_feature(fg))

(l::CGConv)(X::AbstractMatrix, E::AbstractMatrix) = l(l.fg, X, E)

(l::CGConv{true})(M::AbstractMatrix) = l(l.fg, node_feature(l.fg), M)
(l::CGConv{false})(M::AbstractMatrix) = l(l.fg, M, edge_feature(l.fg))


# Compute total squared displacement of a set of coords
function sd{T<:AbstractFloat}(C::AbstractArray{T})
    t = zero(T)
    @inbounds @simd for i in eachindex(C)
        t += C[i] ^ 2
    end
    t
end

# Compute total squared displacement between two sets of coords
function sd{T<:AbstractFloat}(C1::AbstractArray{T}, C2::AbstractArray{T})
    t = zero(T)
    @inbounds @simd for i in eachindex(C1)
        t += (C1[i] - C2[i])^2
    end
    t
end

# Compute total squared displacement between two sets of coords in a periodic box
function sd{T<:AbstractFloat}(bw::T, C1::AbstractArray{T}, C2::AbstractArray{T})
    t = zero(T)
    @inbounds @simd for i in size(C1, 2)
        t += distancesq(bw, C1[1, i], C1[2, i], C1[3, i], C2[1, i], C2[2, i], C2[3, i])
    end
    t
end

# Compute mean squared displacements
msd{T<:AbstractFloat}(C::AbstractArray{T}) = sd(C) / length(C)
msd{T<:AbstractFloat}(C1::AbstractArray{T}, C2::AbstractArray{T}) = sd(C1, C2) / length(C1)
msd{T<:AbstractFloat}(bw::T, C1::AbstractArray{T}, C2::AbstractArray{T}) = sd(bw, C1, C2) / length(C1)

# Compute root mean squared deviations
rmsd{T<:AbstractFloat}(C::AbstractArray{T}) = sqrt(msd(C))
rmsd{T<:AbstractFloat}(C1::AbstractArray{T}, C2::AbstractArray{T}) = sqrt(msd(C1, C2))
rmsd{T<:AbstractFloat}(bw::T, C1::AbstractArray{T}, C2::AbstractArray{T}) = sqrt(msd(bw, C1, C2))

# Compute the magnitude of a vector
mag{T<:AbstractFloat}(C::AbstractVector{T}) = sqrt(sd(C))

# Wrap a set of coords into a periodic box
function wrapcoords!{T<:AbstractFloat}(bw::T, C::AbstractMatrix{T})
    hbw = bw / 2
    @inbounds @simd for i in 1:size(C, 2)
        if C[1] < -hbw C[1] += bw elseif C[1] > hbw C[1] -= bw end
        if C[2] < -hbw C[2] += bw elseif C[2] > hbw C[2] -= bw end
        if C[3] < -hbw C[3] += bw elseif C[3] > hbw C[3] -= bw end
    end
end

# Unwrap each coord in C into its image in IC
function unwrapcoords!{T<:AbstractFloat}(bw::T, C::AbstractMatrix{T}, IC::AbstractMatrix{T})
    hbw = bw / 2
    @inbounds @simd for i in 1:size(C, 2)
        if (C[1, i] - IC[1, i]) < -hbw C[1, i] += bw elseif C[1, i] - IC[1, i] > hbw C[1, i] -= bw end
        if (C[2, i] - IC[2, i]) < -hbw C[2, i] += bw elseif C[2, i] - IC[2, i] > hbw C[2, i] -= bw end
        if (C[3, i] - IC[3, i]) < -hbw C[3, i] += bw elseif C[3, i] - IC[3, i] > hbw C[3, i] -= bw end
    end
end

# Compute the squared distance between two coords
function distancesq{T<:AbstractFloat}(bw::T, x1::T, y1::T, z1::T, x2::T, y2::T, z2::T)
    hbw = bw / 2
    xd = x1 - x2
    yd = y1 - y2
    zd = z1 - z2
    if xd < -hbw xd += bw elseif xd > hbw xd -= bw end
    if yd < -hbw yd += bw elseif yd > hbw yd -= bw end
    if zd < -hbw zd += bw elseif zd > hbw zd -= bw end

    return xd*xd + yd*yd + zd*zd
end

function distancesq{T<:AbstractFloat}(xbw::T, ybw::T, zbw::T, x1::T, y1::T, z1::T, x2::T, y2::T, z2::T)
    xhbw = xbw / 2
    yhbw = ybw / 2
    zhbw = zbw / 2
    xd = x1 - x2
    yd = y1 - y2
    zd = z1 - z2
    if xd < -xhbw xd += xbw elseif xd > xhbw xd -= xbw end
    if yd < -yhbw yd += ybw elseif yd > yhbw yd -= ybw end
    if zd < -zhbw zd += zbw elseif zd > zhbw zd -= zbw end

    return xd*xd + yd*yd + zd*zd
end

distancesq{T<:AbstractFloat}(bw::T, C::AbstractMatrix{T}, i::Int, j::Int) = distancesq(bw, C[1, i], C[2, i], C[3, i], C[1, j], C[2, j], C[3, j])
distancesq{T<:AbstractFloat}(bw::T, C::AbstractMatrix{T}, i::Int, x::T, y::T, z::T) = distancesq(bw, C[1, i], C[2, i], C[3, i], x, y, z)
distancesq{T<:AbstractFloat}(bw::T, C1::AbstractMatrix{T}, C2::AbstractMatrix{T}, i1::Int, i2::Int) = distancesq(bw, C1[1, i1], C1[2, i1], C1[3, i1], C2[1, i2], C2[2, i2], C2[3, i2])

distancesq{T<:AbstractFloat}(xbw::T, ybw::T, zbw::T, C::AbstractMatrix{T}, i::Int, j::Int) = distancesq(xbw, ybw, zbw, C[1, i], C[2, i], C[3, i], C[1, j], C[2, j], C[3, j])

# Compute the distances between a set of coords and a particular coord
function distances!{T<:AbstractFloat}(bw::T, D::AbstractVector{T}, C::AbstractMatrix{T}, x::T, y::T, z::T)
    hbw = bw / 2
    @inbounds @simd for i in 1:size(D, 2)
        xd = C[1, i] - x
        yd = C[2, i] - y
        zd = C[3, i] - z
        if xd < -hbw xd += bw elseif xd > hbw xd -= bw end
        if yd < -hbw yd += bw elseif yd > hbw yd -= bw end
        if zd < -hbw zd += bw elseif zd > hbw zd -= bw end
        D[i] = sqrt(xd^2 + yd^2 + zd^2)
    end
end

function distances!{T<:AbstractFloat}(bw::T, D::AbstractArray{T, 3}, ci::Int, C::AbstractMatrix{T}, indices::AbstractMatrix{Int}, i::Int)
    hbw = bw / 2
    @inbounds begin
        x = C[1, i]
        y = C[2, i]
        z = C[3, i]
        @simd for j in 1:size(indices, 1)
            xd = C[1, indices[j, ci]] - x
            yd = C[2, indices[j, ci]] - y
            zd = C[3, indices[j, ci]] - z
            if xd < -hbw xd += bw elseif xd > hbw xd -= bw end
            if yd < -hbw yd += bw elseif yd > hbw yd -= bw end
            if zd < -hbw zd += bw elseif zd > hbw zd -= bw end
            D[1, j, ci] = xd
            D[2, j, ci] = yd
            D[3, j, ci] = zd
        end
    end
end


using LogisticRegression
using Test

include("simple_models.jl")


"""
    SemidirectProductOperation(action::AbstractGroupAction)

Group operation of a semidirect product group. The operation consists of the operation
`opN` on a normal subgroup `N`, the operation `opH` on a subgroup `H`, and an automorphism
`action` of elements of `H` on `N`. Only the action is stored.
"""
struct SemidirectProductOperation{A<:AbstractGroupAction} <: AbstractGroupOperation
    action::A
end

function show(io::IO, op::SemidirectProductOperation)
    print(io, "SemidirectProductOperation($(op.action))")
end

const SemidirectProductGroup{N,H,A} =
    GroupManifold{ProductManifold{Tuple{N,H}},SemidirectProductOperation{A}}

@doc doc"""
    SemidirectProductGroup(N::GroupManifold, H::GroupManifold, A::AbstractGroupAction)

A group that is the semidirect product of a normal group $N$ and a subgroup $H$, written
$G = N ⋊_θ H$, where $θ: H × N \to N$ is an automorphism action
of $H$ on $N$. The group $G$ has the composition rule

````math
g \circ g' = (n, h) \circ (n', h') = (n \circ θ_h(n'), h \circ h')
````

and the inverse

````math
g^{-1} = (n, h)^{-1} = (θ_{h^{-1}}(n^{-1}), h^{-1}).
````
"""
function SemidirectProductGroup(N::GroupManifold, H::GroupManifold, A::AbstractGroupAction)
    N === g_manifold(A) || error("Subgroup $(N) must be the G-manifold of action $(A)")
    H === base_group(A) || error("Subgroup $(H) must be the base group of action $(A)")
    op = SemidirectProductOperation(A)
    M = ProductManifold(N, H)
    return GroupManifold(M, op)
end

function show(io::IO, G::SemidirectProductGroup)
    M = base_manifold(G)
    N, H = M.manifolds
    A = G.op.action
    print(io, "SemidirectProductGroup($(N), $(H), $(A))")
end

_padpoint!(G::SemidirectProductGroup, y) = y

_padvector!(G::SemidirectProductGroup, v) = v

inv(G::GT, e::Identity{GT}) where {GT<:SemidirectProductGroup} = e

function inv!(G::SemidirectProductGroup, y, x)
    M = base_manifold(G)
    N, H = M.manifolds
    A = G.op.action
    nx, hx = submanifold_components(G, x)
    ny, hy = submanifold_components(G, y)
    inv!(H, hy, hx)
    ninv = inv(N, nx)
    apply!(A, ny, hy, ninv)
    @inbounds _padpoint!(G, y)
    return y
end
inv!(G::AG, y, e::Identity{AG}) where {AG<:SemidirectProductGroup} = identity!(G, y, e)

identity(G::GT, e::Identity{GT}) where {GT<:SemidirectProductGroup} = e

function identity!(G::SemidirectProductGroup, y, x)
    M = base_manifold(G)
    N, H = M.manifolds
    nx, hx = submanifold_components(G, x)
    ny, hy = submanifold_components(G, y)
    identity!(N, ny, nx)
    identity!(H, hy, hx)
    @inbounds _padpoint!(G, y)
    return y
end
identity!(G::GT, e::E, ::E) where {GT<:SemidirectProductGroup,E<:Identity{GT}} = e

compose(G::GT, x, e::Identity{GT}) where {GT<:SemidirectProductGroup} = x
compose(G::GT, e::Identity{GT}, x) where {GT<:SemidirectProductGroup} = x
compose(G::GT, e::E, ::E) where {GT<:SemidirectProductGroup,E<:Identity{GT}} = e

function compose!(G::SemidirectProductGroup, z, x, y)
    M = base_manifold(G)
    N, H = M.manifolds
    A = G.op.action
    nx, hx = submanifold_components(G, x)
    ny, hy = submanifold_components(G, y)
    nz, hz = submanifold_components(G, z)
    compose!(H, hz, hx, hy)
    zₙtmp = apply(A, hx, ny)
    compose!(N, nz, nx, zₙtmp)
    @inbounds _padpoint!(G, z)
    return z
end
compose!(G::GT, z, ::Identity{GT}, y) where {GT<:SemidirectProductGroup} = copyto!(z, y)
compose!(G::GT, z, x, ::Identity{GT}) where {GT<:SemidirectProductGroup} = copyto!(z, x)
function compose!(G::GT, z, e::E, ::E) where {GT<:SemidirectProductGroup,E<:Identity{GT}}
    return identity!(G, z, e)
end

function translate_diff!(G::SemidirectProductGroup, vout, x, y, v, conv::LeftAction)
    M = base_manifold(G)
    N, H = M.manifolds
    A = G.op.action
    nx, hx = submanifold_components(G, x)
    ny, hy = submanifold_components(G, y)
    nv, hv = submanifold_components(G, v)
    nvout, hvout = submanifold_components(G, vout)
    translate_diff!(H, hvout, hx, hy, hv, conv)
    nw = apply_diff(A, hx, ny, nv)
    nz = apply(A, hx, ny)
    translate_diff!(N, nvout, nx, nz, nw, conv)
    @inbounds _padvector!(G, vout)
    return vout
end

function hat!(G::SemidirectProductGroup, V, x, v)
    M = base_manifold(G)
    N, H = M.manifolds
    dimN = manifold_dimension(N)
    dimH = manifold_dimension(H)
    @assert length(v) == dimN + dimH
    nx, hx = submanifold_components(G, x)
    nV, hV = submanifold_components(G, V)
    hat!(N, nV, nx, view(v, 1:dimN))
    hat!(H, hV, hx, view(v, dimN+1:dimN+dimH))
    @inbounds _padvector!(G, V)
    return V
end

function vee!(G::SemidirectProductGroup, v, x, V)
    M = base_manifold(G)
    N, H = M.manifolds
    dimN = manifold_dimension(N)
    dimH = manifold_dimension(H)
    @assert length(v) == dimN + dimH
    nx, hx = submanifold_components(G, x)
    nV, hV = submanifold_components(G, V)
    vee!(N, view(v, 1:dimN), nx, nV)
    vee!(H, view(v, dimN+1:dimN+dimH), hx, hV)
    return v
end

function zero_tangent_vector(G::SemidirectProductGroup, x)
    v = allocate_result(G, zero_tangent_vector, x)
    zero_tangent_vector!(G, v, x)
    return v
end

function zero_tangent_vector!(G::SemidirectProductGroup, v, x)
    M = base_manifold(G)
    N, H = M.manifolds
    nx, hx = submanifold_components(G, x)
    nv, hv = submanifold_components(G, v)
    zero_tangent_vector!(N, nv, nx)
    zero_tangent_vector!(H, hv, hx)
    return v
end

function isapprox(G::SemidirectProductGroup, x, y; kwargs...)
    M = base_manifold(G)
    N, H = M.manifolds
    nx, hx = submanifold_components(G, x)
    ny, hy = submanifold_components(G, y)
    return isapprox(N, nx, ny; kwargs...) && isapprox(H, hx, hy; kwargs...)
end
function isapprox(G::SemidirectProductGroup, x, v, w; kwargs...)
    M = base_manifold(G)
    N, H = M.manifolds
    nx, hx = submanifold_components(G, x)
    nv, hv = submanifold_components(G, v)
    nw, hw = submanifold_components(G, w)
    return isapprox(N, nx, nv, nw; kwargs...) && isapprox(H, hx, hv, hw; kwargs...)
end
function isapprox(G::GT, x, e::Identity{GT}; kwargs...) where {GT<:SemidirectProductGroup}
    return isapprox(G, e, x; kwargs...)
end
function isapprox(G::GT, e::Identity{GT}, x; kwargs...) where {GT<:SemidirectProductGroup}
    return isapprox(G, identity(G, x), x; kwargs...)
end
function isapprox(
    ::GT,
    ::E,
    ::E;
    kwargs...,
) where {GT<:SemidirectProductGroup,E<:Identity{GT}}
    return true
end


struct NmmParameters
    l_max::Int
    dr::Float64
    grid_max::Float64
end

function nmm(sys::NmmSystem, np::NmmParameters, P::Int)
    grid = collect(range(np.dr, np.grid_max; step=np.dr))

    eK_m = zeros(length(grid), length(grid))
    eK_p = zeros(length(grid), length(grid))
    k = sys.mass * P / (2 * hbar^2 * sys.beta)
    for i in eachindex(grid)
        for j in eachindex(grid)
            eK_m[j, i] += exp(-k * (grid[i] - grid[j])^2)
            eK_p[j, i] += exp(-k * (grid[i] + grid[j])^2)
        end
    end
    eK = eK_m - eK_p
    eK ./= maximum(abs.(eK))

    scaling = 0.0
    scaling_event = Threads.Event()

    rho_diag = zeros(length(grid))
    add_lock = Threads.SpinLock()

    Threads.@threads for l in 0:np.l_max
        eV = zeros(length(grid), length(grid))
        for i in eachindex(grid)
            pot_rot = hbar^2 * l * (l + 1) / (2 * sys.mass * grid[i]^2)
            eV[i, i] += exp(-0.5 * sys.beta / P * (sys.pot(grid[i]) + pot_rot))
        end

        eH = eV * eK * eV
        F = eigen(Symmetric(eH))

        if l == 0
            scaling = maximum(abs.(F.values))
            notify(scaling_event)
        else
            wait(scaling_event)
        end

        rho_diag_l = zeros(length(F.values))
        for i in eachindex(rho_diag_l)
            for j in eachindex(rho_diag_l)
                rho_diag_l[i] += F.vectors[i, j]^2 * (F.values[j] / scaling)^P
            end
        end

        lock(add_lock) do
            rho_diag .+= (2l+1) .* rho_diag_l
        end
    end

    rho_diag ./= sum(rho_diag) * (grid[2] - grid[1])

    grid, rho_diag
end


function tsvd(A::NamedArray, k::Integer)
    m, n = size(A.array)
    F = svds(A.array, nsv=k)[1]
    U::Array{Float32, 2}, Z::Array{Float32, 1}, V::Array{Float32, 2} = F.U, F.S, F.V
    @assert size(U, 2) == length(Z) == size(V, 2) == k
    return U, Z, V
end

scale(A) = mapslices(normalize!, A .- mean(A,dims=1), dims=1)
X= flunch.assays["RNA"].norm_count

function RunPCA(X::NamedArray)
    mean(X.array, dims =)
    Y = broadcast(-, X.array, mean(X.array,1))
    tsvd
end

function RunPCA(X::scJuliaObject)
    Y = broadcast(-, X, mean(X,1))
    tsvd
end

# H. Li, G. C. Linderman, et al. "Algorithm 971: An Implementation of a
# Randomized Algorithm for Principal Component Analysis", ACM Transactions on
# Mathematical Software (TOMS), 2017
# DOI: https://doi.org/10.1145/3004053
function rsvd(A::SparseArrays.SparseMatrixCSC{Int32, Int32}, k::Integer; its=3, l=k+5)
    m, n = size(A)
    l = min(l, m, n)
    @assert 0 < k ≤ l ≤ min(m, n)
    Q::SparseArrays.SparseMatrixCSC{Int32, Int32} = rand(eltype(Int32), n, l) .- Int32(0.5)
    Y = A*Q
    F = lu!(Y)
    for i in 1:its
        Y = A'F.L
        F = lu!(Y)
        Y = A*F.L
        if i < its
            F = lu!(Y)
        else
            F = qr!(Y)
        end
    end
    Q = Matrix(F.Q)
    B = Q'A
    W, Σ, V = svd(B)
    U = Q*W
    return U[:,1:k], Σ[1:k], V[:,1:k]
end

Z = count(flunch)
Dispersion = var(Z.array,dims=2) ./ mean(Z.array,dims=2)

feature_data(flunch)

df = DataFrame(
  colour = ["green","blue"],
  shape = ["circle", "triangle"],
  border = ["dotted", "line"]
)

X = flunch
flunch.assays["RNA"].feature_data
insertcols!(feature_data(flunch), 2, :dispersion => Dispersion[:,1], makeunique=false)
feature_data(flunch)
FindVariableFeatures(flunch)
feature_data(flunch)[:dispersion] = Dispersion[:,1]

Dispersion = var(norm_count(flunch).array, dims = 2) ./ mean(norm_count(flunch).array,dims = 2)
GetFeatureData(flunch)[!, :dispersion] = Dispersion[:,1]
varfeatures = @pipe (flunch |> feature_data |> sort(_, [:dispersion]) |>  last(_, nfeatures).features |> reverse)
GetFeatureData(flunch)[!, :varfeatures] = in.(GetFeatureData(flunch)[!, :features], Ref(varfeatures))
flunch
nfeatures = 2000

Dispersion[:,1]


using CompScienceMeshes
using WiltonInts84

const p1 = point(0,0,0)
const p2 = point(1,0,0)
const p3 = point(0,1,0)

const c = point(0.2, 0.2, 0.2)
const r, R = 0.5, 1.3

function payload()
    I,K,G = wiltonints(p1,p2,p3,c,r,R,Val{1})
end

function driver()
    payload()
    Profile.clear_malloc_data()
    payload()
end

driver()


function optimize_acquisition(dim::Int64,
                              #max_feval::Int64,
                              max_time::Float64,
                              x_hint::AbstractVector,
                              y_opt::Real,
                              X::Matrix,
                              K, a, k;
                              verbose::Bool=true,
                              prng=Random.GLOBAL_RNG)
    α(x) = -expected_improvement(x, y_opt, X, K, a, k)
    dims = length(x_hint)

    res = CMAEvolutionStrategy.minimize(
        α,
        x_hint,
        0.5*√(dim);
        lower=zeros(dim),
        upper=ones(dim),
        seed=prng.seed[1],
        xtol=1e-4,
        ftol=1e-5,
        #maxfevals=max_feval,
        maxtime=max_time,
        multi_threading=true,
        verbosity=verbose ? 3 : 0
    )

    x_sol = CMAEvolutionStrategy.xbest(res)
    y_sol = α(x_sol)
    x_sol = clamp.(x_sol, 0, 1)
    return x_sol, y_sol
end

function optimize_mean(dim::Int64,
                       max_feval::Real,
                       X::Matrix,
                       K, a, k)
    data_μs = [ gp_predict(X[:,i], X, K, a, k)[1] for i = 1:size(X,2) ]
    opt_idx    = argmax(data_μs)
    x_opt      = X[:,opt_idx]
    y_opt      = data_μs[opt_idx]
    x_opt, y_opt, opt_idx
end


using ReachabilityAnalysis
using ReachabilityAnalysis: translate, solve
using Plots
using LaTeXStrings

### deterministic case

# IVP parameters
A = [0.0 1; -4π 0]
b = [1.0, -1.0]
B = hcat(b)
X0 = BallInf([0.0, 10.0], 0.1)
X = Universe(2)
u = -3.0
U_raw = Singleton([u])
U = ConstantInput(linear_map(B, U_raw))
X_homog = Universe(3)

# discretization parameters
δ = 0.1
alg = Forward()

# homogenization parameters
αs = [1.0]

# result for heterogeneous system
P_heterog = @ivp(x' = A*x + u, x ∈ X, u ∈ U, x(0) ∈ X0)
Pd_heterog = discretize(P_heterog, δ, alg)
Ω0_heterog = Pd_heterog.x0

# result for homogenized system
Ω0_homog = []
for α in αs
    local A_homog = [A b*u/α; zeros(1, 3)]
    local X0_homog = cartesian_product(X0, Singleton([α]))
    local P_homog = @ivp(x' = A_homog*x, x ∈ X_homog, x(0) ∈ X0_homog)

    Pd_homog = discretize(P_homog, δ, alg)
    local Ω0 = Projection(Pd_homog.x0, [1, 2])
#     Ω0 = overapproximate(Ω0, PolarDirections(30))  # workaround because σ is not available
    push!(Ω0_homog, Ω0)  # project auxiliary variable
end

# plot sets
plot(leg=:bottomleft, legendfontsize=20, tickfontsize=20)
plot!(Ω0_heterog, alpha=0, color=:blue, linecolor=:blue, linealpha=1, lw=2, lab=L"\Omega_0~\textrm{heterog}")
for i in eachindex(αs)
    plot!(Ω0_homog[i], alpha=0.3, color=:green, linecolor=:green, linealpha=1, lw=2, lab=L"\Omega_0~\textrm{homog}")
end
plot!(X0, alpha=1, color="yellow", lab=L"X_0")
# sampling initial states at time t from vertices and edges of X0
# and then plotting the reachable states for some random input signal
x0s = vcat(sample(X0, 0, include_vertices=true),
           sample(X0, 100, sampler=LazySets.FaceSampler(1)))
for x0 in x0s
    local P = @ivp(x' = A*x + u, x ∈ X, u ∈ U, x(0) ∈ x0)
    sol = solve(P, δ, ORBIT(δ=δ/10))
    plot!(sol, vars=(1, 2), c=:magenta, seriestype=:path, marker=:none)
end

savefig("homogenize_deterministic.pdf")

### nondeterministic case

U_raw = BallInf([u], 0.2)
U = ConstantInput(linear_map(B, U_raw))

# result for heterogeneous system
P_heterog = @ivp(x' = A*x + u, x ∈ X, u ∈ U, x(0) ∈ X0)
Pd_heterog = discretize(P_heterog, δ, alg)
Ω0_heterog = Pd_heterog.x0

# result for homogenized system
Ω0_homog = []
for α in αs
    local A_homog = [A b*u/α; zeros(1, 3)]
    local X0_homog = cartesian_product(X0, Singleton([α]))
    U_raw_homog = cartesian_product(translate(U_raw, -[u]), Singleton([0.0]))
    B_homog = [B zeros(size(B, 1)); zeros(size(B, 2)) 0]
    U_homog = ConstantInput(linear_map(B_homog, U_raw_homog))
    local P_homog = @ivp(x' = A_homog*x + u, x ∈ X_homog, u ∈ U_homog, x(0) ∈ X0_homog)

    Pd_homog = discretize(P_homog, δ, alg)
    push!(Ω0_homog, Projection(Pd_homog.x0, [1, 2]))  # project auxiliary variable
end

# plot sets
plot(leg=:bottomleft, legendfontsize=20, tickfontsize=20)
plot!(Ω0_heterog, alpha=0, color=:blue, linecolor=:blue, linealpha=1, lw=2, lab=L"\Omega_0~\textrm{heterog}")
for i in eachindex(αs)
    plot!(Ω0_homog[i], alpha=0.3, color=:green, linecolor=:green, linealpha=1, lw=2, lab=L"\Omega_0~\textrm{homog}")
end
plot!(X0, alpha=1, color="yellow", lab=L"X_0")
# sampling initial states at time t from vertices and edges of X0
# and then plotting the reachable states for some random input signal
x0s = vcat(sample(X0, 0, include_vertices=true),
           sample(X0, 100, sampler=LazySets.FaceSampler(1)))
for x0 in x0s
    local P = @ivp(x' = A*x + u, x ∈ X, u ∈ U, x(0) ∈ x0)
    sol = solve(P, δ, ORBIT(δ=δ/10))
    plot!(sol, vars=(1, 2), c=:magenta, seriestype=:path, marker=:none)
end

savefig("homogenize_nondeterministic.pdf")


# Circular Manifold definition and associated arithmetic

struct Circular <: Manifold
  dof::Int
  addop::Function
  diffop::Function
  getMu
  getLambda
  domain::Tuple{Float64, Float64}
  Circular() = new(1,
                   addtheta,
                   difftheta,
                   getCircMu,
                   getCircLambda,
                   (-pi+0.0,pi-1e-15))
end

function get2DMu(mus, Lambdas; diffop::Function=-, periodicmanifold::Function=(x)->x, initrange::Tuple{Float64, Float64}=(-1e-5,1e-5) )::Float64
  # TODO: can be solved as the null space basis, but requires proper scaling
  gg = (res, x) -> solveresid2DLinear!(res, x, mus, Lambdas, diffop=diffop)
  initr = initrange[2]-initrange[1]
  x0 = [initr*rand()+initrange[1]]
  r = NLsolve.nlsolve(gg, x0)
  xs = sign(r.zero[1] - x0[1])*1e-3 .+ r.zero
  r = NLsolve.nlsolve(gg, xs)
  return periodicmanifold(r.zero[1])
end

function get2DMuMin(mus, Lambdas; diffop::Function=-, periodicmanifold::Function=(x)->x, initrange::Tuple{Float64, Float64}=(-1e-5,1e-5), method=Optim.Newton(), Λμ::Bool=false )::Float64
  # TODO: can be solved as the null space basis, but requires proper scaling
  res = zeros(1)
  # @show round.(mus, digits=3)
  gg = (x) -> (solveresid2DLinear(res, x, mus, Lambdas, diffop=diffop))^2
  initr = initrange[2]-initrange[1]
  # TODO -- do we need periodicmanifold here?
  x0 = [initr*rand()+initrange[1]]
  r = Optim.optimize(gg, x0, method)
  # @show r.minimizer[1]
  return periodicmanifold(r.minimizer[1])
end

# struct SO2Manifold <: Manifold
# end
#
#
# # should not be defined in AMP, since we want IIF indepent of manifolds
# function *(PP::Vector{MB{SO2Manifold,B}}) where B
#   @info "taking manifold product of $(length(PP)) terms"
#   @warn "SO2Manifold: work in progress"
# end
#
# mbr1 = ManifoldBelief(SO2Manifold, 0.0)
# mbr2 = ManifoldBelief(SO2Manifold, 0.0)
#
# *([mbr1;mbr2])

#


module ChaosGameFractals

# Create chaosgame-like fractals
# Translated from https://github.com/python/performance/blob/2ebc46c24c5f816234d8026698470476a3c59d2e/performance/benchmarks/bm_chaos.py
# to Julia
# Original Copyright (C) 2005 Carl Friedrich Bolz

if VERSION >= v"0.7.0-DEV.3052"
    using Printf
end
if VERSION >= v"0.7.0-DEV.3406"
    using Random
end

const DEFAULT_THICKNESS = 0.25
const DEFAULT_WIDTH = 256
const DEFAULT_HEIGHT = 256
const DEFAULT_ITERATIONS = 500000 # 100x more iterations than Python
const DEFAULT_RNG_SEED = 1234

###########
# GVector #
###########

struct GVector{T <: Number}
    x::T
    y::T
    z::T
end

GVector(x=0.0, y=0.0, z=0.0) = GVector(x, y, z)

Base.abs(u::GVector) = sqrt(u.x^2 + u.y^2 + u.z^2)
dist(u::GVector, v::GVector) = sqrt((u.x - v.x)^2 +
                                    (u.y - v.y)^2 +
                                    (u.z - v.z)^2)
Base.:+(u::GVector, v::GVector) = GVector(u.x + v.x, u.y + v.y, u.z + v.z)
Base.:-(u::GVector, v::GVector) = u + (v * -1)
Base.:*(u::GVector, n::Number)= GVector(u.x * n, u.y * n, u.z * n)

function linear_combination(u::GVector, v::GVector, l1, l2=1-l1)
    return GVector(u.x * l1 + v.x * l2,
                   u.y * l1 + v.y * l2,
                   u.z * l1 + v.z * l2,)
end

Base.show(io::IO, u::GVector) = print(io, "<", u.x, ", ", u.y, ", ", u.z, ">")

function getknots(points::Vector{V}, degree::Int) where V <: GVector
    knots = [zeros(Int, degree); 1:(length(points) - degree)]
    append!(knots, fill(length(points) - degree, degree))
    return knots
end

##########
# Spline #
##########

struct Spline{T}
    knots::Vector{Int}
    points::Vector{GVector{T}}
    degree::Int
    d::Vector{GVector{T}}
end

function Spline(points, degree=3, knots = getknots(points, degree))
    if length(points) >  length(knots) - degree + 1
        throw(ArgumentError("too many control points"))
    elseif length(points) < length(knots) - degree + 1
        throw(ArgumentError("not enough control points"))
    elseif any(i -> knots[i] < knots[i-1], 2:length(knots))
        throw(ArgumentError("knots not strictly increasing"))
    end
    Spline(knots, points, degree, similar(points))
end

Base.length(s::Spline) = length(s.points)
getdomain(s::Spline) = s.knots[s.degree], s.knots[length(s.knots) - s.degree + 1]

function Base.getindex(s::Spline, u::Number)
    dom = getdomain(s)
    I = dom[2] - 1
    for ii in (s.degree - 1):(length(s.knots) - s.degree - 1)
        if u >= s.knots[ii + 1] && u < s.knots[ii + 2]
            I = ii
            break
        end
    end
    return I
end

function (s::Spline)(u::Number)
    dom = getdomain(s)
    if u < dom[1] || u > dom[2]
        throw(ArgumentError("Function value not in domain"))
    elseif u == dom[1]
        return s.points[1]
    elseif u == dom[2]
        return s.points[end]
    end
    I = getindex(s, u)
    @inbounds for ii in 0:s.degree
        s.d[ii+1] = s.points[I - s.degree + ii + 2]
    end
    U = s.knots
    @inbounds for ik in 1:s.degree
        for ii in (I - s.degree + ik + 1):(I + 1)
            ua = U[ii + s.degree - ik + 1]
            ub = U[ii]
            co1 = (ua - u) / (ua - ub)
            co2 = (u - ub) / (ua - ub)
            index = ii - I + s.degree - ik - 1
            s.d[index+1] = linear_combination(s.d[index+1], s.d[index + 2], co1, co2)
        end
    end
    return s.d[1]
end

#############
# ChaosGame #
#############

struct ChaosGame{T <: Number}
    splines::Vector{Spline{T}}
    thickness::T
    minx::T
    miny::T
    maxx::T
    maxy::T
    height::T
    width::T
    num_trafos::Vector{Int}
    num_total::Int
end

function ChaosGame(splines, thickness=0.1)
    minx = minimum(p.x for spl in splines for p in spl.points)
    miny = minimum(p.y for spl in splines for p in spl.points)
    maxx = maximum(p.x for spl in splines for p in spl.points)
    maxy = maximum(p.y for spl in splines for p in spl.points)
    height = maxy - miny
    width = maxx - minx
    num_trafos = Int[]
    maxlen = thickness * width / height
    for spl in splines
        len = 0
        curr = spl(0)
        for i in 1:999
            last = curr
            t = 1 / 999 * i
            curr = spl(t)
            len += dist(curr, last)
        end
        push!(num_trafos, max(1, trunc(Int, len / maxlen * 1.5)))
    end
    num_total = sum(num_trafos)
    return ChaosGame(splines, thickness, minx, miny, maxx, maxy, height, width, num_trafos, num_total)
end

function transform_point(point::GVector, game::ChaosGame, trafo = get_random_trafo(game))
    x = (point.x - game.minx) / game.width
    y = (point.y - game.miny) / game.height
    first_trafo = trafo[1] + 1
    start, stop = getdomain(game.splines[first_trafo])

    len = stop - start
    seg_len = len / game.num_trafos[first_trafo]
    t = start + seg_len * trafo[2] + seg_len * x
    basepoint = game.splines[first_trafo](t)
    if t + 1/50000 > stop
        neighbour = game.splines[first_trafo](t - 1/50000)
        derivative = neighbour - basepoint
    else
        neighbour = game.splines[first_trafo](t + 1/50000)
        derivative = basepoint - neighbour
    end

    if abs(derivative) != 0
        fact = 1 / abs(derivative) * (y - 0.5) * game.thickness
        basepoint = GVector(basepoint.x + derivative.y * fact,
                            basepoint.y - derivative.x * fact,
                            basepoint.z)
    end
    return truncate(basepoint, game)
end

function get_random_trafo(game::ChaosGame)
    r = rand(0:game.num_total)
    l = 0
    for i in 0:(length(game.num_trafos)-1)
        if r >= l && r < l + game.num_trafos[i+1]
            return i, rand(0:game.num_trafos[i+1]-1)
        end
        l += game.num_trafos[i+1]
    end
    return length(game.num_trafos) - 1, rand(0:game.num_trafos[end] - 1)
end

function Base.truncate(u::GVector, game::ChaosGame)
    GVector(clamp(u.x, game.minx, game.maxx),
            clamp(u.y, game.miny, game.maxy),
            u.z)
end

function create_image_chaos(game::ChaosGame, w = DEFAULT_HEIGHT,
                            h = DEFAULT_WIDTH, iterations = DEFAULT_ITERATIONS;
                            filename = nothing, seed=DEFAULT_RNG_SEED)

    if VERSION <= v"0.7.0-beta2.171"
        srand(seed)
    else
        Random.seed!(seed)
    end
    im = ones(Int, h, w)
    point = GVector((game.maxx + game.minx) / 2, (game.maxy + game.miny) / 2, 0.0)
    for _ in 1:iterations
        point = transform_point(point, game)

        x = (point.x - game.minx) / game.width * w
        y = (point.y - game.miny) / game.height * h
        xi = trunc(Int, x)
        yi = trunc(Int, y)
        xi == w && (xi -= 1)
        yi == h && (yi -= 1)
        im[h - yi, xi + 1] = 0
    end

    if filename != nothing
        write_ppm(im, filename)
    end
    return im
end

function write_ppm(im, filename::String)
    magic = "P6\n"
    maxval = 255
    h, w = size(im)

    open(filename, "w") do f
        write(f, magic)
        write(f, @sprintf "%i %i\n%i\n" w h maxval)
        for j in 1:w, i in 1:h
            c = round(Int, im[i, j] * 255)
            write(f, UInt8(c), UInt8(c), UInt8(c))
        end
    end
end

const splines = [
        Spline([
            GVector(1.597350, 3.304460, 0.000000),
            GVector(1.575810, 4.123260, 0.000000),
            GVector(1.313210, 5.288350, 0.000000),
            GVector(1.618900, 5.329910, 0.000000),
            GVector(2.889940, 5.502700, 0.000000),
            GVector(2.373060, 4.381830, 0.000000),
            GVector(1.662000, 4.360280, 0.000000)],
            3, [0, 0, 0, 1, 1, 1, 2, 2, 2]),
        Spline([
            GVector(2.804500, 4.017350, 0.000000),
            GVector(2.550500, 3.525230, 0.000000),
            GVector(1.979010, 2.620360, 0.000000),
            GVector(1.979010, 2.620360, 0.000000)],
            3, [0, 0, 0, 1, 1, 1]),
        Spline([
            GVector(2.001670, 4.011320, 0.000000),
            GVector(2.335040, 3.312830, 0.000000),
            GVector(2.366800, 3.233460, 0.000000),
            GVector(2.366800, 3.233460, 0.000000)],
            3, [0, 0, 0, 1, 1, 1])
    ]

const game = ChaosGame(splines)

perf_chaos() = create_image_chaos(game)

end


include("stacknet.jl")
include("logger.jl")

function testing()

    function train(X, Y, net::StackNet)
        const MAXITER = 20000
        const BATCHSIZE = 5

        for epoch = 1:MAXITER
            loss = 0
            for i = 1:size(X)[2]
                # every single sample
                outputs = forward(net, X[:, i])
                loss += net.loss_func(outputs, Y[i])
                backward(net, outputs, Y[i])

                if i % BATCHSIZE == 0
                    batch_update(net, eta=0.03)
                    #dbglog(shownet(net))
                end
            end
            batch_update(net, eta=0.03)
            #dbglog("loss $(loss[1])")
        end
    end

    function test(X, Y, net::StackNet)
        for i = 1:size(X)[2]
            # every single sample
            outputs = forward(net, X[:, i])
            dbglog("loss $(net.loss_func(outputs, Y[i]))")

            infolog(outputs[1], Y[i])
        end
    end

    X = [
    1 2 0 0 1 0 0 -1 -2 -1;
    0 0 -1 -2 -1 1 2 0 0 1
    ]

    testX = [
    2 1 -1 -2;
    -1 -2 2 1
    ]

    testY = [ 1 1 -1 -1 ]
    testY = [ 1 1 0 0 ]

    Y = [ 1 1 1 1 1 -1 -1 -1 -1 -1 ]
    Y = [ 1 1 1 1 1 0 0 0 0 0 ]

    net = StackNet(square_error)
    add_layer(net, PerceptronLayer(2, 3, tanh))
    add_layer(net, PerceptronLayer(3, 1, sigmoid))

    dbglog(shownet(net))
    test(testX, testY, net)

    train(X, Y, net)
    dbglog(shownet(net))

    test(testX, testY, net)

end

# homework main execution
function main()
    X = Array[
    [ 1.58,  2.32,  -5.8],   [ 0.67,  1.58,  -4.78],  [ 1.04,  1.01,  -3.63],
    [-1.49,  2.18,  -3.39],  [-0.41,  1.21,  -4.73],  [1.39,  3.16,  2.87],

    [ 0.21,  0.03,  -2.21],   [ 0.37,  0.28,  -1.8],  [ 0.18,  1.22,  0.16],
    [-0.24,  0.93,  -1.01],  [-1.18,  0.39,  -0.39],  [0.74,  0.96,  -1.16],

    [-1.54,  1.17,  0.64],   [5.41,  3.45,  -1.33],  [ 1.55,  0.99,  2.69],
    [1.86,  3.19,  1.51],    [1.68,  1.79,  -0.87],  [3.51,  -0.22,  -1.39],

    ]

    testX = Array[
    [ 1.20,  1.40,  -1.89],  [-0.92,  1.44,  -3.22],  [ 0.45,  1.33,  -4.38],
    [-0.76,  0.84,  -1.96],
    [-0.38,  1.94,  -0.48],  [0.02,  0.72,  -0.17],  [ 0.44,  1.31,  -0.14],
    [ 0.46,  1.49,  0.68],
    [1.40,  -0.44,  -0.92],  [0.44,  0.83,  1.97],  [ 0.25,  0.68,  -0.99],
    [ 0.66,  -0.45,  0.08]
    ]

    Y = Array[
    [1, 0, 0], [1, 0, 0], [1, 0, 0],
    [1, 0, 0], [1, 0, 0], [1, 0, 0],

    [0, 1, 0], [0, 1, 0], [0, 1, 0],
    [0, 1, 0], [0, 1, 0], [0, 1, 0],

    [0, 0, 1], [0, 0, 1], [0, 0, 1],
    [0, 0, 1], [0, 0, 1], [0, 0, 1]
    ]

    testY = Array[
    [1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0],
    [0, 1, 0], [0, 1, 0], [0, 1, 0], [0, 1, 0],
    [0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1]
    ]

    X = [X; testX]
    Y = [Y; testY]

    const MAXITER = 30000
    const BATCHSIZE = 2
    const ETA = 0.1
    const HIDDEN = 7

    net = StackNet(square_error, batch_size=BATCHSIZE)
    add_layer(net, PerceptronLayer(3, HIDDEN, tanh))
    add_layer(net, PerceptronLayer(HIDDEN, 3, sigmoid))

    net.layers[1].weights = [0.34220853408533164 0.3245861926046375 0.3332052733100308
    0.22859717196907367 0.46779743149877756 0.3036053965321488
    0.10892493071988898 0.5410961612858103 0.3499789079943008
    0.4423683719806599 0.2032223830720614 0.3544092449472787
    0.270107374555619 0.16764827123181456 0.5622443542125665
    0.02227635058706275 0.8910201267019922 0.08670352271094506
    0.4865634514215009 0.4626169052427591 0.05081964333573993]
    net.layers[1].bias = [0.9952117507065281,0.388087128339186,0.8725765350929733,0.41961076086301596,0.1167031740076856,0.9087224278524852,0.05980293821390692]
    net.layers[2].weights = [0.0526149662305822 0.2031494320476992 0.1346865896996537 0.11584528187906622 0.17054130500350598 0.15808138735561705 0.16508103778387567
    0.24831808672833983 0.2403426545152851 0.11561300113671563 0.10207716354505683 0.14601009172035231 0.0947610652356002 0.052877937118650215
    0.2356175051594146 0.008793297652765042 0.07416296981297327 0.08912410166055519 0.15911479330166178 0.19253821277987473 0.2406491196327555]
    net.layers[2].bias = [0.7230274076027488,0.34008837831362837,0.8944012775586168]

    function train()
        for epoch = 1:MAXITER
            loss = 0
            for i = 1:length(X)

                outputs = forward(net, X[i])
                loss += net.loss_func(outputs, Y[i])
                backward(net, outputs, Y[i])

                if i % BATCHSIZE == 0
                    batch_update(net, eta=ETA)
                end

            end
            batch_update(net, eta=ETA)

            dbglog("train loss: $HIDDEN, $BATCHSIZE, $ETA, $epoch, $loss")
        end
    end

    function test()
        for i = 1:length(testX)
            dbglog("------------\n test sample number: $i $(testX[i])")
            outputs = forward(net, testX[i])
            dbglog("loss $(net.loss_func(outputs, testY[i]))")
            infolog(outputs, testY[i])
        end
    end

    #dbglog(shownet(net))
    dbglog(repr(net))
    #test()

    train()

    #dbglog(shownet(net))
    #test()
end

if !isinteractive()
    main()
end


module HierachicalClustering
using Clustering, Distances, Statistics, LinearAlgebra

export fit

function fit(records, labels, dist_type, linkage, normalized=false)
  if normalized
    values = records./sum(records, dims=1)
  else
    values = records
  end
  matrix = nothing
  if dist_type == :euclidean
    matrix = pairwise(Euclidean(), values, dims=1)
  elseif dist_type == :mahalanobis
    cov_i = cov(values)
    matrix = pairwise(Mahalanobis(cov_i, skipchecks=false), values, dims=1)
  elseif dist_type == :cosine
    matrix = pairwise(CosineDist(), values, dims=1)
  end

  clustering = hclust(matrix, linkage=linkage, branchorder=:optimal)
  result = []
  cache = Dict()
  for (i, x) in enumerate(mapslices(x -> [x], clustering.merges, dims=2)[:])
    left, right = nothing, nothing
    if x[1] < 0
      left = labels[abs(x[1])]
    else
      left = pop!(cache, x[1])
    end

    if x[2] < 0
      right = labels[abs(x[2])]
    else
      right = pop!(cache, x[2])
    end

    result = [left, right]
    get!(cache, i, result)
  end

  result
end

end


using Plots, Revise, IJulia, Distributions, GLM, DataFrames, DifferentialEquations

plot(rand(Normal(),5),rand(Normal(),5))
ols = lm(@formula(X ~ Y), DataFrame(X=[1,2],Y=[4,5]))
f(u,p,t) = 1.01*u
u0 = 1/2
tspan = (0.0,1.0)
prob = ODEProblem(f,u0,tspan)
sol = solve(prob, Tsit5(), reltol=1e-8, abstol=1e-8)


using Optim

function PipeFlow(D,P,L;Fluid="Water",
    ΔT=40,
     FlowType="Turbulent",
     AbsRoughness=1.5e-6)

    if Fluid == "Air"
        DynVisc = 20e-6; # Pa*seconds ***Air***
        Dens = 1.225; # ***Air***
        C = 1000;# J/(kg*k) AIR
    elseif Fluid == "Oil"
        DynVisc = 5e-3; # Pa*seconds ***Oil***
        Dens = 900; # ***Oil***
        C = 1800;# J/(kg*k) AIR
    else()
        C = 4187;# J/(kg*k) WATER
        Dens = 1000;# kg/m^3
        DynVisc = 7.97e-4; # Pa*seconds ***Water***
    end

    P = P * 6894.76 # converting from PSI to Pa
    Re = 1000 # Initial guess
    ϵ = AbsRoughness / D;
    if FlowType == "Laminar"

        f, Re, LinVel = Frict(Re, P, L, D, Dens, DynVisc)

    elseif FlowType == "Turbulent"
        f, Re, LinVel = Frict(ϵ, Re, P, L, D, Dens, DynVisc)
    end

    println("Friction factor:$(f)")

    println("Re Num:$(Re)")

    Area = π / 4 * D.^2
    MassFlow = Area * LinVel * Dens
    PowerDiss = MassFlow * C * ΔT
    println("Mass Flow:$MassFlow)")
    println("Max power:$(PowerDiss)")

    return MassFlow, LinVel, PowerDiss
end

function Frict(Re, P, L, D, Dens, DynVisc)

    f = 64 / Re
    LinVel = sqrt(2 * P / (L / D * f * Dens))

    for i = 1:10
        Re = Dens * D * LinVel / DynVisc
        f = 64 / Re
        LinVel = sqrt(2 * P / (L / D * f * Dens))

    end

    return f, Re, LinVel

end

function Frict(ϵ, Re, P, L, D, Dens, DynVisc)
        # A = -2*log((ϵ/3.7)-5.02/Re*log(ϵ/3.7+13/Re)); %"A Review of Explicit Friction Factor Equations"  Zigrang and Sylvester eq. 13
        # FFac = A.^(-2);

        # FFacEq(F)  = 1.14-2*log10(ϵ+9.35/(Re.*sqrt(F)))-1/sqrt(F)
    FFacEq1(F)  = -2 * log10((ϵ / 3.7) - 5.02 / Re * log10(ϵ / 3.7 + 13 / Re)) - 1 / √(F)
    OptimCost1(F) = abs.(FFacEq1(F))
    f = optimize(OptimCost1, 0., 1.).minimizer
    LinVel = sqrt(2 * P / (L / D * f * Dens))

    for i = 1:10
        Re = Dens * D * LinVel / DynVisc
        FFacEq(F)  = -2 * log10((ϵ / 3.7) - 5.02 / Re * log10(ϵ / 3.7 + 13 / Re)) - 1 / √(F)
        OptimCost(F) = abs.(FFacEq(F))
        f = optimize(OptimCost, 0., 1.).minimizer
        LinVel = sqrt(2 * P / (L / D * f * Dens))

    end

    return f, Re, LinVel

end


@testset "Measure" begin
    Mod.@polyvar x y
    @test_throws ArgumentError measure([1, 2], [x, x*y, y])
    @test_throws ArgumentError measure([1, 2, 3, 4], [x, x*y, y])
    μ = measure([1, 0, 2, 3], [x^2*y^2, y*x^2, x*y*x^2, x*y^2])
    @test monomials(μ) == [x^3*y, x^2*y^2, x^2*y, x*y^2]
    @test monomial.(moments(μ)) == [x^3*y, x^2*y^2, x^2*y, x*y^2]
    @test MultivariateMoments.moment_value.(moments(μ)) == [2, 1, 0, 3]
    @test all(nvariables.(moments(μ)) .== 2)
    @test degree.(moments(μ)) == [4, 4, 3, 3]
    @test μ.a == [2, 1, 0, 3]
    @test (-μ).a == [-2, -1, 0, -3]
    @test (2 * μ).a == [4, 2, 0, 6]
    @test (μ * 3).a == [6, 3, 0, 9]
    μ = measure([1, 1], [x, x])
    @test monomials(μ) == [x]
    @test MultivariateMoments.moment_value.(moments(μ)) == [1]
    err = ErrorException("The monomial `x` is occurs twice with different values: `1` and `2`")
    @test_throws err measure([1, 2], [x, x])
    #@test_throws ArgumentError measure([1], [x]) + measure([1], [y])
end


module FinOptions

export

        Black76,
        BlackScholes,
        GBlackScholes,
        KirkSpreadOption,
        GeoAveRate,
        GDelta,
        Delta,
        Elasticity,
        Gamma,
        DGammaDVol,
        GammaP,
        DDeltaDVol,
        Vega,
        Vomma,
        VegaP,
        DVegaDVol,
        Theta,
        Rho,
        RhoFut,
        Rho2,
        Carry,
        Speed,
        StrikeDelta,
        RNDensity,
        PutCallParity,
        VannaVolga,
        SwingOptionPricer

include("Black76.jl")
include("BlackScholes.jl")
include("CND.jl")
#include("CNDEV.jl"),
include("GBlackScholes.jl")
include("GDelta.jl")
include("Greeks.jl")
include("PutCallParity.jl")
include("VannaVolga.jl")
include("SwingOptionPricer.jl")

# package code goes here

end # module


@testset "Data informed compression - Burgers example" begin
    n = 1000 # spatial discretization
    using DifferentialEquations, LinearAlgebra, SparseArrays, LowRankIntegrators
    m = 10

    r = 15 # approximation rank
    l = π # length of spatial domain
    Δx = l/n # step size
    dt = 10e-3
    x_range = Δx/2:Δx:l-Δx/2 # uniform grid
    t_range = 0:dt:1.0

    # uncertainty realizations
    σ = [0.5,0.5]
    ξs = [(ξ_1,ξ_2) for ξ_1 in range(-1,1,length=m), ξ_2 in range(-1,1,length=m)];

    # generate coefficient matrices and store as global variables
    # boundary conditions
    left(i) = i > 1 ? i - 1 : n
    right(i) = i < n ? i + 1 : 1

    # discretized diff operators
    # laplacian (times viscosity)
    Δ = spzeros(n, n)
    ν = 0.005
    for i in 1:n
        Δ[i,left(i)] = ν/Δx^2
        Δ[i,i] = -2ν/Δx^2
        Δ[i,right(i)] = ν/Δx^2
    end

    # gradient
    ∇ = spzeros(n, n)
    for i in 1:n
        ∇[i,left(i)] = -1/2/Δx
        ∇[i,right(i)] = 1/2/Δx
    end

    # generate the data used for comparison
    ub(x) = 0.5*(exp.(cos.(x)) .- 1.5).*sin.(x .+ 2π*0.37) # deterministic initial condition
    uprime(x,ξ,σ) = σ[1]*ξ[1]*sin.(2π*x) .+ σ[2]*ξ[2]*sin.(3π*x) # stochastic fluctuation

    function burgers(ρ, (Δ,∇), t)
        return Δ*ρ - (∇*ρ) .* ρ
    end
    function burgers!(dρ, ρ, (∇,Δ,C,D), t)
        mul!(C, ∇, ρ)
        mul!(dρ, Δ, ρ)
        D .= C .* ρ
        dρ .-= D
    end
    C, D = ones(n), ones(n)
    true_sols = [zeros(n,m^2) for i in 1:length(t_range)]
    ode_prob = ODEProblem(burgers!, ones(n), (0.0, 1.0), (∇,Δ,C,D))
    for (i,ξ) in enumerate(ξs)
        println(i)
        _prob = remake(ode_prob, u0 = ub(x_range) + uprime(x_range, ξ, σ))
        _sol = Array(DifferentialEquations.solve(_prob, saveat=t_range))
        for k in 1:length(t_range)
            true_sols[k][:,i] .= _sol[:,k]
        end
    end

    # Greedy: data_informed
    X0 = truncated_svd(hcat(true_sols[1], true_sols[2]), 15)
    U0 = X0.U
    Z0 = true_sols[1]'*U0
    function interpolate_data(t, data, t_range, dt)
        idx = min(floor(Int64, t/dt) + 1, length(t_range))
        return data[idx]
    end

    FZ(Z,U,t) = Matrix(burgers(TwoFactorRepresentation(U,Z), (Δ,∇),t)' * U)
    data_informed_prob = MatrixHybridProblem(t -> interpolate_data(t, true_sols, t_range, dt), FZ, TwoFactorRepresentation(U0, Z0), (0.0, 1.0))
    data_informed_sol = LowRankIntegrators.solve(data_informed_prob, GreedyIntegrator(), dt)
    @test norm(true_sols[end] - data_informed_sol.Y[end])/norm(true_sols[end]) <= 0.1
end


mask(dE,dEmax) = cospi(dE/(2dEmax))
function mask_v(E, cutoff, i, j)
    dE = E[i:(j-1)] - cutoff
    mask(dE, maximum(E)-cutoff)
end

function mask(H, npartial, cutoff, power = 1.0)
    cutoff /= 27.211
    E = diag(H)
    i = 1
    T = eltype(E)
    cutoff = T(cutoff)
    m = ones(T, length(E))
    for k = 1:npartial-1
        i = findnext(e -> e > cutoff, E, i)
        j = findnext(e -> e <= cutoff, E, i)
        j == 0 && (j=length(E)+1)
        m[i:(j-1)] = mask_v(E, cutoff, i, j)
        i = j
    end
    m[i:end] = 0
    m.^T(power)
end

# This function generates a diagonal matrix which, when applied to a
# wavefunction, will eat up all energies larger than cutoff, and kill
# the highest partial wave completely.
function Egobbler{T<:AbstractFloat}(H::AbstractMatrix{T}, npartial, cutoff, mode)
    if cutoff == 0
        Ψ -> ()
    else
        d = speye(T,size(H,1)) - sparse(Diagonal(mask(H, npartial, cutoff, T(1/8))))
        if mode == :gpu && Magnus.cuda
            d = Magnus.upload(d*one(Complex{T}))
        end
        Ψ -> A_mul_B!(-one(Complex{T}), d, Ψ, one(Complex{T}), Ψ)
    end
end


export norminvAS241, norminvBSMS

const SPLIT1 = 0.425
const SPLIT2 = 5.0
const CONST1 = 0.180625
const CONST2 = 1.6

# Coefficients for P close to 0.5
const A0 = 3.3871328727963666080E0
const A1 = 1.3314166789178437745E+2
const A2 = 1.9715909503065514427E+3
const A3 = 1.3731693765509461125E+4
const A4 = 4.5921953931549871457E+4
const A5 = 6.7265770927008700853E+4
const A6 = 3.3430575583588128105E+4
const A7 = 2.5090809287301226727E+3
const B1 = 4.2313330701600911252E+1
const B2 = 6.8718700749205790830E+2
const B3 = 5.3941960214247511077E+3
const B4 = 2.1213794301586595867E+4
const B5 = 3.9307895800092710610E+4
const B6 = 2.8729085735721942674E+4
const B7 = 5.2264952788528545610E+3

#  Coefficients for P not close to 0, 0.5 or 1.
const C0 = 1.42343711074968357734E0
const C1 = 4.63033784615654529590E0
const C2 = 5.76949722146069140550E0
const C3 = 3.64784832476320460504E0
const C4 = 1.27045825245236838258E0
const C5 = 2.41780725177450611770E-1
const C6 = 2.27238449892691845833E-2
const C7 = 7.74545014278341407640E-4
const D1 = 2.05319162663775882187E0
const D2 = 1.67638483018380384940E0
const D3 = 6.89767334985100004550E-1
const D4 = 1.48103976427480074590E-1
const D5 = 1.51986665636164571966E-2
const D6 = 5.47593808499534494600E-4
const D7 = 1.05075007164441684324E-9

#  Coefficients for P near 0 or 1.
const E0 = 6.65790464350110377720E0
const E1 = 5.46378491116411436990E0
const E2 = 1.78482653991729133580E0
const E3 = 2.96560571828504891230E-1
const E4 = 2.65321895265761230930E-2
const E5 = 1.24266094738807843860E-3
const E6 = 2.71155556874348757815E-5
const E7 = 2.01033439929228813265E-7
const F1 = 5.99832206555887937690E-1
const F2 = 1.36929880922735805310E-1
const F3 = 1.48753612908506148525E-2
const F4 = 7.86869131145613259100E-4
const F5 = 1.84631831751005468180E-5
const F6 = 1.42151175831644588870E-7
const F7 = 2.04426310338993978564E-15

#InverseCdf computes the inverse cumulative normal distribution value using AS241 algorithm
function norminvAS241(p::Float64)::Float64
    #    DOUBLE PRECISION FUNCTION PPND16 (P, IFAULT)
    # ALGORITHM AS241  APPL. STATIST. (1988) VOL. 37, NO. 3
    # Produces the normal deviate Z corresponding to a given lower
    #   tail area of P Z is accurate to about 1 part in 10**16.
    #
    #   The hash sums below are the sums of the mantissas of the
    #   coefficients.   They are included for use in checking
    #  transcription.

    q = 0.0
    r = 0.0
    ppnd16 = 0.0

    q = p - 0.5
    if abs(q) <= SPLIT1
        r = CONST1 - q * q
        ppnd16 =
            q * (
                ((((((A7 * r + A6) * r + A5) * r + A4) * r + A3) * r + A2) * r + A1) * r +
                A0
            ) /
            (((((((B7 * r + B6) * r + B5) * r + B4) * r + B3) * r + B2) * r + B1) * r + 1.0)
        return ppnd16
    else
        if q < 0
            r = p
        else
            r = 1.0 - p
        end

        if r <= 0
            if p == 0.0
                ppnd16 = typemin(Float64)
            elseif p == 1.0
                ppnd16 = typemax(Float64)
            else
                ppnd16 = NaN
            end
            return ppnd16
        end

        r = sqrt(-log(r))
        if r <= SPLIT2
            r = r - CONST2
            ppnd16 =
                (
                    ((((((C7 * r + C6) * r + C5) * r + C4) * r + C3) * r + C2) * r + C1) *
                    r + C0
                ) / (
                    ((((((D7 * r + D6) * r + D5) * r + D4) * r + D3) * r + D2) * r + D1) *
                    r + 1.0
                )
        else
            r = r - SPLIT2
            ppnd16 =
                (
                    ((((((E7 * r + E6) * r + E5) * r + E4) * r + E3) * r + E2) * r + E1) *
                    r + E0
                ) / (
                    ((((((F7 * r + F6) * r + F5) * r + F4) * r + F3) * r + F2) * r + F1) *
                    r + 1.0
                )
        end
        return q < 0 ? -ppnd16 : ppnd16
    end
end

const BSMS_A0 = 2.5066282186077179315
const BSMS_A1 = -18.700226003845536893
const BSMS_A2 = 41.841057826285191701
const BSMS_A3 = -25.965959614983341521

const BSMS_B0 = -8.5075119518749713281
const BSMS_B1 = 23.298480931332967694
const BSMS_B2 = -21.419275595427556433
const BSMS_B3 = 3.2269921657498348465

const BSMS_C0 = 0.3374754822726147
const BSMS_C1 = 0.9761690190917186
const BSMS_C2 = 0.1607979714918209
const BSMS_C3 = 0.0276438810333863
const BSMS_C4 = 0.0038405729373609
const BSMS_C5 = 0.0003951896511919
const BSMS_C6 = 0.0000321767881768
const BSMS_C7 = 0.0000002888167364
const BSMS_C8 = 0.0000003960315187

function norminvBSMS(x::Float64)::Float64
    temp = x - 0.5
    local result
    if abs(temp) < 0.42
        result = temp*temp
        result =
            temp * (((BSMS_A3 * result + BSMS_A2) * result + BSMS_A1) * result + BSMS_A0) /
            (
                (((BSMS_B3 * result + BSMS_B2) * result + BSMS_B1) * result + BSMS_B0) *
                result + 1.0
            )
    else
        if (x < 0.5)
            result = x
        else
            result = 1.0 - x
        end
            result = log(-log(result))
            result =
                BSMS_C0 +
                result * (
                    BSMS_C1 +
                    result * (
                        BSMS_C2 +
                        result * (
                            BSMS_C3 +
                            result * (
                                BSMS_C4 +
                                result * (
                                    BSMS_C5 +
                                    result *
                                    (BSMS_C6 + result * (BSMS_C7 + result * BSMS_C8))
                                )
                            )
                        )
                    )
                )
        if (x < 0.5)
            result = -result
        end
    end

    return result
end


import Optim

# Transformation from C to R
split_(x)  = x[1:Int(length(x)/2)],x[Int(length(x)/2) + 1:end]

"BFGS using MoreThuente"
function optim( z, alpha, c, lb=nothing; linesearch = Optim.LineSearches.MoreThuente() )
    function grad!(stor,x)
        stor[:] = vcat(Fgradient(split_(x)..., c)...)
    end

    result = Optim.optimize( x->residual(split_(x)...,c),
                             grad!,
                             vcat(z, alpha),
                             Optim.BFGS(linesearch = linesearch), Optim.Options(
                                iterations = 14000,
                                allow_f_increases = true))

    return split_(Optim.minimizer(result))
end


# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/FEMQuad.jl/blob/master/LICENSE

using FEMQuad, Test
using FEMQuad: integrate_3d, get_rule

@testset "Gauss-Legendre quadratures in tetrahedrons" begin
    rules = (:GLTET1, :GLTET4, :GLTET5, :GLTET15)
    f(i) = x -> sum(sum(x)^j for j=0:i)
    r(i::Int) = get_rule(i, rules...)

    @test isapprox(integrate_3d(f(0), r(0)), 1/6)
    @test isapprox(integrate_3d(f(1), r(1)), 7/24)
    @test isapprox(integrate_3d(f(2), r(2)), 47/120)
    @test isapprox(integrate_3d(f(3), r(3)), 19/40)
    @test isapprox(integrate_3d(f(4), r(4)), 153/280)
    @test isapprox(integrate_3d(f(5), r(5)), 341/560)
    #@test isapprox(integrate_3d(f(6), r(6)), 3349/5040)
    #@test isapprox(integrate_3d(f(7), r(7)), 3601/5040)
    #@test isapprox(integrate_3d(f(8), r(8)), 42131/55440)
    #@test isapprox(integrate_3d(f(9), r(9)), 44441/55440)
end


# Model: Anand, McFarland, Kihm, Wong (Optimization of Aerosol Penetration through Transport Lines, 1992)
# Based on: straightTubePenetrationAnand1992.m

if "./src" in LOAD_PATH
    # print("in src")
else
    dir = "./src"
    push!(LOAD_PATH,dir)
    # print("not in src")
end

module bronchiole

import physicalConstants: k, g, p, MM

struct inclinedTube
    # CarrierFluidProfile
    T :: Float64          # Carrier fluid temperature (T) SI units: K
    ro :: Float64         # Carrier fluid density (ro) SI units: kg m-3
    Q :: Float64          # Carrier fluid flow rate (Q) SI units: m3 s-1
    mu :: Float64         # Carrier fluid dynamic viscosity (mu) SI units: N s m-2

    # Droplet
    ro_d :: Float64         # Droplet density (ro_d) SI units: kg m-3
    # mu_d :: Float64       # Droplet dynamic viscosity (mu_d) SI units: N s m-2
    d_d :: Float64          # Droplet diameter (dd) SI units: m
    md :: Float64           # Droplet mass (md) SI units: kg

    # Tube
    d :: Float64            # Tube inner diameter (d) SI units: m
    phi :: Float64          # Inclination angle in radians (phi) SI units: rad
    L :: Float64            # Distance from the tube inlet (L) SI units: m

    # Velocities
    Vb :: Float64           # Brownian diffusion velocity (Vb) SI units: m s-1
    U :: Float64            # Fluid flow mean velocity (U) SI units: m s-1
    Vf :: Float64           # Friction velocity (Vf) SI units: m s-1
    Vs :: Float64           # Dimensionless depositional velocity (Vs)
    Vr :: Float64           # Dimensionless depositional velocity (Vr)
    Vt :: Float64           # Turbulent diffusional velocity (Vt)
    Vg :: Float64           # Gravitational settling velocity (Vg)
    Vd :: Float64           # Sum of Vt and Vb is defined as Vd (Vd)
    Ve :: Float64           # Ve is the effective depositional velocity, given by the vector sum of velocities due to turbulent diffusion (Vt)

    # FluidProperties
    Re :: Float64           # Reynolds number (Re)
    f :: Float64            # Fanning friction factor, Blasius equation for turbulent pipe flow (f) dimensionless
    lambda :: Float64       # Model for the mean free path of the carrier fluid (lambda)
    C :: Float64            # Cunningham slip correction (C)
    tau :: Float64          # Droplet relaxation time (tau)

    # Misc.
    extrapl :: Integer      # Flag to test crossed boundaries
    Rplus :: Float64        # Parameter Rplus (Rplus)
    ParamS :: Float64       # Parameter S (ParamS)
    Splus :: Float64        # Parameter Splus (Splus)
    TETAc :: Float64        # Critical Angle (TETAc)

    # Penetration
    P :: Float64            # P = penetration = C/C0 = ratio between concentration at distance x and initial concentration

    function inclinedTube(T, ro, Q, mu, ro_d, d_d, d, phi, L)
        extrapl = 0
        K = 5e-5;

        md = ro_d*(4/3)*(pi*(d_d/2)^3)          # Droplet mass (md) SI units: kg
        Vb = sqrt((k*T)/(2*pi*md))              # Brownian diffusion velocity (Vb) SI units: m s-1
        U=4*Q/(pi*d^2)                          # Fluid flow mean velocity (U) SI units: m s-1
        Re=(U*d*ro)/mu                          # Reynolds number (Re)
        f=0.316/(4*Re^0.25)                     # Fanning friction factor, Blasius equation for turbulent pipe flow (f) dimensionless

        if (Re .< 3000 || Re .> 100000)
            extrapl=extrapl+1
        end

        Vf = U*sqrt(f/2)                        # Friction velocity (Vf) SI units: m s-1
        lambda = (mu/p)*sqrt((pi*k*T)/(2*MM))
        C = 1 + (lambda/d_d)*(2.34+1.05*exp(-.39*d_d/lambda))    # Cunningham slip correction (C)
        tau = (C*ro_d*d_d^2)/(18*mu)            # Droplet relaxation time (tau)
        Rplus = (d_d*Vf*ro)/(2*mu)              # Parameter Rplus (Rplus)
        ParamS = 0.9*tau*Vf + d_d/2             # Parameter S (ParamS)
        Splus = (ParamS*Vf*ro)/(mu)             # Parameter Splus (Splus)

        if (Splus >= 0 && Splus <= 10)
            Vs = 0.05*Splus
        else
            Vs = 0.5 + 0.0125*(Splus - 10)
            extrapl=extrapl+1
        end

        if (Rplus >= 0 && Rplus <= 10)
            Vr = 0.05*Rplus
        else
            Vr = 0.5 + 0.0125*(Rplus - 10)
            extrapl=extrapl+1
        end

        Vt = (Vf*(Vs+Vr))/(4)                  # Turbulent diffusional velocity (Vt)
        Vg = tau*g*cos(phi)                    # Gravitational settling velocity (Vg)
        Vd = Vt + Vb*K                         # Sum of Vt and Vb is defined as Vd (Vd)

        if (Vd>Vg)                           # If Vd>Vg, there is no need to use TETAc, since the integral that originated the Ve equation can be evaluated from 0 to 2pi. Also, arcsine(x) for x>1 is not defined in the real domain, therefore, TETAc would be imaginary.
            Ve=Vd
            print("\n Vd > Vg")
        else
            TETAc = asin(Vd/Vg)                    # Critical angle (TETAc)
            Ve=Vd*TETAc/pi + Vd/2 + Vg*cos(TETAc)/pi        # Ve is the effective depositional velocity, given by the vector sum of velocities due to turbulent diffusion (Vt),
            print("\n Vd < Vg")
        end

        # print(Re)

        print(Ve)

        P = exp(-(pi*d*Ve*L)/(Q))             # P = penetration = C/C0 = ratio between concentration at distance x and initial concentration

        # results = [md, Vb, U, Re, f, extrapl, Vf, lambda, tau, Rplus, ParamS, Splus, Vs, Vr, Vt, Vg, Vd, TETAc, Ve, P]
        # labels = ["md", "Vb", "U", "Re", "f", "extrapl", "Vf", "lambda", "tau", "Rplus", "ParamS", "Splus", "Vs", "Vr", "Vt", "Vg", "Vd", "TETAc", "Ve", "P"]

        # for i in 1:20
        #     print(labels[i], ": ", results[i], '\n')
        # end

        # results = [md, Vb, U, Re, f, extrapl, Vf, lambda, tau, Rplus, ParamS, Splus, Vs, Vr, Vt, Vg, Vd, Ve, P]
        # labels = ["md", "Vb", "U", "Re", "f", "extrapl", "Vf", "lambda", "tau", "Rplus", "ParamS", "Splus", "Vs", "Vr", "Vt", "Vg", "Vd", "Ve", "P"]

        # for i in 1:19
        #     print(labels[i], ": ", results[i], '\n')
        # end

    end

end

dataset1 = [5.4444E-07, 5.8472E-07, 6.2656E-07, 6.6727E-07, 7.2656E-07, 7.6727E-07, 8.3492E-07, 9.0000E-07, 9.6627E-07, 1.0557E-06, 1.1621E-06, 1.2616E-06, 1.3686E-06, 1.4746E-06, 1.5741E-06, 1.6736E-06, 1.7828E-06, 1.8863E-06, 1.9858E-06, 2.1567E-06, 2.3432E-06, 2.5136E-06, 2.6939E-06, 2.8643E-06, 3.0672E-06, 3.3113E-06, 3.5555E-06, 3.8230E-06]
# d_d = 5.4444E-07

# straightTube(T, ro, Q, mu, ro_d, d_d, d, phi, L)
T = 309.5 # Temperature of Air
ro = 1.12027 # Density of Air (http://www.anaesthesia.med.usyd.edu.au/resources/lectures/humidity_clt/humidity.html#:~:text=While%20nose%20breathing%20at%20rest,%25%20to%2070%25%20relative%20humidity.)
# Q = 0.001 # Based on 60L/min flowrate of ventilators
# Q = 1e-4 # Based on 6L/min of average adult male (Guyton and Hall Textbook of Medical Physiology 12th Edition)
Q = 0.00050038/2 # Chapter 3
mu = 1.895e-5 # Dynamic viscosity of air (https://www.engineersedge.com/physics/viscosity_of_air_dynamic_and_kinematic_14483.htm#:~:text=The%20viscosity%20of%20air%20depends,10%2D5%20Pa%C2%B7s%20.&text=At%2025%20%C2%B0C%2C%20the,the%20kinematic%20viscosity%2015.7%20cSt.)
ro_d = 1000 # Droplet density of water
d = 0.018 # Diameter of first inclined tube
# phi = (pi/2) # Inclination angle in radians (phi) SI units: rad
IBA = 1.040216 # Interbronchial angle, average for all ages, 59.6 degrees SI Units: radians Haskin Goodman et. al. "Normal Tracheal Bifurcation Angle"
phi = pi-(pi/2)-(IBA/2) # Inclination angle in radians (phi) SI units: rad
L = 0.118 # Avg length of trachea (https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5900092/#:~:text=On%20average%2C%20the%20length%20of,to%20be%20shorter%20in%20females.&text=Structure%20of%20the%20trachea.)

post_inclinedTube0 = []

for d_d = dataset1
    penetration = inclinedTube(T, ro, Q, mu, ro_d, d_d, d, phi, L)
    push!(post_inclinedTube0, penetration)
end

print(post_inclinedTube0)

# print(inclinedTube(T, ro, Q, mu, ro_d, d_d, d, phi, L))

# Dummy values
# straightTube(309.5, 1, 2, 3, 4, 5, 6, 7, 8)

end