"""
    eulerivp(dudt,tspan,u0,n)

Apply Euler's method to solve the IVP u'=`dudt`(u,t) over the interval `tspan` with
u(`tspan[1]`)=`u0`, using `n` subintervals/steps. Return vectors of times and solution
values.
"""
function eulerivp(dudt,tspan,u0,n)
    a,b = tspan
    h = (b-a)/n
    t = [ a + i*h for i=0:n ]
    u = zeros(n+1)
    u[1] = u0
    for i = 1:n
      u[i+1] = u[i] + h*dudt(u[i],t[i])
    end
    return t,u
end

"""
    eulersys(dudt,tspan,u0,n)

Apply Euler's method to solve the vector-valued IVP u'=`dudt`(u,p,t) over the interval
`tspan` with u(`tspan[1]`)=`u0`, using `n` subintervals/steps.

(This is the same code as `eulerivp`. In the system case it returns a vector of vector
values as the solution.)
"""
function eulersys(dudt,tspan,u0,n)
    # Time discretization.
    a,b = tspan
    h = (b-a)/n
    t = [ a + i*h for i=0:n ]

    # Initial condition and output setup.
    u = Vector{typeof(u0)}(undef,n+1)
    u[1] = u0

    # The time stepping iteration.
    for i = 1:n
        u[i+1] = u[i] + h*dudt(u[i],t[i])
    end
    return t,u
end

"""
    ie2(dudt,tspan,u0,n)

Apply the Improved Euler method to solve the vector-valued IVP u'=`dudt`(u,p,t) over the
interval `tspan` with u(`tspan[1]`)=`u0`, using `n` subintervals/steps. Returns a vector
of times and a vector of solution values/vectors.
"""
function ie2(dudt,tspan,u0,n)
    # Time discretization.
    a,b = tspan
    h = (b-a)/n
    t = [ a + i*h for i=0:n ]

    # Initialize output.
    u = Vector{typeof(u0)}(undef,n+1)
    u[1] = u0

    # Time stepping.
    for i = 1:n
        uhalf = u[i] + h/2*dudt(u[i],t[i]);
        u[i+1] = u[i] + h*dudt(uhalf,t[i]+h/2);
    end
    return t,u
end

"""
    rk4(dudt,tspan,u0,n)

Apply "the" Runge-Kutta 4th order method to solve the vector-valued IVP u'=`dudt`(u,p,t)
over the interval `tspan` with u(`tspan[1]`)=`u0`, using `n` subintervals/steps.
Return a vector of times and a vector of solution values/vectors.
"""
function rk4(dudt,tspan,u0,n)
    # Time discretization.
    a,b = tspan
    h = (b-a)/n
    t = [ a + i*h for i=0:n ]

    # Initialize output.
    u = Vector{typeof(u0)}(undef,n+1)
    u[1] = u0

    # Time stepping.
    for i = 1:n
        k1 = h*dudt( u[i],      t[i]     )
        k2 = h*dudt( u[i]+k1/2, t[i]+h/2 )
        k3 = h*dudt( u[i]+k2/2, t[i]+h/2 )
        k4 = h*dudt( u[i]+k3,   t[i]+h   )
        u[i+1] = u[i] + (k1 + 2*(k2 + k3) + k4)/6
    end
    return t,u
end

"""
    rk23(dudt,tspan,u0,tol)

Apply adaptive embedded RK formula to solve the vector-valued IVP u'=`dudt`(u,p,t)
over the interval `tspan` with u(`tspan[1]`)=`u0`, with error tolerance `tol`.
Return a vector of times and a vector of solution values/vectors.
"""
function rk23(dudt,tspan,u0,tol)
    # Initialize for the first time step.
    t = tspan[1]
    u = [u0];   i = 1;
    h = 0.5*tol^(1/3)
    s1 = dudt(u0,t)

    # Time stepping.
    while t[i] < tspan[2]
        # Detect underflow of the step size.
        if t[i]+h == t[i]
            @warn "Stepsize too small near t=$(t[i])"
            break  # quit time stepping loop
        end

        # New RK stages.
        s2 = dudt( u[i]+(h/2)*s1,   t[i]+h/2   )
        s3 = dudt( u[i]+(3*h/4)*s2, t[i]+3*h/4 )
        unew2 = u[i] + h*(2*s1 + 3*s2 + 4*s3)/9   # 2rd order solution
        s4 = dudt( unew2, t[i]+h )
        err = h*(-5*s1/72 + s2/12 + s3/9 - s4/8)    # 2nd/3rd order difference
        E = norm(err,Inf)                           # error estimate
        maxerr = tol*(1 + norm(u[i],Inf))         # relative/absolute blend

        # Accept the proposed step?
        if E < maxerr     # yes
            t = [ t; t[i] + h]
            push!(u,unew2)
            i = i+1;
            s1 = s4;      # use FSAL property
        end

        # Adjust step size.
        q = 0.8*(maxerr/E)^(1/3)       # conservative optimal step factor
        q = min(q,4)                   # limit stepsize growth
        h = min(q*h,tspan[2]-t[i])     # don't step past the end
    end
    return t,u
end

"""
    ab4(dudt,tspan,u0,n)

Apply the Adams-Bashforth 4th order method to solve the vector-valued IVP u'=`dudt`(u,p,t)
over the interval `tspan` with u(`tspan[1]`)=`u0`, using `n` subintervals/steps.
"""
function ab4(dudt,tspan,u0,n)
    # Time discretization.
    a,b = tspan
    h = (b-a)/n
    t = [ a + i*h for i=0:n ]

    # Constants in the AB4 method.
    k = 4;    sigma = [55, -59, 37, -9]/24;

    # Find starting values by RK4.
    u = Vector{typeof(u0)}(undef,n+1)
    ts,us = rk4(dudt,[a,a+(k-1)*h],u0,k-1)
    u[1:k] = us[1:k]

    # Compute history of u' values, from newest to oldest.
    f = [ dudt(u[k-j],t[k-j]) for j=1:k-1  ]

    # Time stepping.
    for i = k:n
      f = [ dudt(u[i],t[i]), f[1:k-1]... ]   # new value of du/dt
      u[i+1] = u[i] + h*sum(f[j]*sigma[j] for j=1:k)       # advance one step
    end
    return t,u
end

"""
    am2(dudt,tspan,u0,n)

Apply the Adams-Moulton 2nd order method to solve the vector-valued IVP u'=`dudt`(u,p,t)
over the interval `tspan` with u(`tspan[1]`)=`u0`, using `n` subintervals/steps.
"""
function am2(dudt,tspan,u0,n)
    # Time discretization.
    a,b = tspan
    h = (b-a)/n
    t = [ a + i*h for i=0:n ]

    # Initialize output.
    u = Vector{typeof(u0)}(undef,n+1)
    u[1] = u0

    # Time stepping.
    for i = 1:n
        # Data that does not depend on the new value.
        known = u[i] + h/2*dudt(u[i],t[i])
        # Find a root for the new value.
        F = z -> z - h/2*dudt(z,t[i+1]) - known
        unew = levenberg(F,known)
        u[i+1] = unew[:,end]
    end
    return t,u
end

# This version is needed to work with scalar problems using levenberg().
function am2(dudt,tspan,u0::Number,n)
    f = (x,t) -> [dudt(x[1],t)]
    t,u = am2(f,tspan,[u0],n)
    u = [u[1] for u=u]
    return t,u
end


#
# Heap Sort
#

function heapSort(A)

  buildHeap(A)

  n = length(A)
  i = n
  while i > 0

    tmp = A[1]
    A[1] = A[i]
    A[i] = tmp

    heapify(A, 1, i)

    i = i - 1
  end

end

function buildHeap(A)

  n = length(A)
  i = Int(n/2)
  while i > 0

    heapify(A, i, n)

    i = i - 1
  end

end

function heapify(A, idx, idx_max)
  largest = idx
  left = 2 * (idx-1) + 1
  right = 2 * (idx-1) + 2

  if left < idx_max && A[left] > A[idx]
    largest = left
  end
  if right < idx_max && A[right] > A[largest]
    largest = right
  end
  if largest != idx
    tmp = A[idx]
    A[idx] = A[largest]
    A[largest] = tmp
    heapify(A,  largest,  idx_max)
  end
end

function main()

  # list to be sorted
  rng = MersenneTwister(1234);
  A = shuffle(rng, Vector(1:100))
  Aorigin = copy(A)

  # sorting
  @time heapSort(A)

  print("The original list: ")
  println(Aorigin)

  print("The sorted list: ")
  println(A)

end

if contains(@__FILE__, PROGRAM_FILE)
    main()
end


# # Mass dashpot spring

#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/models/massDashpotSpring.ipynb)
#

# ## Free oscillations

using StructuralDynamicsODESolvers, Plots, LinearAlgebra

k  = 2 ; m  = .5 ;  c = 0 ;
u0 = 1 ; v0 = 0 ;

M = m * ones(1, 1)
C = c * ones(1, 1)
K = k * ones(1, 1)
R = zeros(1)

sys = SecondOrderAffineContinuousSystem(M, C, K, R)

U₀ = u0 * ones(1); V₀ = v0 * ones(1);

ivp_free = InitialValueProblem(sys, (U₀, V₀))

NSTEPS = 1000 ;
Δt = 0.005 ;

#-

alg = Bathe(Δt = Δt )
sol = solve(ivp_free, alg, NSTEPS=NSTEPS);

#-

# The following command is the same as `plot(times(sol), displacements(sol, 1))`.

plot(sol, vars=(0, 1))

# ## Forced oscillations
#
# ### Problem definition
# Let us consider now a forcing term $f(t) = A_f \sin(ω_f \cdot t)$
#
ωN = sqrt(k/m)
ωf = ωN * 2
Af = 10.0
R  = [ [ Af * sin(ωf * Δt * (i-1) ) ] for i in 1:NSTEPS+1];

# ### Second order problem resolution

X   = nothing # state constraints are ignored
B   = ones(1, 1)
sys = SecondOrderConstrainedLinearControlContinuousSystem(M, C, K, B, X, R)

ivp_forced_secOrder = InitialValueProblem(sys, (U₀, V₀))

alg = Bathe(Δt = Δt )
sol_secOrder = solve(ivp_forced_secOrder, alg, NSTEPS=NSTEPS);

# ### First order homogeneization formulation
# The problem can be re-formulated as a first order and homogeneous one given by
#
#
# ```math
# \left\{
# \begin{array}{l}
# \dot{u} = v \\
# \dot{v} = -\omega_N^2 u + u_f/m \\
# \dot{u_f} = v_f \\
# \dot{v_f} = -\omega_f^2 u_f
# \end{array}
# \right.
# ```
# The new vector of variables is
#
# ```math
# \textbf{x} = [ u, v, u_f, v_f ]^T
# ```

K = [     0 1     0 0 ;
      -ωN^2 0     1/m 0 ;
          0 0     0 1 ;
          0 0 -ωf^2 0 ] ;

C = -Diagonal(ones(4))
M = zeros(4,4)
R = zeros(4)

sys = SecondOrderAffineContinuousSystem(M, C, K, R)

U₀ = [u0; v0; 0; ωf*Af ] ;

ivp_forced_firOrder = InitialValueProblem(sys, (U₀, U₀) )

alg = BackwardEuler(Δt = Δt )
sol_firOrderA = solve(ivp_forced_firOrder, alg, NSTEPS=NSTEPS);

NSTEPS = NSTEPS*3 ; alg = BackwardEuler(Δt = Δt/3.0 )
sol_firOrderB = solve(ivp_forced_firOrder, alg, NSTEPS=NSTEPS);

# The solution obtained is

plot(sol_secOrder, vars=(0, 1), xlab="time" )
plot!(sol_firOrderA, vars=(0, 1))
plot!(sol_firOrderB, vars=(0, 1))


