### A Pluto.jl notebook ###
# v0.18.4

using Markdown
using InteractiveUtils

# ╔═╡ 0cde3f18-6b7e-4c96-a09e-b5419d481053
using AbstractPlutoDingetjes, HypertextLiteral, Parameters, PlutoDevMacros, Deno_jll

# ╔═╡ 83e7541d-1646-4071-a29a-45a71453b5d6
@only_in_nb using PlutoUI

# ╔═╡ 5ee4e18c-8a03-41c8-ab2c-2c3ff430f5f8
@only_in_nb PlutoUI.TableOfContents()

# ╔═╡ 652695a3-5c8b-4cea-b79c-fe1192df8527
md"# Ingredients"

# ╔═╡ 01e347d4-a998-476a-a549-bbd591330439
@plutoinclude "./d3_abstractions.jl" "all"

# ╔═╡ 9df4facb-6862-4898-aa04-ae944c94969f
md"# Context"

# ╔═╡ 6fdebb9a-3053-4699-bd3b-6fa4c43c3283
md"## Structure"

# ╔═╡ 82090d2a-bc09-4f5e-8377-3452fdaeca46
begin
	Axis = @NamedTuple{domain::Vector{<:Number}, range::Vector{<:Number}}
	struct Context <: D3Component
		xAxis 	   :: Axis
		yAxis 	   :: Axis
		children   :: Vector{<:D3Component}
		attributes :: D3Attr
		embedX     :: Tuple{Float64, Float64}
		embedY     :: Tuple{Float64, Float64}
		id         :: String
		drawAxis   :: Bool
	end

	# TODO: fix typing
	Context(xAxis, yAxis, children::Vector{<:D3Component}, id; attributes=D3Attr(), embedX=(0.0, 1.0), embedY=(0.0, 1.0), drawAxis=true) =
		Context(xAxis, yAxis, children, attributes, embedX, embedY, id, drawAxis)
end;

# ╔═╡ 4557ccac-50fa-448b-b1a8-10c2196d7083
begin
	Scale(ctx, embedX, embedY, id; attributes=D3Attr()) = Context(ctx.xAxis, ctx.yAxis, [ctx], attributes, embedX, embedY, id, ctx.drawAxis)
end

# ╔═╡ 2d9e6f15-1fb5-4fce-b655-93591e8491bb
Restyle(ctx, attributes, id) = Context(ctx.xAxis, ctx.yAxis, ctx.children, attributes, ctx.embedX, ctx.embedY, id, ctx.drawAxis)

# ╔═╡ 33629ee8-2e0b-44ee-99ba-0b942f3edb0c
RestyleOver(ctx, attributes, id) = Context(ctx.xAxis, ctx.yAxis, [ctx], attributes, ctx.embedX, ctx.embedY, id, ctx.drawAxis)

# ╔═╡ 2bc3ddd7-1a16-4dcc-a029-555697987f17
begin
function Base.extrema(children::Vector{<:D3Component})
	extremas = (Base.extrema).(children)
	minxs = (v -> v.minx).(extremas)
	minys = (v -> v.miny).(extremas)
	maxxs = (v -> v.maxx).(extremas)
	maxys = (v -> v.maxy).(extremas)

	return (
		minx=minimum(minxs),
		miny=minimum(minys),
		maxx=maximum(maxxs),
		maxy=maximum(maxys)
	)
end
end

# ╔═╡ 28c2fbc1-fa21-4ace-86c8-d085f19af466
md"## Javascript Snippet"

# ╔═╡ baa7bf03-77f8-4fa1-9635-df02629cfe01
function Base.show(io::IO, m::MIME"text/javascript", ctx::Context)
show(io, m, @js("""
	context(
		ctx,
		x_scale,
		$(PublishToJS(ctx.xAxis.domain)),
		$(PublishToJS(ctx.embedX)),
		y_scale,
		$(PublishToJS(ctx.yAxis.domain)),
		$(PublishToJS(ctx.embedY)),
		$(PublishToJS(to_named_tuple(ctx.attributes))),
		$(ctx.children),
		$(ctx.id),
		$(ctx.drawAxis)
	)
"""))
end

# ╔═╡ 2b0b5165-776a-4d1f-9151-19465d75124c
Base.show(io::IO, m::MIME"text/html", ctx::Context) =
	show(io, m, @htl("""
	<span id=$(ctx.id)>
	<script id="context-$(ctx.id)">

	const {
		d3,
  		context,
  		context_standalone,
  		area, line, shape,
  		area_standalone, line_standalone, shape_standalone
	} = $(import_local_js(bundle_code));

	const svg = this == null ? DOM.svg($(maximum(ctx.xAxis.range)), $(maximum(ctx.yAxis.range))) : this;
	const s = this == null ? d3.select(svg) : this.s;

	context_standalone(
		s,
		$(PublishToJS(ctx.xAxis.domain)),
		$(PublishToJS(ctx.xAxis.range)),
		$(PublishToJS(ctx.yAxis.domain)),
		$(PublishToJS(ctx.yAxis.range)),
		$(PublishToJS(to_named_tuple(ctx.attributes))),
		$(ctx.children),
		$(ctx.id),
		$(ctx.drawAxis)
	)

	const output = svg
	output.s = s
	return output
	</script>
	</span>
"""))

# ╔═╡ 00000000-0000-0000-0000-000000000001
PLUTO_PROJECT_TOML_CONTENTS = """
[deps]
AbstractPlutoDingetjes = "6e696c72-6542-2067-7265-42206c756150"
Deno_jll = "04572ae6-984a-583e-9378-9577a1c2574d"
HypertextLiteral = "ac1192a8-f4b3-4bfe-ba22-af5b92cd3ab2"
Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a"
PlutoDevMacros = "a0499f29-c39b-4c5c-807c-88074221b949"
PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8"

[compat]
AbstractPlutoDingetjes = "~1.1.4"
Deno_jll = "~1.20.4"
HypertextLiteral = "~0.9.3"
Parameters = "~0.12.3"
PlutoDevMacros = "~0.4.5"
PlutoUI = "~0.7.38"
"""

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

julia_version = "1.7.2"
manifest_format = "2.0"

[[deps.AbstractPlutoDingetjes]]
deps = ["Pkg"]
git-tree-sha1 = "8eaf9f1b4921132a4cff3f36a1d9ba923b14a481"
uuid = "6e696c72-6542-2067-7265-42206c756150"
version = "1.1.4"

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

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

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

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

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

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