#========================================================================================#
#	Laboratory 4
#
# Tuples, Pairs, Dict, Symbol, files, Dates, Random numbers and downloading.
# Files and Dates are available in Julia Programming Cookbook
#
# Author: Niall Palfreyman, 13/01/2022
#========================================================================================#
[
	Activity(
		"""
		In this laboratory we introduce several helpful Julia tools that make life a
		little easier: Tuples, Pairs, Dicts, Symbols, Files, DateTimes, Random and downloading.

		A Tuple is an immutable container that can contain several different types. We
		construct Tuples using round brackets, for example:

			my_tuple = (5, 2.718, "Niall")

		What is my_tuple[2]?
		""",
		"my_tuple[2]",
		x -> x==Main.my_tuple[2]
	),
	Activity(
		"""
		You have already seen that size() returns a Tuple. Enter the following code:

			ret_size = size(zeros(3,4))

		What is the value of ret_size[1]? Try changing the value of ret_size[2].
		Finally, what is the type of ret_size?
		""",
		"typeof(ret_size)",
		x -> x==typeof(size(zeros(3,4)))
	),
	Activity(
		"""
		Tuples are especially useful when we want to define anonymous functions with
		more than one argument:

			map((x,y)->3x+2y,4,5)

		Construct a single line mapping that calculates sin(x*y) for corresponding
		elements in the two ranges x = 1:5 and y = 5:-1:1
		""",
		"map((x,y)->sin(x*y),1:5,5:-1:1)",
		x -> x==map((x,y)->sin(x*y),1:5,5:-1:1)
	),
	Activity(
		"""
		A Pair is a structure that contains two objects - typically a key and its
		entry in a dictionary. We construct a Pair like this:

			my_pair = "Yellow Submarine" => "Beatles"

		Construct my_pair and then find the value of last(my_pair):
		""",
		"",
		x -> x==last("Yellow Submarine" => "Beatles")
	),
	Activity(
		"""
		A Dict(ionary) is a hashed database of Pairs. Dicts are very lightweight, so
		we can easily use them in everyday code. We construct a Dict by passing a
		sequence of Pairs to the constructor:

			my_dict = Dict( "pi" => 3.142, "e" => 2.718)

		Construct my_dict, and notice that the Pairs are not necessarily in the same
		order that you entered them. Now look up the value of "pi" in my_dict:
		""",
		"my_dict[\"pi\"]",
		x -> x==3.142
	),
	Activity(
		"""
		We can find out whether our Dict contains the key "e" by using the keys() function:

			"e" in keys(my_dict)

		Delete the entry for "e" from my_dict using the delete!() function. What word do
		you now see in red if you enter my_dict["e"]?
		""",
		"delete!(my_dict,\"e\")",
		x -> x=="ERROR"
	),
	Activity(
		"""
		Now add two extra entries to my_dict:

			my_dict["root2"] = 1.414
			my_dict["epsilon0"] = 8.854e-12

		What is the result of calling haskey(my_dict,"root2")?
		""",
		"",
		x -> x==true
	),
	Activity(
		"""
		We have already met Symbols - they define components of the Julia language, and
		we use them to extend the language with new components. We construct Symbols
		using the colon ':'. Enter the following lines:

			a,b = 2,3
			expr = :(a+b)
			dump(expr)

		What is the value of expr.args?
		""",
		"",
		x -> x==Main.expr.args
	),
	Activity(
		"""
		You can see that the arguments of expr are Symbols waiting to be evaluated. Try:

			typeof(expr.args[2])

		What do you get if you enter string(expr)?
		""",
		"",
		x -> occursin("a+b",replace(x," "=>""))
	),
	Activity(
		"""
		Symbols and Strings are very similar to each other. You will see that graphics
		functions often use Symbols to define special switches such as :red or :filled.

		In fact, a Symbol is a String that has been prepared for being evaluated.
		What do you get if you apply the function eval() to expr?
		""",
		"eval(expr)",
		x -> x==5
	),
	Activity(
		"""
		We use the "splat" operator (...) to convert a collection into a set of
		arguments for a function. Define this function and test it with a few numbers:

			my_function(x,y,z) = x * (y+z)

		Now define this vector: v = [2,3,4]. Suppose we want to use the three numbers
		in v as arguments for my_function. First try it like this: my_function(v), and
		see what happens ...

		It didn't work, did it? Now tell me what you get when you enter this:

			my_function(v...)
		""",
		"",
		x -> x==14
	),
	Activity(
		"""
		Files are an important part of everyday data science. First of all, we need to
		be able to find files in the background filesystem. Enter: pwd() at the Julia
		prompt. This stands for Present Working Directory ...

		What you get back is the path of the current folder. Now enter: readdir() ...

		This gives you a list of filenames in the current folder. Choose a filname in
		this list - for example maybe "filename.ext". Ask whether your file is in the
		current folder. What answer do you get back?
		""",
		"\"filename.ext\" in readdir()",
		x -> x==true
	),
	Activity(
		"""
		pwd() gives us the path of the current folder, and readdir() gives us a vector
		of filenames in it. We can put these together using joinpath(). Find out the
		answer returned by the following function call:

			isfile(joinpath(pwd(),"filename.ext"))

		where filename.ext is the file you previously found in the current directory.
		""",
		"Investigate the value returned by the joinpath() call",
		x -> true
	),
	Activity(
		"""
		Now let's create a new file. The following command creates a file named
		"my_data.txt", and sets up a FILESTREAM named "file" for "w"riting to it:

			file = open("my_data.txt","w")

		Now write some information to the file:

			write( file, "This is my file\nIt belongs to me!\n")

		The value returned by write() is the number of bytes you have written to the
		file. We can also us the print() and println() functions, for example:

			println( file, "It really does!")

		Finally, close() file and tell me the result of isfile("my_data.txt"):
		""",
		"close(file)",
		x -> x==true
	),
	Activity(
		"""
		Congratulations! You have created your first file! Now let's trying
		r(eading) from the file:

			file = open("my_data.txt","r")

		Now enter: readline(file) several times to read the lines of the file.
		Once you have read all the lines, the file is in an end-of-file state:

			eof(file)

		What value does readline() return if you continue to read lines after
		the end-of-file?
		""",
		"readline(file)",
		x -> isempty(x)
	),
	Activity(
		"""
		Now close() the file, then reopen it again to start reading at the
		beginning of the file. We can read all lines of the file at once.
		What is the type of the structure returned by the following code?

			file = open("my_data.txt","r")
			readlines( file)
		""",
		"",
		x -> x <: Vector
	),
	Activity(
		"""
		If our file contained binary data, it would not be possible to read
		it in separate lines - in this case we use the read() function.

		Rewind the file to the beginning using: seekstart(file).
		Enter read(file) to see the characters in the file, and tell me the
		first character:
		""",
		"0x54: Scroll the Julia console back up to see the beginning",
		x -> x==0x54
	),
	Activity(
		"""
		Well, that wasn't very pleasant, was it, with all those characters
		screaming across the screen? Let's do it in a more civilised way this time...

		Rewind the file to the beginning using seekstart().
		Now enter:

			data = read(file);

		Did you remember to write ';' at the end of the line? If not, you
		had the "screaming characters" problem. ';' at the end of a line
		stops the return value being written to the console. Now tell me
		the value of the fifth element of data:
		""",
		"data[5]",
		x -> x==0x20
	),
	Activity(
		"""
		This hex code represents a character. Can you convert the code to
		a character?
		""",
		"Char(data[5])",
		x -> x==' '
	),
	Activity(
		"""
		We can even convert the data entirely to a String like this:

			str = String(data)

		However, this conversion uses up the data values. What value is
		now returned by the function call isempty(data)?
		""",
		"",
		x -> x==true
	),
	Activity(
		"""
		Finally, we must always close a filestream after we have finished
		with it: close(file).
		Also, we should clean up afterwards, so now remove the file we have
		created using rm("my_data.txt"), and tell me the return type of rm():
		""",
		"First call rm(), and then ask: typeof(ans)",
		x -> x==Nothing
	),
	Activity(
		"""
		Now we investigate DateTimes in Julia. Support for date and time
		handling is provided by the Dates package, which we must first load:

			using Dates

		We can access the current time using the now() function:

			datim = Dates.now()

		What is the type of datim?
		""",
		"",
		x -> x==Main.DateTime
	),
	Activity(
		"""
		To create a new date, we pass year, month and day to the constructor:

			Date( 1996, 7, 16)
			Date( 2020, 6)

		What Date value is constructed by the call Date(2022)?
		""",
		"",
		x -> x==Main.Date(2022)
	),
	Activity(
		"""
		We can also create times. Use the minute() function to find the number
		of minutes past the hour in this time:

			DateTime(1992,10,13,6,18)
		""",
		"minute(ans)",
		x -> x==Main.minute(Main.DateTime(1992,10,13,6,18))
	),
	Activity(
		"""
		The module Dates also makes available Periods of time. Use the subtypes()
		function to find the subtypes of Period and also the subtypes of these
		subtypes. How many subtypes does the type TimePeriod have?
		""",
		"subtypes(TimePeriod)",
		x -> x==length(Main.subtypes(Main.TimePeriod))
	),
	Activity(
		"""
		However, we don't just want to construct dates and times - we usually
		want to PARSE (i.e., analyse) them. We can construct a Date from a
		String by passing a DateTime format argument:

			Date("19760915","yyyymmdd")

		For DateTimes, this format gets a little more complicated, so you may
		wish to define your own format:

			format = DateFormat("HH:MM, dd.mm.yyyy")

		Use this format to parse the DateTime "06:18, 13.10.1992". What
		character separates the date from the time in the result?
		""",
		"",
		x -> x=='T'
	),
	Activity(
		"""
		DateTimes contains many useful functions that you can look up at
		docs.julialang.org. For example, use the function dayname() to find
		out the day on which you were born ...

		When you've finished experimenting, just enter reply() to move on
		to the next activity.
		""",
		"",
		x -> true
	),
	Activity(
		"""
		Find out your age in days by subtracting your birthday Date from
		today(), then move on with reply():
		""",
		"",
		x -> true
	),
	Activity(
		"""
		Form a list of the past 8 days by collecting this Range into a Vector:

			today()-Week(1):Day(1):today()
		""",
		"",
		x -> x==Main.eval(:(collect((today()-Week(1)):Day(1):today())))
	),
	Activity(
		"""
		Next we'll look at a very important tool of data science: Random numbers.
		First load the functions that we'll be using:

			using Random: rand, randn, seed!

		By itself, the rand() function returns a pseudo-random Float64 number in the
		half-open interval [0.0,1.0). Try this now.

		If you pass rand() a range as its first argument, it returns a random element
		from that range. If you pass it a tuple of integers, it returns an array with
		the size specified by those integers. Use rand() to produce a (2,3) array of
		integer values between -1 and +1:
		""",
		"rand(Range,Size)",
		x -> (typeof(x) <: Matrix{Int}) && size(x) == (2,3) && all(map(y->in(y,-1:1),x))
	),
	Activity(
		"""
		randn() works just the same as rand(), but returns normally distributed values
		with mean 0.0 and standard deviation 1.0. Create a (5,7) array of such normally
		distributed numbers:
		""",
		"",
		x -> size(x) == (5,7) && sum(x)/length(x) < 0.5
	),
	Activity(
		"""
		When we perform simulations with random numbers, we never know in advance how
		our program will behave. On the one hand, this is certainly good, because many
		biological processes are essentially random. On the other hand, it can be very
		frustrating if you observe some special problem or phenomenon, because you may
		never again be able to reproduce that special situation. Because of this, we
		need to be able to make random-number generation REPRODUCIBLE. We do this by
		SEEDING the random-number generator (rng).

		To do this, we use the function seed!() at the beginning of our program to make
		sure all random numbers follow an identical pattern across separate runs of the
		program. Run the following code several times, then tell me what result you get:

			seed!(123); rand(5)
		""",
		"",
		x -> x==(Main.seed!(123); Main.rand(5))
	),
	Activity(
		"""
		OK, and fi-inally at the end of this very long (phew!) laboratory, we look
		briefly at how to download resources from the internet. First, we load the
		download() function from the package Downloads:

			using Downloads: download

		Next we define the url of our resource:

			url = "https://raw.githubusercontent.com/NiallPalfreyman/Ingolstadt.jl/main/src/Ingolstadt.jl"

		Next, we download this page into a local file:

			file = download(url)

		Use readlines() (don't forget the ';'!) to discover the Date on which Niall
		Palfreyman started writing the Ingolstadt project:
		""",
		"data = readlines(file);",
		x -> x == Main.Date("7/12/2021","d/mm/yyyy")
	),
	Activity(
		"""
		OK, that's the end of this laboratory. The resource we have downloaded is my
		source code - feel free to explore it and use it as much as you like. Bye! :-)
		""",
		"",
		x -> true
	),
]


using LinearAlgebra: norm

"""
linearspace(start, stop, N)

Generate linear space.

Generate a linear sequence of `N` numbers between  `start` and `stop` (i. e.
sequence  of number with uniform intervals inbetween).

# Example
```
julia> linearspace(2.0, 3.0, 5)
2.0:0.25:3.0
```
"""
function linearspace(start, stop, N)
    return collect(range(Float64(start), stop = Float64(stop), length = Int64(N)))
end

"""
gradedspace(start, stop, N, strength=2)

Generate graded space.

Generate a graded sequence of `N` numbers between `start` and `stop`. This
sequence corresponds to separation of adjacent numbers that increases in
proportion corresponding to the power coefficient `strength`
from start to finish.

# Example
```
julia> gradedspace(2.0, 3.0, 5)
5-element Array{Float64,1}:
2.0
2.0625
2.25
2.5625
3.0
```
"""
function gradedspace(start, stop, N, strength=2)
    N = Int64(N)
    x = range(0.0, stop = 1.0, length = N);
    x = x.^strength
    # for i = 1:strength
    #     x = cumsum(x);
    # end
    x = x./maximum(x);
    out = Float64(start) .* (1.0 .- x) .+ Float64(stop) .* x;
end


## sigmoidal functions

export logistic, logistic_prime

logistic(x) = 1/(1 + exp(-x))
logistic_prime(x) = exp(-x)/(1 + exp(-x))^2


using SparseArrays

function clique_expansion_homophily(N,k_range)
    # Compute homophily index in the clique expansion of the hypergraph
    In1 = 0
    In2 = 0
    Cross = 0
    for k = k_range
        for j = 1:k+1
            i = j-1         # number of women in this type hyperedge
            Hik = N[k,j]    # number of hyperedges of this type
            In1 += binomial(i,2)*Hik    # number of extra edges of this type
            In2 += binomial(k-i,2)*Hik
            Cross += Hik*i*(k-i)
        end
    end

    hw = 2*In1/(2*In1 + Cross)
    hm = 2*In2/(2*In2 + Cross)
    return hw, hm
end

function n_baselines(k,n,alpha)
    n1 = round(Int64,floor(n*alpha))
    n2 = n - n1

    b = zeros(k)
    for t = 1:k
        b[t] = binomial(n1-1,t-1)*binomial(n2,k-t)/binomial(n-1,k-1)
    end
    return b

end

function n1_n2_baselines(k,n1,n2)
    n = n1+n2

    b1 = zeros(k)
    b2 = zeros(k)
    for t = 1:k
        b1[t] = binomial(n1-1,t-1)*binomial(n2,k-t)/binomial(n-1,k-1)
        b2[t] = binomial(n2-1,t-1)*binomial(n1,k-t)/binomial(n-1,k-1)
    end
    return b1, b2
end

function n_alt_baselines(k,n,alpha)
    n1 = round(Int64,floor(n*alpha))
    n2 = n - n1

    b = zeros(k)
    denom = 0
    for t = 1:k
        b[t] = binomial(n1,t)*binomial(n2,k-t)
    end
    return b/sum(b)

end

function arbitrary_baselines(k,alpha)
    """
    Returns asymptotic relative baseline scores for a single class that makes up
        a ratio alpha of the total population. Assumes k-uniform hypergraph.

        k = hyperedge size
        alpha = proportion of nodes in the class of interest
    """
    b = zeros(k)
    for j = 1:k
        b[j] = alpha^(j-1)*(1-alpha)^(k-j)*binomial(k-1,j-1)
    end
    return b
end

function alternative_baselines(k,alpha)
    """
    This is for the alternative definition of affinity score, that doesn't
        degree correct.

    Returns asymptotic relative baseline scores for a single class that makes up
        a ratio alpha of the total population. Assumes k-uniform hypergraph.

        k = hyperedge size
        alpha = proportion of nodes in the class of interest
    """
    b = zeros(k)
    denom = sum(alpha^(i)*(1-alpha)^(k-i)/(factorial(i)*factorial(k-i)) for i = 1:k)
    for j = 1:k
        b[j] = alpha^(j)*(1-alpha)^(k-j)/(denom*(factorial(j)*factorial(k-j)))
    end
    return b
end

function baselines(r,alpha)
    """
    Returns asymptotic relative baseline scores for a single class that makes up
        a ratio alpha of the total population. Does this for all hyperedge sizes
        from 1 to r.
    """
    B = zeros(r,r)
    for k = 1:r
        for t = 1:k
            B[k,t] = alpha^(t-1)*(1-alpha)^(k-t)*binomial(k-1,t-1)
        end
    end
    return B
end

function order_degree(H::SparseMatrixCSC{Float64,Int64})
    """Returns hyperedge sizes and degrees for a hypergraph."""
    order = vec(sum(H,dims=2))
    deg = vec(sum(H,dims=1))
    return order, degree
end

function get_hyperedge_counts(H::SparseMatrixCSC{Float64,Int64},classes::Vector{Int64})
    """
    Compute number of each type of hyperedge. Output is a matrix N, such that
        N[k,j] = number of hyperedge of size k that have j-1 nodes of class 1
    """
    order, degree = order_degree(H)
    r = round(Int64,maximum(order))

    # Ensure classes are 0 and 1
    cs = unique(classes)
    @assert(length(cs) == 2)
    @assert(minimum(cs) == 0 && maximum(cs) == 1)
    Hyp = incidence2elist(H)
    N = zeros(r,r+1)
    for edge in Hyp
        board = classes[edge]
        w = sum(board)
        k = length(board)
        if k > 0
            N[k,w+1] += 1
        end
    end
    return N
end

function relative_affinities(H::SparseMatrixCSC{Float64,Int64},classes::Vector{Int64},r::Int64=0)
    """
    Compute relative affinity scores for both classes in a hypergraph H with
        two classes.

        Aff1[k,t] = type-t relative affinity score for size k hyperedges, for class 1
        Aff0[k,t] = type-t relative affinity score for size k hyperedges, for class 0
    """
    N = get_hyperedge_counts(H,classes)
    if r == 0
        r = size(N,1)   # max hyperedge size
    end
    Aff1 = zeros(r,r)
    Aff0 = zeros(r,r)
    for k = 1:r
        c1_denom = sum(i*N[k,i+1] for i = 1:k)
        c0_denom = sum(i*N[k,k-i+1] for i = 1:k)
        for t = 1:k
            Aff1[k,t] = t*N[k,t+1]/c1_denom
            Aff0[k,t] = t*N[k,k-t+1]/c0_denom
        end
    end
    return Aff1, Aff0
end

function affinities_from_N(N)
    r = size(N,1)
    Aff1 = zeros(r,r)
    Aff0 = zeros(r,r)
    for k = 1:r
        c1_denom = sum(i*N[k,i+1] for i = 1:k)
        c0_denom = sum(i*N[k,k-i+1] for i = 1:k)
        for t = 1:k
            Aff1[k,t] = t*N[k,t+1]/c1_denom
            Aff0[k,t] = t*N[k,k-t+1]/c0_denom
        end
    end
    return Aff1, Aff0
end

function relative_affinities_k(H::SparseMatrixCSC{Float64,Int64},classes::Vector{Int64},k::Int64)
    """
    Compute relative affinity scores for both classes in a hypergraph H with
        two classes, only for hyperedges of size k.

        Aff1[t] = type-t relative affinity score for class 1
        Aff0[t] = type-t relative affinity score for class 0
    """
    N_all = get_hyperedge_counts(H,classes)
    N = N_all[k,:]
    Aff1 = zeros(k)
    Aff0 = zeros(k)
    c1_denom = sum(i*N[i+1] for i = 1:k)
    c0_denom = sum(i*N[k-i+1] for i = 1:k)
    for t = 1:k
        Aff1[t] = t*N[t+1]/c1_denom
        Aff0[t] = t*N[k-t+1]/c0_denom
    end
    return Aff1, Aff0
end