[[deps.Deno_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
git-tree-sha1 = "970da1e64a94f13b51c81691c376a1d5a83a0b3c"
uuid = "04572ae6-984a-583e-9378-9577a1c2574d"
version = "1.20.4+0"

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

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

[[deps.Hyperscript]]
deps = ["Test"]
git-tree-sha1 = "8d511d5b81240fc8e6802386302675bdf47737b9"
uuid = "47d2ed2b-36de-50cf-bf87-49c2cf4b8b91"
version = "0.0.4"

[[deps.HypertextLiteral]]
git-tree-sha1 = "2b078b5a615c6c0396c77810d92ee8c6f470d238"
uuid = "ac1192a8-f4b3-4bfe-ba22-af5b92cd3ab2"
version = "0.9.3"

[[deps.IOCapture]]
deps = ["Logging", "Random"]
git-tree-sha1 = "f7be53659ab06ddc986428d3a9dcc95f6fa6705a"
uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89"
version = "0.2.2"

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

[[deps.JLLWrappers]]
deps = ["Preferences"]
git-tree-sha1 = "abc9885a7ca2052a736a600f7fa66209f96506e1"
uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
version = "1.4.1"

[[deps.JSON]]
deps = ["Dates", "Mmap", "Parsers", "Unicode"]
git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "0.21.3"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

[[deps.OrderedCollections]]
git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c"
uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
version = "1.4.1"

[[deps.Parameters]]
deps = ["OrderedCollections", "UnPack"]
git-tree-sha1 = "34c0e9ad262e5f7fc75b10a9952ca7692cfc5fbe"
uuid = "d96e819e-fc66-5662-9728-84c9c7592b0a"
version = "0.12.3"

[[deps.Parsers]]
deps = ["Dates"]
git-tree-sha1 = "1285416549ccfcdf0c50d4997a94331e88d68413"
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
version = "2.3.1"

[[deps.Pkg]]
deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"]
uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"

[[deps.PlutoDevMacros]]
deps = ["MacroTools", "Requires"]
git-tree-sha1 = "994167def8f46d3be21783a76705228430e29632"
uuid = "a0499f29-c39b-4c5c-807c-88074221b949"
version = "0.4.5"

[[deps.PlutoUI]]
deps = ["AbstractPlutoDingetjes", "Base64", "ColorTypes", "Dates", "Hyperscript", "HypertextLiteral", "IOCapture", "InteractiveUtils", "JSON", "Logging", "Markdown", "Random", "Reexport", "UUIDs"]
git-tree-sha1 = "670e559e5c8e191ded66fa9ea89c97f10376bb4c"
uuid = "7f904dfe-b85e-4ff6-b463-dae2292396a8"
version = "0.7.38"

[[deps.Preferences]]
deps = ["TOML"]
git-tree-sha1 = "d3538e7f8a790dc8903519090857ef8e1283eecd"
uuid = "21216c6a-2e73-6563-6e65-726566657250"
version = "1.2.5"

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

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

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

[[deps.Reexport]]
git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b"
uuid = "189a3867-3050-52da-a836-e630ba90ab69"
version = "1.2.2"

[[deps.Requires]]
deps = ["UUIDs"]
git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7"
uuid = "ae029012-a4dd-5104-9daa-d747884805df"
version = "1.3.0"

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

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

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

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

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

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

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

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

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

[[deps.UnPack]]
git-tree-sha1 = "387c1f73762231e86e0c9c5443ce3b4a0a9a0c2b"
uuid = "3a884ed6-31ef-47d7-9d2a-63182c4928ed"
version = "1.0.2"

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

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

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

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

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

# ╔═╡ Cell order:
# ╟─5ee4e18c-8a03-41c8-ab2c-2c3ff430f5f8
# ╠═83e7541d-1646-4071-a29a-45a71453b5d6
# ╟─652695a3-5c8b-4cea-b79c-fe1192df8527
# ╠═0cde3f18-6b7e-4c96-a09e-b5419d481053
# ╠═01e347d4-a998-476a-a549-bbd591330439
# ╟─9df4facb-6862-4898-aa04-ae944c94969f
# ╟─6fdebb9a-3053-4699-bd3b-6fa4c43c3283
# ╠═82090d2a-bc09-4f5e-8377-3452fdaeca46
# ╠═4557ccac-50fa-448b-b1a8-10c2196d7083
# ╠═2d9e6f15-1fb5-4fce-b655-93591e8491bb
# ╠═33629ee8-2e0b-44ee-99ba-0b942f3edb0c
# ╠═2bc3ddd7-1a16-4dcc-a029-555697987f17
# ╟─28c2fbc1-fa21-4ace-86c8-d085f19af466
# ╠═baa7bf03-77f8-4fa1-9635-df02629cfe01
# ╠═2b0b5165-776a-4d1f-9151-19465d75124c
# ╟─00000000-0000-0000-0000-000000000001
# ╟─00000000-0000-0000-0000-000000000002


using ContinuousBenchmarks, FileIO, JLD2

# const ldastandata = [
# Dict(
#   "K" => 2,
#   "V" => 5,
#   "M" => 25,
#   "N" => 262,
#   "w" => [4, 3, 5, 4, 3, 3, 3, 3, 3, 4, 5, 3, 4, 4, 5,
# 3, 4, 4, 4, 3, 5, 4, 5, 2, 3, 3, 1, 5, 5, 1, 4,
# 3, 1, 2, 5, 4, 4, 3, 5, 4, 2, 4, 5, 3, 4, 1, 4,
# 4, 3, 2, 1, 2, 1, 2, 2, 2, 1, 2, 2, 3, 1, 2, 2,
# 4, 4, 5, 4, 5, 5, 4, 3, 5, 4, 4, 4, 2, 2, 1, 1,
# 2, 1, 3, 1, 2, 1, 1, 1, 3, 2, 3, 3, 5, 4, 5, 4,
# 3, 5, 4, 2, 2, 2, 1, 3, 2, 1, 3, 1, 3, 1, 1, 2,
# 1, 2, 2, 4, 4, 4, 5, 5, 4, 4, 5, 4, 3, 3, 3, 1,
# 3, 3, 4, 2, 1, 3, 4, 4, 5, 4, 4, 4, 3, 4, 3, 4,
# 5, 1, 2, 1, 3, 2, 1, 1, 2, 3, 3, 3, 3, 4, 1, 4,
# 4, 4, 4, 3, 4, 4, 1, 2, 2, 3, 3, 1, 1, 4, 1, 3,
# 1, 5, 3, 2, 2, 1, 1, 2, 3, 3, 4, 4, 5, 3, 4, 3,
# 1, 5, 5, 5, 3, 3, 4, 5, 3, 3, 3, 2, 3, 1, 3, 3,
# 1, 3, 1, 5, 5, 5, 2, 2, 3, 3, 3, 1, 1, 5, 5, 5,
# 3, 1, 5, 4, 1, 3, 3, 3, 3, 4, 2, 5, 1, 3, 5, 2,
# 5, 5, 2, 1, 3, 3, 5, 3, 5, 3, 3, 5, 1, 2, 2, 1,
# 1, 2, 1, 2, 3, 1, 1],
#   "doc" => [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2,
# 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
# 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4,
# 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
# 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 8, 8, 8, 8,
# 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9,
# 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
# 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 12,
# 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14,
# 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15,
# 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16,
# 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17,
# 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19,
# 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20,
# 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
# 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23,
# 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24,
# 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
# 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25],
#   "alpha" => [0.5, 0.5],
#   "beta" => [0.2, 0.2, 0.2, 0.2, 0.2]
# )
# ]

const ldastandata = load(joinpath(ContinuousBenchmarks.STAN_DATA_DIR, "ldastandata.jld2"))["data"]
# const ldastandata = load(joinpath(ContinuousBenchmarks.STAN_DATA_DIR, "ldastandataV20K2M25L100.data"))["data"]
# const ldastandata = load(joinpath(ContinuousBenchmarks.STAN_DATA_DIR, "ldastandataV100K5M10L1000.data"))["data"]

# using PyCall
# @pyimport pickle
# f = open("/afs/inf.ed.ac.uk/user/s16/s1672897/fun/20news-sub.pickle", "r")
# data = pickle.loads(pybytes(read(f)))
# const ldastandata = [data]


@testset "zp" begin
    @test iszero(ERFA._zp(randn(3)))
    @test iszero(fill!(randn(3), 0.0))
end

@testset "zpv" begin
    pv1 = [randn(3), randn(3)]
    pv2 = [randn(3), randn(3)]
    @test iszero(ERFA._zpv(pv1))
    @test iszero(fill!.(pv2, 0.0))
end

@testset "zr" begin
    r1 = randn(3, 3)
    r2 = randn(3, 3)
    @test iszero(ERFA._zr(r1))
    @test iszero(fill!(r2, 0.0))
end


module ApplyNoise

using Random
using ImageCore
using ImageCore.MappedArrays
using ImageCore: NumberLike, GenericGrayImage, GenericImage

import ..NoiseAPI: AbstractImageNoise, apply_noise, apply_noise!
import Base: show

# avoid InexactError for Bool array input by promoting it to float
apply_noise(img::AbstractArray{T},
            n::AbstractImageNoise,
            args...; kargs...) where T<:Union{Bool, Gray{Bool}} =
    apply_noise(of_eltype(floattype(eltype(img)), img), n, args...; kargs...)

include("AdditiveWhiteGaussianNoise.jl")

export
    apply_noise, apply_noise!,

    AdditiveWhiteGaussianNoise

end # end module


#!/usr/bin/env julia
using ProgressMeter
using ArgParse

using RealNeuralNetworks
using RealNeuralNetworks.SWCs
using RealNeuralNetworks.Neurons

function parse_commandline()
    s = ArgParseSettings()
    @add_arg_table s begin
        "--inputdir", "-i"
            help = "the input directory to store swc files"
            arg_type = String
            default = "/tmp"
        "--outputpath", "-o"
            help = "the output directory to store binary swc files"
            arg_type = String
            default = "/tmp"
        "--resample-node-distance", "-d"
            help = "mip level of the dataset"
            arg_type = Float32
            default = Float32(100)
    end
    return parse_args(s)
end

function main()
    args = parse_commandline()

    @showprogress 1 "postprocessing..." for fileName in readdir(args["inputdir"])
        fullName = joinpath(expanduser(args["inputdir"]), fileName)
        if endswith(fileName, "swc.bin")
            neuron = Neurons.load_swc_bin(fullName)
        elseif endswith(fileName, ".swc")
            neuron = Neurons.load_swc(fullName)
        elseif isdir(fullName)
            warn("this is a directory: $(fileName)")
        else
            warn("unsupported file format: $(fileName)")
            continue
        end
        neuron = Neurons.remove_hair(neuron)
        neuron = Neurons.remove_redundent_nodes(neuron)
        neuron = Neurons.remove_subtree_in_soma(neuron)
        neuron = Neurons.remove_terminal_blobs(neuron)
        neuron = Neurons.resample(neuron, args["resample-node-distance"])

        # output file
        outputName = joinpath(expanduser(args["outputpath"]), fileName)
        if !isdir(dirname(outputName))
            mkdir(dirname(outputName))
        end
        Neurons.save_swc_bin(neuron, outputName)
    end
end

main()


@testset "Testing Example 1" begin
    local m
    makemap = true
    placement = true
    routing = true
    # Try making a map
    m = Example1.make_map()

    # Try placement - use the Search Move Generator to give it a go.
    place!(m; move_attempts = 5000)

    # Try routing
    route!(m)

    # Get statistics from the map
    MapperCore.report_routing_stats(m)

    expected_links = 12
    hist = MapperCore.global_link_histogram(m)
    # Get the number of global links from this
    found_links = 0
    for (k, v) in hist
        found_links += k * v
    end
    @test found_links == expected_links
    @test MapperCore.check_routing(m)
end

@testset "Testing Example 2" begin
    # Try making a map
    m = Example2.make_map()

    # Try placement
    place!(m)

    # Try routing
    route!(m)

    # Get statistics from the map
    MapperCore.report_routing_stats(m)
    # expect routing to fail because of inadequate resources.
    @test MapperCore.check_routing(m) == false
end


type Tokenizer
    dict::IdDict
    model
end

function Tokenizer()
    dict = IdDict(String["UNKNOWN", " ", "LF"])
    T = Float32
    embed = Embedding(T, 100, 10)
    ls = [Linear(T,70,70), Linear(T,70,4)]
    #=
    g = @graph begin
        chars = identity(:chars)
        x = Var(reshape(chars,1,length(chars)))
        x = embed(x)
        x = window2d(x,10,7,1,1,0,3)
        x = ls[1](x)
        x = relu(x)
        x = ls[2](x)
        x
    end
    =#
    Tokenizer(dict, g)
end

function (t::Tokenizer)(chars::Vector{Char})
    unk = t.dict["UNKNOWN"]
    x = map(chars) do c
        str = c == '\n' ? "LF" : string(c)
        get(t.dict, str, unk)
    end
    y = t.model(x).data
    tags = argmax(y,1)
    decode(chars, tags)
end
(t::Tokenizer)(str::String) = t(Vector{Char}(str))

function decode(chars::Vector{Char}, tags::Vector{Int})
    length(chars) == length(tags) || throw("Length unmatch")
    doc = Vector{String}[]
    tokens = String[]
    buffer = Char[]
    pos = 1
    function push_buffer!(i::Int)
        push!(buffer, chars[i])
    end
    function push_token!()
        length(buffer) == 0 && return
        form = String(buffer)
        #t = Token(pos=pos, form=form)
        push!(tokens, form)
        pos += length(buffer) + 1
        buffer = Char[]
    end
    function push_tokens!()
        length(tokens) == 0 && return
        push!(doc, tokens)
        tokens = String[]
    end

    for i = 1:length(chars)
        t = tags[i]
        if t == 1
            push_buffer!(i)
        elseif t == 2
            continue
        elseif t == 3
            push_buffer!(i)
            push_token!()
        elseif t == 4
            push_buffer!(i)
            push_token!()
            push_tokens!()
        else
            error("Invalid tag: $(t)")
        end
    end
    push_token!()
    push_tokens!()
    doc
end


function get_qzss_orbital_elements(obj::QZSSPropagator)
    return jcall(obj, "getQZSSOrbitalElements", QZSSOrbitalElements, ())
end


using VLModelParametersDB

# execute -
biophysical_dictionary = get_biophysical_parameters_dictionary_for_system(:CF_TXTL)


@testset "Testing MBTR Representation with dc Si" begin
@info("Testing MBTR Representation for dc Si.")
using DescriptorZoo, JuLIP, Test

at = bulk(:Si, cubic=true)
desc = mbtr(at)
mbtr_ref = [
       6.9496778788414781e-01,
       7.1986220284027114e-01,
       7.3354857103049786e-01,
       7.3666491362136577e-01,
       7.3044762991316925e-01,
       7.1649441177128370e-01,
       6.9649901670096226e-01,
       6.7200606559977982e-01,
       6.4422928693791326e-01,
       6.1396345774896632e-01,
       5.8160117982024029e-01,
       5.4724434055754667e-01,
       5.1088099007051713e-01,
       4.7258543971849859e-01,
       4.3269540091997261e-01,
       3.9192569929718940e-01,
       3.5139206883965507e-01,
       3.1253747322315001e-01,
       2.7697310097245148e-01,
       2.4626260205440162e-01,
       2.2168837199196242e-01,
       2.0404146122179259e-01,
       1.9347226369615067e-01,
       1.8942888993870177e-01,
       1.9069597290259666e-01,
       1.9553067571537033e-01,
       2.0187700957831695e-01,
       2.0762645100533930e-01,
       2.1088450453095892e-01,
       2.1020118590035677e-01,
       2.0472935788111615e-01,
       1.9428785263606121e-01,
       1.7932399339294011e-01,
       1.6078869816994643e-01,
       1.3995256578723689e-01,
       1.1819976052859571e-01,
       9.6836522223254493e-02,
       7.6943433911371095e-02,
       5.9287846045598072e-02,
       4.4298754123766174e-02,
       3.2094480531120281e-02,
       2.2546050707531993e-02,
       1.5356935355672306e-02,
       1.0142102152565563e-02,
       6.4943824952075910e-03,
       4.0321099048365632e-03,
       2.4272265052827208e-03,
       1.4166771385286556e-03,
       8.0170343488886913e-04,
       4.3988429175749678e-04,
       2.3401514207793466e-04,
       1.2070688477923311e-04,
       6.0367240455477009e-05,
       2.9271955306051653e-05,
       1.3762052062538477e-05,
       6.2732939120686559e-06,
       2.7726111754441095e-06,
       1.1881276847952497e-06,
       4.9364868828203077e-07,
       1.9886277542685130e-07,
       7.7672905494426746e-08,
       2.9414810799660235e-08,
       1.0800476431045606e-08,
       3.8450325381032670e-09,
       1.3272030624847988e-09,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       0.0000000000000000e+00,
       1.2398167472199218e-04,
       2.0466965823382139e-04,
       3.3061207241311385e-04,
       5.2173848485611489e-04,
       8.0094009760583779e-04,
       1.1899435089817198e-03,
       1.7035053236855724e-03,
       2.3441813321358261e-03,
       3.1009483013927528e-03,
       3.9545161634549318e-03,
       4.8899112139089628e-03,
       5.9133597846795141e-03,
       7.0668491984957685e-03,
       8.4314424400194384e-03,
       1.0111020521181869e-02,
       1.2193069865332386e-02,
       1.4692894495666737e-02,
       1.7499888137509644e-02,
       2.0353055031603211e-02,
       2.2869855093105729e-02,
       2.4633725130110831e-02,
       2.5317115164213068e-02,
       2.4793837213310951e-02,
       2.3192772531288913e-02,
       2.0868780277482388e-02,
       1.8303511496475900e-02,
       1.5977201757051088e-02,
       1.4257195930872905e-02,
       1.3331644622708305e-02,
       1.3192932812138475e-02,
       1.3660594425678267e-02,
       1.4431816164603270e-02,
       1.5151385256259876e-02,
       1.5492278430193351e-02,
       1.5231225266263989e-02,
       1.4297925443659779e-02,
       1.2780983788951015e-02,
       1.0889283804535254e-02,
       8.8861144078574011e-03,
       7.0233923526266361e-03,
       5.4988368908542662e-03,
       4.4434391768754886e-03,
       3.9297916162201411e-03,
       3.9832623543391305e-03,
       4.5816420042560837e-03,
       5.6419071831224138e-03,
       7.0071435486950030e-03,
       8.4531220907505273e-03,
       9.7269400243558211e-03,
       1.0611971687423156e-02,
       1.0994671163099179e-02,
       1.0902061669702406e-02,
       1.0489646076980672e-02,
       9.9821047964225289e-03,
       9.5903851746582964e-03,
       9.4377295304821206e-03,
       9.5215457809027897e-03,
       9.7228581543024953e-03,
       9.8573486302087846e-03,
       9.7472439130392233e-03,
       9.2859442661513743e-03,
       8.4708227519770026e-03,
       7.3939955443089889e-03,
       6.1999404901693931e-03,
       5.0325661627771412e-03,
       3.9953700625636581e-03,
       3.1374375752149155e-03,
       2.4631659427485192e-03,
       1.9536325377179468e-03,
       1.5863710138811391e-03,
       1.3457544477740399e-03,
       1.2231373935468047e-03,
       1.2104996217773907e-03,
       1.2926302382092784e-03,
       1.4420095041531046e-03,
       1.6188292358654966e-03,
       1.7766316270724227e-03,
       1.8719822636020650e-03,
       1.8748519980087389e-03,
       1.7757892555370336e-03,
       1.5870889798633835e-03,
       1.3376348109504447e-03,
       1.0636595744390671e-03,
       7.9902381402190408e-04,
       5.6822246901199369e-04,
       3.8366975535995037e-04,
       2.4690734482912038e-04,
       1.5212987305029596e-04,
       9.0171398219280604e-05,
       5.1629948778467158e-05,
       2.8628346186686757e-05,
       1.5372481389457191e-05,
       7.9716621968390962e-06,
       3.9724906391045570e-06,
       1.8907325837018450e-06,
       8.5416133369256520e-07,
       3.6420537297391663e-07,
       1.4588838652304635e-07,
       5.4698430242977022e-08,
       1.9143156223214137e-08,
]
mbtr_now = vcat(desc[:]...)
tst = [isapprox(mbtr_now[x], mbtr_ref[x]; atol=1e-8) for x=1:length(mbtr_ref)]
println(@test all(tst))
end


@doc doc"""
    MisclassLoss{R<:AbstractFloat} <: SupervisedLoss

Misclassification loss that assigns `1` for misclassified
examples and `0` otherwise. It is a generalization of
`ZeroOneLoss` for more than two classes.

The type parameter `R` specifies the result type of the
loss. Default type is double precision `R = Float64`.
"""
struct MisclassLoss{R<:AbstractFloat} <: SupervisedLoss end

MisclassLoss() = MisclassLoss{Float64}()

# specialize result type
result_type(loss::MisclassLoss{R}, t::Type, o::Type) where R = R

# return floating point to avoid big integers in aggregations
value(::MisclassLoss{R}, agreement::Bool) where R = ifelse(agreement, zero(R), one(R))
deriv(::MisclassLoss{R}, agreement::Bool) where R = zero(R)
deriv2(::MisclassLoss{R}, agreement::Bool) where R = zero(R)

value(loss::MisclassLoss, target, output) = value(loss, target == output)
deriv(loss::MisclassLoss, target, output) = deriv(loss, target == output)
deriv2(loss::MisclassLoss, target, output) = deriv2(loss, target == output)

# specialize aggregation methods (temporary workaround)
for FUN in (:value, :deriv, :deriv2)
    @eval begin
        # by default compute the element-wise result
        function ($FUN)(
                loss::MisclassLoss,
                targets::AbstractVector,
                outputs::AbstractVector)
            ($FUN)(loss, targets, outputs, AggMode.None())
        end

        function ($FUN)(
                loss::MisclassLoss,
                targets::AbstractVector,
                outputs::AbstractVector,
                ::AggMode.None)
            ($FUN).(loss, targets, outputs)
        end

        function ($FUN)(
                loss::MisclassLoss,
                targets::AbstractVector{Q},
                outputs::AbstractVector{T},
                ::AggMode.Sum) where {Q,T}
            out = zero(result_type(loss, Q, T))
            @inbounds @simd for I in CartesianIndices(size(outputs))
                out += ($FUN)(loss, targets[I], outputs[I])
            end
            out
        end

        function ($FUN)(
                loss::MisclassLoss,
                targets::AbstractVector{Q},
                outputs::AbstractVector{T},
                ::AggMode.Mean) where {Q,T}
            nrm = length(targets)
            out = zero(result_type(loss, Q, T))
            @inbounds @simd for I in CartesianIndices(size(outputs))
                out += ($FUN)(loss, targets[I], outputs[I])
            end
            out / nrm
        end

        function ($FUN)(
                loss::MisclassLoss,
                targets::AbstractVector{Q},
                outputs::AbstractVector{T},
                agg::AggMode.WeightedSum) where {Q,T}
            nrm = agg.normalize ? inv(sum(agg.weights)) : inv(one(sum(agg.weights)))
            out = zero(result_type(loss, Q, T)) * (agg.weights[1] * nrm)
            @inbounds @simd for I in CartesianIndices(size(outputs))
                out += ($FUN)(loss, targets[I], outputs[I]) * (agg.weights[I] * nrm)
            end
            out
        end

        function ($FUN)(
                loss::MisclassLoss,
                targets::AbstractVector{Q},
                outputs::AbstractVector{T},
                agg::AggMode.WeightedMean) where {Q,T}
            k   = length(outputs)
            nrm = agg.normalize ? inv(k * sum(agg.weights)) : inv(k * one(sum(agg.weights)))
            out = zero(result_type(loss, Q, T)) * (agg.weights[1] * nrm)
            @inbounds @simd for I in CartesianIndices(size(outputs))
                out += ($FUN)(loss, targets[I], outputs[I]) * (agg.weights[I] * nrm)
            end
            out
        end
    end
end

isminimizable(::MisclassLoss) = false
isdifferentiable(::MisclassLoss) = false
isdifferentiable(::MisclassLoss, at) = at != 0
istwicedifferentiable(::MisclassLoss) = false
istwicedifferentiable(::MisclassLoss, at) = at != 0
isnemitski(::MisclassLoss) = false
islipschitzcont(::MisclassLoss) = false
isconvex(::MisclassLoss) = false
isclasscalibrated(::MisclassLoss) = false
isclipable(::MisclassLoss) = false

# ===============================================================

@doc doc"""
    PoissonLoss <: SupervisedLoss

Loss under a Poisson noise distribution (KL-divergence)

``L(target, output) = exp(output) - target*output``
"""
struct PoissonLoss <: SupervisedLoss end

value(loss::PoissonLoss, target::Number, output::Number) = exp(output) - target*output
deriv(loss::PoissonLoss, target::Number, output::Number) = exp(output) - target
deriv2(loss::PoissonLoss, target::Number, output::Number) = exp(output)

isdifferentiable(::PoissonLoss) = true
isdifferentiable(::PoissonLoss, y, t) = true
istwicedifferentiable(::PoissonLoss) = true
istwicedifferentiable(::PoissonLoss, y, t) = true
islipschitzcont(::PoissonLoss) = false
isconvex(::PoissonLoss) = true
# TODO: isstrictlyconvex(::PoissonLoss) = ?
isstronglyconvex(::PoissonLoss) = false

# ===============================================================

@doc doc"""
    CrossEntropyLoss <: SupervisedLoss

Cross-entropy loss also known as log loss and logistic loss is defined as:

``L(target, output) = - target*ln(output) - (1-target)*ln(1-output)``
"""
struct CrossEntropyLoss <: SupervisedLoss end

function value(loss::CrossEntropyLoss, target::Number, output::Number)
    target >= 0 && target <=1 || error("target must be in [0,1]")
    output >= 0 && output <=1 || error("output must be in [0,1]")
    if target == 0
        -log(1 - output)
    elseif target == 1
        -log(output)
    else
        -(target * log(output) + (1-target) * log(1-output))
    end
end
deriv(loss::CrossEntropyLoss, target::Number, output::Number) = (1-target) / (1-output) - target / output
deriv2(loss::CrossEntropyLoss, target::Number, output::Number) = (1-target) / (1-output)^2 + target / output^2

isdifferentiable(::CrossEntropyLoss) = true
isdifferentiable(::CrossEntropyLoss, y, t) = t != 0 && t != 1
istwicedifferentiable(::CrossEntropyLoss) = true
istwicedifferentiable(::CrossEntropyLoss, y, t) = t != 0 && t != 1
isconvex(::CrossEntropyLoss) = true

# ===============================================================
# L(target, output) = sign(agreement) < 0 ? 1 : 0


module AnnDatas

using HDF5
using SparseArrays
using DataFrames

export AnnData

struct AnnData
    X::SparseMatrixCSC
    obsm::Union{Nothing, Dict{String, Any}}
    obsp::Union{Nothing, Dict{String, Any}}
    uns::Union{Nothing, Dict{String, Any}}
    obs::Union{Nothing, DataFrame}
    var::Union{Nothing, DataFrame}
end

"""
Read a CSR matrix in a SparseMatrixCSC
"""
function read_csr_matrix(g::HDF5.Group)
    attr = attributes(g)
    @assert read(attr["encoding-type"]) == "csr_matrix"
    m, n = read(attr["shape"])

    V           = read(g["data"])
    csr_indices = read(g["indices"]) .+ 1
    csr_indptr  = read(g["indptr"]) .+ 1
    nnz = length(V)

    # easiest way to do this is to go CSR -> COO -> CSC
    I = Vector{Int32}(undef, nnz)
    J = Vector{Int32}(undef, nnz)

    for i in 1:m
        for k in csr_indptr[i]:csr_indptr[i+1]-1
            I[k] = i
            J[k] = csr_indices[k]
        end
    end

    return sparse(I, J, V, m, n)
end

"""
Read a serialized data frame into a DataFrame
"""
function read_dataframe(input::HDF5.File, path::String)
    if !haskey(input, path)
        return nothing
    end
    g = input[path]

    columns = Dict{String, Any}()
    attr = attributes(g)
    @assert read(attr["encoding-type"]) == "dataframe"
    columnorder = read(attr["column-order"])

    for key in keys(g)
        columns[key] = read(g[key])
    end
    df = DataFrame(Dict(key => columns[key] for key in columnorder))
    df[!,"_index"] = read(g["_index"])

    return df
end

"""
General purpose function to read a groups into a dictionary tree structure.
"""
function read_group(input::HDF5.File, path::String)
    if !haskey(input, path)
        return nothing
    end
    g = input[path]

    data = Dict{String, Any}()
    for key in keys(g)
        dataset = g[key]

        if isa(dataset, HDF5.Group)
            attr = attributes(dataset)
            if length(attr) == 0
                # data[key] = read_group(dataset)
                data[key] = read(dataset)
            elseif haskey(attr, "encoding-type") && read(attr["encoding-type"]) == "csr_matrix"
                data[key] = read_csr_matrix(dataset)
            else
                data[key] = read(dataset)
            end
            # TODO: special cases for other types of data
        else
            data[key] = read(dataset)
        end
    end

    return data
end

"""
Read an AnnData struct from the given h5ad filename.
"""
function Base.read(filename::AbstractString, ::Type{AnnData})
    input = h5open(filename)

    if isa(input["X"], HDF5.Group)
        X = read_csr_matrix(input["X"])
    else
        X = read(input["X"])
    end
    obsm = read_group(input, "obsm")
    obsp = read_group(input, "obsp")
    uns  = read_group(input, "uns")
    obs  = read_dataframe(input, "obs")
    var  = read_dataframe(input, "var")

    close(input)

    return AnnData(X, obsm, obsp, uns, obs, var)
end

end # module


export solve,
    Solver,
    solver_startsolutions,
    paths_to_track,
    parameter_homotopy,
    linear_subspace_homotopy

struct SolveStats
    regular::Threads.Atomic{Int}
    regular_real::Threads.Atomic{Int}
    singular::Threads.Atomic{Int}
    singular_real::Threads.Atomic{Int}
end
SolveStats() = SolveStats(Threads.Atomic{Int}.((0, 0, 0, 0))...)

function init!(SS::SolveStats)
    SS.regular[] = SS.regular_real[] = SS.singular[] = SS.singular_real[] = 0
    SS
end

function update!(stats::SolveStats, R::PathResult)
    is_success(R) || return stats

    if is_singular(R)
        Threads.atomic_add!(stats.singular_real, Int(is_real(R)))
        Threads.atomic_add!(stats.singular, 1)
    else
        Threads.atomic_add!(stats.regular_real, Int(is_real(R)))
        Threads.atomic_add!(stats.regular, 1)
    end
    stats
end

"""
    Solver(path_tracker; seed = nothing)

A struct containing multiple copies of `path_tracker`. This contains all pre-allocated
data structures to call [`solve`]. The most convenient way to construct a `Solver` is
via [`solver_startsolutions`](@ref).
"""
struct Solver{T<:AbstractPathTracker}
    trackers::Vector{T}
    seed::Union{Nothing,UInt32}
    stats::SolveStats
    start_system::Union{Nothing,Symbol}
end
Solver(
    tracker::AbstractPathTracker;
    seed::Union{Nothing,UInt32} = nothing,
    start_system = nothing,
) = Solver([tracker], seed, SolveStats(), start_system)

Base.show(io::IO, solver::Solver) = print(io, typeof(solver), "()")

"""
    solver_startsolutions(args...; kwargs...)

Takes the same input as [`solve`](@ref) but instead of directly solving the problem
returns a [`Solver`](@ref) struct and the start solutions.

## Example

Calling `solve(args..; kwargs...)` is equivalent to
```julia
solver, starts = solver_startsolutions(args...; kwargs...)
solve(solver, starts)
```
"""
function solver_startsolutions(
    F::AbstractVector{Expression},
    starts = nothing;
    parameters = Variable[],
    variables = setdiff(variables(F), parameters),
    variable_ordering = variables,
    variable_groups = nothing,
    kwargs...,
)
    sys = System(
        F,
        variables = variable_ordering,
        parameters = parameters,
        variable_groups = variable_groups,
    )
    solver_startsolutions(sys, starts; kwargs...)
end
function solver_startsolutions(
    F::AbstractVector{<:MP.AbstractPolynomial},
    starts = nothing;
    parameters = similar(MP.variables(F), 0),
    variables = setdiff(MP.variables(F), parameters),
    variable_ordering = variables,
    variable_groups = nothing,
    target_parameters = nothing,
    kwargs...,
)
    # handle special case that we have no parameters
    # to shift the coefficients of the polynomials to the parameters
    # this was the behaviour of HC.jl v1
    if isnothing(target_parameters) && isempty(parameters)
        sys, target_parameters = ModelKit.system_with_coefficents_as_params(
            F,
            variables = variable_ordering,
            variable_groups = variable_groups,
        )
    else
        sys = System(
            F,
            variables = variable_ordering,
            parameters = parameters,
            variable_groups = variable_groups,
        )
    end
    solver_startsolutions(sys, starts; target_parameters = target_parameters, kwargs...)
end
function solver_startsolutions(
    F::Union{System,AbstractSystem},
    starts = nothing;
    seed = rand(UInt32),
    start_system = isnothing(variable_groups(F)) ? :polyhedral : :total_degree,
    generic_parameters = nothing,
    p₁ = generic_parameters,
    start_parameters = p₁,
    p₀ = generic_parameters,
    target_parameters = p₀,
    compile::Union{Bool,Symbol} = COMPILE_DEFAULT[],
    start_subspace = nothing,
    target_subspace = nothing,
    intrinsic = nothing,
    kwargs...,
)
    !isnothing(seed) && Random.seed!(seed)

    used_start_system = nothing
    if start_subspace !== nothing
        if target_parameters !== nothing
            F = fix_parameters(F, target_parameters; compile = compile)
        end
        H = linear_subspace_homotopy(
            F,
            start_subspace,
            target_subspace;
            intrinsic = intrinsic,
            compile = compile,
        )
        tracker = EndgameTracker(H; kwargs...)
    else
        if target_subspace !== nothing
            F = slice(F, target_subspace, compile = compile)
        end
        if start_parameters !== nothing
            tracker = parameter_homotopy_tracker(
                F;
                start_parameters = start_parameters,
                target_parameters = target_parameters,
                compile = compile,
                kwargs...,
            )
        elseif start_system == :polyhedral
            used_start_system = :polyhedral
            tracker, starts = polyhedral(
                F;
                compile = compile,
                target_parameters = target_parameters,
                kwargs...,
            )
        elseif start_system == :total_degree
            used_start_system = :total_degree
            tracker, starts = total_degree(
                F;
                compile = compile,
                target_parameters = target_parameters,
                kwargs...,
            )
        else
            throw(
                KeywordArgumentException(
                    :start_system,
                    start_system,
                    "Possible values are: `:polyhedral` and `:total_degree`.",
                ),
            )
        end
    end

    Solver(tracker; seed = seed, start_system = used_start_system), starts
end

function solver_startsolutions(
    G::Union{System,AbstractSystem},
    F::Union{System,AbstractSystem},
    starts = nothing;
    seed = rand(UInt32),
    tracker_options = TrackerOptions(),
    endgame_options = EndgameOptions(),
    kwargs...,
)
    !isnothing(seed) && Random.seed!(seed)
    if F isa SlicedSystem && G isa SlicedSystem && system(F) == system(G)
        H = linear_subspace_homotopy(system(F), linear_subspace(F), linear_subspace(G))
    else
        H = start_target_homotopy(G, F; kwargs...)
    end
    tracker =
        EndgameTracker(H; tracker_options = tracker_options, options = endgame_options)

    Solver(tracker; seed = seed), starts
end

function parameter_homotopy_tracker(
    F::Union{System,AbstractSystem};
    tracker_options = TrackerOptions(),
    endgame_options = EndgameOptions(),
    kwargs...,
)
    H = parameter_homotopy(F; kwargs...)
    EndgameTracker(H; tracker_options = tracker_options, options = endgame_options)
end

"""
    parameter_homotopy(F; start_parameters, target_parameters)

Construct a [`ParameterHomotopy`](@ref). If `F` is homogeneous, then a random affine chart
is chosen (via [`AffineChartHomotopy`](@ref)).
"""
function parameter_homotopy(
    F::Union{System,AbstractSystem};
    generic_parameters = randn(ComplexF64, nparameters(F)),
    p₁ = generic_parameters,
    start_parameters = p₁,
    p₀ = generic_parameters,
    target_parameters = p₀,
    tracker_options = TrackerOptions(),
    endgame_options = EndgameOptions(),
    compile::Union{Bool,Symbol} = COMPILE_DEFAULT[],
    kwargs...,
)
    unsupported_kwargs(kwargs)
    m, n = size(F)
    H = ParameterHomotopy(fixed(F; compile = compile), start_parameters, target_parameters)
    f = System(F)
    if is_homogeneous(f)
        vargroups = variable_groups(f)
        if vargroups === nothing
            m ≥ (n - 1) || throw(FiniteException(n - 1 - m))
            H = on_affine_chart(H)
        else
            m ≥ (n - length(vargroups)) || throw(FiniteException(n - length(vargroups) - m))
            H = on_affine_chart(H, length.(vargroups,) .- 1)
        end
    else
        m ≥ n || throw(FiniteException(n - m))
    end

    H
end

"""
    linear_subspace_homotopy(F, V::LinearSubspace, W::LinearSubspace, intrinsic = nothing)

Constructs an [`IntrinsicSubspaceHomotopy`](@ref) (if `dim(V) < codim(V)` or
`intrinsic = true`) or [`ExtrinsicSubspaceHomotopy`](@ref).
Compared to the direct constructor, this also takes care of homogeneous systems.
"""
function linear_subspace_homotopy(
    F::Union{System,AbstractSystem},
    V::LinearSubspace,
    W::LinearSubspace;
    compile::Union{Bool,Symbol} = COMPILE_DEFAULT[],
    intrinsic = nothing,
)
    # Intrinsic: m+1,dim(V)+1
    # Extrinsic: m+codim(V), dim(V) + codim(V)
    if dim(V) < codim(V) || something(intrinsic, false)
        if is_linear(V) && is_linear(W) && is_homogeneous(System(F))
            IntrinsicSubspaceHomotopy(on_affine_chart(F; compile = compile), V, W)
        else
            IntrinsicSubspaceHomotopy(F, V, W; compile = compile)
        end
    else
        H = ExtrinsicSubspaceHomotopy(F, V, W; compile = compile)
        if is_linear(V) && is_linear(W) && is_homogeneous(System(F))
            on_affine_chart(H)
        else
            H
        end
    end
end

function start_target_homotopy(
    G::Union{System,AbstractSystem},
    F::Union{System,AbstractSystem};
    start_parameters = nothing,
    target_parameters = nothing,
    compile::Union{Bool,Symbol} = COMPILE_DEFAULT[],
    γ = 1.0,
    gamma = γ,
    kwargs...,
)
    unsupported_kwargs(kwargs)
    f, g = System(F), System(G)

    size(F) == size(G) || error("The provided systems don't have the same size.")
    is_homogeneous(f) == is_homogeneous(g) ||
        error("The provided systems are not both homogeneous.")
    variable_groups(f) == variable_groups(g) ||
        error("The provided systems don't decalare the same variable groups.")

    m, n = size(F)

    G = fixed(G; compile = compile)
    if !isnothing(start_parameters)
        G = FixedParameterSystem(G, start_parameters)
    end

    F = fixed(F; compile = compile)
    if !isnothing(target_parameters)
        F = FixedParameterSystem(F, target_parameters)
    end

    H = StraightLineHomotopy(G, F; gamma = gamma)
    if is_homogeneous(f)
        vargroups = variable_groups(f)
        if vargroups === nothing
            m ≥ (n - 1) || throw(FiniteException(n - 1 - m))
            H = on_affine_chart(H)
        else
            m ≥ (n - length(vargroups)) || throw(FiniteException(n - length(vargroups) - m))
            H = on_affine_chart(H, length.(vargroups,) .- 1)
        end
    else
        m ≥ n || throw(FiniteException(n - m))
    end

    H
end

function solver_startsolutions(
    H::Union{Homotopy,AbstractHomotopy},
    starts = nothing;
    compile::Union{Bool,Symbol} = COMPILE_DEFAULT[],
    seed = nothing,
    kwargs...,
)
    !isnothing(seed) && Random.seed!(seed)
    Solver(EndgameTracker(fixed(H; compile = compile)); seed = seed), starts
end

"""
    solve(f; options...)
    solve(f, start_solutions; start_parameters, target_parameters, options...)
    solve(f, start_solutions; start_subspace, target_subspace, options...)
    solve(g, f, start_solutions; options...)
    solve(homotopy, start_solutions; options...)

Solve the given problem. If only a single polynomial system `f` is given, then all
(complex) isolated solutions are computed.
If a system `f` depending on parameters together with start and target parameters is given
then a parameter homotopy is performed.
If two systems `g` and `f` with solutions of `g` are given then the solutions are tracked
during the deformation of `g` to `f`.
Similarly, for a given homotopy `homotopy` ``H(x,t)`` with solutions at ``t=1`` the solutions
at ``t=0`` are computed.
See the documentation for examples.
If the input is a *homogeneous* polynomial system, solutions on a random affine chart of
projective space are computed.

## General Options
The `solve` routines takes the following options:
* `catch_interrupt = true`: If this is `true`, the computation is gracefully stopped and a
  partial result is returned when the computation is interruped.
* `compile = $(COMPILE_DEFAULT[])`: If `true` then a `System` (resp. `Homotopy`) is compiled
  to a straight line program ([`CompiledSystem`](@ref) resp. [`CompiledHomotopy`](@ref))
  for evaluation. This induces a compilation overhead. If `false` then the generated program
  is only interpreted ([`InterpretedSystem`](@ref) resp. [`InterpretedHomotopy`](@ref)).
  This is slower than the compiled version, but does not introduce compilation overhead.
* `endgame_options`: The options and parameters for the endgame.
  See [`EndgameOptions`](@ref).
* `seed`: The random seed used during the computations. The seed is also reported in the
  result. For a given random seed the result is always identical.
* `show_progress= true`: Indicate whether a progress bar should be displayed.
* `stop_early_cb`: Here it is possible to provide a function (or any callable struct) which
  accepts a `PathResult` `r` as input and returns a `Bool`. If `stop_early_cb(r)` is `true`
  then no further paths are tracked and the computation is finished. This is only called
  for successfull paths. This is for example useful if you only want to compute one solution
  of a polynomial system. For this `stop_early_cb = _ -> true` would be sufficient.
* `threading = true`: Enable multi-threading for the computation. The number of
  available threads is controlled by the environment variable `JULIA_NUM_THREADS`.
* `tracker_options`: The options and parameters for the path tracker.
  See [`TrackerOptions`](@ref).

## Options depending on input

If only a polynomial system is given:
* `start_system`: Possible values are `:total_degree` and `:polyhedral`. Depending on the
  choice furhter options are possible. See also [`total_degree`](@ref) and
  [`polyhedral`](@ref).

If a system `f` depending on parameters together with start parameters (or start subspace),
start solutions and *multiple* target parameters (or target subspaces) then the following
options are also available:

* `flatten`: Flatten the output of `transform_result`. This is useful for example if
   `transform_result` returns a vector of solutions, and you only want a single vector of
   solutions as the result (instead of a vector of vector of solutions).
* `transform_parameters = identity`: Transform a parameters values `p` before passing it to
  `target_parameters = ...`.
* `transform_result`: A function taking two arguments, the `result` and the
  parameters `p`. By default this returns the tuple `(result, p)`.

## Basic example

```julia-repl
julia> @var x y;

julia> F = System([x^2+y^2+1, 2x+3y-1])
System of length 2
 2 variables: x, y

 1 + x^2 + y^2
 -1 + 2*x + 3*y

julia> solve(F)
Result with 2 solutions
=======================
• 2 non-singular solutions (0 real)
• 0 singular solutions (0 real)
• 2 paths tracked
• random seed: 0x75a6a462
• start_system: :polyhedral
```
"""
function solve(
    args...;
    show_progress::Bool = true,
    threading::Bool = Threads.nthreads() > 1,
    catch_interrupt::Bool = true,
    target_parameters = nothing,
    stop_early_cb = always_false,
    # many parameter options,
    transform_result = nothing,
    transform_parameters = identity,
    flatten = nothing,
    target_subspaces = nothing,
    kwargs...,
)
    many_parameters = false
    if target_subspaces !== nothing
        many_parameters = true
        solver, starts = solver_startsolutions(
            args...;
            target_subspace = first(target_subspaces),
            kwargs...,
        )
        target_parameters = target_subspaces
    elseif target_parameters !== nothing
        # check if we have many parameters solve
        if !isa(transform_parameters(first(target_parameters)), Number)
            many_parameters = true
            solver, starts = solver_startsolutions(
                args...;
                target_parameters = transform_parameters(first(target_parameters)),
                kwargs...,
            )
        else
            solver, starts = solver_startsolutions(
                args...;
                target_parameters = target_parameters,
                kwargs...,
            )
        end
    else
        solver, starts = solver_startsolutions(args...; kwargs...)
    end
    if many_parameters
        solve(
            solver,
            starts,
            target_parameters;
            show_progress = show_progress,
            threading = threading,
            catch_interrupt = catch_interrupt,
            transform_result = transform_result,
            transform_parameters = transform_parameters,
            flatten = flatten,
        )
    else
        solve(
            solver,
            starts;
            stop_early_cb = stop_early_cb,
            show_progress = show_progress,
            threading = threading,
            catch_interrupt = catch_interrupt,
        )
    end
end

solve(S::Solver, R::Result; kwargs...) =
    solve(S, solutions(R; only_nonsingular = true); kwargs...)
solve(S::Solver, s::AbstractVector{<:Number}; kwargs...) = solve(S, [s]; kwargs...)
solve(S::Solver, starts; kwargs...) = solve(S, collect(starts); kwargs...)
function solve(
    S::Solver,
    starts::AbstractArray;
    stop_early_cb = always_false,
    show_progress::Bool = true,
    threading::Bool = Threads.nthreads() > 1,
    catch_interrupt::Bool = true,
)
    n = length(starts)
    progress = show_progress ? make_progress(n; delay = 0.3) : nothing
    init!(S.stats)
    if threading
        threaded_solve(
            S,
            starts,
            progress,
            stop_early_cb;
            catch_interrupt = catch_interrupt,
        )
    else
        serial_solve(S, starts, progress, stop_early_cb; catch_interrupt = catch_interrupt)
    end
end
(solver::Solver)(starts; kwargs...) = solve(solver, starts; kwargs...)
track(solver::Solver, s; kwargs...) = track(solver.trackers[1], s; kwargs...)

function make_progress(n::Integer; delay::Float64 = 0.0)
    desc = "Tracking $n paths... "
    barlen = min(ProgressMeter.tty_width(desc, stdout, false), 40)
    progress =
        ProgressMeter.Progress(n; dt = 0.2, desc = desc, barlen = barlen, output = stdout)
    progress.tlast += delay
    progress
end
function update_progress!(progress, stats, ntracked)
    t = time()
    if ntracked == progress.n || t > progress.tlast + progress.dt
        showvalues = make_showvalues(stats, ntracked)
        ProgressMeter.update!(progress, ntracked; showvalues = showvalues)
    end
    nothing
end
@noinline function make_showvalues(stats, ntracked)
    showvalues = (("# paths tracked", ntracked),)
    nsols = stats.regular[] + stats.singular[]
    nreal = stats.regular_real[] + stats.singular_real[]
    (
        ("# paths tracked", ntracked),
        ("# non-singular solutions (real)", "$(stats.regular[]) ($(stats.regular_real[]))"),
        ("# singular endpoints (real)", "$(stats.singular[]) ($(stats.singular_real[]))"),
        ("# total solutions (real)", "$(nsols[]) ($(nreal[]))"),
    )
end
update_progress!(::Nothing, stats, ntracked) = nothing

function serial_solve(
    solver::Solver,
    starts,
    progress = nothing,
    stop_early_cb = always_false;
    catch_interrupt::Bool = true,
)
    path_results = Vector{PathResult}()
    tracker = solver.trackers[1]
    try
        for (k, s) in enumerate(starts)
            r = track(tracker, s; path_number = k)
            push!(path_results, r)
            update!(solver.stats, r)
            update_progress!(progress, solver.stats, k)
            if is_success(r) && stop_early_cb(r)
                break
            end
        end
    catch e
        (catch_interrupt && isa(e, InterruptException)) || rethrow(e)
    end

    Result(path_results; seed = solver.seed, start_system = solver.start_system)
end
function threaded_solve(
    solver::Solver,
    S::AbstractArray,
    progress = nothing,
    stop_early_cb = always_false;
    catch_interrupt::Bool = true,
)
    N = length(S)
    path_results = Vector{PathResult}(undef, N)
    interrupted = false
    started = Threads.Atomic{Int}(0)
    finished = Threads.Atomic{Int}(0)
    try
        Threads.resize_nthreads!(solver.trackers)
        tasks = map(enumerate(solver.trackers)) do (i, tracker)
            @tspawnat i begin
                while (k = Threads.atomic_add!(started, 1) + 1) ≤ N && !interrupted
                    r = track(tracker, S[k]; path_number = k)
                    path_results[k] = r
                    nfinished = Threads.atomic_add!(finished, 1) + 1
                    update!(solver.stats, r)
                    update_progress!(progress, solver.stats, nfinished[])
                    if is_success(r) && stop_early_cb(r)
                        interrupted = true
                    end
                end
            end
        end
        for task in tasks
            wait(task)
        end
    catch e
        if (
            isa(e, InterruptException) ||
            (isa(e, TaskFailedException) && isa(e.task.exception, InterruptException))
        )
            interrupted = true
        end
        if !interrupted || !catch_interrupt
            rethrow(e)
        end
    end
    # if we got interrupted we need to remove the unassigned filedds
    if interrupted
        assigned_results = Vector{PathResult}()
        for i = 1:started[]
            if isassigned(path_results, i)
                push!(assigned_results, path_results[i])
            end
        end
        Result(assigned_results; seed = solver.seed, start_system = solver.start_system)
    else
        Result(path_results; seed = solver.seed, start_system = solver.start_system)
    end
end

function start_parameters!(solver::Solver, p)
    for tracker in solver.trackers
        start_parameters!(tracker, p)
    end
    solver
end

function target_parameters!(solver::Solver, p)
    for tracker in solver.trackers
        target_parameters!(tracker, p)
    end
    solver
end
function parameters!(solver::Solver, p, q)
    for tracker in solver.trackers
        parameters!(tracker, p, q)
    end
    solver
end

"""
    paths_to_track(f; optopms..)

Returns the number of paths tracked when calling [`solve`](@ref) with the given arguments.
"""
function paths_to_track(
    f::Union{System,AbstractSystem};
    start_system::Symbol = :polyhedral,
    kwargs...,
)
    paths_to_track(f, Val(start_system); kwargs...)
end

#############################
### Many parameter solver ###
#############################

struct ManySolveStats
    solutions::Threads.Atomic{Int}
end

function solve(
    S::Solver,
    starts,
    target_parameters;
    show_progress::Bool = true,
    threading::Bool = Threads.nthreads() > 1,
    catch_interrupt::Bool = true,
    transform_result = nothing,
    transform_parameters = nothing,
    flatten = nothing,
)
    transform_result = something(transform_result, tuple) # (solutions ∘ first) ∘ tuple
    transform_parameters = something(transform_parameters, identity)
    flatten = something(flatten, false)

    n = length(target_parameters)

    progress = show_progress ? make_many_progress(n; delay = 0.3) : nothing
    many_solve(
        S,
        starts,
        target_parameters,
        progress,
        transform_result,
        transform_parameters,
        Val(flatten);
        catch_interrupt = catch_interrupt,
        threading = threading,
    )
end

function make_many_progress(n::Integer; delay::Float64 = 0.0)
    desc = "Solving for $n parameters... "
    barlen = min(ProgressMeter.tty_width(desc, stdout, false), 40)
    progress =
        ProgressMeter.Progress(n; dt = 0.3, desc = desc, barlen = barlen, output = stdout)
    progress.tlast += delay
    progress
end
function update_many_progress!(progress, results, k, paths_per_param; flatten::Bool)
    t = time()
    if k == progress.n || t > progress.tlast + progress.dt
        showvalues = make_many_showvalues(results, k, paths_per_param; flatten = flatten)
        ProgressMeter.update!(progress, k; showvalues = showvalues)
    end
    nothing
end
@noinline function make_many_showvalues(results, k, paths_per_param; flatten::Bool)
    if flatten
        [
            ("# parameters solved", k),
            ("# paths tracked", paths_per_param * k),
            ("# results", length(results)),
        ]
    else
        [("# parameters solved", k), ("# paths tracked", paths_per_param * k)]
    end
end
update_many_progress!(::Nothing, results, k, paths_per_param; kwargs...) = nothing

function many_solve(
    solver::Solver,
    starts,
    many_target_parameters,
    progress,
    transform_result,
    transform_parameters,
    ::Val{flatten};
    threading::Bool,
    catch_interrupt::Bool,
) where {flatten}
    q = first(many_target_parameters)
    target_parameters!(solver, transform_parameters(q))
    if threading
        res = threaded_solve(solver, starts; catch_interrupt = false)
    else
        res = serial_solve(solver, starts; catch_interrupt = false)
    end
    if flatten
        results = transform_result(res, q)
        if !(results isa AbstractArray)
            throw(ArgumentError("Cannot flatten arguments of type `$(typeof(results))`"))
        end
    else
        results = [transform_result(res, q)]
    end
    k = 1
    m = length(starts)
    update_many_progress!(progress, results, k, m; flatten = flatten)
    try
        for q in Iterators.drop(many_target_parameters, 1)
            target_parameters!(solver, transform_parameters(q))
            if threading
                res = threaded_solve(solver, starts; catch_interrupt = false)
            else
                res = serial_solve(solver, starts; catch_interrupt = false)
            end

            if flatten
                append!(results, transform_result(res, q))
            else
                push!(results, transform_result(res, q))
            end
            k += 1
            update_many_progress!(progress, results, k, m; flatten = flatten)
        end
    catch e
        if !(
            isa(e, InterruptException) ||
            (isa(e, TaskFailedException) && isa(e.task.exception, InterruptException))
        )
            rethrow(e)
        end
    end

    results
end


# *********************************************************************************
# REopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# Redistributions of source code must retain the above copyright notice, this list
# of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or other
# materials provided with the distribution.
#
# Neither the name of the copyright holder nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
# *********************************************************************************
"""
    DomesticHotWaterLoad

There are many ways in which a DomesticHotWaterLoad can be defined:
1. When using either `doe_reference_name` or `blended_doe_reference_names` in an `ElectricLoad` one only needs to provide the input key "DomesticHotWaterLoad" in the `Scenario` (JSON or Dict). In this case the values from DoE reference names from the `ElectricLoad` will be used to define the `DomesticHotWaterLoad`.
2. One can provide the `doe_reference_name` or `blended_doe_reference_names` directly in the `DomesticHotWaterLoad` key within the `Scenario`. These values can be combined with the `annual_mmbtu` or `monthly_mmbtu` inputs to scale the DoE reference profile(s).
3. One can provide the `fuel_loads_mmbtu_per_hour` value in the `DomesticHotWaterLoad` key within the `Scenario`.

```julia
function DomesticHotWaterLoad(;
    doe_reference_name::String = "",
    city::String = "",
    blended_doe_reference_names::Array{String, 1} = String[],
    blended_doe_reference_percents::Array{<:Real,1} = Real[],
    annual_mmbtu::Union{Real, Nothing} = nothing,
    monthly_mmbtu::Array{<:Real,1} = Real[],
    fuel_loads_mmbtu_per_hour::Array{<:Real,1} = Real[],
    time_steps_per_hour::Int = 1,
    latitude::Real=0.0,
    longitude::Real=0.0
)
```
"""
struct DomesticHotWaterLoad
    loads_kw::Array{Real, 1}

    function DomesticHotWaterLoad(;
        doe_reference_name::String = "",
        city::String = "",
        blended_doe_reference_names::Array{String, 1} = String[],
        blended_doe_reference_percents::Array{<:Real,1} = Real[],
        annual_mmbtu::Union{Real, Nothing} = nothing,
        monthly_mmbtu::Array{<:Real,1} = Real[],
        # addressable_load_fraction,  # TODO
        fuel_loads_mmbtu_per_hour::Array{<:Real,1} = Real[],  # TODO shouldn't this be per_time_step?
        time_steps_per_hour::Int = 1,
        latitude::Real=0.0,
        longitude::Real=0.0
    )
        if length(fuel_loads_mmbtu_per_hour) > 0

            if !(length(fuel_loads_mmbtu_per_hour) / time_steps_per_hour ≈ 8760)
                @error "Provided domestic hot water load does not match the time_steps_per_hour."
            end

            loads_kw = fuel_loads_mmbtu_per_hour .* (MMBTU_TO_KWH * EXISTING_BOILER_EFFICIENCY)

        elseif !isempty(doe_reference_name)
            loads_kw = BuiltInDomesticHotWaterLoad(city, doe_reference_name, latitude, longitude, 2017, annual_mmbtu, monthly_mmbtu)

        elseif length(blended_doe_reference_names) > 1 &&
            length(blended_doe_reference_names) == length(blended_doe_reference_percents)
            loads_kw = blend_and_scale_doe_profiles(BuiltInDomesticHotWaterLoad, latitude, longitude, 2017,
                                                    blended_doe_reference_names, blended_doe_reference_percents, city,
                                                    annual_mmbtu, monthly_mmbtu)
        else
            error("Cannot construct DomesticHotWaterLoad. You must provide either [fuel_loads_mmbtu_per_hour],
                [doe_reference_name, city],
                or [blended_doe_reference_names, blended_doe_reference_percents, city].")
        end

        if length(loads_kw) < 8760*time_steps_per_hour
            loads_kw = repeat(loads_kw, inner=Int(time_steps_per_hour / (length(loads_kw)/8760)))
            @info "Repeating domestic hot water loads in each hour to match the time_steps_per_hour."
        end

        new(
            loads_kw
        )
    end
end

"""
    SpaceHeatingLoad

There are many ways in which a SpaceHeatingLoad can be defined:
1. When using either `doe_reference_name` or `blended_doe_reference_names` in an `ElectricLoad` one only needs to provide the input key "SpaceHeatingLoad" in the `Scenario` (JSON or Dict). In this case the values from DoE reference names from the `ElectricLoad` will be used to define the `SpaceHeatingLoad`.
2. One can provide the `doe_reference_name` or `blended_doe_reference_names` directly in the `SpaceHeatingLoad` key within the `Scenario`. These values can be combined with the `annual_mmbtu` or `monthly_mmbtu` inputs to scale the DoE reference profile(s).
3. One can provide the `fuel_loads_mmbtu_per_hour` value in the `SpaceHeatingLoad` key within the `Scenario`.

```julia
function SpaceHeatingLoad(;
    doe_reference_name::String = "",
    city::String = "",
    blended_doe_reference_names::Array{String, 1} = String[],
    blended_doe_reference_percents::Array{<:Real,1} = Real[],
    annual_mmbtu::Union{Real, Nothing} = nothing,
    monthly_mmbtu::Array{<:Real,1} = Real[],
    fuel_loads_mmbtu_per_hour::Array{<:Real,1} = Real[],
    time_steps_per_hour::Int = 1,
    latitude::Real=0.0,
    longitude::Real=0.0
)
```
"""
struct SpaceHeatingLoad
    loads_kw::Array{Real, 1}

    function SpaceHeatingLoad(;
        doe_reference_name::String = "",
        city::String = "",
        blended_doe_reference_names::Array{String, 1} = String[],
        blended_doe_reference_percents::Array{<:Real,1} = Real[],
        annual_mmbtu::Union{Real, Nothing} = nothing,
        monthly_mmbtu::Array{<:Real,1} = Real[],
        # addressable_load_fraction,  # TODO
        fuel_loads_mmbtu_per_hour::Array{<:Real,1} = Real[],
        time_steps_per_hour::Int = 1,
        latitude::Real=0.0,
        longitude::Real=0.0
    )
        if length(fuel_loads_mmbtu_per_hour) > 0

            if !(length(fuel_loads_mmbtu_per_hour) / time_steps_per_hour ≈ 8760)
                @error "Provided space heating load does not match the time_steps_per_hour."
            end

            loads_kw = fuel_loads_mmbtu_per_hour .* (MMBTU_TO_KWH * EXISTING_BOILER_EFFICIENCY)

        elseif !isempty(doe_reference_name)
            loads_kw = BuiltInSpaceHeatingLoad(city, doe_reference_name, latitude, longitude, 2017, annual_mmbtu, monthly_mmbtu)

        elseif length(blended_doe_reference_names) > 1 &&
            length(blended_doe_reference_names) == length(blended_doe_reference_percents)
            loads_kw = blend_and_scale_doe_profiles(BuiltInSpaceHeatingLoad, latitude, longitude, 2017,
                                                    blended_doe_reference_names, blended_doe_reference_percents, city,
                                                    annual_mmbtu, monthly_mmbtu)
        else
            error("Cannot construct BuiltInSpaceHeatingLoad. You must provide either [fuel_loads_mmbtu_per_hour],
                [doe_reference_name, city],
                or [blended_doe_reference_names, blended_doe_reference_percents, city].")
        end

        if length(loads_kw) < 8760*time_steps_per_hour
            loads_kw = repeat(loads_kw, inner=Int(time_steps_per_hour / (length(loads_kw)/8760)))
            @info "Repeating space heating loads in each hour to match the time_steps_per_hour."
        end

        new(
            loads_kw
        )
    end
end

function BuiltInDomesticHotWaterLoad(
    city::String,
    buildingtype::String,
    latitude::Real,
    longitude::Real,
    year::Int,
    annual_mmbtu::Union{<:Real, Nothing}=nothing,
    monthly_mmbtu::Union{Vector{<:Real}, Nothing}=nothing
    )
    dhw_annual_mmbtu = Dict(
        "Miami" => Dict(
            "FastFoodRest" => 53.47209411,
            "FullServiceRest" => 158.0518043,
            "Hospital" => 442.7295435,
            "LargeHotel" => 3713.248373,
            "LargeOffice" => 127.9412792,
            "MediumOffice" => 22.09603477,
            "MidriseApartment" => 158.0580017,
            "Outpatient" => 27.60091429,
            "PrimarySchool" => 105.3179165,
            "RetailStore" => 0.0,
            "SecondarySchool" => 250.1299246,
            "SmallHotel" => 242.232695,
            "SmallOffice" => 9.891779415,
            "StripMall" => 0.0,
            "Supermarket" => 17.94985187,
            "warehouse" => 0.0,
            "FlatLoad" => 333.0450133
        ),
        "Houston" => Dict(
            "FastFoodRest" => 62.56835989,
            "FullServiceRest" => 188.292814,
            "Hospital" => 530.6352726,
            "LargeHotel" => 4685.666667,
            "LargeOffice" => 160.8917808,
            "MediumOffice" => 25.9266894,
            "MidriseApartment" => 199.2544784,
            "Outpatient" => 32.87691943,
            "PrimarySchool" => 128.0705362,
            "RetailStore" => 0.0,
            "SecondarySchool" => 314.7654465,
            "SmallHotel" => 290.6293673,
            "SmallOffice" => 10.27839347,
            "StripMall" => 0.0,
            "Supermarket" => 19.86608717,
            "warehouse" => 0.0,
            "FlatLoad" => 415.6076756
        ),
        "Phoenix" => Dict(
            "FastFoodRest" => 57.34025418,
            "FullServiceRest" => 170.9086319,
            "Hospital" => 480.098265,
            "LargeHotel" => 4127.191046,
            "LargeOffice" => 141.8507451,
            "MediumOffice" => 23.71397275,
            "MidriseApartment" => 175.5949563,
            "Outpatient" => 29.83372212,
            "PrimarySchool" => 116.811664,
            "RetailStore" => 0.0,
            "SecondarySchool" => 285.2339344,
            "SmallHotel" => 262.8487714,
            "SmallOffice" => 10.05557471,
            "StripMall" => 0.0,
            "Supermarket" => 18.7637822,
            "warehouse" => 0.0,
            "FlatLoad" => 368.7653325
        ),
        "Atlanta" => Dict(
            "FastFoodRest" => 71.33170579,
            "FullServiceRest" => 217.4332205,
            "Hospital" => 615.3498557,
            "LargeHotel" => 5622.340656,
            "LargeOffice" => 192.7164525,
            "MediumOffice" => 29.62182675,
            "MidriseApartment" => 238.9315749,
            "Outpatient" => 37.9759973,
            "PrimarySchool" => 148.8362119,
            "RetailStore" => 0.0,
            "SecondarySchool" => 372.2083434,
            "SmallHotel" => 337.2688069,
            "SmallOffice" => 10.65138846,
            "StripMall" => 0.0,
            "Supermarket" => 21.71038069,
            "warehouse" => 0.0,
            "FlatLoad" => 494.7735263
        ),
        "LasVegas" => Dict(
            "FastFoodRest" => 63.63848459,
            "FullServiceRest" => 191.8494897,
            "Hospital" => 540.9697668,
            "LargeHotel" => 4800.331564,
            "LargeOffice" => 164.7154124,
            "MediumOffice" => 26.36796732,
            "MidriseApartment" => 204.1120165,
            "Outpatient" => 33.48190098,
            "PrimarySchool" => 131.9651451,
            "RetailStore" => 0.0,
            "SecondarySchool" => 327.441087,
            "SmallHotel" => 296.3578765,
            "SmallOffice" => 10.32392915,
            "StripMall" => 0.0,
            "Supermarket" => 20.08676069,
            "warehouse" => 0.0,
            "FlatLoad" => 425.7275876
        ),
        "LosAngeles" => Dict(
            "FastFoodRest" => 69.63212501,
            "FullServiceRest" => 211.7827529,
            "Hospital" => 598.9350422,
            "LargeHotel" => 5440.174033,
            "LargeOffice" => 186.6199083,
            "MediumOffice" => 28.91483286,
            "MidriseApartment" => 231.215325,
            "Outpatient" => 37.00823296,
            "PrimarySchool" => 142.8059487,
            "RetailStore" => 0.0,
            "SecondarySchool" => 352.7467563,
            "SmallHotel" => 328.1935523,
            "SmallOffice" => 10.58011717,
            "StripMall" => 0.0,
            "Supermarket" => 21.35337379,
            "warehouse" => 0.0,
            "FlatLoad" => 478.7476251
        ),
        "SanFrancisco" => Dict(
            "FastFoodRest" => 77.13092952,
            "FullServiceRest" => 236.7180594,
            "Hospital" => 671.40531,
            "LargeHotel" => 6241.842643,
            "LargeOffice" => 213.8445094,
            "MediumOffice" => 32.07909301,
            "MidriseApartment" => 265.1697301,
            "Outpatient" => 41.35500136,
            "PrimarySchool" => 160.4507431,
            "RetailStore" => 0.0,
            "SecondarySchool" => 401.395655,
            "SmallHotel" => 368.0979112,
            "SmallOffice" => 10.90004379,
            "StripMall" => 0.0,
            "Supermarket" => 22.9292287,
            "warehouse" => 0.0,
            "FlatLoad" => 546.4574286
        ),
        "Baltimore" => Dict(
            "FastFoodRest" => 78.2191761,
            "FullServiceRest" => 240.338156,
            "Hospital" => 681.9322322,
            "LargeHotel" => 6358.710286,
            "LargeOffice" => 217.7306132,
            "MediumOffice" => 32.52815422,
            "MidriseApartment" => 270.1195541,
            "Outpatient" => 41.96148216,
            "PrimarySchool" => 165.3116185,
            "RetailStore" => 0.0,
            "SecondarySchool" => 417.9512972,
            "SmallHotel" => 373.906416,
            "SmallOffice" => 10.94554028,
            "StripMall" => 0.0,
            "Supermarket" => 23.15795696,
            "warehouse" => 0.0,
            "FlatLoad" => 557.0507802
        ),
        "Albuquerque" => Dict(
            "FastFoodRest" => 76.9149868,
            "FullServiceRest" => 235.9992545,
            "Hospital" => 669.3128607,
            "LargeHotel" => 6219.08303,
            "LargeOffice" => 212.9944774,
            "MediumOffice" => 31.97726287,
            "MidriseApartment" => 264.2063457,
            "Outpatient" => 41.20639013,
            "PrimarySchool" => 162.1556119,
            "RetailStore" => 0.0,
            "SecondarySchool" => 409.1649863,
            "SmallHotel" => 366.9712928,
            "SmallOffice" => 10.88949351,
            "StripMall" => 0.0,
            "Supermarket" => 22.88525618,
            "warehouse" => 0.0,
            "FlatLoad" => 545.235078
        ),
        "Seattle" => Dict(
            "FastFoodRest" => 81.80231236,
            "FullServiceRest" => 252.2609525,
            "Hospital" => 716.6111323,
            "LargeHotel" => 6741.736717,
            "LargeOffice" => 230.8057849,
            "MediumOffice" => 34.04746055,
            "MidriseApartment" => 286.3412104,
            "Outpatient" => 44.07342164,
            "PrimarySchool" => 172.0233322,
            "RetailStore" => 0.0,
            "SecondarySchool" => 434.0806311,
            "SmallHotel" => 392.968915,
            "SmallOffice" => 11.09863592,
            "StripMall" => 0.0,
            "Supermarket" => 23.91178737,
            "warehouse" => 0.0,
            "FlatLoad" => 588.8601433
        ),
        "Chicago" => Dict(
            "FastFoodRest" => 84.2645196,
            "FullServiceRest" => 260.4454844,
            "Hospital" => 740.4172516,
            "LargeHotel" => 7005.083356,
            "LargeOffice" => 239.7065959,
            "MediumOffice" => 35.08184587,
            "MidriseApartment" => 297.4938584,
            "Outpatient" => 45.49600079,
            "PrimarySchool" => 179.4639347,
            "RetailStore" => 0.0,
            "SecondarySchool" => 456.8817409,
            "SmallHotel" => 406.0751832,
            "SmallOffice" => 11.2033023,
            "StripMall" => 0.0,
            "Supermarket" => 24.4292392,
            "warehouse" => 0.0,
            "FlatLoad" => 611.6276445
        ),
        "Boulder" => Dict(
            "FastFoodRest" => 83.95201542,
            "FullServiceRest" => 259.3997752,
            "Hospital" => 737.372005,
            "LargeHotel" => 6971.32924,
            "LargeOffice" => 238.572519,
            "MediumOffice" => 34.9486709,
            "MidriseApartment" => 296.06471,
            "Outpatient" => 45.31437164,
            "PrimarySchool" => 178.3378526,
            "RetailStore" => 0.0,
            "SecondarySchool" => 453.228537,
            "SmallHotel" => 404.4154946,
            "SmallOffice" => 11.18970855,
            "StripMall" => 0.0,
            "Supermarket" => 24.36505320,
            "warehouse" => 0.0,
            "FlatLoad" => 608.6556221
        ),
        "Minneapolis" => Dict(
            "FastFoodRest" => 89.48929949,
            "FullServiceRest" => 277.8184269,
            "Hospital" => 790.9262388,
            "LargeHotel" => 7563.607619,
            "LargeOffice" => 258.6874644,
            "MediumOffice" => 37.28641454,
            "MidriseApartment" => 321.1473562,
            "Outpatient" => 48.51884975,
            "PrimarySchool" => 191.9480118,
            "RetailStore" => 0.0,
            "SecondarySchool" => 491.5554097,
            "SmallHotel" => 433.8738637,
            "SmallOffice" => 11.42620649,
            "StripMall" => 0.0,
            "Supermarket" => 25.53218144,
            "warehouse" => 0.0,
            "FlatLoad" => 658.8635839
        ),
        "Helena" => Dict(
            "FastFoodRest" => 90.44011877,
            "FullServiceRest" => 280.9757902,
            "Hospital" => 800.0940058,
            "LargeHotel" => 7665.023574,
            "LargeOffice" => 262.1461576,
            "MediumOffice" => 37.68905029,
            "MidriseApartment" => 325.4421541,
            "Outpatient" => 49.09222188,
            "PrimarySchool" => 193.4573283,
            "RetailStore" => 0.0,
            "SecondarySchool" => 494.7393735,
            "SmallHotel" => 438.9398731,
            "SmallOffice" => 11.46564268,
            "StripMall" => 0.0,
            "Supermarket" => 25.72866824,
            "warehouse" => 0.0,
            "FlatLoad" => 667.2021224
        ),
        "Duluth" => Dict(
            "FastFoodRest" => 98.10641517,
            "FullServiceRest" => 306.4772907,
            "Hospital" => 874.2611723,
            "LargeHotel" => 8484.906093,
            "LargeOffice" => 290.0193773,
            "MediumOffice" => 40.92475821,
            "MidriseApartment" => 360.161261,
            "Outpatient" => 53.53681127,
            "PrimarySchool" => 211.2386551,
            "RetailStore" => 0.0,
            "SecondarySchool" => 543.3733772,
            "SmallHotel" => 479.7414481,
            "SmallOffice" => 11.79316054,
            "StripMall" => 0.0,
            "Supermarket" => 27.3451629,
            "warehouse" => 0.0,
            "FlatLoad" => 736.3678114
        ),
        "Fairbanks" => Dict(
            "FastFoodRest" => 108.5335945,
            "FullServiceRest" => 341.1572799,
            "Hospital" => 975.1062178,
            "LargeHotel" => 9600.267161,
            "LargeOffice" => 327.8820873,
            "MediumOffice" => 45.32138512,
            "MidriseApartment" => 407.3910855,
            "Outpatient" => 59.6203514,
            "PrimarySchool" => 234.2595741,
            "RetailStore" => 0.0,
            "SecondarySchool" => 604.6838786,
            "SmallHotel" => 535.2525234,
            "SmallOffice" => 12.23744003,
            "StripMall" => 0.0,
            "Supermarket" => 29.53958045,
            "warehouse" => 0.0,
            "FlatLoad" => 830.07826
        )
    )
    if isempty(city)
        city = find_ashrae_zone_city(latitude, longitude)
    end
    if !(buildingtype in default_buildings)
        error("buildingtype $(buildingtype) not in $(default_buildings).")
    end
    if isnothing(annual_mmbtu)
        annual_mmbtu = dhw_annual_mmbtu[city][buildingtype]
    end
    built_in_load("domestic_hot_water", city, buildingtype, year, annual_mmbtu, monthly_mmbtu)
end

function BuiltInSpaceHeatingLoad(
    city::String,
    buildingtype::String,
    latitude::Real,
    longitude::Real,
    year::Int,
    annual_mmbtu::Union{<:Real, Nothing}=nothing,
    monthly_mmbtu::Union{Vector{<:Real}, Nothing}=nothing,
    )
    spaceheating_annual_mmbtu = Dict(
        "Miami" => Dict(
            "FastFoodRest" => 5.426780867,
            "FullServiceRest" => 12.03181471,
            "Hospital" => 6248.413294,
            "LargeHotel" => 198.0691407,
            "LargeOffice" => 168.9731637,
            "MediumOffice" => 0.036985655,
            "MidriseApartment" => 38.70606161,
            "Outpatient" => 2559.185872,
            "PrimarySchool" => 49.78021153,
            "RetailStore" => 12.12015432,
            "SecondarySchool" => 203.5185485,
            "SmallHotel" => 9.098564901,
            "SmallOffice" => 0.312524873,
            "StripMall" => 20.73216748,
            "Supermarket" => 101.2785324,
            "warehouse" => 56.0796017,
            "FlatLoad" => 605.2352137
        ),
        "Houston" => Dict(
            "FastFoodRest" => 85.49111065,
            "FullServiceRest" => 199.7942842,
            "Hospital" => 8732.10385,
            "LargeHotel" => 1307.035548,
            "LargeOffice" => 2229.971744,
            "MediumOffice" => 16.25994314,
            "MidriseApartment" => 386.0269973,
            "Outpatient" => 2829.324307,
            "PrimarySchool" => 469.2532935,
            "RetailStore" => 289.0470815,
            "SecondarySchool" => 2011.1678969999998,
            "SmallHotel" => 108.9825885,
            "SmallOffice" => 19.55157672,
            "StripMall" => 292.23235389999996,
            "Supermarket" => 984.7374347000001,
            "warehouse" => 475.9377273,
            "FlatLoad" => 1277.307359
        ),
        "Phoenix" => Dict(
            "FastFoodRest" => 57.89972381,
            "FullServiceRest" => 147.2569493,
            "Hospital" => 9382.021026,
            "LargeHotel" => 896.790817,
            "LargeOffice" => 1584.061452,
            "MediumOffice" => 1.922551528,
            "MidriseApartment" => 290.9887152,
            "Outpatient" => 3076.340876,
            "PrimarySchool" => 305.573525,
            "RetailStore" => 208.66330580000002,
            "SecondarySchool" => 1400.638544,
            "SmallHotel" => 83.98084516,
            "SmallOffice" => 9.988210938,
            "StripMall" => 230.16060699999997,
            "Supermarket" => 972.3008295,
            "warehouse" => 362.42249280000004,
            "FlatLoad" => 1188.188154
        ),
        "Atlanta" => Dict(
            "FastFoodRest" => 168.8402371,
            "FullServiceRest" => 379.5865464,
            "Hospital" => 10467.659959999999,
            "LargeHotel" => 2427.6589879999997,
            "LargeOffice" => 3624.593975,
            "MediumOffice" => 49.00635733,
            "MidriseApartment" => 718.9316697,
            "Outpatient" => 3186.250588,
            "PrimarySchool" => 931.7212450999999,
            "RetailStore" => 627.2489826000001,
            "SecondarySchool" => 3968.4936420000004,
            "SmallHotel" => 202.26124219999997,
            "SmallOffice" => 42.74797302,
            "StripMall" => 615.2240506,
            "Supermarket" => 1880.5304489999999,
            "warehouse" => 930.9449202,
            "FlatLoad" => 1888.856302
        ),
        "LasVegas" => Dict(
            "FastFoodRest" => 100.0877773,
            "FullServiceRest" => 247.21791319999997,
            "Hospital" => 9100.302056,
            "LargeHotel" => 1500.581408,
            "LargeOffice" => 2479.152321,
            "MediumOffice" => 5.220181581,
            "MidriseApartment" => 487.43122850000003,
            "Outpatient" => 2924.8220460000002,
            "PrimarySchool" => 499.6562223,
            "RetailStore" => 386.0185744,
            "SecondarySchool" => 2277.7410649999997,
            "SmallHotel" => 138.4427074,
            "SmallOffice" => 19.16330622,
            "StripMall" => 389.30494280000005,
            "Supermarket" => 1479.302604,
            "warehouse" => 579.7671637999999,
            "FlatLoad" => 1413.3882199999998
        ),
        "LosAngeles" => Dict(
            "FastFoodRest" => 40.90390152,
            "FullServiceRest" => 97.94277036,
            "Hospital" => 10346.1713,
            "LargeHotel" => 707.848762,
            "LargeOffice" => 1458.148818,
            "MediumOffice" => 0.12342009699999999,
            "MidriseApartment" => 265.2851759,
            "Outpatient" => 3417.120585,
            "PrimarySchool" => 318.73600980000003,
            "RetailStore" => 175.104083,
            "SecondarySchool" => 1198.276619,
            "SmallHotel" => 72.42852638,
            "SmallOffice" => 5.898878347,
            "StripMall" => 193.18730269999998,
            "Supermarket" => 1040.273464,
            "warehouse" => 323.96697819999997,
            "FlatLoad" => 1228.8385369999999
        ),
        "SanFrancisco" => Dict(
            "FastFoodRest" => 127.22328700000001,
            "FullServiceRest" => 362.48645889999995,
            "Hospital" => 11570.9155,
            "LargeHotel" => 1713.3629670000003,
            "LargeOffice" => 2690.1191,
            "MediumOffice" => 3.8159670660000002,
            "MidriseApartment" => 648.4472797999999,
            "Outpatient" => 3299.0539519999998,
            "PrimarySchool" => 818.2159102,
            "RetailStore" => 569.8081034,
            "SecondarySchool" => 3414.148347,
            "SmallHotel" => 189.0244446,
            "SmallOffice" => 27.53039453,
            "StripMall" => 526.2320428,
            "Supermarket" => 2301.616069,
            "warehouse" => 675.6758453,
            "FlatLoad" => 1808.604729
        ),
        "Baltimore" => Dict(
            "FastFoodRest" => 305.2671204,
            "FullServiceRest" => 657.1337578,
            "Hospital" => 11253.61694,
            "LargeHotel" => 3731.0254619999996,
            "LargeOffice" => 5109.311943,
            "MediumOffice" => 116.8101842,
            "MidriseApartment" => 1132.964052,
            "Outpatient" => 3285.227941,
            "PrimarySchool" => 1428.239177,
            "RetailStore" => 1068.034778,
            "SecondarySchool" => 6557.634924,
            "SmallHotel" => 346.3683857,
            "SmallOffice" => 63.29818348,
            "StripMall" => 1075.39546,
            "Supermarket" => 2929.182261,
            "warehouse" => 1568.722061,
            "FlatLoad" => 2539.2645399999997
        ),
        "Albuquerque" => Dict(
            "FastFoodRest" => 199.73581399999998,
            "FullServiceRest" => 398.5712205,
            "Hospital" => 8371.240776999999,
            "LargeHotel" => 2750.8382260000003,
            "LargeOffice" => 3562.0023950000004,
            "MediumOffice" => 47.49307973,
            "MidriseApartment" => 805.0965778,
            "Outpatient" => 2971.868562,
            "PrimarySchool" => 981.4176700999999,
            "RetailStore" => 755.4523907,
            "SecondarySchool" => 4338.227865999999,
            "SmallHotel" => 232.2194443,
            "SmallOffice" => 43.25360481,
            "StripMall" => 760.0982018,
            "Supermarket" => 2302.228741,
            "warehouse" => 1151.250885,
            "FlatLoad" => 1854.437216
        ),
        "Seattle" => Dict(
            "FastFoodRest" => 255.5992711,
            "FullServiceRest" => 627.5634984000001,
            "Hospital" => 11935.157290000001,
            "LargeHotel" => 3343.683348,
            "LargeOffice" => 5266.970348,
            "MediumOffice" => 28.97979768,
            "MidriseApartment" => 1117.5465470000001,
            "Outpatient" => 3468.128914,
            "PrimarySchool" => 1263.541878,
            "RetailStore" => 952.2758742000001,
            "SecondarySchool" => 6367.850187,
            "SmallHotel" => 310.8087307,
            "SmallOffice" => 49.34878545,
            "StripMall" => 969.1074739000001,
            "Supermarket" => 3004.1844929999997,
            "warehouse" => 1137.398514,
            "FlatLoad" => 2506.1340600000003
        ),
        "Chicago" => Dict(
            "FastFoodRest" => 441.93439000000006,
            "FullServiceRest" => 888.3312571,
            "Hospital" => 12329.57943,
            "LargeHotel" => 5104.848129,
            "LargeOffice" => 7706.028917000001,
            "MediumOffice" => 216.01411800000002,
            "MidriseApartment" => 1482.040156,
            "Outpatient" => 3506.5381090000005,
            "PrimarySchool" => 2006.0002120000001,
            "RetailStore" => 1472.8704380000001,
            "SecondarySchool" => 8962.172873,
            "SmallHotel" => 479.4653436000001,
            "SmallOffice" => 94.19308949,
            "StripMall" => 1497.556168,
            "Supermarket" => 3696.2112950000005,
            "warehouse" => 2256.477231,
            "FlatLoad" => 3258.766323
        ),
        "Boulder" => Dict(
            "FastFoodRest" => 306.8980525,
            "FullServiceRest" => 642.8843574,
            "Hospital" => 9169.381845,
            "LargeHotel" => 3975.1080020000004,
            "LargeOffice" => 5027.882454,
            "MediumOffice" => 124.26913059999998,
            "MidriseApartment" => 1098.944993,
            "Outpatient" => 3087.969786,
            "PrimarySchool" => 1356.396807,
            "RetailStore" => 1086.9187570000001,
            "SecondarySchool" => 6268.036872,
            "SmallHotel" => 342.77800099999996,
            "SmallOffice" => 65.95714912,
            "StripMall" => 1093.093638,
            "Supermarket" => 2966.790122,
            "warehouse" => 1704.8648210000001,
            "FlatLoad" => 2394.8859239999997
        ),
        "Minneapolis" => Dict(
            "FastFoodRest" => 588.8854722,
            "FullServiceRest" => 1121.229499,
            "Hospital" => 13031.2313,
            "LargeHotel" => 6359.946704,
            "LargeOffice" => 10199.279129999999,
            "MediumOffice" => 394.1525556,
            "MidriseApartment" => 1814.148381,
            "Outpatient" => 3661.1462229999997,
            "PrimarySchool" => 2600.964302,
            "RetailStore" => 1869.8106289999998,
            "SecondarySchool" => 11963.323859999999,
            "SmallHotel" => 618.0427338999999,
            "SmallOffice" => 128.12525349999999,
            "StripMall" => 1952.731917,
            "Supermarket" => 4529.776664,
            "warehouse" => 3231.223746,
            "FlatLoad" => 4004.001148
        ),
        "Helena" => Dict(
            "FastFoodRest" => 468.8276835,
            "FullServiceRest" => 934.8994934,
            "Hospital" => 10760.57411,
            "LargeHotel" => 5554.910785,
            "LargeOffice" => 7373.056709,
            "MediumOffice" => 239.8330306,
            "MidriseApartment" => 1531.102079,
            "Outpatient" => 3390.42972,
            "PrimarySchool" => 2097.777112,
            "RetailStore" => 1494.85988,
            "SecondarySchool" => 9535.484059,
            "SmallHotel" => 499.85992930000003,
            "SmallOffice" => 98.85818175,
            "StripMall" => 1604.0043970000002,
            "Supermarket" => 3948.5338049999996,
            "warehouse" => 2504.784991,
            "FlatLoad" => 3252.362248
        ),
        "Duluth" => Dict(
            "FastFoodRest" => 738.1353594999999,
            "FullServiceRest" => 1400.36692,
            "Hospital" => 14179.84149,
            "LargeHotel" => 7781.9012760000005,
            "LargeOffice" => 12504.64187,
            "MediumOffice" => 468.2112216,
            "MidriseApartment" => 2204.85149,
            "Outpatient" => 3774.3233130000003,
            "PrimarySchool" => 3160.1200719999997,
            "RetailStore" => 2298.8242920000002,
            "SecondarySchool" => 14468.64346,
            "SmallHotel" => 772.5386662000001,
            "SmallOffice" => 155.8350887,
            "StripMall" => 2411.847491,
            "Supermarket" => 5587.977185,
            "warehouse" => 3962.122014,
            "FlatLoad" => 4741.886326
        ),
        "Fairbanks" => Dict(
            "FastFoodRest" => 1245.3608279999999,
            "FullServiceRest" => 2209.293209,
            "Hospital" => 20759.042680000002,
            "LargeHotel" => 12298.7791,
            "LargeOffice" => 23214.51532,
            "MediumOffice" => 949.8812392000001,
            "MidriseApartment" => 3398.039504,
            "Outpatient" => 4824.076322999999,
            "PrimarySchool" => 6341.861225,
            "RetailStore" => 3869.670979,
            "SecondarySchool" => 25619.149269999998,
            "SmallHotel" => 1264.41064,
            "SmallOffice" => 297.08593010000004,
            "StripMall" => 3934.89178,
            "Supermarket" => 8515.422039000001,
            "warehouse" => 6882.6512680000005,
            "FlatLoad" => 7851.508208
        )
    )
    if isempty(city)
        city = find_ashrae_zone_city(latitude, longitude)
    end
    if !(buildingtype in default_buildings)
        error("buildingtype $(buildingtype) not in $(default_buildings).")
    end
    if isnothing(annual_mmbtu)
        annual_mmbtu = spaceheating_annual_mmbtu[city][buildingtype]
    end
    built_in_load("space_heating", city, buildingtype, year, annual_mmbtu, monthly_mmbtu)
end