function incidence2elist(Hin::SparseMatrixCSC{Float64,Int64})
    """ Convert a hyperedge list to a hypergraph binary incidence matrix. """
    H = sparse(Hin')
    rp = H.rowval
    ci = H.colptr
    Hyperedges = Vector{Vector{Int64}}()
    n,m = size(H)
    for i = 1:m
        startedge = ci[i]
        endedge = ci[i+1]-1
        edge = rp[startedge:endedge]
        push!(Hyperedges,edge)
    end
    return Hyperedges
end

function elist2incidence(Hyperedges::Vector{Vector{Int64}}, N::Int64)
    """
    Take a list of hyperedges and turn it into a hyperedge incidence matrix
    H. N is the number of nodes in the hypergraph.
    H(e,u) = 1  iff node u is in hyperedge e.
    """
    U = Vector{Int64}()
    E = Vector{Int64}()
    M = length(Hyperedges)
    for enum = 1:length(Hyperedges)
        e = Hyperedges[enum]
        for node in e
            push!(U,node)
            push!(E,enum)
        end
    end

    H = sparse(E,U,ones(length(U)),M,N)
    return H
end

function read_hypergraph_data(dataname::String, maxsize::Int64=25)
    """
    Read data from .txt file to incidence matrix, and extract
    class labels.
    """
    classes = Int64[]
    open("../original-data/$dataname/node-labels-$dataname.txt") do f
        for line in eachline(f)
            push!(classes, parse(Int64, line))
        end
    end
    n = length(classes)

    # hyperedges
    EdgeList = Vector{Vector{Int64}}()
    open("../original-data/$dataname/hyperedges-$dataname.txt") do f
        for line in eachline(f)
            edge = [parse(Int64, v) for v in split(line, ',')]
            sort!(edge)
            push!(EdgeList,edge)
        end
    end

    H = elist2incidence(EdgeList,n)

    return H,classes
end

function Hypergraph_to_Scores(H,classes,r)
    n = size(H,2)
    alpha = sum(classes)/n
    B1 = baselines(r,alpha)
    B2 = baselines(r,1-alpha)
    H1, H2 = relative_affinities(H,classes,r)

    N = get_hyperedge_counts(H,classes)
    N = N[1:r,1:r+1]
    R1 = H1./B1
    R2 = H2./B2

    return alpha, B1, B2, H1, H2, R1, R2, N
end

function GHI(H,classes,r)
    """
    Group homophily index: largest value of j such that the top j affinity
    scores are above baseline.
    """
    alpha, B1, B2, H1, H2, R1, R2, N = Hypergraph_to_Scores(H,classes,r)
    Sets1 = zeros(r)
    Sets2 = zeros(r)
    for k = 1:r
        last1 = k
        last2 = k
        c1searching = true
        c2searching = true

        if R1[k,k] < 1
            c1searching = false
            last1 = k+1
        end

        if R2[k,k] < 1
            c2searching = false
            last2 = k+1
        end

        for j = k:-1:1
            # @assert(R1[k,k] >= 1)
            # @assert(R2[k,k] >= 1)
            if R1[k,j] > 1 && c1searching
                last1 = j
            else
                c1searching = false
            end
            if R2[k,j] > 1 && c2searching
                last2 = j
            else
                c2searching = false
            end
            if R1[k,j] < 1 && R2[k,j] < 1
                break
            end
        end
        Sets1[k] = last1
        Sets2[k] = last2
    end

    ## Set-in plot
    S1 = collect(1:r) - Sets1 .+ 1
    S2 = collect(1:r) - Sets2 .+ 1

    return S1, S2
end

function Bootstrap_Affinities(H,k,B,classes)
    n = size(H,2)
    R1 = zeros(B,k)
    R0 = zeros(B,k)

    A1 = zeros(B,k)
    A0 = zeros(B,k)

    order = vec(sum(H,dims = 2))
    Ek = findall(x->x==k,order)

    alpha = sum(classes)/n
    B1 = arbitrary_baselines(k,alpha)
    B0 = arbitrary_baselines(k,1-alpha)
    for i = 1:B
        sam = sample(Ek,length(Ek))
        Hnew = H[sam,:]
        Elist = incidence2elist(Hnew)
        Nk = zeros(k+1)
        for edge in Elist
            elabels = classes[edge]
            t = sum(elabels)
            Nk[t+1] += 1
        end
        c1_denom = sum(i*Nk[i+1] for i = 1:k)
        c0_denom = sum(i*Nk[k-i+1] for i = 1:k)
        for t = 1:k
            A1[i,t] = (t*Nk[t+1]/c1_denom)
            A0[i,t] = (t*Nk[k-t+1]/c0_denom)
            R1[i,t] = (t*Nk[t+1]/c1_denom)/B1[t]
            R0[i,t] = (t*Nk[k-t+1]/c0_denom)/B0[t]
        end
    end

    # Average of the random trials
    MR1 = vec(mean(R1,dims = 1))
    MR0 = vec(mean(R0,dims = 1))
    MA1 = vec(mean(A1,dims = 1))
    MA0 = vec(mean(A0,dims = 1))

    # Compute standard error
    SR1 = zeros(k)
    SR0 = zeros(k)
    SA1 = zeros(k)
    SA0 = zeros(k)
    for t = 1:k
        SR1[t] = StatsBase.std(R1[:,t])
        SR0[t] = StatsBase.std(R0[:,t])
        SA1[t] = StatsBase.std(A1[:,t])
        SA0[t] = StatsBase.std(A0[:,t])
    end

    return R0, R1, MR1, MR0, SR1, SR0, A0, A1, MA1, MA0, SA1, SA0

end

function Bootstrap_From_N(Nk_old,B,alpha)

    k = length(Nk_old)-1

    R1 = zeros(B,k)
    R0 = zeros(B,k)
    A1 = zeros(B,k)
    A0 = zeros(B,k)
    B1 = arbitrary_baselines(k,alpha)
    B0 = arbitrary_baselines(k,1-alpha)

    for i = 1:B
        Nk = zeros(k+1)
        # @show Nk_old
        for j = 1:round(Int64,sum(Nk_old))
            Nk[sample(1:k+1,Nk_old)] += 1
        end
        c1_denom = sum(i*Nk[i+1] for i = 1:k)
        c0_denom = sum(i*Nk[k-i+1] for i = 1:k)
        for t = 1:k
            A1[i,t] = (t*Nk[t+1]/c1_denom)
            A0[i,t] = (t*Nk[k-t+1]/c0_denom)
            R1[i,t] = (t*Nk[t+1]/c1_denom)/B1[t]
            R0[i,t] = (t*Nk[k-t+1]/c0_denom)/B0[t]
        end
    end

    # Average of the random trials
    MR1 = vec(mean(R1,dims = 1))
    MR0 = vec(mean(R0,dims = 1))
    MA1 = vec(mean(A1,dims = 1))
    MA0 = vec(mean(A0,dims = 1))

    # Compute standard error
    SR1 = zeros(k)
    SR0 = zeros(k)
    SA1 = zeros(k)
    SA0 = zeros(k)
    for t = 1:k
        SR1[t] = StatsBase.std(R1[:,t])
        SR0[t] = StatsBase.std(R0[:,t])
        SA1[t] = StatsBase.std(A1[:,t])
        SA0[t] = StatsBase.std(A0[:,t])
    end

    return R0, R1, MR1, MR0, SR1, SR0, A0, A1, MA1, MA0, SA1, SA0

end


#script for solving a 1D BVP of the form
# -a0(x)*u''(x)+a1(x)*u(x)=f(x) for x ∈ (0,1)
# Here a0(x)=1, a1(x)=0, f(x)=4*π^2*sin(2*π*x)
# with Dirichlet boundary conditions u(0) = 0, u(1)=0
include("../src/Solver1D.jl")

a0(x) = 1
a1(x) = 0
f(x) = (4*π^2)*sin(2*π*x)
exact_sol(x) = sin(2*π*x)

ptLeft = 0.
ptRight = 1.
uPtLeft = 0.
uPtRight = 1.
nrb = nrbline(ptLeft, ptRight)

solPtLeft = exact_sol(ptLeft)
solPtRight = exact_sol(ptRight)

bound_left = Boundary1D("Dirichlet", ptLeft, uPtLeft, solPtLeft)
bound_right = Boundary1D("Dirichlet", ptRight, uPtRight, solPtRight)

numElem = 10
degP = 2
new_knots = collect(LinRange(ptLeft, ptRight, numElem+1)[2:end-1])
nrb = nrbdegelev(nrb, [degP-1])
nrb = nrbkntins(nrb, [new_knots])

#refine some more knots at the beginning
ptMid = (ptLeft+ptRight)/2
newer_knots = collect(LinRange(ptLeft, ptMid, numElem+1))[2:end-1]
newer_knots = setdifftol(newer_knots, new_knots)
#@show newer_knots
nrb = nrbkntins(nrb, [newer_knots])

prob_sin = Problem1D(f, a0, a1, [bound_left, bound_right], nrb)
IGAmesh = genMesh(prob_sin.domain)
plotBasisParam(IGAmesh)

gauss_rule = genGaussLegendre(degP+1)
stiff = assemble_stiff(IGAmesh, a0, gauss_rule)
mass = assemble_mass(IGAmesh, a1, gauss_rule)
rhs = assemble_rhs(IGAmesh, f, gauss_rule)
lhs, rhs = applyBCnurbs(prob_sin.boundary_cond, stiff, mass, rhs, nrb)
sol0 = lhs\rhs
plotSol(IGAmesh, real(sol0), "Poisson1D")
plotSolError(IGAmesh, real(sol0), exact_sol, "Poisson1D")


using JuMP, Plots, LinearAlgebra, SparseArrays, Ipopt

#A JuMP model is initialized
mod = Model(optimizer_with_attributes(Ipopt.Optimizer ,  "max_iter" => 100,
                                        "mumps_mem_percent" => 500))
#The parameters of the 2-D body
λ = 45.0                            # Conductivity
c = 460.0                           # Specific heat capacitivity
ρ = 7800.0                          # Material density
L = 0.1                             #Length and width of body are assumed equivalent
α = λ / (c*ρ)                       # Diffusivity
N = 11                              # Number of Discretization points N_x = N_y
h = L / (N - 1)                     #Δx = Δy = h

#System , input and output matrices: θ' = Aθ(t) + Bu(t)
A = spdiagm(0 => -4*ones(N^2))
for i in 1:N^2
    if i%N == 1
        A[i+1,i] = 2
    elseif i%N == 0
        A[i-1,i] = 2
    else
        A[i-1,i] = 1
        A[i+1,i] = 1
    end
    if (i <= N)
        A[i+N,i] = 2
    elseif N^2 - i < N
        A[i- N,i] = 2
    else
        A[i-N,i] = 1
        A[i+N,i] = 1
    end
end
A *= α / (h^2);

B = spzeros(3,N^2);
B[1,1] = 1;
B[2,6] = 1;
B[3,11] = 1;
B *= (2/ (c * ρ * h));
#y(t) = Cθ(t)
C = spzeros(N^2,1)
C[111] = 1.0;
C[115] = 1.0;
C[end] = 1.0;
#Integration from t0 = 0 to tf
tf = 40000.0;                   #Final time
dt = 1.6;                       #Sampling period Δt
steps = round(Int,tf / dt);     #number of time steps in simulation.

u_min = 0.0                     #lower input bound
u_max = 15000.0                 #upper input bound
@variable(mod,θ[1:N^2,1:steps]);                                      # state vector
@variable(mod,u_min <= u[i = 1:3,k = 1:steps - 1] <= u_max);          # Bounded Input u

θinit = 273.0                           #initial temperature of the body
θref = 500.0                            #Reference temperature

@constraint(mod, θ[:,1] .== θinit)      #Initial value added as constraints
#System dynamics
for k in 1:steps - 1,i in 1:N^2
    @constraint(mod,θ[i,k + 1] .==  θ[i,k] + dt * ((A[:,i]' * θ[:,k]) + B[:,i]' * u[:,k]))
end
#Q and R weighing matrices
Q = 10000 * C * C';
R = 0.0001 * I;
#Cost function
@NLobjective(mod, Min, 0.5 * dt * sum(sum(Q[i,i] *(θref - θ[i,k])^2 for i in 1:N^2)
+ sum(R[i,i] * u[i,k]^2 for i in 1:3) for k in 1:steps - 1))
#Optimization process
optimize!(mod)

#Plotting results
states_θstart1 = zeros(steps)
states_θstart2 = zeros(steps)
states_θstart3 = zeros(steps)
states_θend3 = zeros(steps)
states_θend2 = zeros(steps)
states_θend1 = zeros(steps)
input_u1 = zeros(steps - 1)
input_u2 = zeros(steps - 1)
input_u3 = zeros(steps - 1)
for i = 1 : steps-1
    states_θstart1[i] = JuMP.value(θ[1,i])
    states_θstart2[i] = JuMP.value(θ[6,i])
    states_θstart3[i] = JuMP.value(θ[11,i])
    states_θend3[i] = JuMP.value(θ[115,i])
    states_θend2[i] = JuMP.value(θ[85,i])
    states_θend1[i] = JuMP.value(θ[45,i])
    input_u1[i] = JuMP.value(u[1,i])
    input_u2[i] = JuMP.value(u[2,i])
    input_u3[i] = JuMP.value(u[3,i])
end

states_θend3[end] = JuMP.value(θ[end,end])
states_θend2[end] = JuMP.value(θ[end-5,end])
states_θend1[end] = JuMP.value(θ[end-10,end])
states_θstart1[end] = JuMP.value(θ[1,end])
states_θstart2[end] = JuMP.value(θ[6,end])
states_θstart3[end] = JuMP.value(θ[11,end])
#Plotting temperatures at the input nodes
p2 = plot(states_θstart1, label = "θ_in1",legend = false)
p2 = plot!(states_θstart2,label = "θ_in2",legend = false)
p2 = plot!(states_θstart3,label = "θ_in3",legend = false)
savefig("lq_optimal_2D.png")
#Plotting temperatures at the output
p1 = plot(states_θend3,label = "output-3",legend = false)
p1 = plot!(states_θend2,label = "output-2",legend = false)
p1 = plot!(states_θend1,label = "output-1",legend = false)
savefig("lq_optimal_2D-output-bounded.png")
#Plotting the input signals
p3 = plot(input_u1,label = "in_1",legend = false)
p3 = plot!(input_u2,label = "in_2",legend = false)
p3 = plot!(input_u3,label = "in_3",legend = false)
savefig("lq_optimal_2D_inputbounded.png")


## NACA Parametrisations
#==========================================================================================#

# NACA 4-digit parameter functions
naca4_thickness(t_by_c, xc, sharp_trailing_edge :: Bool) = 5 * t_by_c * (0.2969 * √xc - 0.1260 * xc - 0.3516 * xc^2 + 0.2843 * xc^3 - (ifelse(sharp_trailing_edge, 0.1036, 0.1015) * xc^4))

naca4_camberline(pos, cam, xc) = ifelse(
                                        xc < pos,
                                        (cam / pos^2) * xc * (2 * pos - xc),
                                        cam / (1 - pos)^2 * ( (1 - 2 * pos) + 2 * pos * xc - xc^2)
                                       )

naca4_gradient(pos, cam, xc) = atan(2 * cam / (ifelse(xc < pos, pos^2, (1 - pos)^2)) * (pos - xc))

"""
    naca4_coordinates(digits :: NTuple{4, <: Real}, n :: Integer, sharp_trailing_edge :: Bool)

Generate the coordinates of a NACA 4-digit series profile with a specified number of points, and a Boolean flag to specify a sharp or blunt trailing edge.
"""
function naca4_coordinates(digits :: NTuple{4, <: Real}, n :: Integer, sharp_trailing_edge :: Bool)
    # Camber
    cam = digits[1] / 100
    # Position
    pos = digits[2] / 10
    # Thickness-to-chord ratio
    t_by_c = (10 * digits[3] + digits[4]) / 100

    # Cosine spacing
    xs = cosine_spacing(0.5, 1.0, n)

    # Thickness distribution
    thickness = naca4_thickness.(Ref(t_by_c), xs, Ref(sharp_trailing_edge))
    if pos == 0 || cam == 0
        x_upper = xs
        y_upper = thickness
        x_lower = xs
        y_lower = -thickness
    else
        # Compute camberline
        camber  = naca4_camberline.(Ref(pos), Ref(cam), xs)
        # Compute gradients
        grads   = naca4_gradient.(Ref(pos), Ref(cam), xs)
        # Upper surface
        x_upper = @. xs - thickness * sin(grads)
        y_upper = @. camber + thickness * cos(grads)
        # Lower surface
        x_lower = @. xs + thickness * sin(grads)
        y_lower = @. camber - thickness * cos(grads)
    end
    coords = [ [x_upper y_upper][end:-1:2,:];
                x_lower y_lower             ]
end

"""
    naca4(digits :: NTuple{4, <: Real}, n :: Integer = 40; sharp_trailing_edge :: Bool)

Generate a `Foil` of a NACA 4-digit series profile with a specified number of points (40 by default), and a named option to specify a sharp or blunt trailing edge.
"""
naca4(digits :: NTuple{4, <: Real}, n = 40; sharp_trailing_edge = true) = Foil(naca4_coordinates(digits, n, sharp_trailing_edge), string("NACA ", digits...))

naca4(a, b, c, d, n = 40; sharp_trailing_edge = true) = naca4((a,b,c,d), n; sharp_trailing_edge = sharp_trailing_edge)


##
# Demo of using DualNumbers to differentiate with respect to
# changes in parameters
##

using DualNumbers, ApproxFun
# What is the derivative of the function (without differentiating the Chebyshev expansion)?
f=Fun(x->exp(dual(x,1)),[-1,1])
dualpart(f)
# check versus differentiate
norm(realpart(f)'-dualpart(f))

# What is the derivative of the first coefficient with respect to the left endpoint?
f=Fun(exp,Interval(dual(1.0,1),dual(2.0)))
dualpart(f.coefficients[1])
# check versus finite difference calculation:
h=0.00001;(Fun(exp,Interval(1.0+h,2.0)).coefficients[1]-Fun(exp,Interval(1.0,2.0)).coefficients[1])/h

# Or an ApproxFun calculation:
Fun(h->Fun(exp,Interval(1.0+h,2.0)).coefficients[1],[0.,.1])'(0.)

# What is the derivative of the first coefficient with respect to the exponential's constant?
f=Fun(x->exp(dual(x,x)),[-1,1])
dualpart(f.coefficients[1])
# check versus finite difference calculation:
h=0.00001;(Fun(x->exp((1+h)x),[-1,1]).coefficients[1]-Fun(exp,[-1,1]).coefficients[1])/h


using Plots, LaTeXStrings, Statistics

function Deposition(;len, tot_time)
    surf = [0 for i=1:len]
    AvgList = [0.0 for i=1:tot_time]
    for n in 2:tot_time+1
        randsurf = rand(1:len)
        for i in randsurf
            MAX = FindMax(surf, i, len)
            surf[i] = MAX
        end
        AvgList[n-1] = mean(surf)
    end
    return AvgList
end

function sides(n, L)
    if n == L
        return n-1 , 1
    elseif n == 1
        return L , n+1
    else
        return n-1, n+1
    end
end

function FindMax(surface, index_,L_surf)
    i1 , i2 = sides(index_, L_surf)
    maxlen = max(surface[i1],surface[index_] + 1,surface[i2])
    return maxlen
end
function Linear_fit(;Time, AvgList, tot_time)
    A = [hcat(Time[1:tot_time]) reshape(ones(tot_time), tot_time, 1)]
    b = reshape(AvgList[1:tot_time], tot_time, 1)
    line = (A \ b)
    return line
end

iternum = 1000
Parameters = Dict(:len => 300,
                    :tot_time => 50,)
allAvg = [ [0.0 for i in 1:Parameters[:tot_time]] for j = 1:iternum]
meanAvg = [0.0 for i in 1:Parameters[:tot_time]]
vars = [0.0 for i in 1:Parameters[:tot_time]]
for i in 1:iternum
    AvgList = Deposition(;Parameters...)
    allAvg[i] = AvgList
    meanAvg += AvgList
    print("\r$i")
end
meanAvg /= iternum
for i in 1:Parameters[:tot_time]
    vars[i] = std(hcat(allAvg...)[i,:])
end

Time = 0:Parameters[:tot_time]-1

Paraline = Dict(
                :Time => Time[1:end],
                :AvgList => meanAvg[1:end],
                :tot_time => 50)
Line = Linear_fit(;Paraline...)
X = Time[1]:Time[50]
Y = X .* Line[1] .+ Line[2]
Line[2]
plot(X,Y,c=:black,label = L"y = %$(round(Line[1],digits= 3))x + %$(round(Line[2],digits= 3))")
scatter!(Time[1:end], meanAvg[1:end],
    # xlims = (1, Parameters[:tot_time]),
    c = :steelblue,
    xlabel= L"Time",
    ylabel= L"H_{(t)}",
    title= L"~H_{(t)}-Time~\ (L = %$(Parameters[:len]))",
    label = L"Data\ point",
    yerror = vars)
    # legend = nothing)

savefig("../../Figs/Q2/H-t(L=$(Parameters[:len])).pdf")


# Laplacian Eigenmaps
# -------------------
# Laplacian Eigenmaps for Dimensionality Reduction and Data Representation,
# M. Belkin, P. Niyogi, Neural Computation, June 2003; 15 (6):1373-1396

#### LEM type
struct LEM{T <: Real} <: AbstractDimensionalityReduction
    k::Int
    λ::AbstractVector{T}
    t::T
    proj::Projection{T}
    component::Vector{Int}

    LEM{T}(k::Int, λ::AbstractVector{T}, t::T, proj::Projection{T})  where T = new(k, λ, t, proj)
    LEM{T}(k::Int, λ::AbstractVector{T}, t::Float64, proj::Projection{T}, cc::Vector{Int})  where T = new(k, λ, t, proj, cc)
end

## properties
outdim(R::LEM) = size(R.proj, 1)
eigvals(R::LEM) = R.λ
neighbors(R::LEM) = R.k
vertices(R::LEM) = R.component

## show
summary(io::IO, R::LEM) = print(io, "Laplacian Eigenmaps(outdim = $(outdim(R)), neighbors = $(neighbors(R)), t = $(R.t))")

## interface functions
function fit(::Type{LEM}, X::AbstractMatrix{T}; maxoutdim::Int=2, k::Int=12, t::Real=1.0) where {T<:Real}
    # Construct NN graph
    D, E = find_nn(X, k)
    G, C = largest_component(SimpleWeightedGraph(adjmat(D,E)))

    # Compute weights
    W = weights(G)
    W .^= 2
    W ./= maximum(W)

    W[W .> eps(T)] = exp.(-W[W .> eps(T)] ./ convert(T,t))
    D = diagm(0 => sum(W, dims=2)[:])
    L = D - W

    λ, V = decompose(L, D, maxoutdim)
    return LEM{T}(k, λ, t, transpose(V), C)
end

transform(R::LEM) = R.proj


println("*******************************************************")
@testset "qmr" begin
	@testset "real matrix" begin
		A  = sprandn(100,100,.1)
		A  = A + SparseMatrixCSC(100.0I, 100, 100)
		n  = size(A,2)
		D  = diag(A)
		M2 = x -> Vector(D.\x)
		rhs = randn(100)
		tol = 1e-2;

		# test printing and behaviour for early stopping
		xtt = qmr(A,rhs,tol=1e-12,maxIter=3,out=2)
		@test xtt[2]==-1

		# test behaviour for zero rhs
		xtt = qmr(A,0*rhs,tol=tol,maxIter=10,out=2)
		@test xtt[2]==-9
		@test all(xtt[1].==0)
		@test length(xtt[1])==100
		@test eltype(xtt[1])==eltype(rhs)

		x1 = qmr(A,rhs ,tol=tol,maxIter=100,out=1)
		x3 = qmr(A,rhs,tol=tol,maxIter=100,x=randn(size(rhs)))
		x4 = qmr(A,rhs,tol=tol,maxIter=100,M=M2)

		@test norm(A*x1[1]-rhs)/norm(rhs) < tol
		@test norm(A*x3[1]-rhs)/norm(rhs) < tol
		@test norm(A*x4[1]-rhs)/norm(rhs) < tol
		@test norm(x3[1]-x1[1])/norm(x1[1]) < 1e-2
	end

	@testset "complex matrix" begin
		A = sprandn(100,100,.1) + 1im*(sprandn(100,100,.1));
		A  = A + SparseMatrixCSC(100.0I, 100, 100)
		D  = Vector(diag(A))
		M3 = x -> x./D
		rhs = complex(randn(100))
		tol = 1e-3;

		# test behaviour for zero rhs

		xtt = qmr(A,0.0*rhs,tol=tol,maxIter=10,out=2)
		# @test xtt[2]==-9
		# @test all(xtt[1].==0)
		# @test length(xtt[1])==100
		# @test eltype(xtt[1])==eltype(rhs)

		x1 = qmr(A,rhs,tol=tol,maxIter=100)
		x3 = qmr(A,rhs,tol=tol,maxIter=100,x=randn(size(rhs)))
		x4 = qmr(A,rhs,tol=tol,maxIter=100,M=M3)

		@test norm(A*x1[1]-rhs)/norm(rhs) < tol
		@test norm(A*x3[1]-rhs)/norm(rhs) < tol
		@test norm(x3[1]-x1[1])/norm(x1[1]) < 1e-2
	end
end


using Plots
using FFTW

cos_taylor(x) = 1-( x^2/2 )+( x^4/24.0 )-( x^6/720.0 )+( x^8/40320.0 );

"""
Calculate cos(x) with a five-term Taylor series, using only the quarter-waves closest to the origin for increased accuracy.
"""
function fast_cos( x::Float64 )
        if x < -π/2
            x = -π/2 + abs(x+π/2);
            return -cos_taylor(x);
        elseif π/2 < x
            x = π/2 - abs(x-π/2);
            return -cos_taylor(x)
        else
            return cos_taylor(x)
        end
end

x = LinRange(-π, π, 1000);

cosx_taylor = cos_taylor.(x);
cosx_native = cos.(x);

delta = cosx_native-cosx_taylor

# Time domain comparison.
plot( 	x, cosx_native,
	 	label = "Julia native cosine",
        size = (1024, 720),)
plot!( 	x, cosx_taylor,
	 	label = "5-term Taylor series",)
plot!( 	x, delta*100,
	 	label = "Difference*100",)

savefig("figures/cos/time_domain")

taylor_cos2 = fast_cos.(x);
delta2 = cosx_native-taylor_cos2

# Taylor vs first quarter taylor.
plot( 	x, delta*100,
	 	label = "Difference",
        size = (1024, 720),)
plot!( 	x, delta2*100,
	 	label = "Difference (first quarter)",)

savefig("figures/cos/time_domain_comparison2")

"""
Wrap sample to a -π =< sample < π range.
"""
function wrap_phase( sample::Float64 )
    while π <= sample
        sample = sample - 2π;
    end
    while sample < -π
        sample = sample + 2π;
    end
    return sample;
end

# Frequency domain analysis
x = LinRange(0, 1024π, 2^14);
x = wrap_phase.(x);
cosx_taylor = cos_taylor.(x);
cosx_native = cos.(x);

"""
Calculate normalized power spectrum for real signals.
"""
function frequecny_domain_analysis( signal_t )
    power_f = abs.(fft(signal_t)).^2;
    normalized_power = power_f/maximum(power_f);
    power_dB = 10*log10.(normalized_power);
    return power_dB[1:Int(size(power_dB)[1]/2)]
end

omega = LinRange(0, 1, Int(size(x)[1]/2));

# Frequency domain.
plot( 	omega, frequecny_domain_analysis(cosx_native),
	 	label = "Julia native cosine",
        ylims = [-100, 0],
        size = (1024, 720),)
xlabel!("Frequency relative to Fs")
ylabel!("Normalized power [dBC]")

plot!( 	omega, frequecny_domain_analysis(cosx_taylor),
	 	label = "5-term Taylor series",)

savefig("figures/cos/frequency_domain")

# Frequency domain first quarter.
plot( 	omega, frequecny_domain_analysis(cosx_native),
	 	label = "Julia native cosine",
        ylims = [-100, 0],
        size = (1024, 720),)
xlabel!("Frequency relative to Fs")
ylabel!("Normalized power [dBC]")

plot!( 	omega, frequecny_domain_analysis(fast_cos.(x)),
	 	label = "5-term Taylor, first quarter",)

savefig("figures/cos/frequency_domain2")


#using TensorToolbox, Test, LinearAlgebra

T1=dimtree([8,9,5,6,7])
T2=dimtree([2,4,6,8,9])
T3=dimtree(7)

X=float(reshape(collect(1:32),(2,2,2,2,2)))
println("\n\nTesting truncation of tensor X of size ",size(X)," to htensor.")
H1=htrunc(X,T1);
err=norm(full(H1)-X)
println("norm(full(H)-X) = ",err)
@test err ≈ 0 atol=1e-10

H2=htrunc(X,T2)
err=norm(full(H2)-X)
println("norm(full(H)-X) = ",err)
@test err ≈ 0 atol=1e-10

Y=float(reshape(collect(1:2^7),(2,2,2,2,2,2,2)));
H3=htrunc(Y,T3);
@test norm(full(H3)-Y) ≈ 0 atol=1e-10

X=rand(5,4,3,2,4,2);
H=htrunc(X,dimtree(ndims(X)),maxrank=20);
@test norm(full(H)-X) ≈ 0 atol=1e-10

println("\n\n...Testing size of htensor H : ", size(H))
@test size(H) == size(X)
println("\n...Testing ndims of htensor H : ", ndims(H))
@test ndims(H) == ndims(X)

A=MatrixCell(undef,ndims(H))
for n=1:ndims(H)
  A[n]=rand(rand(1:10),size(H,n))
end
err=norm(full(ttm(H,A)) - ttm(full(H),A))
println("\n...Testing ttm for ttensor H and array of matrices A : norm(full(ttm(H,A))-ttm(full(H),A)) = ", err)
@test err ≈ 0 atol=1e-10

println("\n\n...Testing norm of htensor H.")
err=abs(norm(H) - norm(full(H)))
println("|norm(H) - norm(full(H))| = ", err)
@test err ≈ 0 atol=1e-10

println("\n\n...Testing scalar multiplication 3*H.")
err=norm(full(3*H) - 3*full(H))
println("norm(full(3*H) - 3*full(H)) = ", err )
@test err ≈ 0 atol=1e-10

println("\n\nTesting functions plus and minus for htensors H1 and H2.")
T=dimtree(3)
H1=htrunc(rand(5,4,3),T)
H2=htrunc(rand(5,4,3),T)
H=H1+H2
F=full(H1)+full(H2);
err=norm(full(H) - F)
println("norm(full(H1+H2) - (full(H1)+full(H2))) = ",err )
@test err ≈ 0 atol=1e-10
H=H1-H2
F=full(H1)-full(H2);
err=norm(full(H) - F)
println("norm(full(H1+H2) - (full(H1)+full(H2))) = ",err )
@test err ≈ 0 atol=1e-10

println("\n\n...Testing inner product.")
H=innerprod(H1,H2)
err=abs(H - innerprod(full(H1),full(H2)))
println("|innerprod(H1,H2) - innerprod(full(H1),full(H2))| = ",err )
@test err ≈ 0 atol=1e-10

println("\n\nTesting function dropdims for squeezed tensor X and squeezed htensor Y.")
X=rand(1,2,4,3,2)
H=htrunc(X)
Y=dropdims(H)
err = norm(full(Y)-dropdims(X,dims=1))
println("norm(full(Y)-dropdims(X,dims=1)) = ",err)
@test err ≈ 0 atol=1e-10
X=rand(2,1,4,3,2)
H=htrunc(X)
Y=dropdims(H)
err = norm(full(Y)-dropdims(X,dims=2))
println("norm(full(Y)-dropdims(X,dims=2)) = ",err)
@test err ≈ 0 atol=1e-10
X=rand(2,4,1,3,2)
H=htrunc(X)
Y=dropdims(H)
err = norm(full(Y)-dropdims(X,dims=3))
println("norm(full(Y)-dropdims(X,dims=3)) = ",err)
@test err ≈ 0 atol=1e-10
X=rand(2,4,3,1,2)
H=htrunc(X)
Y=dropdims(H)
err = norm(full(Y)-dropdims(X,dims=4))
println("norm(full(Y)-dropdims(X,dims=4)) = ",err)
@test err ≈ 0 atol=1e-10
X=rand(2,4,3,2,1)
H=htrunc(X)
Y=dropdims(H)
err = norm(full(Y)-dropdims(X,dims=5))
println("norm(full(Y)-dropdims(X,dims=5)) = ",err)
@test err ≈ 0 atol=1e-10

H=randhtensor([4,5,2])
println("\n\n...Testing reorthogonalization for random htensor H of size ", size(H),".")
println("Has orthogonal factor matrices: ", H.isorth)
println("After reorthogonalization: ", reorth!(H).isorth)
@test H.isorth

H=htrunc(rand(5,5,5),dimtree(3),maxrank=2);
println("Testing hrank: ",hrank(H))
@test hrank(H)==[1, 2, 2, 2, 2]


"""
Johnson Lindenstrauss effective resistance upperbounding. If retXhat is set to true,
returns the vectors necessary for computing the effective resistance bounds instead
of the actual bounds.

~~~julia
	johnlind(a::SparseMatrixCSC{Tv,Ti}; eps::Tv = 0.5, solver=(la -> augTreeSolver(la,tol=1e-1,maxits=1000,maxtime=10)), retXhat::Bool = false)
~~~
"""

function johnlind{Tv,Ti}(a::SparseMatrixCSC{Tv,Ti};
						eps::Tv = 0.5,
						solver=(la -> augTreeSolver(la,tol=1e-1,maxits=1000,maxtime=10)),
						retXhat::Bool = false)

	n = a.n
	m = length(a.nzval)
	dhat = ceil(Int64, 4 * log(m) / eps^2)

	P = ones(dhat, m)
	for i in 1:dhat
		for j in 1:m
			if rand() < 1/2
				P[i,j] = -P[i,j]
			end
		end
	end
	# we want ||P * x|| = ||x||
	P = P / sqrt(dhat)

	# compute B
	U = Ti[]
	V = Ti[]
	W = Tv[]

	pos = 1
	for i in 1:length(a.nzval)
		while a.colptr[pos + 1] <= i
			pos = pos + 1
		end

		p = a.rowval[i]
		q = pos
		w = sqrt(a.nzval[i])

		if p > q
			aux = p
			p = q
			q = aux
		end

		# multiply B by W
		push!(U, i)
		push!(V, p)
		push!(W, w)

		push!(U, i)
		push!(V, q)
		push!(W, -w)
	end

	B = sparse(U, V, W)

	# Get bs = P * W^(1/2) * B. We already multiplied W by B. Solve for each line. dims are dhat x n
	bs = P * B / sqrt(2)

	f = solver(lap(a) + speye(a.n) * 1e-10)

	# xhat = P * W^(1/2) * B * L ^-1 * ei
	xhat = zeros(n, dhat)
	for i in 1:dhat
		b = reshape(bs[i,:], n)
		b = b - mean(b)
		xhat[:,i] = f(b)
	end

	if retXhat
		return xhat
	end

	# compute the effective resistance
	reff = copy(a)
	pos = 1
	for i in 1:m
		while a.colptr[pos + 1] <= i
			pos = pos + 1
		end

		p = a.rowval[i]
		q = pos

		reff.nzval[i] = norm(xhat[p,:] - xhat[q,:])^2
	end

	return reff

end


# # 210: 2D Nonlinear Poisson with reaction
# ([source code](SOURCE_URL))

module Example210_NonlinearPoisson2D_Reaction

using Printf
using VoronoiFVM
using ExtendableGrids
using GridVisualize

function main(;n=10,Plotter=nothing,verbose=false, unknown_storage=:sparse)

    h=1.0/convert(Float64,n)
    X=collect(0.0:h:1.0)
    Y=collect(0.0:h:1.0)

    grid=VoronoiFVM.Grid(X,Y)
    data = (eps=1.0e-2, k=1.0)

    function reaction!(f,u,node,data)
        f[1]=data.k*(u[1]-u[2])
        f[2]=data.k*(u[2]-u[1])
    end

    function flux!(f,u0,edge,data)
        u=unknowns(edge,u0)
        f[1]=data.eps*(u[1,1]-u[1,2])
        f[2]=data.eps*(u[2,1]-u[2,2])
    end

    function source!(f,node,data)
        x1=node[1]-0.5
        x2=node[2]-0.5
        f[1]=exp(-20*(x1^2+x2^2))
    end

    function storage!(f,u,node,data)
        f[1]=u[1]
        f[2]=u[2]
    end

    physics=VoronoiFVM.Physics(num_species=2,
                               data=data,
                               flux=flux!,
                               storage=storage!,
                               reaction=reaction!,
                               source=source!)

    sys=VoronoiFVM.System(grid,physics,unknown_storage=unknown_storage)

    enable_species!(sys,1,[1])
    enable_species!(sys,2,[1])

    inival=unknowns(sys)
    U=unknowns(sys)
    inival.=0.0

    control=VoronoiFVM.NewtonControl()
    control.verbose=verbose
    control.tol_linear=1.0e-5
    control.max_lureuse=0
    tstep=0.01
    time=0.0
    istep=0
    u15=0
    p=GridVisualizer(Plotter=Plotter,layout=(2,1))
    while time<1
        time=time+tstep
        solve!(U,inival,sys,control=control,tstep=tstep)
        inival.=U
        if verbose
            @printf("time=%g\n",time)
        end
        u15=U[15]
        tstep*=1.0
        istep=istep+1
        scalarplot!(p[1,1],grid,U[1,:],clear=true)
        scalarplot!(p[2,1],grid,U[2,:],show=true)
    end
    return u15
end

function test()
    main(unknown_storage=:sparse) ≈ 0.014566189535134827 &&
        main(unknown_storage=:dense) ≈ 0.014566189535134827
end
end


using HyperbolicPlane, Plots

"""
`regular(n,r)` draw a regular `n`-gon in the hyperbolic plane.
The vertices are distance `r` from the origin.
"""
function regular(n::Int=5, r::Real=1.0)
    plot()
    pts = [ HPoint(r, (k/n)*2pi) for k=0:n ]
    for P in pts
        set_radius(P,3)
    end
    draw(pts)

    segs = [ HSegment(pts[k],pts[k+1]) for k=1:n ]
    draw(segs)
    draw(HPlane())
    finish()
end


@testset "Quaternions" begin

    qx = qrotation(Vec(1, 0, 0), pi / 4)
    @test qx * qx ≈ qrotation(SVector(1.0, 0.0, 0.0), pi / 2)
    @test Base.power_by_squaring(qx, 2) ≈ qrotation(SVector(1.0, 0.0, 0.0), pi / 2)
    theta = pi / 8
    qx = qrotation(SVector(1.0, 0.0, 0.0), theta)
    c = cos(theta); s = sin(theta)
    Rx = [1 0 0; 0 c -s; 0 s c]

    @test Mat3f0(qx) ≈ Rx
    theta = pi / 6
    qy = qrotation(SVector(0.0, 1.0, 0.0), theta)
    c = cos(theta); s = sin(theta)
    Ry = [c 0 s; 0 1 0; -s 0 c]
    @test Mat3f0(qy) ≈ Ry
    theta = 4pi / 3
    qz = qrotation(SVector(0.0, 0.0, 1.0), theta)
    c = cos(theta); s = sin(theta)
    Rz = [c -s 0; s c 0; 0 0 1]
    @test Mat3f0(qz) ≈ Rz

    @test Mat3f0(qx * qy * qz) ≈ Rx * Ry * Rz
    @test Mat3f0(qy * qx * qz) ≈ Ry * Rx * Rz
    @test Mat3f0(qz * qx * qy) ≈ Rz * Rx * Ry

    a, b = qrotation(SVector(0.0, 0.0, 1.0), deg2rad(0)), qrotation(SVector(0.0, 0.0, 1.0), deg2rad(180))
    # @test slerp(a, b, 0.0) ≈ a
    # @test slerp(a, b, 1.0) ≈ b
    # @test slerp(a, b, 0.5) ≈ qrotation([0, 0, 1], deg2rad(90))
    #
    # @test angle(qrotation(SVector(1.0, 0.0, 0.0), 0)) ≈ 0
    # @test angle(qrotation([0, 1, 0], pi / 4)) ≈ pi / 4
    # @test angle(qrotation([0, 0, 1], pi / 2)) ≈ pi / 2
    #
    # let # test numerical stability of angle
    #     ax = randn(3)
    #     for θ in [1e-9, π - 1e-9]
    #         q = qrotation(ax, θ)
    #         @test angle(q) ≈ θ
    #     end
    # end
end


##################################################
## Classical derivative-based, iterative, root-finding algorithms: Newton and Halley
## Historic, we have derivative free versions of similar order

## Newton
"""

    Roots.Newton()

Implements Newton's [method](http://tinyurl.com/b4d7vls): `x_n1 = xn -
f(xn)/f'(xn)`.  This is a quadratically converging method requiring
one derivative. If a derivative is not specified, the `ForwardDiff` package
will be used, as applicable.

Unlike other methods, this method accepts complex inputs.
"""
struct Newton <: UnivariateZeroMethod
end

function callable_function(method::Newton, f::Tuple)
    length(f) == 1 && return FirstDerivative(f[1], D(f[1]))
    FirstDerivative(f[1], f[2])
end
callable_function(method::Newton, f::Any) = FirstDerivative(f, D(f))

function update_state(method::Newton, fs, o::UnivariateZeroState{T}, options::UnivariateZeroOptions) where {T}
    xn = o.xn1
    fxn = o.fxn1
    fpxn = fs.fp(xn)

    if isissue(fpxn)
        o.stopped=true
        return
    end

    xn1 = xn - fxn / fpxn
    fxn1 = fs.f(xn1)
    incfn(o)

    o.xn0, o.xn1 = xn, xn1
    o.fxn0, o.fxn1 = fxn, fxn1

    incsteps(o)

end

## extra work to allow for complex values

## extra work to allow for complex values
function derivative_free_setup(method::Newton, fs::CallableFunction, x0::T;
                                  bracket=missing,
                                  xabstol=zero(T), xreltol=zero(T),
                                  abstol=4*eps(T), reltol=4*eps(T),
                                  maxevals=40, maxfnevals=typemax(Int),
                                  verbose::Bool=false) where {T<:AbstractFloat}
    x = float(x0)

    prob = UnivariateZeroProblem(fs, x, bracket)
    options = UnivariateZeroOptions(xabstol, xreltol, abstol, reltol,  maxevals, maxfnevals, verbose)
    prob, options
end

function derivative_free_setup(method::Newton, fs::CallableFunction, x0::Complex{T};
                                  bracket=missing,
                                  xabstol=zero(T), xreltol=zero(T),
                                  abstol=4*eps(T), reltol=4*eps(T),
                                  maxevals=40, maxfnevals=typemax(Int),
                                  verbose::Bool=false) where {T<:AbstractFloat}
    x = float(x0)
    bracket = missing     # bracket makes no sense for complex input, but one is expected

    prob = UnivariateZeroProblem(fs, x, bracket)
    options = UnivariateZeroOptions(xabstol, xreltol, abstol, reltol,  maxevals, maxfnevals, verbose)
    prob, options
end

"""

Implementation of Newton's method: `x_n1 = x_n - f(x_n)/ f'(x_n)`

Arguments:

* `f::Function` -- function to find zero of

* `fp::Function=D(f)` -- derivative of `f`. Defaults to automatic derivative

* `x0::Number` -- initial guess. For Newton's method this may be complex.

Keyword arguments:

* `ftol`. Stop iterating when |f(xn)| <= max(1, |xn|) * ftol.

* `xtol`. Stop iterating when |xn+1 - xn| <= xtol + max(1, |xn|) * xtolrel

* `xtolrel`. Stop iterating when |xn+1 - xn| <= xtol + max(1, |xn|) * xtolrel

* `maxeval`. Stop iterating if more than this many steps, throw error.

* `maxfneval`. Stop iterating if more than this many function calls, throw error.

* `verbose::Bool=false` Set to `true` to see trace.

"""
newton(f, x0; kwargs...) = find_zero(f, x0, Newton(); kwargs...)
newton(f, fp, x0; kwargs...) = find_zero((f, fp), x0, Newton(); kwargs...)

## Halley

"""

    Roots.Halley()

Implements Halley's [method](http://tinyurl.com/yd83eytb),
`x_n1 = xn - (2 f(xn)*f'(xn)) / (2 f'(xn)^2 - f(xn) * f''(xn))`.
This method is cubically converging, but requires more function calls per step than
other methods.
"""
struct Halley <: UnivariateZeroMethod
end

function callable_function(method::Halley, f::Tuple)
    length(f) == 1 && return SecondDerivative(f[1], D(f[1]), D(f[1],2))
    length(f) == 2 && return SecondDerivative(f[1], f[2], D(f[2],1))
    SecondDerivative(f[1], f[2], f[3])
end
callable_function(method::Halley, f) = SecondDerivative(f, D(f), D(f, 2))

function update_state(method::Halley, fs, o::UnivariateZeroState{T}, options::UnivariateZeroOptions) where {T}
    xn = o.xn1
    fxn = o.fxn1
    fpxn = fs.fp(xn); incfn(o)
    fppxn = fs.fpp(xn); incfn(o)

    xn1 = xn - 2fxn*fpxn / (2*fpxn*fpxn - fxn * fppxn)
    fxn1 = fs.f(xn1); incfn(o)

    o.xn0, o.xn1 = xn, xn1
    o.fxn0, o.fxn1 = fxn, fxn1
    incsteps(o)
end

"""

Implementation of Halley's method. `xn1 = xn - 2f(xn)*f'(xn) / (2*f'(xn)^2 - f(xn) * f''(xn))`

Arguments:

* `f::Function` -- function to find zero of

* `fp::Function=D(f)` -- derivative of `f`. Defaults to automatic derivative

* `fpp:Function=D(f,2)` -- second derivative of `f`.

* `x0::Real` -- initial guess

Keyword arguments:

* `ftol`. Stop iterating when |f(xn)| <= max(1, |xn|) * ftol.

* `xtol`. Stop iterating when |xn+1 - xn| <= xtol + max(1, |xn|) * xtolrel

* `xtolrel`. Stop iterating when |xn+1 - xn| <= xtol + max(1, |xn|) * xtolrel

* `maxeval`. Stop iterating if more than this many steps, throw error.

* `verbose::Bool=false` Set to `true` to see trace.

"""
halley(f,  x0; kwargs...) = find_zero(f, x0, Halley(); kwargs...)
halley(f, fp, x0; kwargs...) = find_zero((f, fp), x0, Halley(); kwargs...)
halley(f, fp, fpp, x0; kwargs...) = find_zero((f, fp, fpp), x0, Halley(); kwargs...)


export GaussianFilter, GaborWavelet, MorletWavelet

abstract type AbstractFilter end

"""
    gabor1d(ω, ξ, σ)

Calculate the Fourier transform of a gabor wavelet.

# Arguments
 - `ω::Vector{Float64}`: Frequencies over which to calculate the filter.
 - `σ::Float64`: Bandwidth of the filter
 - `ξ::Float64`: Central frequency
"""
function gabor1d(ω::Vector{Float64}, σ::Float64, ξ::Float64)
    return exp.(-(ω.-ξ).^2 ./ (2*σ^2))
end
"""
    gabor1d(N, ξ, σ)

Convenience method for calculating the Fourier transform of a gabor wavelet.

# Arguments
 - `ω::Vector{Float64}`: Frequencies over which to calculate the filter.
 - `σ::Float64`: Bandwidth of the filter
 - `ξ::Float64`: Central frequency
"""
function gabor1d(N::Int64, σ::Float64, ξ::Float64)
    ω = FFTW.fftfreq(N)
    return gabor1d(ω[:], σ, ξ)
end

"""
    gauss1d(ω, σ)

Return a Fourier transformed Gaussian lowpass filter.

# Arguments
 - `ω::Vector{Float64}`: Frequencies over which to calculate the filter.
 - `σ::Float64`: Bandwidth of lowpass filter.
"""
function gauss1d(ω::Vector{Float64}, σ::Float64)
    return exp.(-(ω.^2)./(2*σ^2))
end
"""
    gauss1d(N, σ)

Convenience method for calculate the Fourier transform of a Gaussian lowpass filter.

# Argument
 - `N::Int64`: Length of filter in the time domain.
 - `σ::Float64`: Bandwidth of the lowpass filter.
"""
function gauss1d(N::Int64, σ::Float64)
    ω = collect(0.0:N-1)./N
    return gauss1d(ω[:], σ)
end

"""
    morlet1d(gab, lowpass)

Calculate the Fourier transform of a Morlet wavelet.

# Arguments
 - `gab::Vector{Float64}`: Fourier transformed Gabor wavelet
 - `lowpass::Vector{Float64}`: Fourier transformed Gaussian lowpass filter
"""
function morlet1d(gab::Vector{Float64}, lowpass::Vector{Float64})
    # summation factor to ensure first value is 0
    κ = gab[1]/lowpass[1]
    return gab .- κ.*lowpass
end
"""
    morlet1d(N, ξ, σ)

Calculate the Fourier transform of a Morlet wavelet.

# Arguments
 - `N::Int64`: Length of filter in the time domain.
 - `σ::Float64`: Bandwidth of the lowpass filter.
 - `ξ::Float64`: Central frequency.
"""
function morlet1d(N::Int64, σ::Float64, ξ::Float64)
    gab = gabor1d(N, σ, ξ)
    lowpass = gauss1d(N, σ)
    return morlet1d(gab, lowpass)
end

"""
    GaussianFilter

A structure for Fourier transformed Gaussian lowpass Filters

## Fields: GaussianFilter
 | **Field** | **Description** |
 |:----------|:----------------|
 | :σ        | Filter Bandwidth |
 | :ϕ        | Fourier-transformed Gaussian lowpass filter |
"""
struct GaussianFilter <: AbstractFilter
    σ::Float64 # filter bandwidth

    ϕ::Vector{Float64} # Fourier-transformed lowpass Gaussian filter

    function GaussianFilter(N::Int64, σ::Float64)
        ϕ = gauss1d(N, σ)
        return new(σ, ϕ)
    end
end

"""
    GaborWavelet

A structure for Fourier transformed Gabor Wavelets

## Fields: GaborWavelet
 | **Field** | **Description** |
 |:----------|:----------------|
 | :σ        | Filter Bandwidth |
 | :ξ        | Central Frequency |
 | :j        | Maximal subsampling |
 | :γ        | Fourier-transformed Gabor wavelet |
"""
struct GaborWavelet <: AbstractFilter
    σ::Float64 # filter bandwidth
    ξ::Float64 # central frequency
    j::Int64 # 2^j is the maximal subsampling possible without aliasing

    γ::Vector{Float64} # Fourier-transformed Gabor wavelet

    function GaborWavelet(N::Int64, σ::Float64, ξ::Float64, j::Int64)
        γ = gabor1d(N, σ, ξ)
        return new(σ, ξ, j, γ)
    end
end

"""
    MorletWavelet

A structure for Fourier transformed Morlet Wavelets

## Fields: MorletWavelet
 | **Field** | **Description** |
 |:----------|:----------------|
 | :σ        | Filter Bandwidth |
 | :ξ        | Central Frequency |
 | :j        | Maximal subsampling |
 | :ψ        | Fourier-transformed Morlet wavelet |
"""
struct MorletWavelet <: AbstractFilter
    σ::Float64 # filter bandwidth
    ξ::Float64 # central frequency
    j::Int64 # 2^j is the maximal subsampling possible without aliasing

    ψ::Vector{Float64} # Fourier-transformed Morlet wavelet

    function MorletWavelet(N::Int64, σ::Float64, ξ::Float64, j::Int64)
        ψ = morlet1d(N, σ, ξ)
        return new(σ, ξ, j, ψ)
    end
    function MorletWavelet(γ::GaborWavelet, ϕ::GaussianFilter)
        ψ = morlet1d(γ.γ, ϕ.ϕ)
        return new(γ.σ, γ.ξ, γ.j, ψ)
    end
end


#transform tests

# test Parseval's theorem for wavefunctions
function parsevaltest(L,N)
    X,K,dX,dK = makearrays(L,N)
    T = makeT(X,K)

    ψ = randn(N...) + im*randn(N...)

    n1 = sum(abs2.(ψ))*prod(dX)
    ϕ = T.Txk*ψ

    n2 = sum(abs2.(ϕ))*prod(dK)
    return n1,n2
end

L = (88.,)
N = (128,)
# X,K,dX,dK = makearrays(L,N)
# T = makeT(X,K) # TODO: norm problem in makeT for 2,3 dims

n1,n2 = parsevaltest(L,N)
@test n1 ≈ n2

## debug offending test
L = (30.,20.)
N = (30,34)

# X,K,dX,dK = makearrays(L,N)
# T = makeT(X,K) # TODO: norm problem in makeT for 2,3 dims

# ψ = randn(N...) + im*randn(N...)

# n1 = sum(abs2.(ψ))*prod(dX)
# ϕ = T.Txk*ψ

# n2 = sum(abs2.(ϕ))*prod(dK)

n1,n2 = parsevaltest(L,N)
@test n1 ≈ n2

L = (30.,20.,10)
N = (30,34,24)
n1,n2 = parsevaltest(L,N)
@test n1 ≈ n2

L = (30.,20.,10,50.)
N = (30,34,24,22)
n1,n2 = parsevaltest(L,N)
@test n1 ≈ n2


# This file investigates mean square error (MSE) with respect to free variables in 1D interpolation

using FractalTools
using Makie

# Construct interpolation data
f = FractalTools.wen            # For irregular data
# f = FractalTools.sinusoid     # For regular data
line = [[0.], [1.]]
pts = getdata(f, line, 11)

# Construct test data
tpts = getdata(line, 101)
npts = length(tpts)

# Compute errors
fvals = map(pnt -> f(pnt[1]), tpts)
freevars = 0.001 : 0.01 : 0.999
mse = map(freevars) do freevar
    interp = interpolate(pts, Interp1D(freevar))
    ivals = map(pnt -> interp(pnt[1]), tpts)
    sum((fvals - ivals).^2) / npts
end

# Plot mse
fig = Figure()
ax = fig[1, 1] = Axis(fig, xlabel="Free Variable", ylabel="MSE", title="1D Interpolation MSE")
stem!(ax, freevars, mse, color=:black)
save(joinpath(@__DIR__, "interp1d_mse.png"), fig)
display(fig)


# SolarIrradiance.jl
# Copyright (c) 2021 Pierre Galloo-Beauvais, Pierre Haessig
# This code is subject to the terms of the MIT license (see LICENSE.txt)

module SolarIrradiance

export dayofyear, declination, EoT, hour_angle, day_bounds
export cosθavg, cosθZavg, Rb, global_radiation_tilt

include("GHIProcess.jl")
export GHIProcess

using Dates

"""
    declination(n)

Compute the solar declination angle δ of the day `n`, in radians.

δ is the angle between the Sun-Earth direction and the equatorial plane.
It varies between −23.45° on December 21-22 and +23.45° on June 22
and is zero at the equinoxes.
See https://www.pveducation.org/pvcdrom/properties-of-sunlight/declination-angle.

# Examples

Declination at the summer solstice in the northern hemisphere:
```jldoctest
julia> using Dates

julia> n = dayofyear(2021,06,22)
173

julia> δ = declination(n)
0.40924559967108437

julia> round(rad2deg(δ); digits=3)
23.448
```

Declination at March equinoxe:
```jldoctest
julia> n = dayofyear(2021,03,22)
81

julia> round(declination(n); digits=3)
-0.0
```

"""
declination(n) = deg2rad(23.45) * sin(2π*(284+n)/365)

"""
    EoT(n)

Equation of Time on day `n`, in hours.

The Equation of Time is the difference between the apparent (or true) solar time
and the mean solar time.
See https://www.pveducation.org/pvcdrom/properties-of-sunlight/solar-time

# Examples

Minimal value is about -14 minutes in mid February:
```jldoctest
julia> EoT(45)*60
-14.26116933144462
```

Maximal value is about +16 minutes in mid November:
```jldoctest
julia> EoT(306)*60
16.388668719846564
```
"""
function EoT(n)
    B= 2π*(n-1)/365
    return 3.82*(0.000075+0.001868*cos(B)-0.032077*sin(B)-0.014615*cos(2*B)-0.04089*sin(2*B))
end

"""
    hour_angle(n, tc, lon)

Compute the hour angle of the Sun ω (in rad) a civil hour `tc` on day `n`, at longitude `lon` (°).

See also the inverse function `time_hour_angle`.

L'angle horaire du Soleil donne un renseignement sur le temps solaire vrai, il vaut 0 quand le Soleil est à son point culminant de la journée, est négatif avant ce point et positif après ce point.

TODO: add missing time zone input
"""
hour_angle(n, tc, lon) = (tc + lon/15 + EoT(n) - 12)*deg2rad(15)

"""
    time_hour_angle(n, ω, lon)

Compute the civil hour `tc` of an hour angle of the Sun `ω` (in rad) on day `n`,
at longitude `lon` (°).

It is the inverse function of `tc -> hour_angle(n, tc, lon)`.

TODO: add missing time zone input
"""
time_hour_angle(n, ω, lon) = ω/deg2rad(15) - EoT(n) + 12 - lon/15

"""
    day_bounds(n, slope, azimuth, lat)

Compute the hour angle bounds when the sun is above a panel during day `n`.

Panel has a `slope` (in [0, 90°]) and `azimuth` (in [-90°, 90°])
and is located at latitude `lat` (°).

Return `ωu`, `ωd` (in rad) which are respectively the hour angles of
sunrise and sunset *over the panel*.
When `slope` is 0, this corresponds to regulat sunrise and sunset (over the ground).
and there is a dedicated method for this, see `day_bounds(n, lat)`.

Mathematically, with θ the angle of incidence of the Sun on the panel,
this means that cosθ=0 at hour angle `ωu` and `ωd` and cosθ ≥ 0 in between.
"""
function day_bounds(n, slope, azimuth, lat)
    beta=deg2rad(slope)
    gamma=deg2rad(azimuth)
    phi=deg2rad(lat)
    delta=declination(n)

    sinδ=sin(delta)
    cosδ=cos(delta)
    sinϕ=sin(phi)
    cosϕ=cos(phi)
    cosβ=cos(beta)
    sinβ=sin(beta)
    cosγ=cos(gamma)

    A = sinδ*sinϕ*cosβ - sinδ*cosϕ*sinβ*cosγ
    B = cosδ*cosϕ*cosβ + cosδ*sinϕ*sinβ*cosγ
    C = cosδ*sinβ*sin(gamma)

    D = C^2+B^2-A^2
    E = A-B
    if D >= 0
        ωu = 2*atan((-C+sqrt(D))/E)
        ωd = 2*atan((-C-sqrt(D))/E)
    else
        # quick fix to  avoid sqrt(negative number) error,
        # but there may be a more physical way to detect this situation
        ωu = 0.0
        ωd = 0.0
    end

    return ωu, ωd
end

"""
    day_bounds(n, lat)

Compute the hour angle bounds when the sun is above the horizon during day `n`
at latitude `lat` (°).

Return `ωu`, `ωd` (in rad) which are respectively the hour angles of sunrise and sunset.

Mathematically, with θz the solar zenith angle (angle of incidence of the Sun on the ground),
this means that cosθz=0 at hour angle `ωu` and `ωd` and cosθz ≥ 0 in between.
"""
function day_bounds(n, lat)
    phi=deg2rad(lat)

    # Sunset
    ωd = acos(-tan(phi)*tan(declination(n)))
    # Sunrise
    ωu = -ωd

    return ωu, ωd
end

"""
    cosθavg(n, tcini, dt, slope, azimuth, lat, lon)

Cosine of θ, the angle of incidence of the sun on a tilted panel.

Panel has orientation (`slope`, `azimuth` °) and is located at (`lon`, `lat` °).
Value is computed on day `n`, averaged on a time interval `dt`
starting at civil hour `tcini`.

See also: [`cosθZavg`](@ref) for the incidence on the _ground_.
"""
function cosθavg(n, tcini, dt, slope, azimuth, lat, lon)
    beta=deg2rad(slope)
    gamma=deg2rad(azimuth)
    phi=deg2rad(lat)

    ω1=hour_angle(n, tcini, lon)
    ω2=hour_angle(n, tcini+dt, lon)

    ωup, ωdown = day_bounds(n, slope, azimuth, lat)

    if ω1<ωup && ω2<ωup
        return 0.0
    elseif ω1>ωdown && ω2>ωdown
        return 0.0
    elseif ω1<ωup && ω2>ωup
        ω1 = ωup
        tdebut = time_hour_angle(n, ωup, lon)
        tfin = tcini+dt
        dt = tfin-tdebut # TODO: check that the change of `dt` is correct
    elseif ω1<ωdown && ω2>ωdown
        ω2 = ωdown
        tdebut = tcini
        tfin = time_hour_angle(n, ωdown, lon)
        dt = tfin-tdebut # TODO: check that the change of `dt` is correct
    end

    delta = declination(n)

    sinδ = sin(delta)
    cosδ = cos(delta)
    sinϕ = sin(phi)
    cosϕ = cos(phi)
    cosβ = cos(beta)
    sinβ = sin(beta)
    cosγ = cos(gamma)

    a = sinδ*sinϕ*cosβ - sinδ*cosϕ*sinβ*cosγ
    b = cosδ*cosϕ*cosβ + cosδ*sinϕ*sinβ*cosγ
    c = cosδ*sinβ*sin(gamma)
    f = ω2 - ω1
    g = sin(ω2) - sin(ω1)
    h = cos(ω2) - cos(ω1)
    x = (a*f + b*g - c*h)*12/(pi*dt)

    return x
end

"""
    cosθZavg(n, tcini, dt, lat, lon)

Cosine of ``θ_Z``, the solar zenith angle, i.e. the angle of incidence of the sun on the ground.

Location is (`lon`, `lat` °).
Value is computed on day `n`, averaged on a time interval `dt`
starting at civil hour `tcini`.

See also: [`cosθavg`](@ref) for the incidence on a _tilted panel_.
"""
function cosθZavg(n, tcini, dt, lat, lon)
    phi=deg2rad(lat)

    ω1=hour_angle(n, tcini, lon)
    ω2=hour_angle(n, tcini+dt, lon)

    ωup, ωdown = day_bounds(n, lat)

    if ω1<ωup && ω2<ωup
        return 0.0
    elseif ω1>ωdown && ω2>ωdown
        return 0.0
    elseif ω1<ωup && ω2>ωup
        ω1 = ωup
        tdebut = time_hour_angle(n, ωup, lon)
        tfin = tcini+dt
        dt = tfin-tdebut # TODO: check that the change of `dt` is correct
    elseif ω1<ωdown && ω2>ωdown
        ω2 = ωdown
        tdebut = tcini
        tfin = time_hour_angle(n, ωdown, lon)
        dt = tfin-tdebut # TODO: check that the change of `dt` is correct
    end

    delta = declination(n)

    d = cos(phi)*cos(delta)
    e = sin(phi)*sin(delta)
    f = ω2 - ω1
    g = sin(ω2) - sin(ω1)
    x = (d*g + e*f)*12/(pi*dt)

    return x
end

"""
    Rb(cosθ, cosθZ, Rbsat=5)

Rb = cosθ/cosθZ with special care of zeros and saturation at `Rbsat`
"""
function Rb(cosθ, cosθZ, Rbsat=5.)
    if cosθ==0. || cosθZ==0.
        return 0.0
    else
        x = cosθ/cosθZ
    end

    if x>Rbsat
        return Rbsat
    else
        return x
    end
end

"""
    Rb(n, tcini, dt, slope, azimuth, lat, lon, Rbsat=5)

Cette fonction calcule le terme de redressement de GHI direct pour un panneau PV incliné d'un angle `slope`, orienté d'un angle `azimuth`, situé à une latitude `lat` et une longitude `lon` (°). Elle le calcule pour un jour n donné, sur un intervalle donné de durée dt à partir d'un instant initial tcini.

⚠ Fonction avec encore quelques problèmes. Des pics apparaissent dans certaines conditions, une saturation a été mise en place afin que ces pics n'impactent pas les calculs utilisant cette fonction. Cette saturation, choisie empiriquement, empêche le terme de redressement de dépasser la valeur de 5.
"""
function Rb(n, tcini, dt, slope, azimuth, lat, lon, Rbsat=5.)
    cosθ = cosθavg(n, tcini, dt, slope, azimuth, lat, lon)
    cosθZ = cosθZavg(n, tcini, dt, lat, lon)

    return Rb(cosθ, cosθZ, Rbsat)
end

"""
    global_radiation_tilt(GHI::Real, n, tcini, dt, lat, lon, slope, azimuth, albedo;
                          diffuse_model=:EKD82, transpose_model=:HDKR)

Estimate the global irradiance on a tilted panel from horizontal data `GHI`.

Panel has a orientation (`slope`, `azimuth`) and is located at (`lon`, `lat`).
`GHI` data is for day `n`, averaged on a time interval `dt`,
starting at civil hour `tcini` (UTC).

Reflection from surrounding area is modeled by `albedo` in [0,1].

# Empirical models

Many steps of the irradiance transposition from a horizontal plane to a tilted plane
are deduced from geometry. However, two aspects of the estimation are _empirical_,
so that several models are available:
1. splitting the global horizontal irradiance between direct and diffuse components
2. transposing the diffuse irradiance on a tilted plane

## 1. Diffuse fraction estimation

`diffuse_model` sets the model used to estimate the _fraction of diffuse radiation_
from the clearness index:
- `:EKD82`: Erbs, Klein & Duffie (1982). Estimation of the diffuse radiation fraction for
  hourly, daily and monthly-average global radiation. Solar Energy 28 (4), 293–302.
- `:OH77`: Orgill & Hollands (1977). Correlation equation for hourly diffuse radiation
  on a horizontal surface. Sol. Energy 19, 357–359.
- `:CR79`: Collares-Pereira & Rabl (1979), The average distribution of solar radiation
  —correlations between diffuse and hemispherical and between daily and hourly insolation values.
  Solar Energy 22, 155.

## 2. Diffuse irradiance transposition

`transpose_model` sets the model to estimate the diffuse radiation on a tilted plane.
Models differ on their assumption on how the diffuse irradiance is spatially distributed
on the sky hemisphere.
See (Demain 2013) for an introduction to those models and Duffie & Beckman's book for HDKR.
Models are classified between isotropic and anisotropic.

Isotropic models:
- `:LJ62`: Liu & Jordan (1962). Daily insolation on surfaces tilted towards the equator. ASHRAE, 53:526-41.
- `:Ko86`: Korokanis (1986). On the choice of the angle of tilt for south facing solar collectors in the Athens basin area. Solar Energy, 36:217-25.
- `:Ba02`: Badescu (2002). 3D isotropic approximation for solar diffuse irradiance on tilted
surfaces. Renewable Energy, 26:221-3.

Anisotropic models:
- `:Wi82`: Willmot (1982). On the climatic optimization of the tilt and azimuth of
  flat-plate solar collectors. Solar Energy, 28:205-16.
- `:Bu77`: Bugler (1977). The determination of hourly insolation on an inclined plane
  using a diffuse irradiance model based on hourly measured global horizontal insolation.
  Solar Energy, 19:477-91.
- `:MI83`: Ma & Iqbal (1983) Statistical comparison of models for estimating solar radiation
  on inclined surfaces. Solar Energy 31 (3), 313-317.
- `:Ha79`:  Hay (1979). Study of shortwave radiation on non-horizontal surfaces.
  Canadian Climate Centre, Report No. 79-12, Downsview, Ontario.
- `:TC77`:  Temps & Coulson (1977). Solar radiation incident upon slopes of different orientation.
  Solar Energy, 19:179-84.
- `:Kl79`: Klucher (1979). Evaluation of models to predict insolation on tilted surfaces.
  Solar Energy, 23:111-114.
- `:HDKR`: HDKR model (as named in Duffie & Beckman's book) used by HOMER, proposed by
  Reindl, Beckman & Duffie (1990). Evaluation of Hourly Tilted Surface Radiation Models.
  Solar Energy, 45, 9-17.
"""
function global_radiation_tilt(GHI::Real, n, tcini, dt, lat, lon, slope, azimuth, albedo;
                               diffuse_model=:EKD82, transpose_model=:HDKR)
    delta = declination(n)
    phi = deg2rad(lat)
    beta = deg2rad(slope)
    gamma = deg2rad(azimuth)

    # Extraterrestrial normal irradiance
    Gon = 1367*(1+0.033*cos(deg2rad((360*n)/365)))

    # Averaged horizontal extraterrestrial irradiance
    ω1 = hour_angle(n, tcini, lon)
    ω2 = hour_angle(n, tcini+dt, lon)

    # TODO: use the cosθZavg function here instead:
    # Goavg = Gon*cosθZavg, I believe
    a = cos(phi)*cos(delta) * (sin(ω2) - sin(ω1))
    b = sin(phi)*sin(delta) * (ω2 - ω1)
    Goavg = Gon*(a+b)*12/(pi*dt)
    if Goavg < 0
        Goavg = 0.0
    end

    # Clearness index `kt`
    if Goavg > 0
        kt = GHI/Goavg
            if kt > 1
                kt = 1.0
            end
    else
        kt = 0.0
    end

    # Diffuse irradiance, estimated from `kt` and GHI using various models
    if diffuse_model == :EKD82 # Erbs et al. 1982
        if kt<=0.22
            GHIdiffus = (1-0.09*kt)*GHI
        elseif kt>0.8
            GHIdiffus = 0.165*GHI
        else
            GHIdiffus = (0.9511-0.1604*kt+4.388*kt^2-16.638*kt^3+12.336*kt^4)*GHI
        end

    elseif diffuse_model == :OH77 # Orgill & Hollands 1977
        if kt<=0.35
            GHIdiffus = (1-0.249*kt)*GHI
        elseif kt>0.75
            GHIdiffus = 0.177*GHI
        else
            GHIdiffus = (1.557-1.84*kt)*GHI
        end

    elseif diffuse_model == :CR79 # Colarres-Pereira & Rabl 1979
        if kt<=0.17
            GHIdiffus = 0.99*GHI
        elseif kt>=0.80
            GHIdiffus = 0.2*GHI
        elseif kt>0.17 && kt<0.75
            GHIdiffus = (1.188-2.272*kt+9.473*kt^2-21.865*kt^3+14.648*kt^4)*GHI
        else # [0.75, .80[ (not in original publication)
            GHIdiffus = (0.632-0.54*kt)*GHI
        end
    else
        error("unknown diffuse fraction estimation model $diffuse_model")
    end

    # Beam (direct) irradiance:
    GHIdirect=GHI-GHIdiffus

    # Direct (beam) radiation on tilted surface
    # averaged cos(θ) and cos(θz)
    cosθ = cosθavg(n, tcini, dt, slope, azimuth, lat, lon)
    cosθZ = cosθZavg(n, tcini, dt, lat, lon)
    RB=Rb(cosθ, cosθZ)
    Gdirect=RB*GHIdirect

    # Diffuse radiation on tilted surface
    if transpose_model == :LJ62 # Liu & Jordan (1962)
        Gdiffus=(1+cos(beta))*GHIdiffus/2

    elseif transpose_model == :Ko86 # Korokanis (1986)
        Gdiffus=(2+cos(beta))*GHIdiffus/3

    elseif transpose_model == :Ba02 # Badescu (2002)
        Gdiffus=(3+cos(2*beta))*GHIdiffus/4

    elseif transpose_model == :Wi82 # Willmot (1982)
        if cosθ <= 0
            AW = 0.0
            BW = 1.0115-0.20293*beta-0.080823*beta^2
        else
            AW = Gdirect*RB/(1367*cosθ)
            BW = (1.0115-0.20293*beta-0.080823*beta^2)*(1-(Gdirect/(1367*cosθ)))
        end
        Gdiffus = (AW+BW) * GHIdiffus

    elseif transpose_model == :Bu77 # Bugler (1977)
        AB = (1+cos(beta))/2
        if GHIdiffus <= 0
            BB = 0.0
        else
            BB = 0.05*(Gdirect/GHIdiffus)
        end
        if cosθZ <= 0
            CB = cosθ
        else
            CB = cosθ - (1+cos(beta))/(2*cosθZ)
        end
        Gdiffus = (AB + BB*CB) * GHIdiffus

    elseif transpose_model == :MI83 # Ma & Iqbal (1983)
        AM = (1-kt)*(1+cos(beta))/2
        BM = kt*RB
        Gdiffus = (AM+BM)*GHIdiffus

    elseif transpose_model == :Ha79 # Hay (1979)
        if Goavg <= 0
            AH = 1.0
            CH = 0.0
        else
            AH = 1 - (GHIdirect/Goavg)
            CH = RB * (GHIdirect/Goavg)
        end
        BH = (1+cos(beta))/2
        Gdiffus = (AH*BH+CH) * GHIdiffus

    elseif transpose_model == :TC77 # Temps & Coulson (1977)
        ATC = (1+cos(beta))/2
        BTC = 1+(cosθ^2)*(sqrt(1-cosθZ^2))^3
        CTC = 1+(sin(beta/2))^3
        Gdiffus = ATC*BTC*CTC*GHIdiffus

    elseif transpose_model == :Kl79 # Klucher (1979)
        AK = (1+cos(beta))/2
        if GHI <= 0
            FK = 1.0
        else
            FK = 1 - (GHIdiffus/GHI)^2
        end
        BK = 1 + FK*(cosθ^2)*(sqrt(1-cosθZ^2))^3
        CK = 1 + FK*(sin(beta/2))^3
        Gdiffus = AK*BK*CK * GHIdiffus

    elseif transpose_model == :HDKR # HDKR model from Reindl, Beckman & Duffie (1990)
        if Goavg > 0
            Ai = GHIdirect/Goavg
        else
            Ai = 0.0
        end
        if GHI > 0
            f = sqrt(GHIdirect/GHI)
        else
            f = 0.0
        end
        AHDKR = Ai*RB
        BHDKR = 1 - Ai
        CHDKR = (1+cos(beta))/2
        DHDKR = 1 + f*(sin(beta/2))^3
        Gdiffus = (AHDKR + BHDKR*CHDKR*DHDKR) * GHIdiffus

    else
        error("unknown diffuse irradiance transposition model $transpose_model")
    end

    # Reflected irradiance
    Greflechi = GHI * albedo * (1-cos(beta))/2

    # Total irradiance
    Gtot = Gdiffus + Gdirect + Greflechi

    # Saturate output
    if Gtot>2000.
        # TODO: transform fixed threshold into parameter
        Gtot = 2000.
    elseif Gtot<0
        Gtot = 0.
    end

    return Gtot
end

"""
    global_radiation_tilt(n, tcini, dt, lat, lon, fonctionGHI, diffuse_model, transpose_model, GHI_data, slope, azimuth, albedo)

Same as first `global_radiation_tilt(GHI::Real...)`, but

`GHI_data` is the Vector of hourly GHI of a given year, obtained from function `GHIannee`.

Elle prend également en argument une chaîne de caractères `fonctionGHI`, qui permet de choisir l'interpolation des données du GHI. Cette chaîne de caractère peut donc être : "forwardfill", "backwardfill", "centeredfill" et "interpolation".
"""
function global_radiation_tilt(GHI_data::Vector, fonctionGHI, n, tcini, dt, lat, lon, slope, azimuth, albedo;
                               diffuse_model=:EKD82, transpose_model=:HDKR)
    GHI = GHIProcess.GHIavg(GHI_data, n, tcini, dt, fonctionGHI)
    Gtot = global_radiation_tilt(GHI, n, tcini, dt, lat, lon, slope, azimuth, albedo;
                                 diffuse_model=:EKD82, transpose_model=:HDKR)
    return Gtot
end

end # module


# Perform a univariate OLS regression (with non-zero intercept) to estimate the
# per-evaluation execution time of an expression.
#
# Arguments:
#
#     x::Vector{Float64}: The number of times the expression was evaluated.
#
#     y::Vector{Float64}: The total execution time of the expression's
#         (potentially repeated) evaluation.
#
# Returns:
#
#     a::Float64: The intercept of the univariate OLS model.
#
#     b::Float64: The slope of the univariate OLS model.
#
#     r²::Float64: The r-squared of the univariate OLS regresion

function ols(x::Vector{Float64}, y::Vector{Float64})
    a, b = linreg(x, y)
    r² = 1 - var(a + b * x - y) / var(y)
    return a, b, r²
end

function sem_ols(x::Vector{Float64}, y::Vector{Float64})
    a, b = linreg(x, y)
    n = length(x)
    residuals = y - (a + b * x)
    sem_b = sqrt(((1 / (n - 2)) * sum(residuals.^2)) / sum((x - mean(x)).^2))
    return sem_b
end


using ProximalOperators
using LinearAlgebra

# Nonsmooth + regularization

mu, lam = 2.0, 3.0

f = NormL1(mu)
g = Regularize(f, lam)
h = ElasticNet(mu, lam)

x = randn(10)

gx = call_test(g, x)
hx = call_test(h, x)

@test abs(gx - hx)/(1+abs(gx)) <= 1e-12

yg, gy = prox_test(g, x, 0.5)
yh, hy = prox_test(h, x, 0.5)

@test abs(gy - hy)/(1+abs(gy)) <= 1e-12
@test norm(yg - yh, Inf)/(1+norm(yg, Inf)) <= 1e-12

# Smooth + regularization (test also gradient)

m, n = 10, 20
lam = 1.5

A = randn(m, n)
b = randn(m)

f = LeastSquares(A, b)
g = Regularize(f, lam)

x = randn(n)
res = A*x-b

gx = call_test(g, x)

@test abs(0.5*norm(res)^2 + (0.5*lam)*norm(x)^2 - gx)/(1+abs(gx)) <= 1e-12

prox_test(g, x, 0.7)
grad_gx, gx1 = gradient_test(g, x)

@test abs(gx - gx1)/(1+abs(gx)) <= 1e-12
@test norm(grad_gx - A'*(A*x - b) - lam*x, Inf)/(1+norm(grad_gx, Inf)) <= 1e-12


##########################################################
# Rafał Nowak
#
# Instytut Informatyki
# Uniwersytet Wrocławski
#
#
# Pomocniczne materiały do wykładu z analizy numerycznej
#
##########################################################
two = Float32(2.0);

d = 32-1;
t = 23;

#
# Obliczanie największej liczby dodatniej w arytmetyce Float32
#
cmax = 2^(d-t-1)-1; # największa cecha

x1 = two^cmax * ( two - two^(-t) )
@printf("najw. dod. liczba w arytm. Float32 = %.1f =\n", x1);

# alternatywny sposób
x2 = prevfloat(typemax(Float32));
@printf("najw. dod. liczba w arytm. Float32 = %.1f\n", x2);

assert( x1==x2 )

#
# Obliczanie najmniejszej liczby dodatniej (subnormalnej) w arytmetyce Float32
#
 # największa cecha
cmin = -2^(d-t-1)+2; # niestety jedna liczba -128 i -127 są już zarezerwowane na (±Inf+NaN i Subnorm)
x1 = two^cmin * two^(-t)
@printf("najm. dod. liczba subnormalna w arytm. Float32 = %.16e\n", x1);
# alternatywny sposób
x2 = nextfloat(float32(0.0));
@printf("najm. dod. liczba subnormalna w arytm. Float32 = %.16e\n", x2);
assert( x1==x2 )


δ(i, j) = Int64(==(i, j))

diagm(x::Vector) = [δ(i, j)*x[i] for i in 1:length(x), j in 1:length(x)]

I(n) = [δ(i, j) for i = 1:n, j = 1:n]

vtanh(x::Vector) = tanh.(x)

dtanh(x::Vector) = I(length(x)) - diagm([i^2 for i in x])

relu(z::Vector) = max.(0, z)

leasSquareCost(o, t) = sum((o - t).^2)/2
dLeastSquareCost(o, t) = o-t

softmaxLikelihood(o, t) = -log(o[argmax(t)])
dSoftmaxLikelihood(o, t) = o - t

function drelu(z::Vector)
  dvec = Int64.(>(0).(z))
  return diagm(dvec[:, 1])
end

function softmax(z::Vector)
  shift = maximum(z)

  z = exp.(z .- shift)
  total = sum(z)
  return z ./ total
end

function dsoftmax(z::Vector)
  n = length(z)

  w = softmax(z)
  e = ones(n, 1)

  return w * e' .* (I(n) - e * w')
end

mean(x) = sum(x)/length(x)

activations = Dict(:vtanh => (vtanh, dtanh), :relu => (relu, drelu), :softmax => (softmax, dsoftmax))


@doc raw"""
    g = first_order_system(K, τ)

construct a first-order transfer function with gain `K` and time constant `τ`:

$$g(s)=\frac{K}{\tau s+1}$$

# example
```jldoctest
K = 1.0
τ = 3.0
g = first_order_system(K, τ)
# output
    1.0
-----------
3.0*s + 1.0
```

# returns
* `g::TransferFunction`: the first order transfer function. well, (0, 1) order.
"""
first_order_system(K::Float64, τ::Float64) = K / (τ * s + 1)

@doc raw"""
    g = second_order_system(K, τ, ξ)

construct a second-order transfer function with gain `K`, time constant `τ`, and damping coefficient `ξ`:

$$g(s)=\frac{K}{\tau^2 s^2 + 2\tau \xi s +1}$$

# example
```jldoctest
K = 1.0
τ = 2.0
ξ = 0.1
g = second_order_system(K, τ, ξ)
# output
         1.0
---------------------
4.0*s^2 + 0.4*s + 1.0
```

# returns
* `g::TransferFunction`: the second order transfer function. well, (0, 2) order.
"""
second_order_system(K::Float64, τ::Float64, ξ::Float64) = K / (τ^2 * s^2 + 2 * τ * ξ * s + 1)

@doc raw"""
    τ = time_constant(g)

compute the time constant τ of an order (0, 1) or order (0, 2) transfer function.

order (0, 1) representation:

$$g(s)=\frac{K}{\tau s+1}$$

order (0, 2) representation:

$$g(s)=\frac{K}{\tau^2 s^2 + 2\tau \xi s +1}$$

# returns
`τ::Float64`: the time constant.

# examples

```jldoctest
g = 4 / (6 * s + 2)
time_constant(g)
# output
3.0

g = 1.0 / (8 * s^2 + 0.8 * s + 2)
time_constant(g)
# output
2.0
```
"""
function time_constant(g::TransferFunction)
    if system_order(g) == (0, 1)
        # xx / (a s + b) ==> τ = a / b
        return g.denominator[1] / g.denominator[0]
    elseif system_order(g) == (0, 2)
        # xx / (a s^2 + b s + c) ==> τ = sqrt(a/c)
        return sqrt(g.denominator[2] / g.denominator[0])
    else
        error("`time_constant` only supports order (0, 1) and (0, 2) transfer functions.")
    end
end

@doc raw"""
    ξ = damping_coefficient(g)

compute the damping coefficient ξ of an order (0, 2) transfer function.

order (0, 2) representation:

$$g(s)=\frac{K}{\tau^2 s^2 + 2\tau \xi s +1}$$

# returns
`ξ::Float64`: the damping coefficient

# examples
```jldoctest
g = 1.0 / (8 * s^2 + 0.8 * s + 2)
damping_coefficient(g)
# output
0.1
```
"""
function damping_coefficient(g::TransferFunction)
    if system_order(g) != (0, 2)
        error("`damping_coefficient` only pertains to (0, 2) transfer functions.")
    end
    τ = time_constant(g)
    # xx / (a s² + b s + c) then 2τξ = b / c
    τξ2 = g.denominator[1] / g.denominator[0]
    return τξ2 / (2 * τ)
end


"""
    statsrank(x::Matrix{T}, ranktol::Real=1e-8) where {T<:AbstractFloat}

Return the numerical column rank and a pivot vector.

The rank is determined from the absolute values of the diagonal of R from
a pivoted QR decomposition, relative to the first (and, hence, largest)
element of this vector.

In the full-rank case the pivot vector is `collect(axes(x, 2))`.
"""
function statsrank(x::AbstractMatrix{T}; ranktol=1e-8) where {T<:AbstractFloat}
    m, n = size(x)
    piv = collect(axes(x, 2))

    iszero(n) && return (rank=n, piv=piv)

    qrpiv = pivoted_qr(x)
    dvec = abs.(diag(qrpiv.R))
    fdv = first(dvec)
    cmp = fdv * ranktol
    (last(dvec) > cmp) && return (rank=n, piv=piv)

    rank = searchsortedlast(dvec, cmp; rev=true)
    @assert rank < n
    piv = qrpiv.p
    v1 = first(eachcol(x))
    if all(isone, v1) && first(piv) ≠ 1
        # make sure the first column isn't moved by inflating v1
        v1 .*= (fdv + one(fdv)) / sqrt(m)
        qrpiv = pivoted_qr(x)
        piv = qrpiv.p
        fill!(v1, one(T))    # restore the contents of the first column
    end

    # maintain original column order for the linearly independent columns
    sort!(view(piv, 1:rank))
    return (rank=rank, piv=piv)
end